@relume_io/partner-node 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/check.ts","../src/bin.ts"],"sourcesContent":["// The package's command line: `npx @relume_io/partner-node check …`.\n// Kept apart from bin.ts so tests can run it without spawning a process.\n\nimport { parseArgs } from 'node:util';\n\nimport {\n CHECK_CHARGE_CREDITS,\n checkEndpoint,\n type CheckResult,\n} from './check.ts';\n\nexport const USAGE = `Usage: RELUME_SECRET_KEY=sk_test_… npx @relume_io/partner-node check --url <endpoint> --user <userId>\n\nChecks your partner endpoint against the Relume partner protocol: signatures,\nuser.get, credits.get, and the idempotency and decline rules for charges and\nreversals. It also checks that another site can't read your session route.\n\n --url The endpoint Relume calls, e.g. https://partner.example/relume/calls\n --user A user your wallet knows, with at least ${CHECK_CHARGE_CREDITS} credits. The check\n charges that user and gives every credit back.\n --session-url The session route your page's embed calls. Defaults to\n /session next to a --url ending in /calls.\n --secret-key Your test secret key. Prefer $RELUME_SECRET_KEY, the default:\n a key on the command line lands in your shell history, CI\n logs and the process list.\n --live Allow an sk_live_ key. The check then charges a real wallet.\n -h, --help Show this help.\n\nExits 0 when every check passes, 1 when any fails or can't run, and 2 for\nbad arguments.`;\n\nexport interface CliIo {\n log: (line: string) => void;\n error: (line: string) => void;\n env: Record<string, string | undefined>;\n fetch?: typeof fetch;\n}\n\nconst LABEL: Record<CheckResult['status'], string> = {\n pass: 'PASS',\n fail: 'FAIL',\n skip: 'SKIP',\n};\n\n/** Runs the command line and returns the exit code. */\nexport async function runCli(args: string[], io: CliIo): Promise<number> {\n const usageError = (message: string) => {\n io.error(`${message}\\n\\n${USAGE}`);\n return 2;\n };\n\n let parsed;\n try {\n parsed = parseArgs({\n args,\n allowPositionals: true,\n options: {\n url: { type: 'string' },\n user: { type: 'string' },\n 'session-url': { type: 'string' },\n 'secret-key': { type: 'string' },\n live: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n },\n });\n } catch (error) {\n return usageError(error instanceof Error ? error.message : String(error));\n }\n const { values, positionals } = parsed;\n if (values.help) {\n io.log(USAGE);\n return 0;\n }\n if (positionals.length !== 1 || positionals[0] !== 'check') {\n return usageError(\n positionals.length === 0\n ? 'Missing the command: check.'\n : `Unknown command: ${positionals.join(' ')}.`,\n );\n }\n\n const url = values.url?.trim() ?? '';\n let endpoint: URL;\n try {\n endpoint = new URL(url);\n } catch {\n return usageError(\n url === '' ? '--url is required.' : `--url isn't a URL: ${url}`,\n );\n }\n if (endpoint.protocol !== 'https:' && endpoint.protocol !== 'http:') {\n return usageError('--url must be an http or https URL.');\n }\n\n let sessionUrl: string | undefined;\n if (values['session-url'] !== undefined) {\n const value = values['session-url'].trim();\n let parsed: URL | null = null;\n try {\n parsed = new URL(value);\n } catch {\n // Refused below.\n }\n if (parsed?.protocol !== 'https:' && parsed?.protocol !== 'http:') {\n return usageError(`--session-url must be an http or https URL: ${value}`);\n }\n sessionUrl = parsed.href;\n }\n\n const userId = values.user ?? '';\n if (userId.length < 1 || userId.length > 128) {\n return usageError('--user is required: a user id of 1 to 128 characters.');\n }\n\n const secretKey = (\n values['secret-key'] ??\n io.env.RELUME_SECRET_KEY ??\n ''\n ).trim();\n if (secretKey === '') {\n return usageError('Pass --secret-key or set RELUME_SECRET_KEY.');\n }\n if (secretKey.startsWith('pk_')) {\n return usageError(\n 'That is a publishable key. Pass the secret key (sk_test_…).',\n );\n }\n if (secretKey.startsWith('sk_live_') && !values.live) {\n return usageError(\n 'That is a live key, and the check charges the user. Use your test key, or pass --live to run it against a live wallet.',\n );\n }\n\n io.log(`Checking ${endpoint.href} as user \"${userId}\"\\n`);\n const report = await checkEndpoint({\n url: endpoint.href,\n secretKey,\n userId,\n sessionUrl,\n fetch: io.fetch,\n onResult: (result) => {\n io.log(`${LABEL[result.status]} ${result.name}`);\n if (result.detail) io.log(` ${result.detail}`);\n },\n });\n\n const count = (status: CheckResult['status']) =>\n report.results.filter((result) => result.status === status).length;\n const skipped = count('skip');\n io.log(\n `\\n${count('pass')} passed, ${count('fail')} failed${skipped ? `, ${skipped} skipped` : ''}`,\n );\n for (const note of report.notes) io.log(`NOTE ${note}`);\n return report.ok ? 0 : 1;\n}\n","// The conformance checker behind `npx @relume_io/partner-node check`.\n// It calls a partner endpoint the way Relume does (docs/partner-protocol/v1.md\n// sections 5 and 6) and reports which rules of the contract it keeps, most of\n// all the two that protect real users: a repeated chargeId debits once, and a\n// reversal of a charge that was never debited changes nothing.\n//\n// It needs the endpoint URL, the secret key and a user whose wallet holds at\n// least CHECK_CHARGE_CREDITS. It charges that user and gives every credit\n// back, so run it against a test user on your test endpoint.\n//\n// It also asks the session route (section 4) for a session the way another\n// site's page would, fails a CORS setup that would let that page read it,\n// and checks the route refuses a request from another site or without the\n// embed's header.\n\nimport {\n absent,\n isEmail,\n isHttpsUrl,\n isRecord,\n isString,\n isWhole,\n} from './guards.ts';\nimport { EMBED_HEADER, EMBED_HEADER_VALUE } from './session.ts';\nimport { REQUEST_ID_HEADER, SIGNATURE_HEADER, signCall } from './signing.ts';\n\nexport interface CheckOptions {\n /** The endpoint Relume calls, e.g. `https://partner.example/relume/calls`. */\n url: string;\n secretKey: string;\n /** A user your wallet knows, with at least CHECK_CHARGE_CREDITS. */\n userId: string;\n /**\n * The session route the embed calls. Defaults to `session` next to a\n * `url` ending in `/calls`, where `@relume_io/partner-node` mounts it.\n */\n sessionUrl?: string;\n fetch?: typeof fetch;\n /** The clock, in milliseconds. For tests. */\n now?: () => number;\n /** Called as each check finishes, so the CLI can print as it goes. */\n onResult?: (result: CheckResult) => void;\n}\n\nexport type CheckStatus = 'pass' | 'fail' | 'skip';\n\nexport interface CheckResult {\n name: string;\n status: CheckStatus;\n /** Why it failed or didn't run. */\n detail?: string;\n}\n\nexport interface CheckReport {\n /** True when every check ran and passed. A skipped check is not a pass. */\n ok: boolean;\n results: CheckResult[];\n /** What the run left behind, e.g. a balance its reversals didn't restore. */\n notes: string[];\n}\n\n/**\n * What the checker charges. Two, so a reversal of one credit leaves one to\n * show whether a repeated reversalId credits again.\n */\nexport const CHECK_CHARGE_CREDITS = 2;\n\n// Relume's own timeouts (protocol section 5): an endpoint slower than these\n// fails for real users too.\nconst READ_TIMEOUT_MS = 4_000;\nconst CHARGE_TIMEOUT_MS = 4_000;\nconst REVERSE_TIMEOUT_MS = 10_000;\n\n/** Further than the 300 seconds a verifier allows, either way. */\nconst SKEW_SECONDS = 600;\n\n/** The page the session route is asked from. */\nexport const FOREIGN_ORIGIN = 'https://evil.example';\n\nconst SESSION_CORS = \"doesn't let another site read the session route\";\nconst SESSION_HEADER =\n 'refuses a session request without the Relume-Embed header';\nconst SESSION_CROSS_SITE = 'refuses a session request from another site';\nconst SESSION_CHECKS = [SESSION_CORS, SESSION_HEADER, SESSION_CROSS_SITE];\n\nconst ACCEPT = 'accepts a correctly signed call';\nconst ROTATION = 'accepts a key rotation header, with your key second';\nconst OTHER_KEY = 'rejects a signature made with another key';\nconst TAMPERED = 'rejects a body changed after signing';\nconst STALE = 'rejects a timestamp more than 5 minutes old';\nconst FUTURE = 'rejects a timestamp more than 5 minutes ahead';\nconst UNSIGNED = 'rejects a call with no signature';\nconst USER_GET = 'answers user.get with a valid profile';\nconst UNKNOWN_USER = \"answers user.get 404 for a user it doesn't know\";\nconst CREDITS_GET = 'answers credits.get with a valid balance';\nconst DEBIT = 'debits a charge';\nconst REPEAT_CHARGE = 'debits only once when the same charge is sent twice';\nconst DECLINE = \"answers 402 to a charge the user can't afford\";\nconst UNKNOWN_REVERSAL = 'changes nothing when an unknown charge is reversed';\nconst LATE_CHARGE = 'refuses a charge that arrives after its reversal';\nconst REVERSAL = 'credits back a reversal';\nconst REPEAT_REVERSAL = 'credits a repeated reversalId only once';\nconst WALLET = [\n DEBIT,\n REPEAT_CHARGE,\n DECLINE,\n UNKNOWN_REVERSAL,\n LATE_CHARGE,\n REVERSAL,\n REPEAT_REVERSAL,\n];\n\nconst MALFORMED = Symbol('malformed');\n\ntype Body = Record<string, unknown> | typeof MALFORMED;\n\ninterface Responded {\n answered: true;\n status: number;\n /** A 2xx. */\n ok: boolean;\n body: Body;\n}\n\ntype Answer = Responded | { answered: false; reason: string };\n\ntype Ok = Responded & { ok: true };\n\nfunction isOk(answer: Answer): answer is Ok {\n return answer.answered && answer.ok;\n}\n\n/** An empty body counts as `{}`; anything but a JSON object is malformed. */\nfunction parseBody(text: string): Body {\n if (text.trim() === '') return {};\n try {\n const value: unknown = JSON.parse(text);\n return isRecord(value) ? value : MALFORMED;\n } catch {\n return MALFORMED;\n }\n}\n\n/** `answered 500 (internal_error)`, `timed out after 4s`, … */\nfunction said(answer: Answer): string {\n if (!answer.answered) return answer.reason;\n const error =\n answer.body !== MALFORMED && isRecord(answer.body.error)\n ? answer.body.error.code\n : undefined;\n return typeof error === 'string'\n ? `answered ${answer.status} (${error})`\n : `answered ${answer.status}`;\n}\n\n/** The problem with a user.get body, or null when Relume would store it. */\nfunction profileProblem(body: Body): string | null {\n if (body === MALFORMED) return \"the body isn't a JSON object\";\n const { name, email, avatarUrl } = body;\n if (!absent(name) && !isString(name, 1, 200)) {\n return 'name must be a string of at most 200 characters';\n }\n if (!absent(email) && !isEmail(email)) {\n return 'email must be a valid email address';\n }\n if (!absent(avatarUrl) && !isHttpsUrl(avatarUrl)) {\n return 'avatarUrl must be an https URL';\n }\n return null;\n}\n\n/**\n * The problem with a 2xx charge or reversal body, or null. `balance` is\n * optional, but null isn't \"left out\": Relume reads it as a malformed\n * answer, which turns a charge into an unknown one.\n */\nfunction optionalBalanceProblem(body: Body): string | null {\n if (body === MALFORMED) return \"the body isn't a JSON object\";\n if (body.balance === undefined || isWhole(body.balance, 0)) return null;\n return `balance was ${JSON.stringify(body.balance)}; leave it out or send a whole number, zero or more`;\n}\n\nfunction bodyBalance(body: Body): number | undefined {\n return body !== MALFORMED && isWhole(body.balance, 0)\n ? body.balance\n : undefined;\n}\n\n/** The session route beside a calls route the SDK mounted, or null. */\nexport function sessionUrlFor(callsUrl: string): string | null {\n const url = new URL(callsUrl);\n if (!/\\/calls\\/?$/.test(url.pathname)) return null;\n url.pathname = url.pathname.replace(/calls\\/?$/, 'session');\n url.search = '';\n return url.href;\n}\n\nfunction randomId(prefix: string): string {\n return `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`;\n}\n\nfunction networkReason(error: unknown): string {\n const cause = error instanceof Error ? error.cause : undefined;\n const code = isRecord(cause) ? cause.code : undefined;\n return typeof code === 'string'\n ? `couldn't connect (${code})`\n : `couldn't connect (${error instanceof Error ? error.message : String(error)})`;\n}\n\nexport async function checkEndpoint(\n options: CheckOptions,\n): Promise<CheckReport> {\n const { url, secretKey, userId } = options;\n const fetchImpl = options.fetch ?? ((...args) => fetch(...args));\n const now = options.now ?? Date.now;\n const nowSeconds = () => Math.floor(now() / 1000);\n const results: CheckResult[] = [];\n const notes: string[] = [];\n\n function record(result: CheckResult): void {\n results.push(result);\n options.onResult?.(result);\n }\n\n /** Runs one check: it returns why it failed, or null for a pass. */\n async function run(\n name: string,\n check: () => Promise<string | null>,\n ): Promise<boolean> {\n let failure: string | null;\n try {\n failure = await check();\n } catch (error) {\n failure = `the checker failed: ${error instanceof Error ? error.message : String(error)}`;\n }\n record(\n failure === null\n ? { name, status: 'pass' }\n : { name, status: 'fail', detail: failure },\n );\n return failure === null;\n }\n\n function skip(names: readonly string[], detail: string): void {\n for (const name of names) record({ name, status: 'skip', detail });\n }\n\n async function post(\n rawBody: string,\n header: string | null,\n timeoutMs: number,\n ): Promise<Answer> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n [REQUEST_ID_HEADER]: randomId('check'),\n };\n if (header !== null) headers[SIGNATURE_HEADER] = header;\n try {\n // Relume never follows redirects, so neither does the checker.\n const response = await fetchImpl(url, {\n method: 'POST',\n headers,\n body: rawBody,\n redirect: 'manual',\n signal: AbortSignal.timeout(timeoutMs),\n });\n const text = await response.text();\n if (response.status >= 300 && response.status < 400) {\n return {\n answered: false,\n reason: `redirected with ${response.status} (Relume never follows redirects)`,\n };\n }\n return {\n answered: true,\n status: response.status,\n ok: response.status >= 200 && response.status < 300,\n body: parseBody(text),\n };\n } catch (error) {\n return {\n answered: false,\n reason:\n error instanceof Error && error.name === 'TimeoutError'\n ? `timed out after ${timeoutMs / 1000}s`\n : networkReason(error),\n };\n }\n }\n\n async function signed(\n body: Record<string, unknown>,\n timeoutMs: number,\n ): Promise<Answer> {\n const rawBody = JSON.stringify(body);\n return post(\n rawBody,\n await signCall(secretKey, nowSeconds(), rawBody),\n timeoutMs,\n );\n }\n\n /** The balance from credits.get, or why it couldn't be read. */\n async function readBalance(): Promise<\n { balance: number } | { failure: string }\n > {\n const answer = await signed(\n { type: 'credits.get', userId },\n READ_TIMEOUT_MS,\n );\n if (!isOk(answer)) {\n return { failure: `credits.get ${said(answer)}; expected 200` };\n }\n if (answer.body === MALFORMED) {\n return {\n failure: `credits.get answered ${answer.status} with a body that isn't a JSON object`,\n };\n }\n if (!isWhole(answer.body.balance, 0)) {\n return {\n failure: `credits.get answered balance ${JSON.stringify(answer.body.balance)}; expected a whole number, zero or more`,\n };\n }\n return { balance: answer.body.balance };\n }\n\n /** One request to the session route, and the origin its CORS grants. */\n async function askSession(\n sessionUrl: string,\n init: RequestInit,\n ): Promise<{ answer: Answer; allowOrigin: string | null }> {\n try {\n const response = await fetchImpl(sessionUrl, {\n ...init,\n redirect: 'manual',\n signal: AbortSignal.timeout(READ_TIMEOUT_MS),\n });\n const text = await response.text();\n return {\n answer: {\n answered: true,\n status: response.status,\n ok: response.status >= 200 && response.status < 300,\n body: parseBody(text),\n },\n allowOrigin:\n response.headers.get('access-control-allow-origin')?.trim() ?? null,\n };\n } catch (error) {\n const reason =\n error instanceof Error && error.name === 'TimeoutError'\n ? `timed out after ${READ_TIMEOUT_MS / 1000}s`\n : networkReason(error);\n return { answer: { answered: false, reason }, allowOrigin: null };\n }\n }\n\n // Whether the session route answered at all, for the checks after the\n // first. A holder object, as for `found` below.\n const session = { answered: false };\n\n /**\n * The problem with the session route's CORS, or null. It asks the way a\n * page on another site would: the preflight for the embed's header, then\n * the GET. A route that grants that page's origin lets it read what the\n * route answers for a signed-in user, a client secret included.\n */\n async function sessionCorsProblem(\n sessionUrl: string,\n ): Promise<string | null> {\n const probes: [what: string, init: RequestInit][] = [\n [\n 'the preflight',\n {\n method: 'OPTIONS',\n headers: {\n Origin: FOREIGN_ORIGIN,\n 'Access-Control-Request-Method': 'GET',\n 'Access-Control-Request-Headers': EMBED_HEADER.toLowerCase(),\n },\n },\n ],\n [\n 'GET',\n {\n method: 'GET',\n headers: {\n Origin: FOREIGN_ORIGIN,\n [EMBED_HEADER]: EMBED_HEADER_VALUE,\n 'Sec-Fetch-Site': 'cross-site',\n 'Sec-Fetch-Mode': 'cors',\n },\n },\n ],\n ];\n // Both probes run even after a failure, so the GET says whether the\n // route is there for the checks after this one.\n let problem: string | null = null;\n for (const [what, init] of probes) {\n const { answer, allowOrigin } = await askSession(sessionUrl, init);\n if (!answer.answered) return `${what} to ${sessionUrl} ${answer.reason}`;\n if (allowOrigin === FOREIGN_ORIGIN) {\n problem ??= `${what} to ${sessionUrl} from ${FOREIGN_ORIGIN} answered Access-Control-Allow-Origin: ${FOREIGN_ORIGIN}, so a page on any site can read what this route answers for a signed-in user. Allow only your own page origins, never the request's Origin`;\n }\n if (init.method === 'GET') {\n session.answered = true;\n }\n }\n return problem;\n }\n\n /** Whether the session route is somewhere else: a 404 to the embed's request. */\n async function sessionRouteMissing(sessionUrl: string): Promise<boolean> {\n const { answer } = await askSession(sessionUrl, {\n method: 'GET',\n headers: { [EMBED_HEADER]: EMBED_HEADER_VALUE },\n });\n return answer.answered && answer.status === 404;\n }\n\n /** A session request the route must refuse with `status` and `code`. */\n async function sessionRefusalProblem(\n sessionUrl: string,\n headers: Record<string, string>,\n status: number,\n code: string,\n ): Promise<string | null> {\n const { answer } = await askSession(sessionUrl, {\n method: 'GET',\n headers,\n });\n if (answer.answered && answer.status === status) return null;\n if (answer.answered && answer.status === 401) {\n return `${said(answer)}: it looked for a signed-in user first. Answer ${status} ${code} before reading the user`;\n }\n return `${said(answer)}; expected ${status} ${code}`;\n }\n\n // The session route. It needs no key or user, so it runs whatever the\n // calls below find.\n const sessionUrl = options.sessionUrl ?? sessionUrlFor(url);\n const notFound = sessionUrl && (await sessionRouteMissing(sessionUrl));\n if (sessionUrl === null || notFound) {\n // Looked in the wrong place, which says nothing about the route.\n skip(\n SESSION_CHECKS,\n sessionUrl === null\n ? \"--url doesn't end in /calls; pass --session-url with the session route your page's embed calls\"\n : `GET ${sessionUrl} answered 404; pass --session-url with the session route your page's embed calls`,\n );\n } else {\n await run(SESSION_CORS, () => sessionCorsProblem(sessionUrl));\n if (!session.answered) {\n skip(SESSION_CHECKS.slice(1), 'needs the session route to answer');\n } else {\n await run(SESSION_HEADER, () =>\n sessionRefusalProblem(sessionUrl, {}, 400, 'embed_header_missing'),\n );\n await run(SESSION_CROSS_SITE, () =>\n sessionRefusalProblem(\n sessionUrl,\n {\n [EMBED_HEADER]: EMBED_HEADER_VALUE,\n 'Sec-Fetch-Site': 'cross-site',\n },\n 403,\n 'cross_site_request',\n ),\n );\n }\n }\n\n // What the checks find out for later ones. A holder object, because\n // TypeScript narrows a plain `let` assigned inside a callback to its\n // initial value.\n const found: {\n wrongKey: boolean;\n balance: number | null;\n charged: Ok | null;\n reversed: boolean;\n } = { wrongKey: false, balance: null, charged: null, reversed: false };\n\n // Signatures. Every probe is a credits.get, which changes nothing, so a\n // forged call that gets through does no harm here.\n const read = JSON.stringify({ type: 'credits.get', userId });\n // An unknown field, which the partner must ignore: only the signature\n // check can tell this body from the one that was signed.\n const tampered = JSON.stringify({\n type: 'credits.get',\n userId,\n note: 'changed after signing',\n });\n const otherKey = randomId('sk_test_relume_check');\n const t = nowSeconds();\n\n await run(ACCEPT, async () => {\n const answer = await post(\n read,\n await signCall(secretKey, t, read),\n READ_TIMEOUT_MS,\n );\n if (isOk(answer)) return null;\n if (answer.answered && answer.status === 401) {\n found.wrongKey = true;\n return `${said(answer)} to a signed credits.get: is --secret-key the key this endpoint verifies with?`;\n }\n return `${said(answer)} to a signed credits.get; expected 200`;\n });\n // Every other check would fail the same way, or pass for the wrong reason.\n if (found.wrongKey) {\n skip(\n [\n ROTATION,\n OTHER_KEY,\n TAMPERED,\n STALE,\n FUTURE,\n UNSIGNED,\n USER_GET,\n UNKNOWN_USER,\n CREDITS_GET,\n ...WALLET,\n ],\n 'needs the endpoint to accept a correctly signed call',\n );\n return { ok: false, results, notes };\n }\n\n await run(ROTATION, async () => {\n const answer = await post(\n read,\n await signCall([otherKey, secretKey], t, read),\n READ_TIMEOUT_MS,\n );\n return isOk(answer)\n ? null\n : `${said(answer)}; accept a call when any v1= value matches your key`;\n });\n\n const forgeries: [name: string, rawBody: string, header: string | null][] = [\n [OTHER_KEY, read, await signCall(otherKey, t, read)],\n [TAMPERED, tampered, await signCall(secretKey, t, read)],\n [STALE, read, await signCall(secretKey, t - SKEW_SECONDS, read)],\n [FUTURE, read, await signCall(secretKey, t + SKEW_SECONDS, read)],\n [UNSIGNED, read, null],\n ];\n for (const [name, rawBody, header] of forgeries) {\n await run(name, async () => {\n const answer = await post(rawBody, header, READ_TIMEOUT_MS);\n if (answer.answered && answer.status === 401) return null;\n if (isOk(answer)) {\n return `${said(answer)}: it acted on a call Relume didn't sign; answer 401 invalid_signature`;\n }\n return `${said(answer)}; expected 401 invalid_signature`;\n });\n }\n\n // The reads.\n await run(USER_GET, async () => {\n const answer = await signed({ type: 'user.get', userId }, READ_TIMEOUT_MS);\n if (answer.answered && answer.status === 404) {\n return `answered 404: it doesn't know user \"${userId}\". Pass a --user your wallet knows`;\n }\n if (!isOk(answer)) {\n return `${said(answer)}; expected 200 { name?, email?, avatarUrl? }`;\n }\n return profileProblem(answer.body);\n });\n // A 404 is how Relume refuses a session for someone without a wallet; a\n // profile for any id would open one.\n await run(UNKNOWN_USER, async () => {\n const answer = await signed(\n { type: 'user.get', userId: randomId('u_check_unknown') },\n READ_TIMEOUT_MS,\n );\n return answer.answered && answer.status === 404\n ? null\n : `${said(answer)} for a user id your wallet has never seen; expected 404`;\n });\n\n await run(CREDITS_GET, async () => {\n const result = await readBalance();\n if ('failure' in result) return result.failure;\n found.balance = result.balance;\n return null;\n });\n\n // The wallet. Each check compares against the balance the one before it\n // read, so one broken rule doesn't fail every check after it.\n const start = found.balance;\n if (start === null) {\n skip(WALLET, 'needs a working credits.get');\n return { ok: false, results, notes };\n }\n if (start < CHECK_CHARGE_CREDITS) {\n skip(\n WALLET,\n `user \"${userId}\" has a balance of ${start}; top it up to at least ${CHECK_CHARGE_CREDITS} (the check gives it back)`,\n );\n return { ok: false, results, notes };\n }\n\n let current = start;\n /** Charges the checker may have debited, and what's left of each to give back. */\n const toReverse = new Map<\n string,\n { partnerCredits: number; relumeCredits: number }\n >();\n\n /** Reads the balance and checks it moved by `delta`, or says how it didn't. */\n async function expectBalance(\n delta: number,\n what: string,\n ): Promise<string | null> {\n const result = await readBalance();\n if ('failure' in result) return result.failure;\n const before = current;\n current = result.balance;\n if (result.balance === before + delta) return null;\n return `${what} moved the balance from ${before} to ${result.balance}; expected ${before + delta}`;\n }\n\n /** Reverses a charge the checker made, the way Relume reverses one it can't finish. */\n async function giveBack(id: string): Promise<void> {\n const left = toReverse.get(id);\n if (left === undefined) return;\n toReverse.delete(id);\n const answer = await signed(\n {\n type: 'credits.reverse',\n userId,\n reversalId: randomId('rv_check'),\n ...left,\n chargeId: id,\n reason: 'work_failed',\n },\n REVERSE_TIMEOUT_MS,\n );\n if (!isOk(answer)) notes.push(`Giving back charge ${id} ${said(answer)}.`);\n }\n\n /**\n * Runs a wallet check, then re-reads the balance if it failed: a failure\n * can leave the balance anywhere, and the next check measures from here.\n */\n async function walletCheck(\n name: string,\n check: () => Promise<string | null>,\n ): Promise<void> {\n if (await run(name, check)) return;\n const result = await readBalance();\n if ('balance' in result) current = result.balance;\n }\n\n // The give-back runs whatever happens below, even if printing a result\n // throws (an EPIPE from `check | head`): the user must not be left short.\n try {\n const chargeId = randomId('ch_check');\n const charge = JSON.stringify({\n type: 'credits.charge',\n userId,\n chargeId,\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n action: 'chat.message',\n description: 'Relume conformance check (given back)',\n });\n\n await walletCheck(DEBIT, async () => {\n toReverse.set(chargeId, {\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n });\n const answer = await post(\n charge,\n await signCall(secretKey, nowSeconds(), charge),\n CHARGE_TIMEOUT_MS,\n );\n if (!isOk(answer)) {\n return `${said(answer)} to a charge of ${CHECK_CHARGE_CREDITS} from a balance of ${current}; expected 200`;\n }\n found.charged = answer;\n const problem =\n optionalBalanceProblem(answer.body) ??\n (await expectBalance(-CHECK_CHARGE_CREDITS, 'the charge'));\n if (problem) return problem;\n const reported = bodyBalance(answer.body);\n return reported === undefined || reported === current\n ? null\n : `the charge answered balance ${reported}, but credits.get says ${current}`;\n });\n\n const { charged } = found;\n if (charged === null) {\n skip([REPEAT_CHARGE], 'needs the first charge to succeed');\n } else {\n await walletCheck(REPEAT_CHARGE, async () => {\n // The same body, signed again: what an inline retry sends.\n const answer = await post(\n charge,\n await signCall(secretKey, nowSeconds(), charge),\n CHARGE_TIMEOUT_MS,\n );\n if (!answer.answered || answer.status !== charged.status) {\n return `the repeat ${said(answer)}, the first ${said(charged)}; answer a repeated chargeId with the first result`;\n }\n const moved = await expectBalance(0, 'the repeat');\n if (moved) return `${moved}: it debited the same chargeId twice`;\n const was = bodyBalance(charged.body);\n const again = bodyBalance(answer.body);\n return was === undefined || again === undefined || was === again\n ? null\n : `the repeat answered balance ${again}, the first ${was}; answer a repeated chargeId with the first result`;\n });\n }\n\n await walletCheck(DECLINE, async () => {\n const declinedId = randomId('ch_check');\n const amount = current + 1;\n toReverse.set(declinedId, {\n partnerCredits: amount,\n relumeCredits: CHECK_CHARGE_CREDITS,\n });\n const answer = await signed(\n {\n type: 'credits.charge',\n userId,\n chargeId: declinedId,\n partnerCredits: amount,\n relumeCredits: CHECK_CHARGE_CREDITS,\n action: 'chat.message',\n description: 'Relume conformance check (should be declined)',\n },\n CHARGE_TIMEOUT_MS,\n );\n if (isOk(answer)) {\n // Now, so the overdrawn balance doesn't fail the checks after this one.\n await giveBack(declinedId);\n return `${said(answer)}: it accepted a charge of ${amount} from a balance of ${current}; answer 402`;\n }\n if (!answer.answered || answer.status !== 402) {\n // A 4xx refused the charge, so there's nothing to give back. A 5xx or\n // a timeout may have debited first, so that stays queued.\n if (answer.answered && answer.status < 500)\n toReverse.delete(declinedId);\n return `${said(answer)} to a charge of ${amount} from a balance of ${current}; expected 402`;\n }\n // Relume never sends a reversal for a decline.\n toReverse.delete(declinedId);\n const moved = await expectBalance(0, 'the declined charge');\n if (moved) return moved;\n // A 402 body may be unreadable, but a balance it does give is shown.\n const said402 = bodyBalance(answer.body);\n return said402 === undefined || said402 === current\n ? null\n : `the 402 answered balance ${said402}, but credits.get says ${current}`;\n });\n\n // A charge Relume gave up on, reversed before the wallet saw it.\n const lateId = randomId('ch_check_never_sent');\n let reversedFirst = false;\n await walletCheck(UNKNOWN_REVERSAL, async () => {\n const answer = await signed(\n {\n type: 'credits.reverse',\n userId,\n reversalId: randomId('rv_check'),\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n chargeId: lateId,\n reason: 'charge_unknown',\n },\n REVERSE_TIMEOUT_MS,\n );\n if (!isOk(answer)) {\n return `${said(answer)}; expected 200, crediting nothing (Relume retries any other answer for 72 hours)`;\n }\n reversedFirst = true;\n const problem = optionalBalanceProblem(answer.body);\n if (problem) return problem;\n const moved = await expectBalance(0, 'the reversal');\n return moved && `${moved}: it credited a charge it never debited`;\n });\n\n if (!reversedFirst) {\n skip([LATE_CHARGE], 'needs the reversal of an unknown charge to succeed');\n } else {\n // The same charge, arriving late: an attempt from a wallet that\n // stalled and caught up after Relume gave up and reversed it.\n await walletCheck(LATE_CHARGE, async () => {\n toReverse.set(lateId, {\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n });\n const answer = await signed(\n {\n type: 'credits.charge',\n userId,\n chargeId: lateId,\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n action: 'chat.message',\n description: 'Relume conformance check (should be refused)',\n },\n CHARGE_TIMEOUT_MS,\n );\n const moved = await expectBalance(0, 'the late charge');\n if (moved) {\n return `${moved}: it debited a charge it had already answered a reversal for; record a reversed chargeId as declined`;\n }\n toReverse.delete(lateId);\n // Relume stopped waiting for this charge, so any refusal will do.\n return isOk(answer)\n ? `${said(answer)}; expected 402, debiting nothing`\n : null;\n });\n }\n\n // Half of the first charge, so a repeat that credits again shows.\n const reversal = JSON.stringify({\n type: 'credits.reverse',\n userId,\n reversalId: randomId('rv_check'),\n partnerCredits: 1,\n relumeCredits: 1,\n chargeId,\n reason: 'work_failed',\n });\n if (charged === null) {\n skip([REVERSAL, REPEAT_REVERSAL], 'needs the first charge to succeed');\n } else {\n await walletCheck(REVERSAL, async () => {\n const answer = await post(\n reversal,\n await signCall(secretKey, nowSeconds(), reversal),\n REVERSE_TIMEOUT_MS,\n );\n if (!isOk(answer)) return `${said(answer)}; expected 200`;\n found.reversed = true;\n toReverse.set(chargeId, {\n partnerCredits: CHECK_CHARGE_CREDITS - 1,\n relumeCredits: CHECK_CHARGE_CREDITS - 1,\n });\n return (\n optionalBalanceProblem(answer.body) ??\n (await expectBalance(1, 'a reversal of 1 credit'))\n );\n });\n if (!found.reversed) {\n skip([REPEAT_REVERSAL], 'needs the reversal to succeed');\n } else {\n await walletCheck(REPEAT_REVERSAL, async () => {\n const answer = await post(\n reversal,\n await signCall(secretKey, nowSeconds(), reversal),\n REVERSE_TIMEOUT_MS,\n );\n if (!isOk(answer)) return `the repeat ${said(answer)}; expected 200`;\n const moved = await expectBalance(0, 'the repeat');\n return moved && `${moved}: it credited the same reversalId twice`;\n });\n }\n }\n } finally {\n for (const id of [...toReverse.keys()]) await giveBack(id);\n }\n const end = await readBalance();\n if ('failure' in end) {\n notes.push(`Couldn't read the final balance: ${end.failure}.`);\n } else if (end.balance !== start) {\n notes.push(\n `User \"${userId}\" has a balance of ${end.balance}; it was ${start} before the check. Correct it in your wallet.`,\n );\n }\n\n const ok = results.every((result) => result.status === 'pass');\n return { ok, results, notes };\n}\n","#!/usr/bin/env node\nimport { runCli } from './cli.ts';\n\n// No top-level await: the CommonJS build runs this file too.\nvoid runCli(process.argv.slice(2), {\n log: (line) => console.log(line),\n error: (line) => console.error(line),\n env: process.env,\n}).then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n console.error(\n `The check stopped: ${error instanceof Error ? error.message : String(error)}`,\n );\n process.exitCode = 1;\n },\n);\n"],"mappings":"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","names":["sessionUrl","parsed"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/check.ts","../src/bin.ts"],"sourcesContent":["// The package's command line: `npx @relume_io/partner-node check …`.\n// Kept apart from bin.ts so tests can run it without spawning a process.\n\nimport { parseArgs } from 'node:util';\n\nimport { keyEnvironment } from './calls.ts';\nimport {\n CHECK_CHARGE_CREDITS,\n checkEndpoint,\n type CheckResult,\n} from './check.ts';\n\nexport const USAGE = `Usage: RELUME_SECRET_KEY=sk_test_… npx @relume_io/partner-node check --url <endpoint> --user <userId>\n\nChecks your partner endpoint against the Relume partner protocol: signatures,\nuser.get, credits.get, and the idempotency and decline rules for charges and\nreversals. It also checks that another site can't read your session route.\n\n --url The endpoint Relume calls, e.g. https://partner.example/relume/calls\n --user A user your wallet knows, with at least ${CHECK_CHARGE_CREDITS} credits. The check\n charges that user and gives every credit back.\n --session-url The session route your page's embed calls. Defaults to\n /session next to a --url ending in /calls.\n --session-header\n A header for every request to the session route, as\n \"Name: value\". Repeatable. Pass a test user's cookie when a\n login answers that route before the SDK does. Prefer\n $RELUME_SESSION_HEADERS, one header a line: a cookie is a\n credential too.\n --secret-key Your test secret key. Prefer $RELUME_SECRET_KEY, the default:\n a key on the command line lands in your shell history, CI\n logs and the process list.\n --live Allow an sk_live_ key. The check then charges a real wallet.\n -h, --help Show this help.\n\nExits 0 when every check passes, 1 when any fails or can't run, and 2 for\nbad arguments.`;\n\n/** An HTTP header name (RFC 9110 token). */\nconst HEADER_NAME = /^[!#$%&'*+.^`|~\\w-]+$/;\n/** A value fetch can send (RFC 9110 field-value). */\nconst HEADER_VALUE = /^[\\t\\x20-\\x7e\\x80-\\xff]*$/;\n/** The headers the session checks vary, which --session-header can't fix. */\nconst PROBE_HEADERS =\n /^(relume-embed|origin|sec-fetch-.*|access-control-request-.*)$/i;\n\nexport interface CliIo {\n log: (line: string) => void;\n error: (line: string) => void;\n env: Record<string, string | undefined>;\n fetch?: typeof fetch;\n}\n\nconst LABEL: Record<CheckResult['status'], string> = {\n pass: 'PASS',\n fail: 'FAIL',\n skip: 'SKIP',\n};\n\n/** Runs the command line and returns the exit code. */\nexport async function runCli(args: string[], io: CliIo): Promise<number> {\n const usageError = (message: string) => {\n io.error(`${message}\\n\\n${USAGE}`);\n return 2;\n };\n\n let parsed;\n try {\n parsed = parseArgs({\n args,\n allowPositionals: true,\n options: {\n url: { type: 'string' },\n user: { type: 'string' },\n 'session-url': { type: 'string' },\n 'session-header': { type: 'string', multiple: true },\n 'secret-key': { type: 'string' },\n live: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n },\n });\n } catch (error) {\n return usageError(error instanceof Error ? error.message : String(error));\n }\n const { values, positionals } = parsed;\n if (values.help) {\n io.log(USAGE);\n return 0;\n }\n if (positionals.length !== 1 || positionals[0] !== 'check') {\n return usageError(\n positionals.length === 0\n ? 'Missing the command: check.'\n : `Unknown command: ${positionals.join(' ')}.`,\n );\n }\n\n const url = values.url?.trim() ?? '';\n let endpoint: URL;\n try {\n endpoint = new URL(url);\n } catch {\n return usageError(\n url === '' ? '--url is required.' : `--url isn't a URL: ${url}`,\n );\n }\n if (endpoint.protocol !== 'https:' && endpoint.protocol !== 'http:') {\n return usageError('--url must be an http or https URL.');\n }\n\n let sessionUrl: string | undefined;\n if (values['session-url'] !== undefined) {\n const value = values['session-url'].trim();\n let parsed: URL | null = null;\n try {\n parsed = new URL(value);\n } catch {\n // Refused below.\n }\n if (parsed?.protocol !== 'https:' && parsed?.protocol !== 'http:') {\n return usageError(`--session-url must be an http or https URL: ${value}`);\n }\n sessionUrl = parsed.href;\n }\n\n // Errors name the header but never echo its value, a cookie, say.\n const headerLines =\n values['session-header'] ??\n (io.env.RELUME_SESSION_HEADERS ?? '')\n .split('\\n')\n .filter((line) => line.trim() !== '');\n let sessionHeaders: Record<string, string> | undefined;\n for (const header of headerLines) {\n const colon = header.indexOf(':');\n const name = header.slice(0, colon).trim();\n const value = header.slice(colon + 1).trim();\n if (colon < 0 || !HEADER_NAME.test(name)) {\n return usageError('Each session header must be \"Name: value\".');\n }\n if (!HEADER_VALUE.test(value)) {\n return usageError(\n `The ${name} session header's value has characters a header can't carry.`,\n );\n }\n if (PROBE_HEADERS.test(name)) {\n return usageError(\n `A session header can't set ${name}: the check sets it to test the route.`,\n );\n }\n sessionHeaders = { ...sessionHeaders, [name]: value };\n }\n\n const userId = values.user ?? '';\n if (userId.length < 1 || userId.length > 128) {\n return usageError('--user is required: a user id of 1 to 128 characters.');\n }\n\n const secretKey = (\n values['secret-key'] ??\n io.env.RELUME_SECRET_KEY ??\n ''\n ).trim();\n if (secretKey === '') {\n return usageError('Pass --secret-key or set RELUME_SECRET_KEY.');\n }\n if (secretKey.startsWith('pk_')) {\n return usageError(\n 'That is a publishable key. Pass the secret key (sk_test_…).',\n );\n }\n if (keyEnvironment(secretKey) === null) {\n return usageError(\n 'The secret key must start with sk_test_ (or sk_live_ with --live).',\n );\n }\n if (secretKey.startsWith('sk_live_') && !values.live) {\n return usageError(\n 'That is a live key, and the check charges the user. Use your test key, or pass --live to run it against a live wallet.',\n );\n }\n\n io.log(`Checking ${endpoint.href} as user \"${userId}\"\\n`);\n const report = await checkEndpoint({\n url: endpoint.href,\n secretKey,\n userId,\n sessionUrl,\n sessionHeaders,\n fetch: io.fetch,\n onResult: (result) => {\n io.log(`${LABEL[result.status]} ${result.name}`);\n if (result.detail) io.log(` ${result.detail}`);\n },\n });\n\n const count = (status: CheckResult['status']) =>\n report.results.filter((result) => result.status === status).length;\n const skipped = count('skip');\n io.log(\n `\\n${count('pass')} passed, ${count('fail')} failed${skipped ? `, ${skipped} skipped` : ''}`,\n );\n for (const note of report.notes) io.log(`NOTE ${note}`);\n return report.ok ? 0 : 1;\n}\n","// The conformance checker behind `npx @relume_io/partner-node check`.\n// It calls a partner endpoint the way Relume does (docs/partner-protocol/v1.md\n// sections 5 and 6) and reports which rules of the contract it keeps, most of\n// all the two that protect real users: a repeated chargeId debits once, and a\n// reversal of a charge that was never debited changes nothing. Every call\n// carries the secret key's environment, and one for the other environment\n// must be refused.\n//\n// It needs the endpoint URL, the secret key and a user whose wallet holds at\n// least CHECK_CHARGE_CREDITS. It charges that user and gives every credit\n// back, so run it against a test user on your test endpoint.\n//\n// It also asks the session route (section 4) for a session the way another\n// site's page would, fails a CORS setup that would let that page read it,\n// and checks the route refuses a request from another site or without the\n// embed's header.\n\nimport { keyEnvironment } from './calls.ts';\nimport {\n absent,\n isEmail,\n isHttpsUrl,\n isRecord,\n isString,\n isWhole,\n} from './guards.ts';\nimport { EMBED_HEADER, EMBED_HEADER_VALUE } from './session.ts';\nimport { REQUEST_ID_HEADER, SIGNATURE_HEADER, signCall } from './signing.ts';\n\nexport interface CheckOptions {\n /** The endpoint Relume calls, e.g. `https://partner.example/relume/calls`. */\n url: string;\n secretKey: string;\n /** A user your wallet knows, with at least CHECK_CHARGE_CREDITS. */\n userId: string;\n /**\n * The session route the embed calls. Defaults to `session` next to a\n * `url` ending in `/calls`, where `@relume_io/partner-node` mounts it.\n */\n sessionUrl?: string;\n /**\n * Headers sent on every request to the session route, for a login in front\n * of it: a test user's cookie, say. Without them the refusal checks can only\n * see that login's 401.\n */\n sessionHeaders?: Record<string, string>;\n fetch?: typeof fetch;\n /** The clock, in milliseconds. For tests. */\n now?: () => number;\n /** Called as each check finishes, so the CLI can print as it goes. */\n onResult?: (result: CheckResult) => void;\n}\n\n/** A request the checker makes to the session route. */\ninterface SessionProbe {\n method: 'GET' | 'OPTIONS';\n headers: Record<string, string>;\n}\n\nexport type CheckStatus = 'pass' | 'fail' | 'skip';\n\nexport interface CheckResult {\n name: string;\n status: CheckStatus;\n /** Why it failed or didn't run. */\n detail?: string;\n}\n\nexport interface CheckReport {\n /** True when every check ran and passed. A skipped check is not a pass. */\n ok: boolean;\n results: CheckResult[];\n /** What the run left behind, e.g. a balance its reversals didn't restore. */\n notes: string[];\n}\n\n/**\n * What the checker charges. Two, so a reversal of one credit leaves one to\n * show whether a repeated reversalId credits again.\n */\nexport const CHECK_CHARGE_CREDITS = 2;\n\n// Relume's own timeouts (protocol section 5): an endpoint slower than these\n// fails for real users too.\nconst READ_TIMEOUT_MS = 4_000;\nconst CHARGE_TIMEOUT_MS = 4_000;\nconst REVERSE_TIMEOUT_MS = 10_000;\n\n/** Further than the 300 seconds a verifier allows, either way. */\nconst SKEW_SECONDS = 600;\n\n/** The page the session route is asked from. */\nexport const FOREIGN_ORIGIN = 'https://evil.example';\n\nconst SESSION_CORS = \"doesn't let another site read the session route\";\nconst SESSION_HEADER =\n 'refuses a session request without the Relume-Embed header';\nconst SESSION_CROSS_SITE = 'refuses a session request from another site';\nconst SESSION_CHECKS = [SESSION_CORS, SESSION_HEADER, SESSION_CROSS_SITE];\n\nconst ACCEPT = 'accepts a correctly signed call';\nconst ROTATION = 'accepts a key rotation header, with your key second';\nconst OTHER_KEY = 'rejects a signature made with another key';\nconst TAMPERED = 'rejects a body changed after signing';\nconst STALE = 'rejects a timestamp more than 5 minutes old';\nconst FUTURE = 'rejects a timestamp more than 5 minutes ahead';\nconst UNSIGNED = 'rejects a call with no signature';\nconst OTHER_ENVIRONMENT = 'refuses a call for the other environment';\nconst USER_GET = 'answers user.get with a valid profile';\nconst UNKNOWN_USER = \"answers user.get 404 for a user it doesn't know\";\nconst CREDITS_GET = 'answers credits.get with a valid balance';\nconst DEBIT = 'debits a charge';\nconst REPEAT_CHARGE = 'debits only once when the same charge is sent twice';\nconst DECLINE = \"answers 402 to a charge the user can't afford\";\nconst UNKNOWN_REVERSAL = 'changes nothing when an unknown charge is reversed';\nconst LATE_CHARGE = 'refuses a charge that arrives after its reversal';\nconst REVERSAL = 'credits back a reversal';\nconst REPEAT_REVERSAL = 'credits a repeated reversalId only once';\nconst WALLET = [\n DEBIT,\n REPEAT_CHARGE,\n DECLINE,\n UNKNOWN_REVERSAL,\n LATE_CHARGE,\n REVERSAL,\n REPEAT_REVERSAL,\n];\n\nconst MALFORMED = Symbol('malformed');\n\ntype Body = Record<string, unknown> | typeof MALFORMED;\n\ninterface Responded {\n answered: true;\n status: number;\n /** A 2xx. */\n ok: boolean;\n body: Body;\n}\n\ntype Answer = Responded | { answered: false; reason: string };\n\ntype Ok = Responded & { ok: true };\n\nfunction isOk(answer: Answer): answer is Ok {\n return answer.answered && answer.ok;\n}\n\n/** An empty body counts as `{}`; anything but a JSON object is malformed. */\nfunction parseBody(text: string): Body {\n if (text.trim() === '') return {};\n try {\n const value: unknown = JSON.parse(text);\n return isRecord(value) ? value : MALFORMED;\n } catch {\n return MALFORMED;\n }\n}\n\n/** `answered 500 (internal_error)`, `timed out after 4s`, … */\nfunction said(answer: Answer): string {\n if (!answer.answered) return answer.reason;\n const error =\n answer.body !== MALFORMED && isRecord(answer.body.error)\n ? answer.body.error.code\n : undefined;\n return typeof error === 'string'\n ? `answered ${answer.status} (${error})`\n : `answered ${answer.status}`;\n}\n\n/** The problem with a user.get body, or null when Relume would store it. */\nfunction profileProblem(body: Body): string | null {\n if (body === MALFORMED) return \"the body isn't a JSON object\";\n const { name, email, avatarUrl } = body;\n if (!absent(name) && !isString(name, 1, 200)) {\n return 'name must be a string of at most 200 characters';\n }\n if (!absent(email) && !isEmail(email)) {\n return 'email must be a valid email address';\n }\n if (!absent(avatarUrl) && !isHttpsUrl(avatarUrl)) {\n return 'avatarUrl must be an https URL';\n }\n return null;\n}\n\n/**\n * The problem with a 2xx charge or reversal body, or null. `balance` is\n * optional, but null isn't \"left out\": Relume reads it as a malformed\n * answer, which turns a charge into an unknown one.\n */\nfunction optionalBalanceProblem(body: Body): string | null {\n if (body === MALFORMED) return \"the body isn't a JSON object\";\n if (body.balance === undefined || isWhole(body.balance, 0)) return null;\n return `balance was ${JSON.stringify(body.balance)}; leave it out or send a whole number, zero or more`;\n}\n\nfunction bodyBalance(body: Body): number | undefined {\n return body !== MALFORMED && isWhole(body.balance, 0)\n ? body.balance\n : undefined;\n}\n\n/** The session route beside a calls route the SDK mounted, or null. */\nexport function sessionUrlFor(callsUrl: string): string | null {\n const url = new URL(callsUrl);\n if (!/\\/calls\\/?$/.test(url.pathname)) return null;\n url.pathname = url.pathname.replace(/calls\\/?$/, 'session');\n url.search = '';\n return url.href;\n}\n\nfunction randomId(prefix: string): string {\n return `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`;\n}\n\nfunction networkReason(error: unknown): string {\n const cause = error instanceof Error ? error.cause : undefined;\n const code = isRecord(cause) ? cause.code : undefined;\n return typeof code === 'string'\n ? `couldn't connect (${code})`\n : `couldn't connect (${error instanceof Error ? error.message : String(error)})`;\n}\n\nexport async function checkEndpoint(\n options: CheckOptions,\n): Promise<CheckReport> {\n const { url, secretKey, userId } = options;\n const environment = keyEnvironment(secretKey);\n if (environment === null) {\n throw new TypeError('The secret key must start with sk_test_ or sk_live_.');\n }\n const otherEnvironment = environment === 'test' ? 'live' : 'test';\n const fetchImpl = options.fetch ?? ((...args) => fetch(...args));\n const now = options.now ?? Date.now;\n const nowSeconds = () => Math.floor(now() / 1000);\n const results: CheckResult[] = [];\n const notes: string[] = [];\n\n function record(result: CheckResult): void {\n results.push(result);\n options.onResult?.(result);\n }\n\n /** Runs one check: it returns why it failed, or null for a pass. */\n async function run(\n name: string,\n check: () => Promise<string | null>,\n ): Promise<boolean> {\n let failure: string | null;\n try {\n failure = await check();\n } catch (error) {\n failure = `the checker failed: ${error instanceof Error ? error.message : String(error)}`;\n }\n record(\n failure === null\n ? { name, status: 'pass' }\n : { name, status: 'fail', detail: failure },\n );\n return failure === null;\n }\n\n function skip(names: readonly string[], detail: string): void {\n for (const name of names) record({ name, status: 'skip', detail });\n }\n\n async function post(\n rawBody: string,\n header: string | null,\n timeoutMs: number,\n ): Promise<Answer> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n [REQUEST_ID_HEADER]: randomId('check'),\n };\n if (header !== null) headers[SIGNATURE_HEADER] = header;\n try {\n // Relume never follows redirects, so neither does the checker.\n const response = await fetchImpl(url, {\n method: 'POST',\n headers,\n body: rawBody,\n redirect: 'manual',\n signal: AbortSignal.timeout(timeoutMs),\n });\n const text = await response.text();\n if (response.status >= 300 && response.status < 400) {\n return {\n answered: false,\n reason: `redirected with ${response.status} (Relume never follows redirects)`,\n };\n }\n return {\n answered: true,\n status: response.status,\n ok: response.status >= 200 && response.status < 300,\n body: parseBody(text),\n };\n } catch (error) {\n return {\n answered: false,\n reason:\n error instanceof Error && error.name === 'TimeoutError'\n ? `timed out after ${timeoutMs / 1000}s`\n : networkReason(error),\n };\n }\n }\n\n async function signed(\n body: Record<string, unknown>,\n timeoutMs: number,\n ): Promise<Answer> {\n const rawBody = JSON.stringify(body);\n return post(\n rawBody,\n await signCall(secretKey, nowSeconds(), rawBody),\n timeoutMs,\n );\n }\n\n /** The balance from credits.get, or why it couldn't be read. */\n async function readBalance(): Promise<\n { balance: number } | { failure: string }\n > {\n const answer = await signed(\n { type: 'credits.get', environment, userId },\n READ_TIMEOUT_MS,\n );\n if (!isOk(answer)) {\n return { failure: `credits.get ${said(answer)}; expected 200` };\n }\n if (answer.body === MALFORMED) {\n return {\n failure: `credits.get answered ${answer.status} with a body that isn't a JSON object`,\n };\n }\n if (!isWhole(answer.body.balance, 0)) {\n return {\n failure: `credits.get answered balance ${JSON.stringify(answer.body.balance)}; expected a whole number, zero or more`,\n };\n }\n return { balance: answer.body.balance };\n }\n\n /** One request to the session route, and the origin its CORS grants. */\n async function askSession(\n sessionUrl: string,\n init: SessionProbe,\n ): Promise<{ answer: Answer; allowOrigin: string | null }> {\n try {\n const response = await fetchImpl(sessionUrl, {\n ...init,\n headers: { ...options.sessionHeaders, ...init.headers },\n redirect: 'manual',\n signal: AbortSignal.timeout(READ_TIMEOUT_MS),\n });\n const text = await response.text();\n return {\n answer: {\n answered: true,\n status: response.status,\n ok: response.status >= 200 && response.status < 300,\n body: parseBody(text),\n },\n allowOrigin:\n response.headers.get('access-control-allow-origin')?.trim() ?? null,\n };\n } catch (error) {\n const reason =\n error instanceof Error && error.name === 'TimeoutError'\n ? `timed out after ${READ_TIMEOUT_MS / 1000}s`\n : networkReason(error);\n return { answer: { answered: false, reason }, allowOrigin: null };\n }\n }\n\n // Whether the session route answered at all, for the checks after the\n // first. A holder object, as for `found` below.\n const session = { answered: false };\n\n /**\n * The problem with the session route's CORS, or null. It asks the way a\n * page on another site would: the preflight for the embed's header, then\n * the GET. A route that grants that page's origin lets it read what the\n * route answers for a signed-in user, a client secret included.\n */\n async function sessionCorsProblem(\n sessionUrl: string,\n ): Promise<string | null> {\n const probes: [what: string, init: SessionProbe][] = [\n [\n 'the preflight',\n {\n method: 'OPTIONS',\n headers: {\n Origin: FOREIGN_ORIGIN,\n 'Access-Control-Request-Method': 'GET',\n 'Access-Control-Request-Headers': EMBED_HEADER.toLowerCase(),\n },\n },\n ],\n [\n 'GET',\n {\n method: 'GET',\n headers: {\n Origin: FOREIGN_ORIGIN,\n [EMBED_HEADER]: EMBED_HEADER_VALUE,\n 'Sec-Fetch-Site': 'cross-site',\n 'Sec-Fetch-Mode': 'cors',\n },\n },\n ],\n ];\n // Both probes run even after a failure, so the GET says whether the\n // route is there for the checks after this one.\n let problem: string | null = null;\n for (const [what, init] of probes) {\n const { answer, allowOrigin } = await askSession(sessionUrl, init);\n if (!answer.answered) return `${what} to ${sessionUrl} ${answer.reason}`;\n if (allowOrigin === FOREIGN_ORIGIN) {\n problem ??= `${what} to ${sessionUrl} from ${FOREIGN_ORIGIN} answered Access-Control-Allow-Origin: ${FOREIGN_ORIGIN}, so a page on any site can read what this route answers for a signed-in user. Allow only your own page origins, never the request's Origin`;\n }\n if (init.method === 'GET') {\n session.answered = true;\n }\n }\n return problem;\n }\n\n /** Whether the session route is somewhere else: a 404 to the embed's request. */\n async function sessionRouteMissing(sessionUrl: string): Promise<boolean> {\n const { answer } = await askSession(sessionUrl, {\n method: 'GET',\n headers: { [EMBED_HEADER]: EMBED_HEADER_VALUE },\n });\n return answer.answered && answer.status === 404;\n }\n\n /** A session request the route must refuse with `status` and `code`. */\n async function sessionRefusalProblem(\n sessionUrl: string,\n headers: Record<string, string>,\n status: number,\n code: string,\n ): Promise<string | null> {\n const { answer } = await askSession(sessionUrl, {\n method: 'GET',\n headers,\n });\n if (answer.answered && answer.status === status) return null;\n if (answer.answered && answer.status === 401) {\n // The SDK refuses before it reads the user, so a 401 is a login in\n // front of the route, or a route that reads the user first.\n return options.sessionHeaders\n ? `${said(answer)} with the session headers: they didn't get past the login (an expired cookie?), or the route looked for a signed-in user first. Answer ${status} ${code} before reading the user`\n : `${said(answer)}: something asked for a signed-in user first. If that's a login in front of the route, pass --session-header with a test user's cookie so the check gets past it; otherwise answer ${status} ${code} before reading the user`;\n }\n return `${said(answer)}; expected ${status} ${code}`;\n }\n\n // The session route. It needs no key or user, so it runs whatever the\n // calls below find.\n const sessionUrl = options.sessionUrl ?? sessionUrlFor(url);\n const notFound = sessionUrl && (await sessionRouteMissing(sessionUrl));\n if (sessionUrl === null || notFound) {\n // Looked in the wrong place, which says nothing about the route.\n skip(\n SESSION_CHECKS,\n sessionUrl === null\n ? \"--url doesn't end in /calls; pass --session-url with the session route your page's embed calls\"\n : `GET ${sessionUrl} answered 404; pass --session-url with the session route your page's embed calls`,\n );\n } else {\n await run(SESSION_CORS, () => sessionCorsProblem(sessionUrl));\n if (!session.answered) {\n skip(SESSION_CHECKS.slice(1), 'needs the session route to answer');\n } else {\n await run(SESSION_HEADER, () =>\n sessionRefusalProblem(sessionUrl, {}, 400, 'embed_header_missing'),\n );\n await run(SESSION_CROSS_SITE, () =>\n sessionRefusalProblem(\n sessionUrl,\n {\n [EMBED_HEADER]: EMBED_HEADER_VALUE,\n 'Sec-Fetch-Site': 'cross-site',\n },\n 403,\n 'cross_site_request',\n ),\n );\n }\n }\n\n // What the checks find out for later ones. A holder object, because\n // TypeScript narrows a plain `let` assigned inside a callback to its\n // initial value.\n const found: {\n wrongKey: boolean;\n balance: number | null;\n charged: Ok | null;\n reversed: boolean;\n } = { wrongKey: false, balance: null, charged: null, reversed: false };\n\n // Signatures. Every probe is a credits.get, which changes nothing, so a\n // forged call that gets through does no harm here.\n const read = JSON.stringify({ type: 'credits.get', environment, userId });\n // An unknown field, which the partner must ignore: only the signature\n // check can tell this body from the one that was signed.\n const tampered = JSON.stringify({\n type: 'credits.get',\n environment,\n userId,\n note: 'changed after signing',\n });\n const otherKey = randomId('sk_test_relume_check');\n const t = nowSeconds();\n\n await run(ACCEPT, async () => {\n const answer = await post(\n read,\n await signCall(secretKey, t, read),\n READ_TIMEOUT_MS,\n );\n if (isOk(answer)) return null;\n if (answer.answered && answer.status === 401) {\n found.wrongKey = true;\n return `${said(answer)} to a signed credits.get: is --secret-key the key this endpoint verifies with?`;\n }\n return `${said(answer)} to a signed credits.get; expected 200`;\n });\n // Every other check would fail the same way, or pass for the wrong reason.\n if (found.wrongKey) {\n skip(\n [\n ROTATION,\n OTHER_KEY,\n TAMPERED,\n STALE,\n FUTURE,\n UNSIGNED,\n OTHER_ENVIRONMENT,\n USER_GET,\n UNKNOWN_USER,\n CREDITS_GET,\n ...WALLET,\n ],\n 'needs the endpoint to accept a correctly signed call',\n );\n return { ok: false, results, notes };\n }\n\n await run(ROTATION, async () => {\n const answer = await post(\n read,\n await signCall([otherKey, secretKey], t, read),\n READ_TIMEOUT_MS,\n );\n return isOk(answer)\n ? null\n : `${said(answer)}; accept a call when any v1= value matches your key`;\n });\n\n const forgeries: [name: string, rawBody: string, header: string | null][] = [\n [OTHER_KEY, read, await signCall(otherKey, t, read)],\n [TAMPERED, tampered, await signCall(secretKey, t, read)],\n [STALE, read, await signCall(secretKey, t - SKEW_SECONDS, read)],\n [FUTURE, read, await signCall(secretKey, t + SKEW_SECONDS, read)],\n [UNSIGNED, read, null],\n ];\n for (const [name, rawBody, header] of forgeries) {\n await run(name, async () => {\n const answer = await post(rawBody, header, READ_TIMEOUT_MS);\n if (answer.answered && answer.status === 401) return null;\n if (isOk(answer)) {\n return `${said(answer)}: it acted on a call Relume didn't sign; answer 401 invalid_signature`;\n }\n return `${said(answer)}; expected 401 invalid_signature`;\n });\n }\n\n // Signed with this key, but for the other environment: what a partner that\n // verifies test and live calls on one URL must not act on.\n await run(OTHER_ENVIRONMENT, async () => {\n const answer = await signed(\n { type: 'credits.get', environment: otherEnvironment, userId },\n READ_TIMEOUT_MS,\n );\n if (answer.answered && answer.status === 400) return null;\n if (isOk(answer)) {\n return `${said(answer)} to a ${otherEnvironment} call signed with your ${environment} key: check the body's environment is your key's, and answer 400 environment_mismatch`;\n }\n return `${said(answer)}; expected 400 environment_mismatch`;\n });\n\n // The reads.\n await run(USER_GET, async () => {\n const answer = await signed(\n { type: 'user.get', environment, userId },\n READ_TIMEOUT_MS,\n );\n if (answer.answered && answer.status === 404) {\n return `answered 404: it doesn't know user \"${userId}\". Pass a --user your wallet knows`;\n }\n if (!isOk(answer)) {\n return `${said(answer)}; expected 200 { name?, email?, avatarUrl? }`;\n }\n return profileProblem(answer.body);\n });\n // A 404 is how Relume refuses a session for someone without a wallet; a\n // profile for any id would open one.\n await run(UNKNOWN_USER, async () => {\n const answer = await signed(\n { type: 'user.get', environment, userId: randomId('u_check_unknown') },\n READ_TIMEOUT_MS,\n );\n return answer.answered && answer.status === 404\n ? null\n : `${said(answer)} for a user id your wallet has never seen; expected 404`;\n });\n\n await run(CREDITS_GET, async () => {\n const result = await readBalance();\n if ('failure' in result) return result.failure;\n found.balance = result.balance;\n return null;\n });\n\n // The wallet. Each check compares against the balance the one before it\n // read, so one broken rule doesn't fail every check after it.\n const start = found.balance;\n if (start === null) {\n skip(WALLET, 'needs a working credits.get');\n return { ok: false, results, notes };\n }\n if (start < CHECK_CHARGE_CREDITS) {\n skip(\n WALLET,\n `user \"${userId}\" has a balance of ${start}; top it up to at least ${CHECK_CHARGE_CREDITS} (the check gives it back)`,\n );\n return { ok: false, results, notes };\n }\n\n let current = start;\n /** Charges the checker may have debited, and what's left of each to give back. */\n const toReverse = new Map<\n string,\n { partnerCredits: number; relumeCredits: number }\n >();\n\n /** Reads the balance and checks it moved by `delta`, or says how it didn't. */\n async function expectBalance(\n delta: number,\n what: string,\n ): Promise<string | null> {\n const result = await readBalance();\n if ('failure' in result) return result.failure;\n const before = current;\n current = result.balance;\n if (result.balance === before + delta) return null;\n return `${what} moved the balance from ${before} to ${result.balance}; expected ${before + delta}`;\n }\n\n /** Reverses a charge the checker made, the way Relume reverses one it can't finish. */\n async function giveBack(id: string): Promise<void> {\n const left = toReverse.get(id);\n if (left === undefined) return;\n toReverse.delete(id);\n const answer = await signed(\n {\n type: 'credits.reverse',\n environment,\n userId,\n reversalId: randomId('rv_check'),\n ...left,\n chargeId: id,\n reason: 'work_failed',\n },\n REVERSE_TIMEOUT_MS,\n );\n if (!isOk(answer)) notes.push(`Giving back charge ${id} ${said(answer)}.`);\n }\n\n /**\n * Runs a wallet check, then re-reads the balance if it failed: a failure\n * can leave the balance anywhere, and the next check measures from here.\n */\n async function walletCheck(\n name: string,\n check: () => Promise<string | null>,\n ): Promise<void> {\n if (await run(name, check)) return;\n const result = await readBalance();\n if ('balance' in result) current = result.balance;\n }\n\n // The give-back runs whatever happens below, even if printing a result\n // throws (an EPIPE from `check | head`): the user must not be left short.\n try {\n const chargeId = randomId('ch_check');\n const charge = JSON.stringify({\n type: 'credits.charge',\n environment,\n userId,\n chargeId,\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n action: 'chat.message',\n description: 'Relume conformance check (given back)',\n });\n\n await walletCheck(DEBIT, async () => {\n toReverse.set(chargeId, {\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n });\n const answer = await post(\n charge,\n await signCall(secretKey, nowSeconds(), charge),\n CHARGE_TIMEOUT_MS,\n );\n if (!isOk(answer)) {\n return `${said(answer)} to a charge of ${CHECK_CHARGE_CREDITS} from a balance of ${current}; expected 200`;\n }\n found.charged = answer;\n const problem =\n optionalBalanceProblem(answer.body) ??\n (await expectBalance(-CHECK_CHARGE_CREDITS, 'the charge'));\n if (problem) return problem;\n const reported = bodyBalance(answer.body);\n return reported === undefined || reported === current\n ? null\n : `the charge answered balance ${reported}, but credits.get says ${current}`;\n });\n\n const { charged } = found;\n if (charged === null) {\n skip([REPEAT_CHARGE], 'needs the first charge to succeed');\n } else {\n await walletCheck(REPEAT_CHARGE, async () => {\n // The same body, signed again: what an inline retry sends.\n const answer = await post(\n charge,\n await signCall(secretKey, nowSeconds(), charge),\n CHARGE_TIMEOUT_MS,\n );\n if (!answer.answered || answer.status !== charged.status) {\n return `the repeat ${said(answer)}, the first ${said(charged)}; answer a repeated chargeId with the first result`;\n }\n const moved = await expectBalance(0, 'the repeat');\n if (moved) return `${moved}: it debited the same chargeId twice`;\n const was = bodyBalance(charged.body);\n const again = bodyBalance(answer.body);\n return was === undefined || again === undefined || was === again\n ? null\n : `the repeat answered balance ${again}, the first ${was}; answer a repeated chargeId with the first result`;\n });\n }\n\n await walletCheck(DECLINE, async () => {\n const declinedId = randomId('ch_check');\n const amount = current + 1;\n toReverse.set(declinedId, {\n partnerCredits: amount,\n relumeCredits: CHECK_CHARGE_CREDITS,\n });\n const answer = await signed(\n {\n type: 'credits.charge',\n environment,\n userId,\n chargeId: declinedId,\n partnerCredits: amount,\n relumeCredits: CHECK_CHARGE_CREDITS,\n action: 'chat.message',\n description: 'Relume conformance check (should be declined)',\n },\n CHARGE_TIMEOUT_MS,\n );\n if (isOk(answer)) {\n // Now, so the overdrawn balance doesn't fail the checks after this one.\n await giveBack(declinedId);\n return `${said(answer)}: it accepted a charge of ${amount} from a balance of ${current}; answer 402`;\n }\n if (!answer.answered || answer.status !== 402) {\n // A 4xx refused the charge, so there's nothing to give back. A 5xx or\n // a timeout may have debited first, so that stays queued.\n if (answer.answered && answer.status < 500)\n toReverse.delete(declinedId);\n return `${said(answer)} to a charge of ${amount} from a balance of ${current}; expected 402`;\n }\n // Relume never sends a reversal for a decline.\n toReverse.delete(declinedId);\n const moved = await expectBalance(0, 'the declined charge');\n if (moved) return moved;\n // A 402 body may be unreadable, but a balance it does give is shown.\n const said402 = bodyBalance(answer.body);\n return said402 === undefined || said402 === current\n ? null\n : `the 402 answered balance ${said402}, but credits.get says ${current}`;\n });\n\n // A charge Relume gave up on, reversed before the wallet saw it.\n const lateId = randomId('ch_check_never_sent');\n let reversedFirst = false;\n await walletCheck(UNKNOWN_REVERSAL, async () => {\n const answer = await signed(\n {\n type: 'credits.reverse',\n environment,\n userId,\n reversalId: randomId('rv_check'),\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n chargeId: lateId,\n reason: 'charge_unknown',\n },\n REVERSE_TIMEOUT_MS,\n );\n if (!isOk(answer)) {\n return `${said(answer)}; expected 200, crediting nothing (Relume retries any other answer for 72 hours)`;\n }\n reversedFirst = true;\n const problem = optionalBalanceProblem(answer.body);\n if (problem) return problem;\n const moved = await expectBalance(0, 'the reversal');\n return moved && `${moved}: it credited a charge it never debited`;\n });\n\n if (!reversedFirst) {\n skip([LATE_CHARGE], 'needs the reversal of an unknown charge to succeed');\n } else {\n // The same charge, arriving late: an attempt from a wallet that\n // stalled and caught up after Relume gave up and reversed it.\n await walletCheck(LATE_CHARGE, async () => {\n toReverse.set(lateId, {\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n });\n const answer = await signed(\n {\n type: 'credits.charge',\n environment,\n userId,\n chargeId: lateId,\n partnerCredits: CHECK_CHARGE_CREDITS,\n relumeCredits: CHECK_CHARGE_CREDITS,\n action: 'chat.message',\n description: 'Relume conformance check (should be refused)',\n },\n CHARGE_TIMEOUT_MS,\n );\n const moved = await expectBalance(0, 'the late charge');\n if (moved) {\n return `${moved}: it debited a charge it had already answered a reversal for; record a reversed chargeId as declined`;\n }\n toReverse.delete(lateId);\n // Relume stopped waiting for this charge, so any refusal will do.\n return isOk(answer)\n ? `${said(answer)}; expected 402, debiting nothing`\n : null;\n });\n }\n\n // Half of the first charge, so a repeat that credits again shows.\n const reversal = JSON.stringify({\n type: 'credits.reverse',\n environment,\n userId,\n reversalId: randomId('rv_check'),\n partnerCredits: 1,\n relumeCredits: 1,\n chargeId,\n reason: 'work_failed',\n });\n if (charged === null) {\n skip([REVERSAL, REPEAT_REVERSAL], 'needs the first charge to succeed');\n } else {\n await walletCheck(REVERSAL, async () => {\n const answer = await post(\n reversal,\n await signCall(secretKey, nowSeconds(), reversal),\n REVERSE_TIMEOUT_MS,\n );\n if (!isOk(answer)) return `${said(answer)}; expected 200`;\n found.reversed = true;\n toReverse.set(chargeId, {\n partnerCredits: CHECK_CHARGE_CREDITS - 1,\n relumeCredits: CHECK_CHARGE_CREDITS - 1,\n });\n return (\n optionalBalanceProblem(answer.body) ??\n (await expectBalance(1, 'a reversal of 1 credit'))\n );\n });\n if (!found.reversed) {\n skip([REPEAT_REVERSAL], 'needs the reversal to succeed');\n } else {\n await walletCheck(REPEAT_REVERSAL, async () => {\n const answer = await post(\n reversal,\n await signCall(secretKey, nowSeconds(), reversal),\n REVERSE_TIMEOUT_MS,\n );\n if (!isOk(answer)) return `the repeat ${said(answer)}; expected 200`;\n const moved = await expectBalance(0, 'the repeat');\n return moved && `${moved}: it credited the same reversalId twice`;\n });\n }\n }\n } finally {\n for (const id of [...toReverse.keys()]) await giveBack(id);\n }\n const end = await readBalance();\n if ('failure' in end) {\n notes.push(`Couldn't read the final balance: ${end.failure}.`);\n } else if (end.balance !== start) {\n notes.push(\n `User \"${userId}\" has a balance of ${end.balance}; it was ${start} before the check. Correct it in your wallet.`,\n );\n }\n\n const ok = results.every((result) => result.status === 'pass');\n return { ok, results, notes };\n}\n","#!/usr/bin/env node\nimport { runCli } from './cli.ts';\n\n// No top-level await: the CommonJS build runs this file too.\nvoid runCli(process.argv.slice(2), {\n log: (line) => console.log(line),\n error: (line) => console.error(line),\n env: process.env,\n}).then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n console.error(\n `The check stopped: ${error instanceof Error ? error.message : String(error)}`,\n );\n process.exitCode = 1;\n },\n);\n"],"mappings":"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","names":["sessionUrl","parsed"]}
@@ -24,6 +24,187 @@ function isHttpsUrl(value) {
24
24
  }
