import assert from "node:assert/strict" import { readFileSync } from "node:fs" import test from "node:test" import vm from "node:vm" import ts from "typescript" const storage = () => { const values = new Map() return { getItem: (key) => values.get(key) ?? null, setItem: (key, value) => values.set(key, value), removeItem: (key) => values.delete(key) } } const deferred = () => { let resolve, reject const promise = new Promise((yes, no) => { resolve = yes; reject = no }) return { promise, resolve, reject } } const token = (identityKey, channelCode = "support", mode = "guest", extra = {}) => `test.${Buffer.from(JSON.stringify({ identityKey, channelCode, identityScope: `v1:${mode}:test-channel-scope`, ...extra })).toString("base64url")}.not-a-real-signature` const exchange = (identityKey = "guest:server-assigned", channelCode = "support", mode = "guest") => ({ identityKey, customerSessionToken: token(identityKey, channelCode, mode), expiresAt: "2099-01-01T00:00:00Z", customer: { id: 1, name: "Visitor" }, }) function harness(request) { let config = { baseUrl: "https://support.example", channelId: "support", externalId: "client-guest" } const localStorage = storage(), sessionStorage = storage() const source = readFileSync(new URL("./im.ts", import.meta.url), "utf8") const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }) const module = { exports: {} } const mocks = { "@/lib/api/client": { request }, "@/i18n/messages": { translateCurrentMessage: (key) => key }, "@/lib/sdk/runtime-config": { readSupportChatRuntimeConfig: () => config }, "@/lib/utils": { generateUUID: () => "browser-local-guest", parseApiTimestamp: Date.parse }, } vm.runInNewContext(compiled.outputText, { module, exports: module.exports, require: (name) => { if (!mocks[name]) throw new Error(`Unexpected import ${name}`); return mocks[name] }, process: { env: {} }, window: { localStorage, sessionStorage }, atob, TextDecoder, URLSearchParams }) return { api: module.exports, sessionStorage, setConfig: (next) => { config = { ...config, ...next } } } } test("concurrent session ensures share one guest exchange and cache server-owned identity", async () => { const pending = deferred() let calls = 0 const h = harness(() => { calls++; return pending.promise }) const first = h.api.ensureCustomerSession(), second = h.api.ensureCustomerSession() assert.equal(calls, 1) pending.resolve(exchange()) const [left, right] = await Promise.all([first, second]) assert.equal(left.identityKey, "guest:server-assigned") assert.equal(right.identityKey, left.identityKey) assert.equal(left.guestContext, "client-guest") await h.api.ensureCustomerSession() assert.equal(calls, 1) }) test("guest token cannot be reused under a different initiating visitor context", async () => { let calls = 0 const h = harness(async () => exchange(`guest:server-${++calls}`)) await h.api.ensureCustomerSession() h.setConfig({ externalId: "someone-else" }) assert.equal(h.api.getCustomerSessionToken(), "") const next = await h.api.ensureCustomerSession() assert.equal(calls, 2) assert.equal(next.identityKey, "guest:server-2") }) test("legacy unscoped tokens are re-exchanged instead of sent to protected endpoints", async () => { let calls = 0 const h = harness(async () => { calls++; return exchange() }) h.sessionStorage.setItem("cs_ai_agent_customer_session", JSON.stringify({ ...exchange("guest:client-guest"), customerSessionToken: token("guest:client-guest", "support", "guest", { identityScope: undefined }), baseUrl: "https://support.example", channelId: "support", guestContext: "client-guest" })) assert.equal(h.api.getCustomerSessionToken(), "") await h.api.ensureCustomerSession() assert.equal(calls, 1) }) test("late session exchange from old identity cannot overwrite new session", async () => { const old = deferred() let calls = 0 const h = harness(() => ++calls === 1 ? old.promise : Promise.resolve(exchange("guest:new-server"))) const first = h.api.ensureCustomerSession() h.setConfig({ externalId: "new-visitor" }) const next = await h.api.ensureCustomerSession() old.resolve(exchange("guest:old-server")) await assert.rejects(first, /supportAuthenticationRequired/) assert.equal(h.api.readCustomerSession().identityKey, next.identityKey) }) test("A to B to A entry changes invalidate old same-value exchange", async () => { const old = deferred() let calls = 0 const h = harness(() => ++calls === 1 ? old.promise : Promise.resolve(exchange("guest:new-A"))) const first = h.api.ensureCustomerSession() h.setConfig({ externalId: "B" }) h.api.getCustomerIdentityScope() h.setConfig({ externalId: "client-guest" }) await h.api.ensureCustomerSession() old.resolve(exchange("guest:old-A")) await assert.rejects(first, /supportAuthenticationRequired/) assert.equal(h.api.readCustomerSession().identityKey, "guest:new-A") }) test("customer token rotation preserves scope, but replacement guest identity changes it", async () => { let calls = 0 const h = harness(async () => exchange(`guest:server-${++calls}`)) await h.api.ensureCustomerSession() const scope = h.api.getCustomerIdentityScope() h.api.applyCustomerSessionRefresh({ customerSessionToken: token("guest:server-1", "support", "guest", { revision: 2 }), expiresAt: "2099-02-01T00:00:00Z" }) assert.equal(h.api.getCustomerIdentityScope(), scope) await h.api.exchangeCustomerSession() assert.notEqual(h.api.getCustomerIdentityScope(), scope) }) test("refresh with another auth mode or channel cannot poison cached session", async () => { const h = harness(async () => exchange()) await h.api.ensureCustomerSession() const original = h.api.getCustomerSessionToken() h.api.applyCustomerSessionRefresh({ customerSessionToken: token("guest:server-assigned", "support", "jwt"), expiresAt: "2099-02-01T00:00:00Z" }) assert.equal(h.api.getCustomerSessionToken(), original) h.api.applyCustomerSessionRefresh({ customerSessionToken: token("guest:server-assigned", "other"), expiresAt: "2099-02-01T00:00:00Z" }) assert.equal(h.api.getCustomerSessionToken(), original) }) test("signed identity still requires exact user ID and correct auth mode", async () => { const h = harness(async () => exchange("user:wrong", "support", "hmac")) h.setConfig({ externalId: "expected", externalIdSignature: "test-not-a-real-signature", externalIdSignedAt: 1 }) await assert.rejects(h.api.ensureCustomerSession(), /supportAuthenticationRequired/) assert.equal(h.api.getCustomerSessionToken(), "") }) test("matching renewal arriving during old token expiry window keeps guest identity", async () => { const h = harness(async () => exchange()) await h.api.ensureCustomerSession() const cached = h.api.readCustomerSession() h.sessionStorage.setItem("cs_ai_agent_customer_session", JSON.stringify({ ...cached, expiresAt: "2000-01-01T00:00:00Z" })) const renewed = token(cached.identityKey, "support", "guest", { revision: 2 }) h.api.applyCustomerSessionRefresh({ customerSessionToken: renewed, expiresAt: "2099-02-01T00:00:00Z" }) assert.equal(h.api.getCustomerSessionToken(), renewed) }) test("near-expiry guest renews its existing identity instead of exchanging a fresh guest", async () => { const routes = [] const h = harness(async (route, options) => { routes.push(route) if (route.endsWith("session_refresh")) { assert.equal(options.method, "POST") assert.ok(options.headers.Authorization.startsWith("Bearer ")) } return exchange("guest:original") }) await h.api.ensureCustomerSession() const cached = h.api.readCustomerSession() h.sessionStorage.setItem("cs_ai_agent_customer_session", JSON.stringify({ ...cached, expiresAt: new Date(Date.now() + 4000).toISOString() })) const resumed = await h.api.ensureCustomerSession() assert.equal(resumed.identityKey, cached.identityKey) assert.deepEqual(routes, ["/api/customer/session_exchange", "/api/customer/session_refresh"]) }) test("failed near-expiry refresh never falls back to a different anonymous guest", async () => { let calls = 0 const h = harness(async (route) => { calls++ if (route.endsWith("session_refresh")) throw new Error("offline") return exchange("guest:original") }) await h.api.ensureCustomerSession() const cached = h.api.readCustomerSession() h.sessionStorage.setItem("cs_ai_agent_customer_session", JSON.stringify({ ...cached, expiresAt: new Date(Date.now() + 4000).toISOString() })) await assert.rejects(h.api.ensureCustomerSession(), /offline/) assert.equal(calls, 2) assert.equal(h.api.readCustomerSession().identityKey, "guest:original") })