fix: enforce paired scope baselines on reconnect

This commit is contained in:
Peter Steinberger
2026-04-05 07:53:34 +01:00
parent 19b7fbaa73
commit 20b08f1a85
3 changed files with 29 additions and 23 deletions

View File

@@ -84,7 +84,7 @@ describe("talk-voice plugin", () => {
text:
"Talk voice status:\n" +
"- provider: microsoft\n" +
"- talk.voiceId: en-US-AvaNeural\n" +
"- talk.providers.microsoft.voiceId: en-US-AvaNeural\n" +
"- microsoft.apiKey: secret…",
});
});

View File

@@ -408,6 +408,7 @@ describe("callGateway url resolution", () => {
"operator.write",
"operator.approvals",
"operator.pairing",
"operator.talk.secrets",
],
},
])("scope selection: $label", async ({ call, expectedScopes }) => {

View File

@@ -738,21 +738,20 @@ export function attachGatewayWsMessageHandler(params: {
sharedAuthOk,
authMethod,
});
const skipPairing =
shouldSkipLocalBackendSelfPairing({
connectParams,
locality: pairingLocality,
hasBrowserOriginHeader,
sharedAuthOk,
authMethod,
}) ||
shouldSkipControlUiPairing(
controlUiAuthPolicy,
role,
trustedProxyAuthOk,
resolvedAuth.mode,
);
if (device && devicePublicKey && !skipPairing) {
const skipLocalBackendSelfPairing = shouldSkipLocalBackendSelfPairing({
connectParams,
locality: pairingLocality,
hasBrowserOriginHeader,
sharedAuthOk,
authMethod,
});
const skipControlUiPairingForDevice = shouldSkipControlUiPairing(
controlUiAuthPolicy,
role,
trustedProxyAuthOk,
resolvedAuth.mode,
);
if (device && devicePublicKey) {
const formatAuditList = (items: string[] | undefined): string => {
if (!items || items.length === 0) {
return "<none>";
@@ -968,9 +967,15 @@ export function attachGatewayWsMessageHandler(params: {
const paired = await getPairedDevice(device.id);
const isPaired = paired?.publicKey === devicePublicKey;
if (!isPaired) {
const ok = await requirePairing("not-paired", paired);
if (!ok) {
return;
if (!(skipLocalBackendSelfPairing || skipControlUiPairingForDevice)) {
// Initial local backend/control-ui self-pairing can bypass the
// pairing prompt, but only while the device is still unpaired.
// Once a device is paired, reconnects must stay inside the
// approved role/scope baseline below.
const ok = await requirePairing("not-paired", paired);
if (!ok) {
return;
}
}
} else {
const claimedPlatform = connectParams.client.platform;
@@ -1001,10 +1006,10 @@ export function attachGatewayWsMessageHandler(params: {
}
}
const pairedRoles = listEffectivePairedDeviceRoles(paired);
const pairedScopes = Array.isArray(paired.scopes)
? paired.scopes
: Array.isArray(paired.approvedScopes)
? paired.approvedScopes
const pairedScopes = Array.isArray(paired.approvedScopes)
? paired.approvedScopes
: Array.isArray(paired.scopes)
? paired.scopes
: [];
const allowedRoles = new Set(pairedRoles);
if (allowedRoles.size === 0) {