25
25
  }
26
26
 
27
+ // src/calls.ts
28
+ var CALL_TYPES = [
29
+ "user.get",
30
+ "credits.get",
31
+ "credits.charge",
32
+ "credits.reverse"
33
+ ];
34
+ function keyEnvironment(secretKey) {
35
+ if (secretKey.startsWith("sk_test_")) return "test";
36
+ if (secretKey.startsWith("sk_live_")) return "live";
37
+ return null;
38
+ }
39
+ var InvalidCallbackResultError = class extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = "InvalidCallbackResultError";
43
+ }
44
+ };
45
+ function optionalString(value, max) {
46
+ if (value === void 0 || value === null) {
47
+ return { ok: true, value: void 0 };
48
+ }
49
+ return isString(value, 1, max) ? { ok: true, value } : { ok: false };
50
+ }
51
+ function isCallType(value) {
52
+ return CALL_TYPES.some((type) => type === value);
53
+ }
54
+ function parseCall(body, environment) {
55
+ if (!isRecord(body)) {
56
+ return {
57
+ ok: false,
58
+ code: "invalid_request",
59
+ message: "The body must be a JSON object."
60
+ };
61
+ }
62
+ const { type } = body;
63
+ if (!isCallType(type)) {
64
+ return {
65
+ ok: false,
66
+ code: "unsupported_call_type",
67
+ message: `Unsupported call type: ${JSON.stringify(type ?? null).slice(0, 64)}.`
68
+ };
69
+ }
70
+ const invalid = (field) => ({
71
+ ok: false,
72
+ code: "invalid_request",
73
+ message: `Invalid ${field} for ${type}.`,
74
+ type
75
+ });
76
+ if (body.environment !== void 0 && body.environment !== "test" && body.environment !== "live") {
77
+ return invalid("environment");
78
+ }
79
+ if (body.environment !== void 0 && body.environment !== environment) {
80
+ return {
81
+ ok: false,
82
+ code: "environment_mismatch",
83
+ message: `This ${type} is for ${body.environment}, but this server's secret key is for ${environment}.`,
84
+ type
85
+ };
86
+ }
87
+ const { userId } = body;
88
+ if (!isString(userId, 1, 128)) return invalid("userId");
89
+ switch (type) {
90
+ case "user.get":
91
+ case "credits.get":
92
+ return { ok: true, call: { type, userId } };
93
+ case "credits.charge": {
94
+ const { chargeId, partnerCredits, relumeCredits, action, description } = body;
95
+ if (!isString(chargeId, 1, 64)) return invalid("chargeId");
96
+ if (!isWhole(partnerCredits, 1)) return invalid("partnerCredits");
97
+ if (!isWhole(relumeCredits, 0)) return invalid("relumeCredits");
98
+ if (!isString(action, 1, 200)) return invalid("action");
99
+ if (!isString(description, 0, 200)) return invalid("description");
100
+ const siteId = optionalString(body.siteId, 200);
101
+ if (!siteId.ok) return invalid("siteId");
102
+ const pageId = optionalString(body.pageId, 200);
103
+ if (!pageId.ok) return invalid("pageId");
104
+ const charge = {
105
+ userId,
106
+ chargeId,
107
+ partnerCredits,
108
+ relumeCredits,
109
+ action,
110
+ description
111
+ };
112
+ if (siteId.value !== void 0) charge.siteId = siteId.value;
113
+ if (pageId.value !== void 0) charge.pageId = pageId.value;
114
+ return { ok: true, call: { type, charge } };
115
+ }
116
+ case "credits.reverse": {
117
+ const { reversalId, partnerCredits, relumeCredits, reason } = body;
118
+ if (!isString(reversalId, 1, 64)) return invalid("reversalId");
119
+ if (!isWhole(partnerCredits, 1)) return invalid("partnerCredits");
120
+ if (!isWhole(relumeCredits, 0)) return invalid("relumeCredits");
121
+ if (!isString(reason, 1, 200)) return invalid("reason");
122
+ const chargeId = optionalString(body.chargeId, 64);
123
+ if (!chargeId.ok) return invalid("chargeId");
124
+ const reversal = {
125
+ userId,
126
+ reversalId,
127
+ partnerCredits,
128
+ relumeCredits,
129
+ reason
130
+ };
131
+ if (chargeId.value !== void 0) reversal.chargeId = chargeId.value;
132
+ return { ok: true, call: { type, reversal } };
133
+ }
134
+ }
135
+ }
136
+ function balanceOrThrow(value, callback) {
137
+ if (!isWhole(value, 0)) {
138
+ throw new InvalidCallbackResultError(
139
+ `${callback} must return a balance that is a whole number, zero or more; got ${JSON.stringify(value)}.`
140
+ );
141
+ }
142
+ return value;
143
+ }
144
+ function checkUserProfile(value) {
145
+ if (value === null || value === void 0) return null;
146
+ if (!isRecord(value)) {
147
+ throw new InvalidCallbackResultError(
148
+ "onGetUser must return { name?, email?, avatarUrl? } or null."
149
+ );
150
+ }
151
+ const profile = {};
152
+ const { name, email, avatarUrl } = value;
153
+ if (!absent(name)) {
154
+ if (!isString(name, 1, 200)) {
155
+ throw new InvalidCallbackResultError(
156
+ "onGetUser: name must be a string of at most 200 characters."
157
+ );
158
+ }
159
+ profile.name = name;
160
+ }
161
+ if (!absent(email)) {
162
+ if (!isEmail(email)) {
163
+ throw new InvalidCallbackResultError(
164
+ "onGetUser: email must be a valid email address."
165
+ );
166
+ }
167
+ profile.email = email;
168
+ }
169
+ if (!absent(avatarUrl)) {
170
+ if (!isHttpsUrl(avatarUrl)) {
171
+ throw new InvalidCallbackResultError(
172
+ "onGetUser: avatarUrl must be an https URL."
173
+ );
174
+ }
175
+ profile.avatarUrl = avatarUrl;
176
+ }
177
+ return profile;
178
+ }
179
+ function checkCredits(value) {
180
+ if (!isRecord(value)) {
181
+ throw new InvalidCallbackResultError(
182
+ "onGetCredits must return { balance }."
183
+ );
184
+ }
185
+ return { balance: balanceOrThrow(value.balance, "onGetCredits") };
186
+ }
187
+ function optionalBalance(value, callback) {
188
+ return absent(value.balance) ? {} : { balance: balanceOrThrow(value.balance, callback) };
189
+ }
190
+ function checkChargeResult(value) {
191
+ if (!isRecord(value) || typeof value.ok !== "boolean") {
192
+ throw new InvalidCallbackResultError(
193
+ "onChargeCredits must return { ok: boolean, balance? }."
194
+ );
195
+ }
196
+ return { ok: value.ok, ...optionalBalance(value, "onChargeCredits") };
197
+ }
198
+ function checkReversalResult(value) {
199
+ if (value === void 0 || value === null) return {};
200
+ if (!isRecord(value)) {
201
+ throw new InvalidCallbackResultError(
202
+ "onReverseCharge must return { balance? } or nothing."
203
+ );
204
+ }
205
+ return optionalBalance(value, "onReverseCharge");
206
+ }
207
+
27
208
  // src/response.ts
