/// import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AssistantCloudAnonymousAuthStrategy, AssistantCloudJWTAuthStrategy, readAnonymousRefreshToken, } from "../AssistantCloudAuthStrategy"; import { CloudResponseError } from "../cloudResponse"; const baseUrl = "https://test.example.com"; const accessToken = `${Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")}.${Buffer.from(JSON.stringify({ exp: 4102444800 })).toString("base64url")}.sig`; const createAccessToken = (subject: string) => `${Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")}.${Buffer.from(JSON.stringify({ exp: 4102444800, sub: subject })).toString("base64url")}.sig`; const refreshToken = { token: "r1", expires_at: "2099-01-01", }; const refreshTokenKey = `aui:refresh_token:${baseUrl}`; let originalLocalStorageDescriptor: PropertyDescriptor | undefined; const installLocalStorage = (storage: Storage): void => { Object.defineProperty(globalThis, "localStorage", { configurable: true, value: storage, }); }; const mockAnonymousTokenFetch = (nextRefreshToken = refreshToken) => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: nextRefreshToken, }), }); vi.stubGlobal("fetch", fetchMock); return fetchMock; }; describe("AssistantCloudAnonymousAuthStrategy", () => { beforeEach(() => { originalLocalStorageDescriptor = Object.getOwnPropertyDescriptor( globalThis, "localStorage", ); vi.stubGlobal("navigator", {}); }); afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); if (originalLocalStorageDescriptor) { Object.defineProperty( globalThis, "localStorage", originalLocalStorageDescriptor, ); } else { delete (globalThis as { localStorage?: Storage }).localStorage; } }); it("reads the stored anonymous refresh token", () => { const values = new Map([[refreshTokenKey, JSON.stringify(refreshToken)]]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); expect(readAnonymousRefreshToken(baseUrl)).toBe(refreshToken.token); }); it("reads the token stored for a base url given with a trailing slash", () => { // The two-step cast below erases the contextual Storage typing the plain // `as Storage` sites get, so this parameter needs its own annotation. installLocalStorage({ getItem: (key: string) => key === refreshTokenKey ? JSON.stringify(refreshToken) : null, setItem: vi.fn(), removeItem: vi.fn(), } as unknown as Storage); expect(readAnonymousRefreshToken(`${baseUrl}/`)).toBe(refreshToken.token); }); it("returns null when no anonymous refresh token is stored", () => { installLocalStorage({ getItem: () => null, setItem: vi.fn(), removeItem: vi.fn(), } as unknown as Storage); expect(readAnonymousRefreshToken(baseUrl)).toBeNull(); }); it.each([30_000, 0, -1])( "returns null when the stored anonymous refresh token expires in %i ms", (expiresIn) => { vi.useFakeTimers(); const now = Date.UTC(2026, 8, 4, 12, 0, 0); vi.setSystemTime(now); const values = new Map([ [ refreshTokenKey, JSON.stringify({ token: refreshToken.token, expires_at: new Date(now + expiresIn).toISOString(), }), ], ]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); expect(readAnonymousRefreshToken(baseUrl)).toBeNull(); }, ); it.each([ "2099-01-01", "2099-01-01T00:00:00Z", "2099-01-01T00:00:00+0000", "2099-01-01T00:00:00", "2099-01-01 00:00:00+00", ])("persists refresh tokens with expiry %s", async (expiresAt) => { const nextRefreshToken = { ...refreshToken, expires_at: expiresAt }; const values = new Map(); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); const fetchMock = mockAnonymousTokenFetch(nextRefreshToken); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(nextRefreshToken)); expect(fetchMock).toHaveBeenCalledWith( `${baseUrl}/v1/auth/tokens/anonymous`, { method: "POST", signal: expect.any(AbortSignal) }, ); }); it("deduplicates concurrent anonymous token requests", async () => { delete (globalThis as { localStorage?: Storage }).localStorage; const fetchMock = mockAnonymousTokenFetch(); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect( Promise.all([ strategy.getAuthHeaders(), strategy.getAuthHeaders(), strategy.getAuthHeaders(), ]), ).resolves.toEqual([ { Authorization: `Bearer ${accessToken}` }, { Authorization: `Bearer ${accessToken}` }, { Authorization: `Bearer ${accessToken}` }, ]); expect(fetchMock).toHaveBeenCalledTimes(1); }); it("deduplicates anonymous token requests across strategy instances", async () => { const values = new Map(); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); const fetchMock = mockAnonymousTokenFetch(); const first = new AssistantCloudAnonymousAuthStrategy(baseUrl); const second = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect( Promise.all([first.getAuthHeaders(), second.getAuthHeaders()]), ).resolves.toEqual([ { Authorization: `Bearer ${accessToken}` }, { Authorization: `Bearer ${accessToken}` }, ]); expect(fetchMock).toHaveBeenCalledTimes(1); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken)); }); it("reuses an in-flight anonymous token request after invalidation", async () => { const values = new Map(); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); let resolveResponse: (response: Response) => void = () => {}; const response = new Promise((resolve) => { resolveResponse = resolve; }); const fetchMock = vi .fn() .mockImplementationOnce(() => response); vi.stubGlobal("fetch", fetchMock); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); const firstRequest = strategy.getAuthHeaders(); await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce()); strategy.invalidate(); const secondRequest = strategy.getAuthHeaders(); expect(fetchMock).toHaveBeenCalledOnce(); resolveResponse({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: refreshToken, }), } as unknown as Response); await expect(firstRequest).resolves.toBe(false); await expect(secondRequest).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); expect(fetchMock).toHaveBeenCalledOnce(); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken)); }); it("coordinates anonymous token requests across realms", async () => { const values = new Map(); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); let lockTail: Promise = Promise.resolve(); const lockRequest = vi.fn( (_name: string, callback: () => Promise) => { const request = lockTail.then(callback); lockTail = request.then( () => undefined, () => undefined, ); return request; }, ); vi.stubGlobal("navigator", { locks: { request: lockRequest } }); const rotatedRefreshToken = { token: "r2", expires_at: "2099-01-01" }; const response = (refreshTokenValue: typeof refreshToken) => ({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: refreshTokenValue, }), }); const fetchMock = vi .fn() .mockResolvedValueOnce(response(refreshToken)) .mockResolvedValueOnce(response(rotatedRefreshToken)); vi.stubGlobal("fetch", fetchMock); const first = new AssistantCloudAnonymousAuthStrategy(baseUrl); vi.resetModules(); const { AssistantCloudAnonymousAuthStrategy: Second } = await import("../AssistantCloudAuthStrategy"); await expect( Promise.all([ first.getAuthHeaders(), new Second(baseUrl).getAuthHeaders(), ]), ).resolves.toEqual([ { Authorization: `Bearer ${accessToken}` }, { Authorization: `Bearer ${accessToken}` }, ]); expect(fetchMock).toHaveBeenNthCalledWith( 1, `${baseUrl}/v1/auth/tokens/anonymous`, { method: "POST", signal: expect.any(AbortSignal) }, ); expect(fetchMock).toHaveBeenNthCalledWith( 2, `${baseUrl}/v1/auth/tokens/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken.token }), signal: expect.any(AbortSignal), }, ); expect(lockRequest).toHaveBeenCalledTimes(2); expect(values.get(refreshTokenKey)).toBe( JSON.stringify(rotatedRefreshToken), ); }); it("retries shared anonymous token requests after a failure", async () => { const values = new Map(); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); const failure = new Error("authentication unavailable"); const fetchMock = vi .fn() .mockRejectedValueOnce(failure) .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: refreshToken, }), }); vi.stubGlobal("fetch", fetchMock); const first = new AssistantCloudAnonymousAuthStrategy(baseUrl); const second = new AssistantCloudAnonymousAuthStrategy(baseUrl); await Promise.all([ expect(first.getAuthHeaders()).rejects.toBe(failure), expect(second.getAuthHeaders()).rejects.toBe(failure), ]); expect(fetchMock).toHaveBeenCalledTimes(1); await expect( new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(), ).resolves.toEqual({ Authorization: `Bearer ${accessToken}` }); expect(fetchMock).toHaveBeenCalledTimes(2); }); it("aborts shared anonymous token requests at the deadline before retrying", async () => { vi.useFakeTimers(); const values = new Map(); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); let requestSignal: AbortSignal | null | undefined; const fetchMock = vi .fn() .mockImplementationOnce((_input: RequestInfo | URL, init?: RequestInit) => Promise.resolve({ ok: true, json: () => new Promise((_resolve, reject) => { requestSignal = init?.signal; requestSignal?.addEventListener( "abort", () => reject(requestSignal?.reason), { once: true }, ); }), } as unknown as Response), ) .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: refreshToken, }), }); vi.stubGlobal("fetch", fetchMock); const first = new AssistantCloudAnonymousAuthStrategy(baseUrl); const second = new AssistantCloudAnonymousAuthStrategy(baseUrl); const firstRequest = expect(first.getAuthHeaders()).rejects.toThrow( "Assistant Cloud anonymous token request timed out after 30000ms", ); const secondRequest = expect(second.getAuthHeaders()).rejects.toThrow( "Assistant Cloud anonymous token request timed out after 30000ms", ); await vi.advanceTimersByTimeAsync(0); expect(fetchMock).toHaveBeenCalledTimes(1); expect(requestSignal).toBeInstanceOf(AbortSignal); await vi.advanceTimersByTimeAsync(29_999); expect.soft(requestSignal?.aborted).toBe(false); await vi.advanceTimersByTimeAsync(1); await Promise.all([firstRequest, secondRequest]); expect(requestSignal?.aborted).toBe(true); expect(values.has(refreshTokenKey)).toBe(false); await expect( new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(), ).resolves.toEqual({ Authorization: `Bearer ${accessToken}` }); expect(fetchMock).toHaveBeenCalledTimes(2); }); it("aborts refresh requests at the deadline without replacing the identity", async () => { vi.useFakeTimers(); const values = new Map([[refreshTokenKey, JSON.stringify(refreshToken)]]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); let requestSignal: AbortSignal | null | undefined; const rotatedRefreshToken = { token: "r2", expires_at: "2099-02-01" }; const fetchMock = vi .fn() .mockImplementationOnce( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((_resolve, reject) => { requestSignal = init?.signal; requestSignal?.addEventListener( "abort", () => reject(requestSignal?.reason), { once: true }, ); }), ) .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: rotatedRefreshToken, }), }); vi.stubGlobal("fetch", fetchMock); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); const request = expect(strategy.getAuthHeaders()).rejects.toThrow( "Assistant Cloud refresh token request timed out after 30000ms", ); expect(requestSignal).toBeInstanceOf(AbortSignal); await vi.advanceTimersByTimeAsync(29_999); expect.soft(requestSignal?.aborted).toBe(false); await vi.advanceTimersByTimeAsync(1); await request; expect(requestSignal?.aborted).toBe(true); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken)); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); expect(fetchMock).toHaveBeenCalledTimes(2); expect(values.get(refreshTokenKey)).toBe( JSON.stringify(rotatedRefreshToken), ); }); it("keeps anonymous token requests independent without localStorage", async () => { delete (globalThis as { localStorage?: Storage }).localStorage; const lockRequest = vi.fn(); vi.stubGlobal("navigator", { locks: { request: lockRequest } }); const fetchMock = mockAnonymousTokenFetch(); const first = new AssistantCloudAnonymousAuthStrategy(baseUrl); const second = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect( Promise.all([first.getAuthHeaders(), second.getAuthHeaders()]), ).resolves.toEqual([ { Authorization: `Bearer ${accessToken}` }, { Authorization: `Bearer ${accessToken}` }, ]); expect(fetchMock).toHaveBeenCalledTimes(2); expect(lockRequest).not.toHaveBeenCalled(); }); it("scopes anonymous refresh tokens by backend", async () => { const secondBaseUrl = "https://other.example.com"; const values = new Map(); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); const secondRefreshToken = { token: "r2", expires_at: "2099-01-01", }; const fetchMock = vi .fn() .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: refreshToken, }), }) .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: secondRefreshToken, }), }); vi.stubGlobal("fetch", fetchMock); await Promise.all([ new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(), new AssistantCloudAnonymousAuthStrategy(secondBaseUrl).getAuthHeaders(), ]); expect(fetchMock).toHaveBeenNthCalledWith( 1, `${baseUrl}/v1/auth/tokens/anonymous`, { method: "POST", signal: expect.any(AbortSignal) }, ); expect(fetchMock).toHaveBeenNthCalledWith( 2, `${secondBaseUrl}/v1/auth/tokens/anonymous`, { method: "POST", signal: expect.any(AbortSignal) }, ); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken)); expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe( JSON.stringify(secondRefreshToken), ); }); it("migrates the legacy refresh token to the first backend", async () => { const secondBaseUrl = "https://other.example.com"; const secondRefreshToken = { token: "r2", expires_at: "2099-01-01", }; const values = new Map([ ["aui:refresh_token", JSON.stringify(refreshToken)], ]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); const fetchMock = vi .fn() .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken }), }) .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: secondRefreshToken, }), }); vi.stubGlobal("fetch", fetchMock); await new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(); await new AssistantCloudAnonymousAuthStrategy( secondBaseUrl, ).getAuthHeaders(); expect(fetchMock).toHaveBeenNthCalledWith( 1, `${baseUrl}/v1/auth/tokens/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken.token }), signal: expect.any(AbortSignal), }, ); expect(fetchMock).toHaveBeenNthCalledWith( 2, `${secondBaseUrl}/v1/auth/tokens/anonymous`, { method: "POST", signal: expect.any(AbortSignal) }, ); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken)); expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe( JSON.stringify(secondRefreshToken), ); expect(values.has("aui:refresh_token")).toBe(false); }); it("retires the legacy refresh token when a scoped token exists", async () => { const secondBaseUrl = "https://other.example.com"; const scopedRefreshToken = { token: "scoped-r1", expires_at: "2099-01-01", }; const secondRefreshToken = { token: "r2", expires_at: "2099-01-01", }; const values = new Map([ ["aui:refresh_token", JSON.stringify(refreshToken)], [refreshTokenKey, JSON.stringify(scopedRefreshToken)], ]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); const fetchMock = vi .fn() .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken }), }) .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: secondRefreshToken, }), }); vi.stubGlobal("fetch", fetchMock); await new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(); await new AssistantCloudAnonymousAuthStrategy( secondBaseUrl, ).getAuthHeaders(); expect(fetchMock).toHaveBeenNthCalledWith( 1, `${baseUrl}/v1/auth/tokens/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: scopedRefreshToken.token }), signal: expect.any(AbortSignal), }, ); expect(fetchMock).toHaveBeenNthCalledWith( 2, `${secondBaseUrl}/v1/auth/tokens/anonymous`, { method: "POST", signal: expect.any(AbortSignal) }, ); expect(values.has("aui:refresh_token")).toBe(false); expect(values.get(`aui:refresh_token:${secondBaseUrl}`)).toBe( JSON.stringify(secondRefreshToken), ); }); it("returns an anonymous access token without localStorage", async () => { delete (globalThis as { localStorage?: Storage }).localStorage; mockAnonymousTokenFetch(); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); }); it("returns an anonymous access token when localStorage access is blocked", async () => { Object.defineProperty(globalThis, "localStorage", { configurable: true, get() { throw new DOMException("blocked", "SecurityError"); }, }); mockAnonymousTokenFetch(); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); }); it("returns an anonymous access token when storage methods throw", async () => { const getItem = vi .fn<(key: string) => string | null>() .mockReturnValueOnce( JSON.stringify({ token: "expired", expires_at: "2022-01-01" }), ) .mockImplementation(() => { throw new DOMException("blocked", "SecurityError"); }); const setItem = vi.fn(() => { throw new DOMException("blocked", "SecurityError"); }); const removeItem = vi.fn(() => { throw new DOMException("blocked", "SecurityError"); }); installLocalStorage({ getItem, setItem, removeItem } as unknown as Storage); mockAnonymousTokenFetch(); await expect( new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(), ).resolves.toEqual({ Authorization: `Bearer ${accessToken}` }); await expect( new AssistantCloudAnonymousAuthStrategy(baseUrl).getAuthHeaders(), ).resolves.toEqual({ Authorization: `Bearer ${accessToken}` }); expect(getItem).toHaveBeenCalledTimes(2); expect(setItem).toHaveBeenCalledTimes(2); expect(removeItem).toHaveBeenCalledTimes(2); }); it("treats corrupted refresh token JSON as absent", async () => { const values = new Map([["aui:refresh_token", "not-json{"]]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); mockAnonymousTokenFetch(); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken)); expect(values.has("aui:refresh_token")).toBe(false); }); it("rejects malformed anonymous token responses without persisting them", async () => { const setItem = vi.fn(); installLocalStorage({ getItem: () => null, setItem, removeItem: vi.fn(), } as unknown as Storage); vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: "not-a-refresh-token", }), }), ); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).rejects.toThrow( new CloudResponseError( 'Invalid Assistant Cloud response for "anonymous auth token response.refresh_token": expected an object', ), ); expect(setItem).not.toHaveBeenCalled(); }); it("rejects empty refresh token expiry without persisting it", async () => { const setItem = vi.fn(); installLocalStorage({ getItem: () => null, setItem, removeItem: vi.fn(), } as unknown as Storage); vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: { token: "r2", expires_at: "", }, }), }), ); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).rejects.toThrow( new CloudResponseError( 'Invalid Assistant Cloud response for "anonymous auth token response.refresh_token.expires_at": expected a non-empty string', ), ); expect(setItem).not.toHaveBeenCalled(); }); it("contextualizes malformed refresh token responses", async () => { installLocalStorage({ getItem: () => JSON.stringify(refreshToken), setItem: vi.fn(), removeItem: vi.fn(), } as unknown as Storage); vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({ access_token: 123 }), }), ); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).rejects.toThrow( new CloudResponseError( 'Invalid Assistant Cloud response for "refresh auth token response.access_token": expected a string', ), ); }); it("accepts valid refresh responses without a rotated refresh token", async () => { const setItem = vi.fn(); installLocalStorage({ getItem: () => JSON.stringify(refreshToken), setItem, removeItem: vi.fn(), } as unknown as Storage); const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: null, }), }); vi.stubGlobal("fetch", fetchMock); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); expect(fetchMock).toHaveBeenCalledWith( `${baseUrl}/v1/auth/tokens/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken.token }), signal: expect.any(AbortSignal), }, ); expect(setItem).not.toHaveBeenCalled(); }); it.each([429, 500, 503])( "preserves the anonymous identity after transient status %i", async (status) => { const values = new Map([[refreshTokenKey, JSON.stringify(refreshToken)]]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); const fetchMock = vi.fn().mockResolvedValue({ ok: false, status }); vi.stubGlobal("fetch", fetchMock); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).rejects.toThrow( `Assistant Cloud token refresh failed with status ${status}`, ); expect(fetchMock).toHaveBeenCalledTimes(1); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken)); }, ); it.each([401, 403])( "replaces an anonymous identity after refresh is rejected with %i", async (status) => { const replacementRefreshToken = { token: "r2", expires_at: "2099-02-01", }; const values = new Map([[refreshTokenKey, JSON.stringify(refreshToken)]]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); const fetchMock = vi .fn() .mockResolvedValueOnce({ ok: false, status }) .mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({ access_token: accessToken, refresh_token: replacementRefreshToken, }), }); vi.stubGlobal("fetch", fetchMock); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); expect(fetchMock).toHaveBeenNthCalledWith( 2, `${baseUrl}/v1/auth/tokens/anonymous`, { method: "POST", signal: expect.any(AbortSignal) }, ); expect(values.get(refreshTokenKey)).toBe( JSON.stringify(replacementRefreshToken), ); }, ); it("preserves a rejected refresh token until its replacement succeeds", async () => { const values = new Map([[refreshTokenKey, JSON.stringify(refreshToken)]]); installLocalStorage({ getItem: (key) => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); }, removeItem: (key) => { values.delete(key); }, } as Storage); vi.stubGlobal( "fetch", vi .fn() .mockResolvedValueOnce({ ok: false, status: 403 }) .mockResolvedValueOnce({ ok: false, status: 503 }), ); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).resolves.toBe(false); expect(values.get(refreshTokenKey)).toBe(JSON.stringify(refreshToken)); }); it("contextualizes invalid JSON token responses", async () => { delete (globalThis as { localStorage?: Storage }).localStorage; vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockRejectedValue(new SyntaxError("Unexpected token")), }), ); const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl); await expect(strategy.getAuthHeaders()).rejects.toThrow( new CloudResponseError( 'Invalid Assistant Cloud response for "anonymous auth token response": expected valid JSON', ), ); }); }); describe("AssistantCloudJWTAuthStrategy", () => { it("discards an in-flight provider token after invalidation", async () => { let resolveUserAToken: (token: string) => void = () => {}; const userAToken = new Promise((resolve) => { resolveUserAToken = resolve; }); const authToken = vi .fn<() => Promise>() .mockImplementationOnce(() => userAToken) .mockResolvedValueOnce(null); const strategy = new AssistantCloudJWTAuthStrategy(authToken); const userARequest = strategy.getAuthHeaders(); strategy.invalidate(); await expect(strategy.getAuthHeaders()).resolves.toBe(false); resolveUserAToken(createAccessToken("user-a")); await expect(userARequest).resolves.toBe(false); expect(authToken).toHaveBeenCalledTimes(2); }); it("shares an in-flight provider token request", async () => { let resolveToken: (token: string) => void = () => {}; const token = new Promise((resolve) => { resolveToken = resolve; }); const authToken = vi.fn(() => token); const strategy = new AssistantCloudJWTAuthStrategy(authToken); const first = strategy.getAuthHeaders(); const second = strategy.getAuthHeaders(); resolveToken(accessToken); await expect(first).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); await expect(second).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); expect(authToken).toHaveBeenCalledOnce(); }); it("retries token acquisition after a failed request", async () => { const authToken = vi .fn<() => Promise>() .mockRejectedValueOnce(new Error("authentication unavailable")) .mockResolvedValueOnce(accessToken); const strategy = new AssistantCloudJWTAuthStrategy(authToken); await expect(strategy.getAuthHeaders()).rejects.toThrow( "authentication unavailable", ); await expect(strategy.getAuthHeaders()).resolves.toEqual({ Authorization: `Bearer ${accessToken}`, }); expect(authToken).toHaveBeenCalledTimes(2); }); });