fix(auth): coerce persisted device auth tokens

This commit is contained in:
Vincent Koc
2026-05-31 15:08:25 +02:00
parent fbde572491
commit 9518d1f27c
4 changed files with 85 additions and 46 deletions

View File

@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import {
clearDeviceAuthTokenFromStore,
coerceDeviceAuthStore,
loadDeviceAuthTokenFromStore,
storeDeviceAuthTokenInStore,
type DeviceAuthStoreAdapter,
@@ -113,6 +114,43 @@ describe("device-auth-store", () => {
});
});
it("coerces raw persisted stores into canonical token maps", () => {
expect(
coerceDeviceAuthStore({
version: 1,
deviceId: "device-1",
tokens: {
" operator ": {
token: "operator-token",
role: { nested: "bad" },
scopes: ["operator.write", "operator.read", 42],
updatedAtMs: "bad-time",
},
broken: {
token: 123,
role: "broken",
scopes: [],
updatedAtMs: 1,
},
},
}),
).toEqual({
version: 1,
deviceId: "device-1",
tokens: {
operator: {
token: "operator-token",
role: "operator",
scopes: ["operator.read", "operator.write"],
updatedAtMs: 0,
},
},
});
expect(coerceDeviceAuthStore({ version: 2, deviceId: "device-1", tokens: {} })).toBeNull();
expect(coerceDeviceAuthStore({ version: 1, deviceId: "device-1", tokens: [] })).toBeNull();
});
it("stores normalized roles and deduped sorted scopes while preserving same-device tokens", () => {
vi.spyOn(Date, "now").mockReturnValue(1234);
const { adapter, writes, readStore } = createAdapter({

View File

@@ -45,6 +45,20 @@ function copyCanonicalDeviceAuthTokens(
return out;
}
export function coerceDeviceAuthStore(value: unknown): DeviceAuthStore | null {
if (!isRecord(value) || value.version !== 1 || typeof value.deviceId !== "string") {
return null;
}
if (!isRecord(value.tokens)) {
return null;
}
return {
version: 1,
deviceId: value.deviceId,
tokens: copyCanonicalDeviceAuthTokens(value.tokens),
};
}
export function loadDeviceAuthTokenFromStore(params: {
adapter: DeviceAuthStoreAdapter;
deviceId: string;