28
209
  var MAX_BODY_BYTES = 1024 * 1024;
29
210
  var JSON_HEADERS = {
@@ -244,6 +425,13 @@ export {
244
425
  absent,
245
426
  isEmail,
246
427
  isHttpsUrl,
428
+ keyEnvironment,
429
+ InvalidCallbackResultError,
430
+ parseCall,
431
+ checkUserProfile,
432
+ checkCredits,
433
+ checkChargeResult,
434
+ checkReversalResult,
247
435
  MAX_BODY_BYTES,
248
436
  jsonResponse,
249
437
  errorResponse,
@@ -264,4 +452,4 @@ export {
264
452
  parseSignatureHeader,
265
453
  verifySignature
266
454
  };
267
- //# sourceMappingURL=chunk-HPVVBUR7.js.map
455
+ //# sourceMappingURL=chunk-6WSDAFUV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/guards.ts","../src/calls.ts","../src/response.ts","../src/session.ts","../src/signing.ts"],"sourcesContent":["// Value checks shared by the call handler (calls.ts) and the conformance\n// checker (check.ts): the checker is only useful if it accepts exactly what\n// the handler, and Relume behind it, accept.\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function isString(\n value: unknown,\n min: number,\n max: number,\n): value is string {\n return (\n typeof value === 'string' && value.length >= min && value.length <= max\n );\n}\n\nexport function isWhole(value: unknown, min: number): value is number {\n return (\n typeof value === 'number' && Number.isSafeInteger(value) && value >= min\n );\n}\n\n/** null and '' count as a field left out, as they do for Relume. */\nexport function absent(value: unknown): boolean {\n return value === undefined || value === null || value === '';\n}\n\n// Zod 4's default email pattern, which Relume validates user.get with\n// (backend/src/services/partners/partner-calls.ts). Change both together.\nconst EMAIL =\n /^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$/;\n\nexport function isEmail(value: unknown): value is string {\n return typeof value === 'string' && EMAIL.test(value);\n}\n\nexport function isHttpsUrl(value: unknown): value is string {\n if (typeof value !== 'string') return false;\n try {\n return new URL(value).protocol === 'https:';\n } catch {\n return false;\n }\n}\n","// Validates the body of a signed call and what each callback returns. Hand\n// written so the package has no runtime dependencies. Unknown request fields\n// are dropped (the protocol may add fields at any time); unknown `action` and\n// `reason` values are accepted. A call for the other environment is refused,\n// so a test call can never move a live wallet.\n\nimport {\n absent,\n isEmail,\n isHttpsUrl,\n isRecord,\n isString,\n isWhole,\n} from './guards.ts';\nimport type {\n CallType,\n Charge,\n ChargeResult,\n Credits,\n Environment,\n Reversal,\n ReversalResult,\n UserProfile,\n} from './types.ts';\n\nexport const CALL_TYPES: readonly CallType[] = [\n 'user.get',\n 'credits.get',\n 'credits.charge',\n 'credits.reverse',\n];\n\n/** The environment a secret key belongs to, from its prefix; null for neither. */\nexport function keyEnvironment(secretKey: string): Environment | null {\n if (secretKey.startsWith('sk_test_')) return 'test';\n if (secretKey.startsWith('sk_live_')) return 'live';\n return null;\n}\n\nexport type IncomingCall =\n | { type: 'user.get'; userId: string }\n | { type: 'credits.get'; userId: string }\n | { type: 'credits.charge'; charge: Charge }\n | { type: 'credits.reverse'; reversal: Reversal };\n\nexport type ParseCallResult =\n | { ok: true; call: IncomingCall }\n | {\n ok: false;\n code:\n | 'invalid_request'\n | 'unsupported_call_type'\n | 'environment_mismatch';\n message: string;\n /** The call's type, when it had a known one. */\n type?: CallType;\n };\n\n/** Thrown when a callback returns something the protocol doesn't allow. */\nexport class InvalidCallbackResultError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidCallbackResultError';\n }\n}\n\n/** Optional strings arrive omitted; null counts as omitted too. */\nfunction optionalString(\n value: unknown,\n max: number,\n): { ok: true; value: string | undefined } | { ok: false } {\n if (value === undefined || value === null) {\n return { ok: true, value: undefined };\n }\n return isString(value, 1, max) ? { ok: true, value } : { ok: false };\n}\n\nfunction isCallType(value: unknown): value is CallType {\n return CALL_TYPES.some((type) => type === value);\n}\n\n/** `environment` is the secret key's: a call for the other one is refused. */\nexport function parseCall(\n body: unknown,\n environment: Environment,\n): ParseCallResult {\n if (!isRecord(body)) {\n return {\n ok: false,\n code: 'invalid_request',\n message: 'The body must be a JSON object.',\n };\n }\n const { type } = body;\n if (!isCallType(type)) {\n return {\n ok: false,\n code: 'unsupported_call_type',\n message: `Unsupported call type: ${JSON.stringify(type ?? null).slice(0, 64)}.`,\n };\n }\n const invalid = (field: string): ParseCallResult => ({\n ok: false,\n code: 'invalid_request',\n message: `Invalid ${field} for ${type}.`,\n type,\n });\n\n // A call from a Relume deploy older than the field has no environment;\n // that is nothing to check, until a later major release requires it. A\n // value Relume never sends is refused.\n if (\n body.environment !== undefined &&\n body.environment !== 'test' &&\n body.environment !== 'live'\n ) {\n return invalid('environment');\n }\n if (body.environment !== undefined && body.environment !== environment) {\n return {\n ok: false,\n code: 'environment_mismatch',\n message: `This ${type} is for ${body.environment}, but this server's secret key is for ${environment}.`,\n type,\n };\n }\n\n const { userId } = body;\n if (!isString(userId, 1, 128)) return invalid('userId');\n\n switch (type) {\n case 'user.get':\n case 'credits.get':\n return { ok: true, call: { type, userId } };\n\n case 'credits.charge': {\n const { chargeId, partnerCredits, relumeCredits, action, description } =\n body;\n if (!isString(chargeId, 1, 64)) return invalid('chargeId');\n if (!isWhole(partnerCredits, 1)) return invalid('partnerCredits');\n if (!isWhole(relumeCredits, 0)) return invalid('relumeCredits');\n if (!isString(action, 1, 200)) return invalid('action');\n if (!isString(description, 0, 200)) return invalid('description');\n const siteId = optionalString(body.siteId, 200);\n if (!siteId.ok) return invalid('siteId');\n const pageId = optionalString(body.pageId, 200);\n if (!pageId.ok) return invalid('pageId');\n const charge: Charge = {\n userId,\n chargeId,\n partnerCredits,\n relumeCredits,\n action,\n description,\n };\n if (siteId.value !== undefined) charge.siteId = siteId.value;\n if (pageId.value !== undefined) charge.pageId = pageId.value;\n return { ok: true, call: { type, charge } };\n }\n\n case 'credits.reverse': {\n const { reversalId, partnerCredits, relumeCredits, reason } = body;\n if (!isString(reversalId, 1, 64)) return invalid('reversalId');\n if (!isWhole(partnerCredits, 1)) return invalid('partnerCredits');\n if (!isWhole(relumeCredits, 0)) return invalid('relumeCredits');\n if (!isString(reason, 1, 200)) return invalid('reason');\n const chargeId = optionalString(body.chargeId, 64);\n if (!chargeId.ok) return invalid('chargeId');\n const reversal: Reversal = {\n userId,\n reversalId,\n partnerCredits,\n relumeCredits,\n reason,\n };\n if (chargeId.value !== undefined) reversal.chargeId = chargeId.value;\n return { ok: true, call: { type, reversal } };\n }\n }\n}\n\nfunction balanceOrThrow(value: unknown, callback: string): number {\n if (!isWhole(value, 0)) {\n throw new InvalidCallbackResultError(\n `${callback} must return a balance that is a whole number, zero or more; got ${JSON.stringify(value)}.`,\n );\n }\n return value;\n}\n\n/** The 200 body for user.get, or null for a 404. */\nexport function checkUserProfile(value: unknown): UserProfile | null {\n if (value === null || value === undefined) return null;\n if (!isRecord(value)) {\n throw new InvalidCallbackResultError(\n 'onGetUser must return { name?, email?, avatarUrl? } or null.',\n );\n }\n const profile: UserProfile = {};\n const { name, email, avatarUrl } = value;\n if (!absent(name)) {\n if (!isString(name, 1, 200)) {\n throw new InvalidCallbackResultError(\n 'onGetUser: name must be a string of at most 200 characters.',\n );\n }\n profile.name = name;\n }\n if (!absent(email)) {\n if (!isEmail(email)) {\n throw new InvalidCallbackResultError(\n 'onGetUser: email must be a valid email address.',\n );\n }\n profile.email = email;\n }\n if (!absent(avatarUrl)) {\n if (!isHttpsUrl(avatarUrl)) {\n throw new InvalidCallbackResultError(\n 'onGetUser: avatarUrl must be an https URL.',\n );\n }\n profile.avatarUrl = avatarUrl;\n }\n return profile;\n}\n\nexport function checkCredits(value: unknown): Credits {\n if (!isRecord(value)) {\n throw new InvalidCallbackResultError(\n 'onGetCredits must return { balance }.',\n );\n }\n return { balance: balanceOrThrow(value.balance, 'onGetCredits') };\n}\n\nfunction optionalBalance(\n value: Record<string, unknown>,\n callback: string,\n): { balance?: number } {\n return absent(value.balance)\n ? {}\n : { balance: balanceOrThrow(value.balance, callback) };\n}\n\nexport function checkChargeResult(\n value: unknown,\n): Required<Pick<ChargeResult, 'ok'>> & { balance?: number } {\n if (!isRecord(value) || typeof value.ok !== 'boolean') {\n throw new InvalidCallbackResultError(\n 'onChargeCredits must return { ok: boolean, balance? }.',\n );\n }\n return { ok: value.ok, ...optionalBalance(value, 'onChargeCredits') };\n}\n\nexport function checkReversalResult(value: unknown): ReversalResult & {\n balance?: number;\n} {\n if (value === undefined || value === null) return {};\n if (!isRecord(value)) {\n throw new InvalidCallbackResultError(\n 'onReverseCharge must return { balance? } or nothing.',\n );\n }\n return optionalBalance(value, 'onReverseCharge');\n}\n","import type { RelumeResponse } from './types.ts';\n\n/** Bodies above this are refused with 413. Relume's calls are a few hundred bytes. */\nexport const MAX_BODY_BYTES = 1024 * 1024;\n\nconst JSON_HEADERS = {\n 'Content-Type': 'application/json; charset=utf-8',\n 'Cache-Control': 'no-store',\n} as const;\n\nexport function jsonResponse(\n status: number,\n body: unknown,\n headers: Record<string, string> = {},\n): RelumeResponse {\n return { status, headers: { ...JSON_HEADERS, ...headers }, body };\n}\n\nexport function errorResponse(\n status: number,\n code: string,\n message: string,\n headers?: Record<string, string>,\n): RelumeResponse {\n return jsonResponse(status, { error: { code, message } }, headers);\n}\n\nexport type Route = 'session' | 'calls';\n\nconst METHOD_FOR: Record<Route, string> = { session: 'GET', calls: 'POST' };\n\n/**\n * fetchHandler: which route a path is for, by its last segment. The framework\n * already routed the request here (a Next.js route file, Hono's\n * `app.all('/relume/*')`), so this works under any prefix.\n */\nexport function routeOf(pathname: string): Route | null {\n const last = pathname.replace(/\\/+$/, '').split('/').pop();\n return last === 'session' || last === 'calls' ? last : null;\n}\n\n/**\n * express(): exact paths only. Middleware sees every request in the app, so\n * matching any path ending in /calls would swallow the partner's own routes.\n * Under app.use('/relume', …) Express strips the mount path (and sets\n * req.baseUrl); mounted at the root, or behind bare node:http, the full\n * /relume/… path is expected.\n */\nexport function mountedRouteOf(\n pathname: string,\n mounted: boolean,\n): Route | null {\n const path = pathname.replace(/(.)\\/+$/, '$1');\n const prefix = mounted ? '' : '/relume';\n if (path === `${prefix}/session`) return 'session';\n if (path === `${prefix}/calls`) return 'calls';\n return null;\n}\n\n/** A 405 for the wrong method on a known route, or null when it's right. */\nexport function methodNotAllowed(\n route: Route,\n method: string | undefined,\n): RelumeResponse | null {\n const allowed = METHOD_FOR[route];\n // No HEAD on the session route: it would mint a client secret per probe.\n // OPTIONS is a CORS preflight, which the adapters leave to the app.\n if (method === allowed) return null;\n return errorResponse(\n 405,\n 'method_not_allowed',\n `Use ${allowed} for this route.`,\n { Allow: allowed },\n );\n}\n\nexport function payloadTooLarge(): RelumeResponse {\n return errorResponse(413, 'payload_too_large', 'The body is too large.');\n}\n\n/** A preflight that reached the handler: no CORS headers, so no grant. */\nexport function preflight(route: Route): RelumeResponse {\n return { status: 204, headers: { Allow: METHOD_FOR[route] }, body: null };\n}\n\n/** The parts of a client the adapters use. */\nexport interface Handlers<TRequest> {\n handleCall(input: {\n rawBody: string | Uint8Array;\n signatureHeader: string | null | undefined;\n requestId?: string | null;\n }): Promise<RelumeResponse>;\n handleSession(request: TRequest): Promise<RelumeResponse>;\n}\n","// Defences for `GET /relume/session`, which mints a client secret for whoever\n// the request's cookies say is signed in. Any site can make a signed-in\n// user's browser send that request; only the partner's CORS stops it reading\n// the answer, and a CORS setup that reflects Origin would hand the secret\n// over. So the route also refuses what only another site sends.\n\nimport { isRecord } from './guards.ts';\nimport { errorResponse } from './response.ts';\nimport type { RelumeResponse } from './types.ts';\n\n/**\n * Sent by `@relume_io/partner-embed` on every session request. A custom\n * header makes a cross-origin request need a CORS preflight, which a plain\n * link, form or `<img>` on another site can't get.\n */\nexport const EMBED_HEADER = 'Relume-Embed';\nexport const EMBED_HEADER_VALUE = '1';\n\n/**\n * What `handleSession` needs from a request: its headers. The fetch\n * `Request`, Express's request and node:http's `IncomingMessage` all fit.\n */\nexport interface SessionRequest {\n headers: unknown;\n}\n\ntype HeaderReader = (name: string) => string | null;\n\ninterface HeadersLike {\n get(name: string): string | null;\n}\n\nfunction isHeadersLike(value: unknown): value is HeadersLike {\n return isRecord(value) && typeof value.get === 'function';\n}\n\n/**\n * The request's headers, from a fetch `Request`, a node:http (Express)\n * request, or any object with `headers`. Null when it has none we can read.\n */\nfunction headerReader(request: unknown): HeaderReader | null {\n const headers = isRecord(request) ? request.headers : undefined;\n if (isHeadersLike(headers)) return (name) => headers.get(name);\n if (!isRecord(headers)) return null;\n return (name) => {\n const wanted = name.toLowerCase();\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() !== wanted) continue;\n const first = Array.isArray(value) ? value[0] : value;\n return typeof first === 'string' ? first : null;\n }\n return null;\n };\n}\n\nexport interface SessionRefusal {\n response: RelumeResponse;\n /** For onHandlerError: a refusal that points at the partner's setup. */\n error?: Error;\n}\n\n/** Why the session route refuses this request, or null to go on. */\nexport function refuseSessionRequest(request: unknown): SessionRefusal | null {\n const header = headerReader(request);\n if (!header) {\n return {\n response: errorResponse(\n 500,\n 'internal_error',\n 'The partner failed to handle the call.',\n ),\n error: new TypeError(\n \"handleSession couldn't read the request's headers. Pass it the fetch Request, the Node request, or an object with headers.\",\n ),\n };\n }\n // Browsers send Sec-Fetch-Site on every request. Another site is never a\n // legitimate caller, whatever the partner's CORS allows.\n if (header('sec-fetch-site')?.trim().toLowerCase() === 'cross-site') {\n return {\n response: errorResponse(\n 403,\n 'cross_site_request',\n 'The session route only answers your own pages.',\n ),\n };\n }\n if (header(EMBED_HEADER)?.trim() !== EMBED_HEADER_VALUE) {\n return {\n response: errorResponse(\n 400,\n 'embed_header_missing',\n `The session route needs the ${EMBED_HEADER} header that @relume_io/partner-embed sends.`,\n ),\n error: new Error(\n `GET /relume/session was refused: no ${EMBED_HEADER}: ${EMBED_HEADER_VALUE} header. @relume_io/partner-embed sends it from 2.0.0; update the script tag or package. On another origin, allow the header in the route's CORS preflight.`,\n ),\n };\n }\n return null;\n}\n","// Relume-Signature verification (docs/partner-protocol/v1.md section 6).\n//\n// Relume-Signature: t=<unix seconds>,v1=<hex hmac-sha256(sk, t + \".\" + rawBody)>[,v1=…]\n//\n// Mirrors backend/src/services/partners/signing.ts and passes\n// docs/partner-protocol/test-vectors.json. Uses Web Crypto rather than\n// node:crypto so the same code runs on Node, Workers, Deno, Bun and edge\n// runtimes. Never log a secret or a signature.\n\nexport const SIGNATURE_HEADER = 'Relume-Signature';\nexport const REQUEST_ID_HEADER = 'Relume-Request-Id';\nexport const SIGNATURE_SCHEME = 'v1';\n/** How far a call's timestamp may be from this server's clock, either way. */\nexport const SIGNATURE_TOLERANCE_SECONDS = 5 * 60;\n/** The current key plus the previous one during a rotation overlap. */\nexport const MAX_SIGNATURES = 2;\n\nconst HEX_SHA256 = /^[0-9a-f]{64}$/i;\n// Canonical decimal: other implementations HMAC the t= text as written, so a\n// leading zero would verify here and fail there.\nconst UNIX_SECONDS = /^(0|[1-9]\\d{0,11})$/;\n\nexport type SignatureFailure =\n | 'malformed_header'\n | 'signature_mismatch'\n | 'timestamp_out_of_tolerance';\n\nexport type VerifySignatureResult =\n | { valid: true; timestamp: number }\n | { valid: false; reason: SignatureFailure };\n\nexport interface ParsedSignatureHeader {\n timestamp: number;\n /** Every v1= value in header order, including ones that aren't hex. */\n signatures: string[];\n}\n\nconst encoder = new TextEncoder();\n\nfunction bodyBytes(rawBody: string | Uint8Array): Uint8Array {\n return typeof rawBody === 'string' ? encoder.encode(rawBody) : rawBody;\n}\n\n/** `t + \".\" + rawBody` as bytes: the exact message the HMAC covers. */\nfunction signedMessage(\n timestamp: number,\n rawBody: string | Uint8Array,\n): Uint8Array<ArrayBuffer> {\n const prefix = encoder.encode(`${timestamp}.`);\n const body = bodyBytes(rawBody);\n const message = new Uint8Array(prefix.length + body.length);\n message.set(prefix, 0);\n message.set(body, prefix.length);\n return message;\n}\n\nfunction hmacKey(secret: string, usage: 'sign' | 'verify'): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n [usage],\n );\n}\n\nfunction toHex(bytes: ArrayBuffer): string {\n return Array.from(new Uint8Array(bytes), (byte) =>\n byte.toString(16).padStart(2, '0'),\n ).join('');\n}\n\nfunction fromHex(hex: string): Uint8Array<ArrayBuffer> {\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i += 1) {\n bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\nfunction assertTimestamp(timestamp: number): void {\n if (!Number.isSafeInteger(timestamp) || timestamp < 0) {\n throw new TypeError('timestamp must be whole unix seconds');\n }\n}\n\n/** Lowercase hex HMAC-SHA256 of `t + \".\" + rawBody`, keyed by the secret key. */\nexport async function computeSignature(\n secret: string,\n timestamp: number,\n rawBody: string | Uint8Array,\n): Promise<string> {\n if (typeof secret !== 'string' || secret.length === 0) {\n throw new TypeError('secret must be a non-empty string');\n }\n assertTimestamp(timestamp);\n const key = await hmacKey(secret, 'sign');\n return toHex(\n await crypto.subtle.sign('HMAC', key, signedMessage(timestamp, rawBody)),\n );\n}\n\n/**\n * Builds a Relume-Signature header the way Relume does: newest secret first,\n * a second one only during a rotation. For a partner's own tests; a partner's\n * server never needs to sign anything.\n */\nexport async function signCall(\n secrets: string | readonly string[],\n timestamp: number,\n rawBody: string | Uint8Array,\n): Promise<string> {\n const list = typeof secrets === 'string' ? [secrets] : secrets;\n if (list.length === 0 || list.length > MAX_SIGNATURES) {\n throw new TypeError(\n `expected 1 to ${MAX_SIGNATURES} secrets, got ${list.length}`,\n );\n }\n const signatures = await Promise.all(\n list.map(async (secret) => {\n const hex = await computeSignature(secret, timestamp, rawBody);\n return `${SIGNATURE_SCHEME}=${hex}`;\n }),\n );\n return [`t=${timestamp}`, ...signatures].join(',');\n}\n\n/**\n * Null when the header has no `t`, more than one `t`, a `t` that isn't\n * canonical unix seconds, no v1= value, or more than two v1= values. Entries\n * with another name (a future `v2=`) are ignored.\n */\nexport function parseSignatureHeader(\n header: string,\n): ParsedSignatureHeader | null {\n let timestamp: number | null = null;\n const signatures: string[] = [];\n for (const part of header.split(',')) {\n const separator = part.indexOf('=');\n if (separator === -1) continue;\n const name = part.slice(0, separator).trim();\n const value = part.slice(separator + 1).trim();\n if (name === 't') {\n if (timestamp !== null || !UNIX_SECONDS.test(value)) return null;\n timestamp = Number(value);\n } else if (name === SIGNATURE_SCHEME) {\n if (signatures.length === MAX_SIGNATURES) return null;\n signatures.push(value);\n }\n }\n if (timestamp === null || signatures.length === 0) return null;\n return { timestamp, signatures };\n}\n\n/**\n * Verifies a call: the header parses, any v1= value matches the HMAC under\n * `secret` (compared in constant time by Web Crypto), and `t` is within 300\n * seconds of `nowSeconds`. The signature is checked before the timestamp, so\n * a stale result is only reported for a call Relume really signed.\n */\nexport async function verifySignature(input: {\n secret: string;\n header: string | null | undefined;\n rawBody: string | Uint8Array;\n /** Defaults to the current time. */\n nowSeconds?: number;\n}): Promise<VerifySignatureResult> {\n const parsed = input.header ? parseSignatureHeader(input.header) : null;\n if (parsed === null) return { valid: false, reason: 'malformed_header' };\n // A blank key (an unset env var) rejects the call; it never skips the check.\n if (typeof input.secret !== 'string' || input.secret.length === 0) {\n return { valid: false, reason: 'signature_mismatch' };\n }\n\n const key = await hmacKey(input.secret, 'verify');\n const message = signedMessage(parsed.timestamp, input.rawBody);\n // Check every value, so the time taken doesn't depend on which one matched.\n const results = await Promise.all(\n parsed.signatures.map((signature) =>\n HEX_SHA256.test(signature)\n ? crypto.subtle.verify('HMAC', key, fromHex(signature), message)\n : Promise.resolve(false),\n ),\n );\n if (!results.includes(true)) {\n return { valid: false, reason: 'signature_mismatch' };\n }\n\n const now = input.nowSeconds ?? Math.floor(Date.now() / 1000);\n if (Math.abs(now - parsed.timestamp) > SIGNATURE_TOLERANCE_SECONDS) {\n return { valid: false, reason: 'timestamp_out_of_tolerance' };\n }\n return { valid: true, timestamp: parsed.timestamp };\n}\n"],"mappings":"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","names":[]}