Skip to content

Commit c33bac5

Browse files
committed
fix(gateway): preserve device token for fallback auth during SPA navigation
1 parent 4db6349 commit c33bac5

File tree

2 files changed

+15
-12
lines changed

2 files changed

+15
-12
lines changed

src/gateway/client.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ describe("GatewayClient connect auth payload", () => {
383383
);
384384
}
385385

386-
it("uses explicit shared token and does not inject stored device token", () => {
386+
it("sends stored device token alongside explicit shared token for fallback auth", () => {
387387
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
388388
const client = new GatewayClient({
389389
url: "ws://127.0.0.1:18789",
@@ -397,12 +397,12 @@ describe("GatewayClient connect auth payload", () => {
397397

398398
expect(connectFrameFrom(ws)).toMatchObject({
399399
token: "shared-token",
400+
deviceToken: "stored-device-token",
400401
});
401-
expect(connectFrameFrom(ws).deviceToken).toBeUndefined();
402402
client.stop();
403403
});
404404

405-
it("uses explicit shared password and does not inject stored device token", () => {
405+
it("sends stored device token alongside explicit shared password for fallback auth", () => {
406406
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
407407
const client = new GatewayClient({
408408
url: "ws://127.0.0.1:18789",
@@ -416,9 +416,11 @@ describe("GatewayClient connect auth payload", () => {
416416

417417
expect(connectFrameFrom(ws)).toMatchObject({
418418
password: "shared-password", // pragma: allowlist secret
419+
deviceToken: "stored-device-token",
419420
});
420-
expect(connectFrameFrom(ws).token).toBeUndefined();
421-
expect(connectFrameFrom(ws).deviceToken).toBeUndefined();
421+
// When password is the primary credential, auth.token falls back to
422+
// the resolved device token for legacy compatibility.
423+
expect(connectFrameFrom(ws).token).toBe("stored-device-token");
422424
client.stop();
423425
});
424426

src/gateway/client.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,14 @@ export class GatewayClient {
253253
const storedToken = this.opts.deviceIdentity
254254
? loadDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role })?.token
255255
: null;
256-
// Keep shared gateway credentials explicit. Persisted per-device tokens only
257-
// participate when no explicit shared token/password is provided.
258-
const resolvedDeviceToken =
259-
explicitDeviceToken ??
260-
(!(explicitGatewayToken || this.opts.password?.trim())
261-
? (storedToken ?? undefined)
262-
: undefined);
256+
// Always resolve the device token independently of shared credentials.
257+
// The gateway server already supports deviceToken fallback when the
258+
// shared token/password is absent or invalid (see server auth matrix
259+
// test "uses explicit auth.deviceToken fallback when shared token is
260+
// wrong"). Suppressing the stored token here caused Control-UI
261+
// reconnects to fail with "device identity required" after SPA
262+
// navigation dropped the shared token from the URL hash (#39611).
263+
const resolvedDeviceToken = explicitDeviceToken ?? storedToken ?? undefined;
263264
// Legacy compatibility: keep `auth.token` populated for device-token auth when
264265
// no explicit shared token is present.
265266
const authToken = explicitGatewayToken ?? resolvedDeviceToken;

0 commit comments

Comments
 (0)