refactor(gateway): simplify connect auth parsing

This commit is contained in:
Peter Steinberger
2026-04-04 22:22:18 +09:00
parent 68ec7c9bbf
commit 3758a0ce5b
2 changed files with 66 additions and 43 deletions

View File

@@ -412,48 +412,37 @@ describe("GatewayClient connect auth payload", () => {
storeDeviceAuthTokenMock.mockReset();
});
function connectFrameFrom(ws: MockWebSocket) {
type ParsedConnectRequest = {
id?: string;
params?: {
scopes?: string[];
auth?: {
token?: string;
bootstrapToken?: string;
deviceToken?: string;
password?: string;
};
};
};
function parseConnectRequest(ws: MockWebSocket): ParsedConnectRequest {
const raw = ws.sent.find((frame) => frame.includes('"method":"connect"'));
if (!raw) {
throw new Error("missing connect frame");
}
const parsed = JSON.parse(raw) as {
params?: {
auth?: {
token?: string;
bootstrapToken?: string;
deviceToken?: string;
password?: string;
};
};
};
return parsed.params?.auth ?? {};
return JSON.parse(raw) as ParsedConnectRequest;
}
function connectFrameFrom(ws: MockWebSocket) {
return parseConnectRequest(ws).params?.auth ?? {};
}
function connectScopesFrom(ws: MockWebSocket) {
const raw = ws.sent.find((frame) => frame.includes('"method":"connect"'));
expect(raw).toBeTruthy();
const parsed = JSON.parse(raw ?? "{}") as {
params?: {
scopes?: string[];
};
};
return parsed.params?.scopes ?? [];
return parseConnectRequest(ws).params?.scopes ?? [];
}
function connectRequestFrom(ws: MockWebSocket) {
const raw = ws.sent.find((frame) => frame.includes('"method":"connect"'));
expect(raw).toBeTruthy();
return JSON.parse(raw ?? "{}") as {
id?: string;
params?: {
scopes?: string[];
auth?: {
token?: string;
deviceToken?: string;
};
};
};
return parseConnectRequest(ws);
}
function emitConnectChallenge(ws: MockWebSocket, nonce = "nonce-1") {

View File

@@ -66,6 +66,11 @@ type SelectedConnectAuth = {
usingStoredDeviceToken?: boolean;
};
type StoredDeviceAuth = {
token?: string;
scopes?: string[];
};
class GatewayClientRequestError extends Error {
readonly gatewayCode: string;
readonly details?: unknown;
@@ -435,12 +440,10 @@ export class GatewayClient {
}
: undefined;
const signedAtMs = Date.now();
// Reuse cached scopes only when the client is reusing the cached device token.
// Explicit device tokens should keep the caller-requested scope set.
const scopes =
usingStoredDeviceToken && storedScopes && storedScopes.length > 0
? storedScopes
: (this.opts.scopes ?? ["operator.admin"]);
const scopes = this.resolveConnectScopes({
usingStoredDeviceToken,
storedScopes,
});
const platform = this.opts.platform ?? process.platform;
const device = (() => {
if (!this.opts.deviceIdentity) {
@@ -540,6 +543,39 @@ export class GatewayClient {
});
}
private resolveConnectScopes(params: {
usingStoredDeviceToken?: boolean;
storedScopes?: string[];
}): string[] {
// Reuse cached scopes only when the client is reusing the cached device token.
// Explicit device tokens should keep the caller-requested scope set.
if (
params.usingStoredDeviceToken &&
Array.isArray(params.storedScopes) &&
params.storedScopes.length > 0
) {
return params.storedScopes;
}
return this.opts.scopes ?? ["operator.admin"];
}
private loadStoredDeviceAuth(role: string): StoredDeviceAuth | null {
if (!this.opts.deviceIdentity) {
return null;
}
const storedAuth = loadDeviceAuthToken({
deviceId: this.opts.deviceIdentity.deviceId,
role,
});
if (!storedAuth) {
return null;
}
return {
token: storedAuth.token,
scopes: storedAuth.scopes,
};
}
private shouldPauseReconnectAfterAuthFailure(detailCode: string | null): boolean {
if (!detailCode) {
return false;
@@ -626,9 +662,7 @@ export class GatewayClient {
const explicitBootstrapToken = this.opts.bootstrapToken?.trim() || undefined;
const explicitDeviceToken = this.opts.deviceToken?.trim() || undefined;
const authPassword = this.opts.password?.trim() || undefined;
const storedAuth = this.opts.deviceIdentity
? loadDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role })
: null;
const storedAuth = this.loadStoredDeviceAuth(role);
const storedToken = storedAuth?.token ?? null;
const storedScopes = storedAuth?.scopes;
const shouldUseDeviceRetryToken =
@@ -643,7 +677,7 @@ export class GatewayClient {
(!(explicitGatewayToken || authPassword) && (!explicitBootstrapToken || Boolean(storedToken)))
? (storedToken ?? undefined)
: undefined);
const usingStoredDeviceToken =
const reusingStoredDeviceToken =
Boolean(resolvedDeviceToken) &&
!explicitDeviceToken &&
Boolean(storedToken) &&
@@ -662,7 +696,7 @@ export class GatewayClient {
resolvedDeviceToken,
storedToken: storedToken ?? undefined,
storedScopes,
usingStoredDeviceToken,
usingStoredDeviceToken: reusingStoredDeviceToken,
};
}