Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
174 lines
8.6 KiB
JavaScript
174 lines
8.6 KiB
JavaScript
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 moduleContext = { 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: moduleContext, exports: moduleContext.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: moduleContext.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")
|
|
})
|