Skip to content

Commit b8ee16a

Browse files
chenyangjun-xyClawSweeper
andcommitted
fix(gateway): separate auth check from device identity omission for auth-none
Separate two concerns that were conflated in callGatewayWithScopes: 1. **Shared-auth check** (hasLocalBackendSharedAuth): auth mode none satisfies the local-backend shared-auth requirement → no error thrown 2. **Device identity omission** (omitDeviceIdentity): auth-none keeps the existing device-less self-pairing bypass (shouldSkipLocalBackendSelfPairing) on the server side → device identity intentionally omitted Previously the PR removed allowAuthNone entirely, making auth-none identity-bearing. Per codex review, auth-none must remain device-less because the server-side bypass already preserves scopes correctly for device-less local backend connections. Co-authored-by: ClawSweeper <[email protected]>
1 parent 9582911 commit b8ee16a

2 files changed

Lines changed: 18 additions & 9 deletions

File tree

src/gateway/call.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ describe("callGateway url resolution", () => {
923923
expect(lastClientOptions?.deviceIdentity).toBeNull();
924924
});
925925

926-
it("preserves device identity with local backend auth-none (operator scopes survive)", async () => {
926+
it("satisfies local backend shared auth with auth-none without a device identity", async () => {
927927
getRuntimeConfig.mockReturnValue({
928928
gateway: { mode: "local", bind: "loopback", auth: { mode: "none" } },
929929
});
@@ -940,8 +940,10 @@ describe("callGateway url resolution", () => {
940940
expect(lastClientOptions?.scopes).toEqual(["operator.read", "operator.pairing"]);
941941
expect(lastClientOptions?.token).toBeUndefined();
942942
expect(lastClientOptions?.password).toBeUndefined();
943-
// auth mode "none" no longer strips device identity; operator scopes like operator.write must survive
944-
expect(lastClientOptions?.deviceIdentity).not.toBeNull();
943+
// auth mode "none" keeps the device-less self-pairing bypass; device identity
944+
// is intentionally omitted so the server-side shouldSkipLocalBackendSelfPairing
945+
// can preserve requested scopes without requiring paired-device checks.
946+
expect(lastClientOptions?.deviceIdentity).toBeNull();
945947
});
946948

947949
it("rejects required local backend shared auth for remote targets", async () => {

src/gateway/call.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -485,10 +485,12 @@ function shouldOmitDeviceIdentityForGatewayCall(params: {
485485
url: string;
486486
token?: string;
487487
password?: string;
488+
allowAuthNone?: boolean;
488489
}): boolean {
489490
const mode = params.opts.mode ?? GATEWAY_CLIENT_MODES.CLI;
490491
const clientName = params.opts.clientName ?? GATEWAY_CLIENT_NAMES.CLI;
491-
const hasDirectLocalBackendAuth = Boolean(params.token || params.password);
492+
const hasDirectLocalBackendAuth =
493+
Boolean(params.token || params.password) || params.allowAuthNone === true;
492494
return (
493495
mode === GATEWAY_CLIENT_MODES.BACKEND &&
494496
clientName === GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT &&
@@ -1151,17 +1153,22 @@ async function callGatewayWithScopes<T = Record<string, unknown>>(
11511153
const tlsFingerprint = await resolveGatewayTlsFingerprint({ opts, context, url });
11521154
const token = useStoredDeviceAuth ? undefined : resolvedCredentials.token;
11531155
const password = useStoredDeviceAuth ? undefined : resolvedCredentials.password;
1154-
// auth mode "none" satisfies the local-backend shared-auth check but must not
1155-
// strip device identity, which would drop operator scopes like operator.write.
1156+
// auth mode "none" satisfies the local-backend shared-auth requirement without
1157+
// token / password credentials. It also triggers device-identity omission so
1158+
// the server-side device-less self-pairing bypass (shouldSkipLocalBackendSelfPairing)
1159+
// preserves requested scopes — pairing the two concerns keeps the auth-none
1160+
// path safe.
1161+
const allowAuthNone =
1162+
opts.requireLocalBackendSharedAuth === true &&
1163+
resolveGatewayCallAuth(context.config).mode === "none";
11561164
const hasLocalBackendSharedAuth =
1157-
Boolean(token || password) ||
1158-
(opts.requireLocalBackendSharedAuth === true &&
1159-
resolveGatewayCallAuth(context.config).mode === "none");
1165+
Boolean(token || password) || allowAuthNone;
11601166
const omitDeviceIdentity = shouldOmitDeviceIdentityForGatewayCall({
11611167
opts,
11621168
url,
11631169
token,
11641170
password,
1171+
allowAuthNone,
11651172
});
11661173
if (opts.requireLocalBackendSharedAuth && !hasLocalBackendSharedAuth) {
11671174
throw new GatewayLocalBackendSharedAuthUnavailableError(

0 commit comments

Comments
 (0)