@relume_io/partner-node 1.0.0 → 2.0.1
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/LICENSE +21 -0
- package/README.md +60 -23
- package/dist/bin.cjs +186 -9
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +185 -10
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-LOMLIKT2.js → chunk-HPVVBUR7.js} +137 -27
- package/dist/chunk-HPVVBUR7.js.map +1 -0
- package/dist/index.cjs +91 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +45 -47
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-LOMLIKT2.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,60 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
+
EMBED_HEADER,
|
|
3
|
+
EMBED_HEADER_VALUE,
|
|
4
|
+
MAX_BODY_BYTES,
|
|
2
5
|
MAX_SIGNATURES,
|
|
3
6
|
REQUEST_ID_HEADER,
|
|
4
7
|
SIGNATURE_HEADER,
|
|
5
8
|
SIGNATURE_TOLERANCE_SECONDS,
|
|
6
9
|
absent,
|
|
7
10
|
computeSignature,
|
|
11
|
+
errorResponse,
|
|
8
12
|
isEmail,
|
|
9
13
|
isHttpsUrl,
|
|
10
14
|
isRecord,
|
|
11
15
|
isString,
|
|
12
16
|
isWhole,
|
|
17
|
+
jsonResponse,
|
|
18
|
+
methodNotAllowed,
|
|
19
|
+
mountedRouteOf,
|
|
13
20
|
parseSignatureHeader,
|
|
21
|
+
payloadTooLarge,
|
|
22
|
+
preflight,
|
|
23
|
+
refuseSessionRequest,
|
|
24
|
+
routeOf,
|
|
14
25
|
signCall,
|
|
15
26
|
verifySignature
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
|
|
18
|
-
// src/response.ts
|
|
19
|
-
var MAX_BODY_BYTES = 1024 * 1024;
|
|
20
|
-
var JSON_HEADERS = {
|
|
21
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
22
|
-
"Cache-Control": "no-store"
|
|
23
|
-
};
|
|
24
|
-
function jsonResponse(status, body, headers = {}) {
|
|
25
|
-
return { status, headers: { ...JSON_HEADERS, ...headers }, body };
|
|
26
|
-
}
|
|
27
|
-
function errorResponse(status, code, message, headers) {
|
|
28
|
-
return jsonResponse(status, { error: { code, message } }, headers);
|
|
29
|
-
}
|
|
30
|
-
var METHOD_FOR = { session: "GET", calls: "POST" };
|
|
31
|
-
function routeOf(pathname) {
|
|
32
|
-
const last = pathname.replace(/\/+$/, "").split("/").pop();
|
|
33
|
-
return last === "session" || last === "calls" ? last : null;
|
|
34
|
-
}
|
|
35
|
-
function mountedRouteOf(pathname, mounted) {
|
|
36
|
-
const path = pathname.replace(/(.)\/+$/, "$1");
|
|
37
|
-
const prefix = mounted ? "" : "/relume";
|
|
38
|
-
if (path === `${prefix}/session`) return "session";
|
|
39
|
-
if (path === `${prefix}/calls`) return "calls";
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
function methodNotAllowed(route, method) {
|
|
43
|
-
const allowed = METHOD_FOR[route];
|
|
44
|
-
if (method === allowed) return null;
|
|
45
|
-
return errorResponse(
|
|
46
|
-
405,
|
|
47
|
-
"method_not_allowed",
|
|
48
|
-
`Use ${allowed} for this route.`,
|
|
49
|
-
{ Allow: allowed }
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
function payloadTooLarge() {
|
|
53
|
-
return errorResponse(413, "payload_too_large", "The body is too large.");
|
|
54
|
-
}
|
|
55
|
-
function preflight(route) {
|
|
56
|
-
return { status: 204, headers: { Allow: METHOD_FOR[route] }, body: null };
|
|
57
|
-
}
|
|
27
|
+
} from "./chunk-HPVVBUR7.js";
|
|
58
28
|
|
|
59
29
|
// src/adapters/express.ts
|
|
60
30
|
var PayloadTooLargeError = class extends Error {
|
|
@@ -256,7 +226,7 @@ function fetchHandler(client, maxBodyBytes) {
|
|
|
256
226
|
|
|
257
227
|
// src/api.ts
|
|
258
228
|
var DEFAULT_API_BASE_URL = "https://apis.relume.io/nova/partner/v1";
|
|
259
|
-
var SDK_VERSION = "
|
|
229
|
+
var SDK_VERSION = "2.0.1";
|
|
260
230
|
var USER_AGENT = `relume-partner-node/${SDK_VERSION}`;
|
|
261
231
|
var DEFAULT_API_TIMEOUT_MS = 15e3;
|
|
262
232
|
var DEFAULT_MAX_RETRIES = 2;
|
|
@@ -782,7 +752,7 @@ var Relume = class _Relume {
|
|
|
782
752
|
this.#now = options.now ?? Date.now;
|
|
783
753
|
this.#api = {
|
|
784
754
|
secretKey,
|
|
785
|
-
baseUrl: (options.apiBaseUrl ?? DEFAULT_API_BASE_URL)
|
|
755
|
+
baseUrl: checkApiBaseUrl(options.apiBaseUrl ?? DEFAULT_API_BASE_URL),
|
|
786
756
|
timeoutMs: options.apiTimeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
787
757
|
fetch: options.fetch ?? ((...args) => fetch(...args)),
|
|
788
758
|
maxRetries,
|
|
@@ -944,11 +914,21 @@ var Relume = class _Relume {
|
|
|
944
914
|
}
|
|
945
915
|
}
|
|
946
916
|
/**
|
|
947
|
-
* `GET /relume/session`, framework-agnostic:
|
|
948
|
-
*
|
|
949
|
-
*
|
|
917
|
+
* `GET /relume/session`, framework-agnostic: refuses a request from
|
|
918
|
+
* another site or without the embed's `Relume-Embed` header, works out the
|
|
919
|
+
* signed-in user with `getUserIdFromRequest`, calls `POST /sessions` and
|
|
920
|
+
* returns the client secret, or passes the API's error through unchanged.
|
|
921
|
+
* `request` must carry its headers: the fetch Request, the Node request,
|
|
922
|
+
* or an object with `headers`.
|
|
950
923
|
*/
|
|
951
924
|
async handleSession(request) {
|
|
925
|
+
const refused = refuseSessionRequest(request);
|
|
926
|
+
if (refused) {
|
|
927
|
+
if (refused.error) {
|
|
928
|
+
this.#onHandlerError(refused.error, { route: "session" });
|
|
929
|
+
}
|
|
930
|
+
return refused.response;
|
|
931
|
+
}
|
|
952
932
|
const resolve = this.#callbacks.getUserIdFromRequest;
|
|
953
933
|
if (!resolve) {
|
|
954
934
|
this.#onHandlerError(
|
|
@@ -1019,6 +999,22 @@ var Relume = class _Relume {
|
|
|
1019
999
|
return jsonResponse(response.status, body);
|
|
1020
1000
|
}
|
|
1021
1001
|
};
|
|
1002
|
+
var LOOPBACK = /^(localhost|127(\.\d{1,3}){3}|\[::1\])$/;
|
|
1003
|
+
function checkApiBaseUrl(value) {
|
|
1004
|
+
let url;
|
|
1005
|
+
try {
|
|
1006
|
+
url = new URL(value);
|
|
1007
|
+
} catch {
|
|
1008
|
+
throw new TypeError(`Relume.init: apiBaseUrl isn't a URL: ${value}`);
|
|
1009
|
+
}
|
|
1010
|
+
const local = url.protocol === "http:" && LOOPBACK.test(url.hostname);
|
|
1011
|
+
if (url.protocol !== "https:" && !local) {
|
|
1012
|
+
throw new TypeError(
|
|
1013
|
+
"Relume.init: apiBaseUrl must be an https URL (http only for localhost): every request carries your secret key."
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return value.replace(/\/+$/, "");
|
|
1017
|
+
}
|
|
1022
1018
|
function assertFunction(callback, name) {
|
|
1023
1019
|
if (typeof callback !== "function") {
|
|
1024
1020
|
throw new TypeError(`relume.${name} expects a function.`);
|
|
@@ -1027,6 +1023,8 @@ function assertFunction(callback, name) {
|
|
|
1027
1023
|
}
|
|
1028
1024
|
export {
|
|
1029
1025
|
DEFAULT_API_BASE_URL,
|
|
1026
|
+
EMBED_HEADER,
|
|
1027
|
+
EMBED_HEADER_VALUE,
|
|
1030
1028
|
InvalidCallbackResultError,
|
|
1031
1029
|
MAX_SIGNATURES,
|
|
1032
1030
|
PagedList,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/response.ts","../src/adapters/express.ts","../src/adapters/fetch.ts","../src/api.ts","../src/calls.ts","../src/pages.ts","../src/resources.ts","../src/client.ts"],"sourcesContent":["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","// Express adapter. Uses only what node:http provides, so it also works with\n// Connect, Polka or a bare http.createServer, and needs no express import.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nimport {\n errorResponse,\n methodNotAllowed,\n mountedRouteOf,\n payloadTooLarge,\n preflight,\n type Handlers,\n type Route,\n} from '../response.ts';\nimport { REQUEST_ID_HEADER, SIGNATURE_HEADER } from '../signing.ts';\nimport type { RelumeResponse } from '../types.ts';\n\n/** An Express request, or a plain node:http one. */\nexport type NodeRequest = IncomingMessage & {\n body?: unknown;\n /** Set by Express to the mount path, e.g. '/relume'. */\n baseUrl?: string;\n};\n\nexport type NodeHandler = (\n request: NodeRequest,\n response: ServerResponse,\n next?: (error?: unknown) => void,\n) => void;\n\nclass PayloadTooLargeError extends Error {}\n\nclass BodyAlreadyReadError extends Error {\n constructor() {\n super(\n 'The request body was already read by a body parser, so the Relume signature ' +\n \"can't be checked. Mount relume.express() before express.json(), or \" +\n \"give it express.raw({ type: 'application/json' }).\",\n );\n this.name = 'BodyAlreadyReadError';\n }\n}\n\nfunction header(request: IncomingMessage, name: string): string | null {\n const value = request.headers[name.toLowerCase()];\n if (Array.isArray(value)) return value[0] ?? null;\n return value ?? null;\n}\n\nasync function readRawBody(\n request: NodeRequest,\n maxBodyBytes: number,\n): Promise<string | Uint8Array> {\n // express.raw() and express.text() leave the exact bytes on req.body.\n if (request.body instanceof Uint8Array) return request.body;\n if (typeof request.body === 'string') return request.body;\n // Another parser (express.json()) consumed the stream and kept only a\n // re-serialisable object, which can never verify.\n if (request.readableEnded) throw new BodyAlreadyReadError();\n\n if (Number(header(request, 'content-length')) > maxBodyBytes) {\n throw new PayloadTooLargeError();\n }\n // Events rather than for await: breaking out of an async iterator destroys\n // the socket, and the sender would get a reset instead of the 413. Over the\n // cap we stop listening and resume, so Node discards the rest unbuffered.\n return new Promise((resolve, reject) => {\n const chunks: Uint8Array[] = [];\n let size = 0;\n const cleanup = () => {\n request.off('data', onData);\n request.off('end', onEnd);\n request.off('error', onError);\n };\n const onData = (chunk: Uint8Array | string) => {\n const bytes =\n typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk;\n size += bytes.byteLength;\n if (size > maxBodyBytes) {\n cleanup();\n chunks.length = 0;\n request.resume();\n reject(new PayloadTooLargeError());\n return;\n }\n chunks.push(bytes);\n };\n const onEnd = () => {\n cleanup();\n const body = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n resolve(body);\n };\n const onError = (error: unknown) => {\n cleanup();\n reject(error);\n };\n request.on('data', onData);\n request.on('end', onEnd);\n request.on('error', onError);\n });\n}\n\nfunction send(\n request: IncomingMessage,\n response: ServerResponse,\n result: RelumeResponse,\n): void {\n if (response.headersSent) return;\n response.statusCode = result.status;\n for (const [name, value] of Object.entries(result.headers)) {\n response.setHeader(name, value);\n }\n response.end(\n request.method === 'HEAD' || result.status === 204\n ? undefined\n : JSON.stringify(result.body),\n );\n}\n\nexport function expressHandler(\n client: Handlers<any>,\n maxBodyBytes: number,\n onError: (error: unknown, route: Route) => void,\n): NodeHandler {\n return (request, response, next) => {\n const route = mountedRouteOf(\n new URL(request.url ?? '/', 'http://localhost').pathname,\n Boolean(request.baseUrl),\n );\n if (!route) {\n if (next) next();\n else\n send(request, response, errorResponse(404, 'not_found', 'Not found.'));\n return;\n }\n // A CORS preflight goes on to the app's CORS middleware, wherever it is.\n if (request.method === 'OPTIONS') {\n if (next) next();\n else send(request, response, preflight(route));\n return;\n }\n const wrongMethod = methodNotAllowed(route, request.method);\n if (wrongMethod) {\n send(request, response, wrongMethod);\n return;\n }\n\n const run = async (): Promise<RelumeResponse> => {\n if (route === 'session') return client.handleSession(request);\n let rawBody: string | Uint8Array;\n try {\n rawBody = await readRawBody(request, maxBodyBytes);\n } catch (error) {\n if (error instanceof PayloadTooLargeError) return payloadTooLarge();\n if (error instanceof BodyAlreadyReadError) {\n onError(error, route);\n return errorResponse(\n 500,\n 'internal_error',\n 'The partner failed to handle the call.',\n );\n }\n throw error;\n }\n return client.handleCall({\n rawBody,\n signatureHeader: header(request, SIGNATURE_HEADER),\n requestId: header(request, REQUEST_ID_HEADER),\n });\n };\n\n run().then(\n (result) => send(request, response, result),\n (error) => {\n if (next) next(error);\n else {\n onError(error, route);\n send(\n request,\n response,\n errorResponse(\n 500,\n 'internal_error',\n 'The partner failed to handle the call.',\n ),\n );\n }\n },\n );\n };\n}\n","// fetch-style adapter: (Request) => Response, for Next.js route handlers,\n// Hono, Cloudflare Workers, Bun and Deno.\n\nimport {\n errorResponse,\n methodNotAllowed,\n payloadTooLarge,\n preflight,\n routeOf,\n type Handlers,\n} from '../response.ts';\nimport { REQUEST_ID_HEADER, SIGNATURE_HEADER } from '../signing.ts';\nimport type { RelumeResponse } from '../types.ts';\n\nfunction toResponse(response: RelumeResponse, head: boolean): Response {\n const empty = head || response.status === 204;\n return new Response(empty ? null : JSON.stringify(response.body), {\n status: response.status,\n headers: response.headers,\n });\n}\n\n/**\n * The body's bytes, or null once it passes the cap. Reads chunk by chunk, so\n * a chunked or HTTP/2 body with no content-length can't be buffered past the\n * cap before the signature is checked.\n */\nasync function readCapped(\n request: Request,\n maxBodyBytes: number,\n): Promise<Uint8Array | null> {\n if (Number(request.headers.get('content-length')) > maxBodyBytes) {\n return null;\n }\n if (!request.body) return new Uint8Array(0);\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > maxBodyBytes) {\n // Release rather than cancel: the runtime discards the rest unread,\n // and the sender still gets the 413 instead of a reset.\n reader.releaseLock();\n return null;\n }\n chunks.push(value);\n }\n const body = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nexport function fetchHandler(\n client: Handlers<any>,\n maxBodyBytes: number,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n const head = request.method === 'HEAD';\n const route = routeOf(new URL(request.url).pathname);\n if (!route) {\n return toResponse(errorResponse(404, 'not_found', 'Not found.'), head);\n }\n // Your CORS middleware answers preflights before this when it's in front.\n if (request.method === 'OPTIONS') return toResponse(preflight(route), true);\n const wrongMethod = methodNotAllowed(route, request.method);\n if (wrongMethod) return toResponse(wrongMethod, head);\n\n if (route === 'session') {\n return toResponse(await client.handleSession(request), head);\n }\n\n const rawBody = await readCapped(request, maxBodyBytes);\n if (rawBody === null) return toResponse(payloadTooLarge(), false);\n return toResponse(\n await client.handleCall({\n rawBody,\n signatureHeader: request.headers.get(SIGNATURE_HEADER),\n requestId: request.headers.get(REQUEST_ID_HEADER),\n }),\n false,\n );\n };\n}\n","// The partner API (docs/partner-protocol/v1.md section 7), authenticated with\n// the secret key: one request, the retry loop around it, and the errors it\n// throws. The typed methods are in resources.ts.\n\nimport { isRecord } from './guards.ts';\nimport { REQUEST_ID_HEADER } from './signing.ts';\nimport type { PartnerApiErrorCode, Session } from './wire.ts';\n\nexport const DEFAULT_API_BASE_URL = 'https://apis.relume.io/nova/partner/v1';\n/** package.json's version; a test keeps them equal. */\nexport const SDK_VERSION = '1.0.0';\n/**\n * Sent on every request. Relume's firewall refuses requests with no\n * User-Agent, and Cloudflare Workers' fetch doesn't add one.\n */\nexport const USER_AGENT = `relume-partner-node/${SDK_VERSION}`;\n/** POST /sessions waits on user.get for a new user (4s), so allow more. */\nexport const DEFAULT_API_TIMEOUT_MS = 15_000;\n/** Retries after the first attempt, for 429s and (where safe) 5xx answers. */\nexport const DEFAULT_MAX_RETRIES = 2;\n\nconst RETRY_BASE_DELAY_MS = 500;\nconst RETRY_MAX_DELAY_MS = 8_000;\n/**\n * A 429's Retry-After longer than this isn't waited out: the 429 is thrown.\n * A 5xx's Retry-After (a proxy or maintenance page) is capped at the backoff\n * ceiling instead, so the retries still happen.\n */\nconst MAX_RETRY_AFTER_MS = 60_000;\n/**\n * Every other 5xx is retried, Cloudflare's 520 to 527 included: 501 and 505\n * won't change on a retry.\n */\nconst NON_RETRYABLE_5XX = new Set([501, 505]);\n\n/**\n * A partner API `error.code`, or one the SDK sets itself: `timeout` and\n * `network_error` when no response arrived, `unexpected_response` when one\n * arrived without the expected body. New API codes may appear.\n */\nexport type RelumeErrorCode =\n | PartnerApiErrorCode\n | 'timeout'\n | 'network_error'\n | 'unexpected_response'\n | (string & {});\n\n/** An error answer from the partner API, or a request that never got one. */\nexport class RelumeError extends Error {\n /** The HTTP status, or 0 when no response arrived. */\n readonly status: number;\n /** The API's `error.code`, safe to switch on. */\n readonly code: RelumeErrorCode;\n readonly requestId: string | null;\n\n constructor(input: {\n status: number;\n code: RelumeErrorCode;\n message: string;\n requestId?: string | null;\n cause?: unknown;\n }) {\n super(input.message, { cause: input.cause });\n this.name = 'RelumeError';\n this.status = input.status;\n this.code = input.code;\n this.requestId = input.requestId ?? null;\n }\n}\n\nexport interface ApiConfig {\n secretKey: string;\n baseUrl: string;\n timeoutMs: number;\n fetch: typeof fetch;\n maxRetries: number;\n sleep: (ms: number) => Promise<void>;\n now: () => number;\n}\n\nexport interface ApiResponse {\n status: number;\n /** The parsed JSON body, or null when it wasn't JSON. */\n body: unknown;\n requestId: string | null;\n /** Retry-After in milliseconds, or null when absent or unreadable. */\n retryAfterMs: number | null;\n}\n\nexport type Query = Record<string, string | number | undefined>;\n\n/** One request. Throws RelumeError (status 0) only when no response arrived. */\nexport async function apiRequest(\n config: ApiConfig,\n method: 'GET' | 'POST',\n path: string,\n options: { body?: unknown; query?: Query } = {},\n): Promise<ApiResponse> {\n const { body, query } = options;\n let response: Response;\n try {\n response = await config.fetch(\n `${config.baseUrl}${path}${queryString(query)}`,\n {\n method,\n headers: {\n Authorization: `Bearer ${config.secretKey}`,\n Accept: 'application/json',\n 'User-Agent': USER_AGENT,\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(config.timeoutMs),\n // Not 'error': Cloudflare Workers refuse it. A redirect is turned\n // into a network error below instead.\n redirect: 'manual',\n },\n );\n } catch (cause) {\n const timedOut =\n cause instanceof Error &&\n (cause.name === 'TimeoutError' || cause.name === 'AbortError');\n throw new RelumeError({\n status: 0,\n code: timedOut ? 'timeout' : 'network_error',\n message: timedOut\n ? `The Relume API didn't answer within ${config.timeoutMs}ms.`\n : \"Couldn't reach the Relume API.\",\n cause,\n });\n }\n\n // The API never redirects, so one means the base URL is wrong. Browsers\n // report a manual redirect as an opaque status 0.\n if (\n response.type === 'opaqueredirect' ||\n (response.status >= 300 && response.status < 400)\n ) {\n const status = response.status === 0 ? '' : ` (${response.status})`;\n throw new RedirectedError({\n status: 0,\n code: 'network_error',\n message: `The Relume API answered with a redirect${status}; check apiBaseUrl.`,\n });\n }\n\n const text = await response.text().catch(() => '');\n let parsed: unknown = null;\n if (text) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n }\n return {\n status: response.status,\n body: parsed,\n requestId: response.headers.get(REQUEST_ID_HEADER),\n retryAfterMs: parseRetryAfter(\n response.headers.get('Retry-After'),\n config.now(),\n ),\n };\n}\n\nfunction queryString(query: Query | undefined): string {\n if (!query) return '';\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) params.set(key, String(value));\n }\n const encoded = params.toString();\n return encoded ? `?${encoded}` : '';\n}\n\n/** Seconds, or an HTTP date. */\nexport function parseRetryAfter(\n header: string | null,\n nowMs: number,\n): number | null {\n if (header === null) return null;\n const value = header.trim();\n if (/^\\d+$/.test(value)) return Number(value) * 1000;\n const date = Date.parse(value);\n return Number.isNaN(date) ? null : Math.max(0, date - nowMs);\n}\n\n/** The API's `{ error: { code, message } }`, or a generic one. */\nexport function errorFromResponse(response: ApiResponse): RelumeError {\n const error = isRecord(response.body) ? response.body.error : undefined;\n const code = isRecord(error) ? error.code : undefined;\n const message = isRecord(error) ? error.message : undefined;\n return new RelumeError({\n status: response.status,\n code: typeof code === 'string' ? code : 'unexpected_response',\n message:\n typeof message === 'string'\n ? message\n : `The Relume API answered ${response.status}.`,\n requestId: response.requestId,\n });\n}\n\n/**\n * Which failures a request may retry. Every request retries a 429, which is\n * refused before any work. `unsafe` requests (they aren't idempotent and\n * have a lasting effect, like creating a site) retry nothing else: a 5xx or\n * a timeout may mean the work was done.\n */\nexport type RetryPolicy = 'safe' | 'unsafe';\n\nexport interface RequestSpec<T> {\n method: 'GET' | 'POST';\n path: string;\n query?: Query;\n body?: unknown;\n retry: RetryPolicy;\n /** A light check that the body is the object asked for. */\n isResult: (body: unknown) => body is T;\n}\n\n/**\n * A request with retries: 429s after Retry-After, and for `safe` requests\n * 5xx answers and failed connections with exponential backoff and jitter.\n * Throws RelumeError once retries run out or the answer is final.\n */\n/** A redirect from the API. Callers see a plain RelumeError. */\nclass RedirectedError extends RelumeError {}\n\nexport async function requestJson<T>(\n config: ApiConfig,\n spec: RequestSpec<T>,\n): Promise<T> {\n for (let attempt = 0; ; attempt += 1) {\n const canRetry = attempt < config.maxRetries;\n let response: ApiResponse;\n try {\n response = await apiRequest(config, spec.method, spec.path, {\n body: spec.body,\n query: spec.query,\n });\n } catch (error) {\n // A redirect isn't transient: retrying only delays the error.\n const transient = !(error instanceof RedirectedError);\n if (!canRetry || spec.retry !== 'safe' || !transient) throw error;\n await config.sleep(backoffMs(attempt));\n continue;\n }\n\n if (response.status >= 200 && response.status < 300) {\n if (spec.isResult(response.body)) return response.body;\n throw new RelumeError({\n status: response.status,\n code: 'unexpected_response',\n message: `${spec.method} ${spec.path} answered ${response.status} without the expected body.`,\n requestId: response.requestId,\n });\n }\n\n const retryable =\n response.status === 429 ||\n (spec.retry === 'safe' &&\n response.status >= 500 &&\n !NON_RETRYABLE_5XX.has(response.status));\n const delay =\n response.retryAfterMs === null\n ? backoffMs(attempt)\n : response.status === 429\n ? response.retryAfterMs\n : Math.min(response.retryAfterMs, RETRY_MAX_DELAY_MS);\n if (!canRetry || !retryable || delay > MAX_RETRY_AFTER_MS) {\n throw errorFromResponse(response);\n }\n await config.sleep(delay);\n }\n}\n\n/** Full jitter: anywhere up to 500ms, 1s, 2s, … capped at 8s. */\nfunction backoffMs(attempt: number): number {\n const ceiling = Math.min(\n RETRY_MAX_DELAY_MS,\n RETRY_BASE_DELAY_MS * 2 ** attempt,\n );\n return Math.round(Math.random() * ceiling);\n}\n\nexport function isSession(value: unknown): value is Session {\n return (\n isRecord(value) &&\n typeof value.clientSecret === 'string' &&\n typeof value.expiresAt === 'string'\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.\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 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\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: 'invalid_request' | 'unsupported_call_type';\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\nexport function parseCall(body: unknown): 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 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 { RelumeError } from './api.ts';\nimport { isRecord } from './guards.ts';\nimport type { Page } from './wire.ts';\n\nexport function isPage(value: unknown): value is Page<unknown> {\n return (\n isRecord(value) &&\n Array.isArray(value.data) &&\n typeof value.nextCursor === 'string' &&\n typeof value.hasMore === 'boolean'\n );\n}\n\n/**\n * A list endpoint, fetched a page at a time as you iterate:\n *\n * for await (const entry of relume.ledger.list({ after })) { … }\n *\n * Nothing is requested until iteration starts, and each iteration starts\n * again from the `after` the list was made with. Iterate `pages()` instead to\n * handle (and checkpoint) a page at a time. Iterate a list once at a time:\n * every iteration moves the same `nextCursor`, so two at once would leave it\n * wherever the last one got to.\n */\nexport class PagedList<T> implements AsyncIterable<T> {\n readonly #fetchPage: (after: string | undefined) => Promise<Page<T>>;\n readonly #after: string | undefined;\n #nextCursor: string | null;\n\n constructor(\n fetchPage: (after: string | undefined) => Promise<Page<T>>,\n after: string | undefined,\n ) {\n this.#fetchPage = fetchPage;\n this.#after = after;\n this.#nextCursor = after ?? null;\n }\n\n /**\n * Where to resume: the `nextCursor` of the last page whose items have all\n * been handed out, or the starting `after` (null when there was none)\n * before that. Once iteration ends, store it and pass it as `after` later\n * to get only what is new (on the ledger; site cursors don't poll).\n */\n get nextCursor(): string | null {\n return this.#nextCursor;\n }\n\n /** Every page in turn. The cursor advances when you ask for the next one. */\n async *pages(): AsyncGenerator<Page<T>, void, undefined> {\n let after = this.#after;\n for (;;) {\n const page = await this.#fetchPage(after);\n yield page;\n this.#nextCursor = page.nextCursor;\n if (!page.hasMore) return;\n if (page.nextCursor === after) {\n // A page that says there's more but doesn't move would loop forever.\n throw new RelumeError({\n status: 200,\n code: 'unexpected_response',\n message: 'The Relume API returned hasMore with the same cursor.',\n });\n }\n after = page.nextCursor;\n }\n }\n\n async *[Symbol.asyncIterator](): AsyncGenerator<T, void, undefined> {\n for await (const page of this.pages()) yield* page.data;\n }\n\n /** Every item in one array. Mind the size of what you're listing. */\n async toArray(): Promise<T[]> {\n const items: T[] = [];\n for await (const item of this) items.push(item);\n return items;\n }\n}\n","// A typed method for every partner API endpoint (protocol section 7).\n\nimport { isSession, requestJson, type ApiConfig, type Query } from './api.ts';\nimport { isRecord } from './guards.ts';\nimport { isPage, PagedList } from './pages.ts';\nimport type {\n CreatedSite,\n CreateSessionBody,\n CreateSiteBody,\n Export,\n LedgerEntry,\n LedgerEntryDetail,\n Page,\n PageQuery,\n Price,\n PriceList,\n Session,\n Site,\n UsageGroup,\n UsageQuery,\n UsageRow,\n User,\n} from './wire.ts';\n\nexport interface UsageParams extends Omit<UsageQuery, 'groupBy'> {\n /** One row per combination of these. Omitted: one row for the whole range. */\n groupBy?: readonly UsageGroup[];\n}\n\nexport interface PartnerApi {\n sessions: {\n /**\n * POST /sessions: a single-use client secret for this user's embed. The\n * first call for a userId creates their Relume account with user.get.\n */\n create(input: CreateSessionBody): Promise<Session>;\n };\n users: {\n /** GET /users/{userId}. `user_not_found` until their first session. */\n get(userId: string): Promise<User>;\n /**\n * POST /users/{userId}/deactivate: cuts off their sessions and export\n * links; data is kept. Idempotent, and retried on a 5xx.\n */\n deactivate(userId: string): Promise<User>;\n /** POST /users/{userId}/reactivate. `user_suspended` if Relume staff suspended them. */\n reactivate(userId: string): Promise<User>;\n };\n sites: {\n /**\n * GET /users/{userId}/sites, most recently updated first. Fetch from the\n * beginning each time: new and changed sites sort to the front, so a\n * stored cursor never shows them.\n */\n list(userId: string, params?: PageQuery): PagedList<Site>;\n /** POST /users/{userId}/sites: an empty site. Not idempotent, so a 5xx isn't retried. */\n create(userId: string, input?: CreateSiteBody): Promise<CreatedSite>;\n /** GET /sites/{siteId}. */\n get(siteId: string): Promise<Site>;\n };\n exports: {\n /**\n * GET /sites/{siteId}/exports/{exportId}. Not available yet: answers\n * `not_implemented` (501) until exports ship.\n */\n get(siteId: string, exportId: string): Promise<Export>;\n };\n ledger: {\n /**\n * GET /ledger, in change order: an entry appears again each time its\n * status changes, so upsert on `id`. Store the list's `nextCursor` when\n * you're done and pass it as `after` to get only what changed since.\n */\n list(params?: PageQuery): PagedList<LedgerEntry>;\n /** GET /ledger/{id}: one entry by chargeId or reversalId, with partnerResponse. */\n get(id: string): Promise<LedgerEntryDetail>;\n };\n usage: {\n /** GET /usage: credits totalled over [from, to), ordered by day, user, action. */\n get(params: UsageParams): PagedList<UsageRow>;\n };\n prices: {\n /** GET /prices: every billable action's price in this environment. */\n list(): Promise<Price[]>;\n };\n}\n\n// Light checks that the body is the object asked for, by its string ids.\n// The other fields are as the protocol says; unknown ones are kept, as it\n// requires.\nfunction hasStrings<T>(\n ...keys: (keyof T & string)[]\n): (value: unknown) => value is T {\n return (value): value is T =>\n isRecord(value) && keys.every((key) => typeof value[key] === 'string');\n}\n\nconst isUser = hasStrings<User>('userId', 'status');\nconst isSite = hasStrings<Site>('siteId', 'userId');\nconst isCreatedSite = hasStrings<CreatedSite>('siteId');\nconst isExport = hasStrings<Export>('exportId', 'status');\nconst isLedgerEntryDetail = hasStrings<LedgerEntryDetail>(\n 'id',\n 'type',\n 'status',\n);\n\nfunction isPriceList(value: unknown): value is PriceList {\n return isRecord(value) && Array.isArray(value.data);\n}\n\n/**\n * A path segment from a caller's id: never empty, always escaped. Throws, so\n * the Promise-returning methods call it inside an async function and reject.\n */\nfunction segment(name: string, value: unknown): string {\n if (typeof value !== 'string' || value === '') {\n throw new TypeError(`${name} must be a non-empty string.`);\n }\n return encodeURIComponent(value);\n}\n\nexport function partnerApi(config: ApiConfig): PartnerApi {\n const get = <T>(\n path: string,\n isResult: (body: unknown) => body is T,\n query?: Query,\n ) =>\n requestJson(config, {\n method: 'GET',\n path,\n query,\n retry: 'safe',\n isResult,\n });\n\n const list = <T>(\n path: string,\n { after, ...query }: Query & { after?: string },\n ) =>\n new PagedList<T>(\n (cursor) =>\n get(path, (body): body is Page<T> => isPage(body), {\n ...query,\n after: cursor,\n }),\n after,\n );\n\n return {\n sessions: {\n create: async ({ userId }) =>\n requestJson(config, {\n method: 'POST',\n path: '/sessions',\n body: { userId } satisfies CreateSessionBody,\n // Safe to retry: Relume provisions a new user once (later sessions\n // wait for or find the account), so a retry at most mints another\n // client secret.\n retry: 'safe',\n isResult: isSession,\n }),\n },\n users: {\n get: async (userId) => get(`/users/${segment('userId', userId)}`, isUser),\n deactivate: async (userId) =>\n requestJson(config, {\n method: 'POST',\n path: `/users/${segment('userId', userId)}/deactivate`,\n retry: 'safe',\n isResult: isUser,\n }),\n reactivate: async (userId) =>\n requestJson(config, {\n method: 'POST',\n path: `/users/${segment('userId', userId)}/reactivate`,\n retry: 'safe',\n isResult: isUser,\n }),\n },\n sites: {\n list: (userId, params = {}) =>\n list<Site>(`/users/${segment('userId', userId)}/sites`, { ...params }),\n create: async (userId, input = {}) =>\n requestJson(config, {\n method: 'POST',\n path: `/users/${segment('userId', userId)}/sites`,\n body: (input.name === undefined\n ? {}\n : { name: input.name }) satisfies CreateSiteBody,\n retry: 'unsafe',\n isResult: isCreatedSite,\n }),\n get: async (siteId) => get(`/sites/${segment('siteId', siteId)}`, isSite),\n },\n exports: {\n get: async (siteId, exportId) =>\n get(\n `/sites/${segment('siteId', siteId)}/exports/${segment('exportId', exportId)}`,\n isExport,\n ),\n },\n ledger: {\n list: (params = {}) => list<LedgerEntry>('/ledger', { ...params }),\n get: async (id) =>\n get(`/ledger/${segment('id', id)}`, isLedgerEntryDetail),\n },\n usage: {\n get: ({ groupBy, ...params }) =>\n list<UsageRow>('/usage', {\n ...params,\n groupBy: groupBy?.length ? groupBy.join(',') : undefined,\n }),\n },\n prices: {\n list: async () => (await get('/prices', isPriceList)).data,\n },\n };\n}\n","import { expressHandler, type NodeHandler } from './adapters/express.ts';\nimport { fetchHandler } from './adapters/fetch.ts';\nimport {\n apiRequest,\n DEFAULT_API_BASE_URL,\n DEFAULT_API_TIMEOUT_MS,\n DEFAULT_MAX_RETRIES,\n errorFromResponse,\n isSession,\n RelumeError,\n type ApiConfig,\n} from './api.ts';\nimport {\n checkChargeResult,\n checkCredits,\n checkReversalResult,\n checkUserProfile,\n parseCall,\n type IncomingCall,\n} from './calls.ts';\nimport { partnerApi, type PartnerApi } from './resources.ts';\nimport { errorResponse, jsonResponse, MAX_BODY_BYTES } from './response.ts';\nimport { verifySignature } from './signing.ts';\nimport type {\n CallContext,\n CallType,\n ChargeCreditsCallback,\n GetCreditsCallback,\n GetUserCallback,\n GetUserIdFromRequest,\n HandlerErrorInfo,\n RelumeResponse,\n ReverseChargeCallback,\n} from './types.ts';\n\nexport interface RelumeOptions {\n /** `sk_test_…` or `sk_live_…`. Server only: never send it to a browser. */\n secretKey: string;\n /** Defaults to production. Relume gives you another URL for staging. */\n apiBaseUrl?: string;\n /** Per partner API request attempt. Default 15 seconds. */\n apiTimeoutMs?: number;\n /**\n * Retries after the first attempt. Default 2. A 429 is retried after its\n * Retry-After; a 5xx or a failed connection with backoff, except on\n * `sites.create`, which isn't idempotent. 0 turns retries off.\n */\n maxRetries?: number;\n /** Replace `fetch` for partner API requests, e.g. to add a proxy agent. */\n fetch?: typeof fetch;\n /**\n * Called when a callback throws or returns something the protocol doesn't\n * allow, or the session route fails. Defaults to console.error. The caller\n * always gets a 5xx; Relume never sees the error's message.\n */\n onHandlerError?: (error: unknown, info: HandlerErrorInfo) => void;\n /** The clock, in milliseconds. For tests. */\n now?: () => number;\n /** Waits between retries. For tests. */\n sleep?: (ms: number) => Promise<void>;\n}\n\ninterface Callbacks<TRequest> {\n getUserIdFromRequest?: GetUserIdFromRequest<TRequest>;\n onGetUser?: GetUserCallback;\n onGetCredits?: GetCreditsCallback;\n onChargeCredits?: ChargeCreditsCallback;\n onReverseCharge?: ReverseChargeCallback;\n}\n\nconst CALLBACK_FOR: Record<CallType, keyof Callbacks<unknown>> = {\n 'user.get': 'onGetUser',\n 'credits.get': 'onGetCredits',\n 'credits.charge': 'onChargeCredits',\n 'credits.reverse': 'onReverseCharge',\n};\n\nconst INTERNAL_ERROR = () =>\n errorResponse(\n 500,\n 'internal_error',\n 'The partner failed to handle the call.',\n );\n\nfunction defaultOnHandlerError(error: unknown, info: HandlerErrorInfo): void {\n const where =\n info.route === 'calls' ? `${info.type ?? 'call'}` : 'GET /relume/session';\n const requestId = info.requestId\n ? ` (Relume-Request-Id ${info.requestId})`\n : '';\n console.error(`[relume] ${where} failed${requestId}:`, error);\n}\n\n/**\n * A Relume partner client. Create one with `Relume.init`; there is no global\n * state, so test and live clients can sit side by side.\n *\n * `TRequest` is the request your `getUserIdFromRequest` receives: the\n * Express/Node request for `express()`, the `Request` for `fetchHandler`.\n */\nexport class Relume<TRequest = unknown> {\n // The partner API. Each method throws RelumeError when the API refuses.\n readonly sessions: PartnerApi['sessions'];\n readonly users: PartnerApi['users'];\n readonly sites: PartnerApi['sites'];\n readonly exports: PartnerApi['exports'];\n readonly ledger: PartnerApi['ledger'];\n readonly usage: PartnerApi['usage'];\n readonly prices: PartnerApi['prices'];\n\n /**\n * A fetch-style handler for Next.js route handlers, Hono, Cloudflare\n * Workers, Bun and Deno. Answers `GET …/session` and `POST …/calls`.\n */\n readonly fetchHandler: (request: Request) => Promise<Response>;\n\n readonly #secretKey: string;\n readonly #api: ApiConfig;\n readonly #callbacks: Callbacks<TRequest> = {};\n readonly #onHandlerError: (error: unknown, info: HandlerErrorInfo) => void;\n readonly #now: () => number;\n\n static init<TRequest = unknown>(options: RelumeOptions): Relume<TRequest> {\n return new Relume<TRequest>(options);\n }\n\n constructor(options: RelumeOptions) {\n // A key read from a file or .env often ends in a newline, which would\n // make every signature check and API request fail.\n const secretKey =\n typeof options?.secretKey === 'string' ? options.secretKey.trim() : '';\n if (secretKey === '') {\n throw new TypeError(\n 'Relume.init: secretKey is required (sk_test_… or sk_live_…). Is the environment variable set?',\n );\n }\n if (secretKey.startsWith('pk_')) {\n throw new TypeError(\n 'Relume.init: that is a publishable key. Pass the secret key (sk_…); the publishable key goes to the embed.',\n );\n }\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {\n throw new TypeError(\n 'Relume.init: maxRetries must be a whole number, 0 or more.',\n );\n }\n this.#secretKey = secretKey;\n this.#now = options.now ?? Date.now;\n this.#api = {\n secretKey,\n baseUrl: (options.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\\/+$/, ''),\n timeoutMs: options.apiTimeoutMs ?? DEFAULT_API_TIMEOUT_MS,\n fetch: options.fetch ?? ((...args) => fetch(...args)),\n maxRetries,\n sleep:\n options.sleep ??\n ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),\n now: this.#now,\n };\n this.#onHandlerError = options.onHandlerError ?? defaultOnHandlerError;\n\n const api = partnerApi(this.#api);\n this.sessions = api.sessions;\n this.users = api.users;\n this.sites = api.sites;\n this.exports = api.exports;\n this.ledger = api.ledger;\n this.usage = api.usage;\n this.prices = api.prices;\n this.fetchHandler = fetchHandler(this, MAX_BODY_BYTES);\n }\n\n /** Who is signed in, for the browser's `GET /relume/session`. */\n getUserIdFromRequest(callback: GetUserIdFromRequest<TRequest>): this {\n this.#callbacks.getUserIdFromRequest = assertFunction(\n callback,\n 'getUserIdFromRequest',\n );\n return this;\n }\n\n /** Answers `user.get`: the user's profile, or null if you don't know them. */\n onGetUser(callback: GetUserCallback): this {\n this.#callbacks.onGetUser = assertFunction(callback, 'onGetUser');\n return this;\n }\n\n /** Answers `credits.get`: `{ balance }` in your own credits. */\n onGetCredits(callback: GetCreditsCallback): this {\n this.#callbacks.onGetCredits = assertFunction(callback, 'onGetCredits');\n return this;\n }\n\n /**\n * Answers `credits.charge`. Debit `partnerCredits` (never `relumeCredits`),\n * at most once per `chargeId`, and return `{ ok, balance }`.\n */\n onChargeCredits(callback: ChargeCreditsCallback): this {\n this.#callbacks.onChargeCredits = assertFunction(\n callback,\n 'onChargeCredits',\n );\n return this;\n }\n\n /**\n * Answers `credits.reverse`. Credit back `partnerCredits` (never\n * `relumeCredits`), at most once per `reversalId`. If `chargeId` names a\n * debit you never made, change nothing and return normally.\n */\n onReverseCharge(callback: ReverseChargeCallback): this {\n this.#callbacks.onReverseCharge = assertFunction(\n callback,\n 'onReverseCharge',\n );\n return this;\n }\n\n /**\n * Express (or Connect, or plain `node:http`) middleware. Mount it before\n * any body parser: `app.use('/relume', relume.express())`.\n */\n express(): NodeHandler {\n return expressHandler(this, MAX_BODY_BYTES, (error, route) =>\n this.#onHandlerError(error, { route }),\n );\n }\n\n /**\n * `POST /relume/calls`, framework-agnostic: verifies the signature and the\n * 5-minute window, routes by `type`, runs your callback and validates what\n * it returns. Pass the raw body exactly as received.\n */\n async handleCall(input: {\n rawBody: string | Uint8Array;\n signatureHeader: string | null | undefined;\n requestId?: string | null;\n }): Promise<RelumeResponse> {\n const requestId = input.requestId ?? null;\n const verified = await verifySignature({\n secret: this.#secretKey,\n header: input.signatureHeader,\n rawBody: input.rawBody,\n nowSeconds: Math.floor(this.#now() / 1000),\n });\n if (!verified.valid) {\n const message =\n verified.reason === 'timestamp_out_of_tolerance'\n ? 'The signature timestamp is more than 5 minutes from this server’s clock.'\n : verified.reason === 'malformed_header'\n ? 'Missing or malformed Relume-Signature header.'\n : \"The signature doesn't match this server's secret key.\";\n return errorResponse(401, 'invalid_signature', message);\n }\n\n let body: unknown;\n try {\n body =\n typeof input.rawBody === 'string'\n ? JSON.parse(input.rawBody)\n : JSON.parse(new TextDecoder().decode(input.rawBody));\n } catch {\n return errorResponse(\n 400,\n 'invalid_request',\n 'The body is not valid JSON.',\n );\n }\n\n const parsed = parseCall(body);\n if (!parsed.ok) return errorResponse(400, parsed.code, parsed.message);\n\n const { call } = parsed;\n const context: CallContext = { type: call.type, requestId };\n const callbackName = CALLBACK_FOR[call.type];\n if (!this.#callbacks[callbackName]) {\n this.#onHandlerError(\n new Error(\n `No ${callbackName} callback is registered, so ${call.type} was answered 400.`,\n ),\n { route: 'calls', type: call.type, requestId },\n );\n return errorResponse(\n 400,\n 'unsupported_call_type',\n `This partner doesn't handle ${call.type}.`,\n );\n }\n\n try {\n return await this.#runCallback(call, context);\n } catch (error) {\n this.#onHandlerError(error, {\n route: 'calls',\n type: call.type,\n requestId,\n });\n return INTERNAL_ERROR();\n }\n }\n\n async #runCallback(\n call: IncomingCall,\n context: CallContext,\n ): Promise<RelumeResponse> {\n const callbacks = this.#callbacks;\n switch (call.type) {\n case 'user.get': {\n const profile = checkUserProfile(\n await callbacks.onGetUser!(call.userId, context),\n );\n return profile\n ? jsonResponse(200, profile)\n : errorResponse(404, 'user_not_found', 'Unknown user.');\n }\n case 'credits.get':\n return jsonResponse(\n 200,\n checkCredits(await callbacks.onGetCredits!(call.userId, context)),\n );\n case 'credits.charge': {\n const { ok, ...rest } = checkChargeResult(\n await callbacks.onChargeCredits!(call.charge, context),\n );\n return ok\n ? jsonResponse(200, rest)\n : jsonResponse(402, {\n ...rest,\n error: {\n code: 'insufficient_credits',\n message: \"The user can't afford this action.\",\n },\n });\n }\n case 'credits.reverse':\n return jsonResponse(\n 200,\n checkReversalResult(\n await callbacks.onReverseCharge!(call.reversal, context),\n ),\n );\n }\n }\n\n /**\n * `GET /relume/session`, framework-agnostic: works out the signed-in user\n * with `getUserIdFromRequest`, calls `POST /sessions` and returns the\n * client secret, or passes the API's error through unchanged.\n */\n async handleSession(request: TRequest): Promise<RelumeResponse> {\n const resolve = this.#callbacks.getUserIdFromRequest;\n if (!resolve) {\n this.#onHandlerError(\n new Error('No getUserIdFromRequest callback is registered.'),\n { route: 'session' },\n );\n return INTERNAL_ERROR();\n }\n\n let userId: string;\n try {\n const resolved = await resolve(request);\n if (resolved === null || resolved === undefined || resolved === '') {\n return errorResponse(401, 'not_signed_in', 'Nobody is signed in.');\n }\n if (typeof resolved === 'number' && Number.isSafeInteger(resolved)) {\n userId = String(resolved);\n } else if (\n typeof resolved === 'string' &&\n resolved.length >= 1 &&\n resolved.length <= 128\n ) {\n userId = resolved;\n } else {\n throw new TypeError(\n 'getUserIdFromRequest must return a string of 1 to 128 characters, a whole number, or null.',\n );\n }\n } catch (error) {\n this.#onHandlerError(error, { route: 'session' });\n return INTERNAL_ERROR();\n }\n\n let response;\n try {\n // One attempt, no retries: a browser is waiting on this route.\n response = await apiRequest(this.#api, 'POST', '/sessions', {\n body: { userId },\n });\n } catch (error) {\n this.#onHandlerError(error, { route: 'session' });\n const status =\n error instanceof RelumeError && error.code === 'timeout' ? 504 : 502;\n return errorResponse(status, 'session_failed', \"Couldn't reach Relume.\");\n }\n\n if (response.status >= 200 && response.status < 300) {\n if (isSession(response.body)) {\n return jsonResponse(200, {\n clientSecret: response.body.clientSecret,\n expiresAt: response.body.expiresAt,\n });\n }\n this.#onHandlerError(errorFromResponse(response), {\n route: 'session',\n requestId: response.requestId,\n });\n return errorResponse(\n 502,\n 'session_failed',\n 'Relume answered without a session.',\n );\n }\n\n // Pass Relume's error through, so the embed can tell user_deactivated\n // from a failure. A body without `error` gets a generic one.\n const hasError =\n typeof response.body === 'object' &&\n response.body !== null &&\n 'error' in response.body;\n const body = hasError\n ? response.body\n : {\n error: {\n code: 'session_failed',\n message: `Relume answered ${response.status}.`,\n },\n };\n if (response.status === 401) {\n // Our own key was refused: the partner's problem, worth logging.\n this.#onHandlerError(errorFromResponse(response), {\n route: 'session',\n requestId: response.requestId,\n });\n }\n return jsonResponse(response.status, body);\n }\n}\n\nfunction assertFunction<T>(callback: T, name: string): T {\n if (typeof callback !== 'function') {\n throw new TypeError(`relume.${name} expects a function.`);\n }\n return callback;\n}\n"],"mappings":"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","names":["header"]}
|
|
1
|
+
{"version":3,"sources":["../src/adapters/express.ts","../src/adapters/fetch.ts","../src/api.ts","../src/calls.ts","../src/pages.ts","../src/resources.ts","../src/client.ts"],"sourcesContent":["// Express adapter. Uses only what node:http provides, so it also works with\n// Connect, Polka or a bare http.createServer, and needs no express import.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nimport {\n errorResponse,\n methodNotAllowed,\n mountedRouteOf,\n payloadTooLarge,\n preflight,\n type Handlers,\n type Route,\n} from '../response.ts';\nimport { REQUEST_ID_HEADER, SIGNATURE_HEADER } from '../signing.ts';\nimport type { RelumeResponse } from '../types.ts';\n\n/** An Express request, or a plain node:http one. */\nexport type NodeRequest = IncomingMessage & {\n body?: unknown;\n /** Set by Express to the mount path, e.g. '/relume'. */\n baseUrl?: string;\n};\n\nexport type NodeHandler = (\n request: NodeRequest,\n response: ServerResponse,\n next?: (error?: unknown) => void,\n) => void;\n\nclass PayloadTooLargeError extends Error {}\n\nclass BodyAlreadyReadError extends Error {\n constructor() {\n super(\n 'The request body was already read by a body parser, so the Relume signature ' +\n \"can't be checked. Mount relume.express() before express.json(), or \" +\n \"give it express.raw({ type: 'application/json' }).\",\n );\n this.name = 'BodyAlreadyReadError';\n }\n}\n\nfunction header(request: IncomingMessage, name: string): string | null {\n const value = request.headers[name.toLowerCase()];\n if (Array.isArray(value)) return value[0] ?? null;\n return value ?? null;\n}\n\nasync function readRawBody(\n request: NodeRequest,\n maxBodyBytes: number,\n): Promise<string | Uint8Array> {\n // express.raw() and express.text() leave the exact bytes on req.body.\n if (request.body instanceof Uint8Array) return request.body;\n if (typeof request.body === 'string') return request.body;\n // Another parser (express.json()) consumed the stream and kept only a\n // re-serialisable object, which can never verify.\n if (request.readableEnded) throw new BodyAlreadyReadError();\n\n if (Number(header(request, 'content-length')) > maxBodyBytes) {\n throw new PayloadTooLargeError();\n }\n // Events rather than for await: breaking out of an async iterator destroys\n // the socket, and the sender would get a reset instead of the 413. Over the\n // cap we stop listening and resume, so Node discards the rest unbuffered.\n return new Promise((resolve, reject) => {\n const chunks: Uint8Array[] = [];\n let size = 0;\n const cleanup = () => {\n request.off('data', onData);\n request.off('end', onEnd);\n request.off('error', onError);\n };\n const onData = (chunk: Uint8Array | string) => {\n const bytes =\n typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk;\n size += bytes.byteLength;\n if (size > maxBodyBytes) {\n cleanup();\n chunks.length = 0;\n request.resume();\n reject(new PayloadTooLargeError());\n return;\n }\n chunks.push(bytes);\n };\n const onEnd = () => {\n cleanup();\n const body = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n resolve(body);\n };\n const onError = (error: unknown) => {\n cleanup();\n reject(error);\n };\n request.on('data', onData);\n request.on('end', onEnd);\n request.on('error', onError);\n });\n}\n\nfunction send(\n request: IncomingMessage,\n response: ServerResponse,\n result: RelumeResponse,\n): void {\n if (response.headersSent) return;\n response.statusCode = result.status;\n for (const [name, value] of Object.entries(result.headers)) {\n response.setHeader(name, value);\n }\n response.end(\n request.method === 'HEAD' || result.status === 204\n ? undefined\n : JSON.stringify(result.body),\n );\n}\n\nexport function expressHandler(\n client: Handlers<any>,\n maxBodyBytes: number,\n onError: (error: unknown, route: Route) => void,\n): NodeHandler {\n return (request, response, next) => {\n const route = mountedRouteOf(\n new URL(request.url ?? '/', 'http://localhost').pathname,\n Boolean(request.baseUrl),\n );\n if (!route) {\n if (next) next();\n else\n send(request, response, errorResponse(404, 'not_found', 'Not found.'));\n return;\n }\n // A CORS preflight goes on to the app's CORS middleware, wherever it is.\n if (request.method === 'OPTIONS') {\n if (next) next();\n else send(request, response, preflight(route));\n return;\n }\n const wrongMethod = methodNotAllowed(route, request.method);\n if (wrongMethod) {\n send(request, response, wrongMethod);\n return;\n }\n\n const run = async (): Promise<RelumeResponse> => {\n if (route === 'session') return client.handleSession(request);\n let rawBody: string | Uint8Array;\n try {\n rawBody = await readRawBody(request, maxBodyBytes);\n } catch (error) {\n if (error instanceof PayloadTooLargeError) return payloadTooLarge();\n if (error instanceof BodyAlreadyReadError) {\n onError(error, route);\n return errorResponse(\n 500,\n 'internal_error',\n 'The partner failed to handle the call.',\n );\n }\n throw error;\n }\n return client.handleCall({\n rawBody,\n signatureHeader: header(request, SIGNATURE_HEADER),\n requestId: header(request, REQUEST_ID_HEADER),\n });\n };\n\n run().then(\n (result) => send(request, response, result),\n (error) => {\n if (next) next(error);\n else {\n onError(error, route);\n send(\n request,\n response,\n errorResponse(\n 500,\n 'internal_error',\n 'The partner failed to handle the call.',\n ),\n );\n }\n },\n );\n };\n}\n","// fetch-style adapter: (Request) => Response, for Next.js route handlers,\n// Hono, Cloudflare Workers, Bun and Deno.\n\nimport {\n errorResponse,\n methodNotAllowed,\n payloadTooLarge,\n preflight,\n routeOf,\n type Handlers,\n} from '../response.ts';\nimport { REQUEST_ID_HEADER, SIGNATURE_HEADER } from '../signing.ts';\nimport type { RelumeResponse } from '../types.ts';\n\nfunction toResponse(response: RelumeResponse, head: boolean): Response {\n const empty = head || response.status === 204;\n return new Response(empty ? null : JSON.stringify(response.body), {\n status: response.status,\n headers: response.headers,\n });\n}\n\n/**\n * The body's bytes, or null once it passes the cap. Reads chunk by chunk, so\n * a chunked or HTTP/2 body with no content-length can't be buffered past the\n * cap before the signature is checked.\n */\nasync function readCapped(\n request: Request,\n maxBodyBytes: number,\n): Promise<Uint8Array | null> {\n if (Number(request.headers.get('content-length')) > maxBodyBytes) {\n return null;\n }\n if (!request.body) return new Uint8Array(0);\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > maxBodyBytes) {\n // Release rather than cancel: the runtime discards the rest unread,\n // and the sender still gets the 413 instead of a reset.\n reader.releaseLock();\n return null;\n }\n chunks.push(value);\n }\n const body = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nexport function fetchHandler(\n client: Handlers<any>,\n maxBodyBytes: number,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n const head = request.method === 'HEAD';\n const route = routeOf(new URL(request.url).pathname);\n if (!route) {\n return toResponse(errorResponse(404, 'not_found', 'Not found.'), head);\n }\n // Your CORS middleware answers preflights before this when it's in front.\n if (request.method === 'OPTIONS') return toResponse(preflight(route), true);\n const wrongMethod = methodNotAllowed(route, request.method);\n if (wrongMethod) return toResponse(wrongMethod, head);\n\n if (route === 'session') {\n return toResponse(await client.handleSession(request), head);\n }\n\n const rawBody = await readCapped(request, maxBodyBytes);\n if (rawBody === null) return toResponse(payloadTooLarge(), false);\n return toResponse(\n await client.handleCall({\n rawBody,\n signatureHeader: request.headers.get(SIGNATURE_HEADER),\n requestId: request.headers.get(REQUEST_ID_HEADER),\n }),\n false,\n );\n };\n}\n","// The partner API (docs/partner-protocol/v1.md section 7), authenticated with\n// the secret key: one request, the retry loop around it, and the errors it\n// throws. The typed methods are in resources.ts.\n\nimport { isRecord } from './guards.ts';\nimport { REQUEST_ID_HEADER } from './signing.ts';\nimport type { PartnerApiErrorCode, Session } from './wire.ts';\n\nexport const DEFAULT_API_BASE_URL = 'https://apis.relume.io/nova/partner/v1';\n/** package.json's version; a test keeps them equal. */\nexport const SDK_VERSION = '2.0.1';\n/**\n * Sent on every request. Relume's firewall refuses requests with no\n * User-Agent, and Cloudflare Workers' fetch doesn't add one.\n */\nexport const USER_AGENT = `relume-partner-node/${SDK_VERSION}`;\n/** POST /sessions waits on user.get for a new user (4s), so allow more. */\nexport const DEFAULT_API_TIMEOUT_MS = 15_000;\n/** Retries after the first attempt, for 429s and (where safe) 5xx answers. */\nexport const DEFAULT_MAX_RETRIES = 2;\n\nconst RETRY_BASE_DELAY_MS = 500;\nconst RETRY_MAX_DELAY_MS = 8_000;\n/**\n * A 429's Retry-After longer than this isn't waited out: the 429 is thrown.\n * A 5xx's Retry-After (a proxy or maintenance page) is capped at the backoff\n * ceiling instead, so the retries still happen.\n */\nconst MAX_RETRY_AFTER_MS = 60_000;\n/**\n * Every other 5xx is retried, Cloudflare's 520 to 527 included: 501 and 505\n * won't change on a retry.\n */\nconst NON_RETRYABLE_5XX = new Set([501, 505]);\n\n/**\n * A partner API `error.code`, or one the SDK sets itself: `timeout` and\n * `network_error` when no response arrived, `unexpected_response` when one\n * arrived without the expected body. New API codes may appear.\n */\nexport type RelumeErrorCode =\n | PartnerApiErrorCode\n | 'timeout'\n | 'network_error'\n | 'unexpected_response'\n | (string & {});\n\n/** An error answer from the partner API, or a request that never got one. */\nexport class RelumeError extends Error {\n /** The HTTP status, or 0 when no response arrived. */\n readonly status: number;\n /** The API's `error.code`, safe to switch on. */\n readonly code: RelumeErrorCode;\n readonly requestId: string | null;\n\n constructor(input: {\n status: number;\n code: RelumeErrorCode;\n message: string;\n requestId?: string | null;\n cause?: unknown;\n }) {\n super(input.message, { cause: input.cause });\n this.name = 'RelumeError';\n this.status = input.status;\n this.code = input.code;\n this.requestId = input.requestId ?? null;\n }\n}\n\nexport interface ApiConfig {\n secretKey: string;\n baseUrl: string;\n timeoutMs: number;\n fetch: typeof fetch;\n maxRetries: number;\n sleep: (ms: number) => Promise<void>;\n now: () => number;\n}\n\nexport interface ApiResponse {\n status: number;\n /** The parsed JSON body, or null when it wasn't JSON. */\n body: unknown;\n requestId: string | null;\n /** Retry-After in milliseconds, or null when absent or unreadable. */\n retryAfterMs: number | null;\n}\n\nexport type Query = Record<string, string | number | undefined>;\n\n/** One request. Throws RelumeError (status 0) only when no response arrived. */\nexport async function apiRequest(\n config: ApiConfig,\n method: 'GET' | 'POST',\n path: string,\n options: { body?: unknown; query?: Query } = {},\n): Promise<ApiResponse> {\n const { body, query } = options;\n let response: Response;\n try {\n response = await config.fetch(\n `${config.baseUrl}${path}${queryString(query)}`,\n {\n method,\n headers: {\n Authorization: `Bearer ${config.secretKey}`,\n Accept: 'application/json',\n 'User-Agent': USER_AGENT,\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(config.timeoutMs),\n // Not 'error': Cloudflare Workers refuse it. A redirect is turned\n // into a network error below instead.\n redirect: 'manual',\n },\n );\n } catch (cause) {\n const timedOut =\n cause instanceof Error &&\n (cause.name === 'TimeoutError' || cause.name === 'AbortError');\n throw new RelumeError({\n status: 0,\n code: timedOut ? 'timeout' : 'network_error',\n message: timedOut\n ? `The Relume API didn't answer within ${config.timeoutMs}ms.`\n : \"Couldn't reach the Relume API.\",\n cause,\n });\n }\n\n // The API never redirects, so one means the base URL is wrong. Browsers\n // report a manual redirect as an opaque status 0.\n if (\n response.type === 'opaqueredirect' ||\n (response.status >= 300 && response.status < 400)\n ) {\n const status = response.status === 0 ? '' : ` (${response.status})`;\n throw new RedirectedError({\n status: 0,\n code: 'network_error',\n message: `The Relume API answered with a redirect${status}; check apiBaseUrl.`,\n });\n }\n\n const text = await response.text().catch(() => '');\n let parsed: unknown = null;\n if (text) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n }\n return {\n status: response.status,\n body: parsed,\n requestId: response.headers.get(REQUEST_ID_HEADER),\n retryAfterMs: parseRetryAfter(\n response.headers.get('Retry-After'),\n config.now(),\n ),\n };\n}\n\nfunction queryString(query: Query | undefined): string {\n if (!query) return '';\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) params.set(key, String(value));\n }\n const encoded = params.toString();\n return encoded ? `?${encoded}` : '';\n}\n\n/** Seconds, or an HTTP date. */\nexport function parseRetryAfter(\n header: string | null,\n nowMs: number,\n): number | null {\n if (header === null) return null;\n const value = header.trim();\n if (/^\\d+$/.test(value)) return Number(value) * 1000;\n const date = Date.parse(value);\n return Number.isNaN(date) ? null : Math.max(0, date - nowMs);\n}\n\n/** The API's `{ error: { code, message } }`, or a generic one. */\nexport function errorFromResponse(response: ApiResponse): RelumeError {\n const error = isRecord(response.body) ? response.body.error : undefined;\n const code = isRecord(error) ? error.code : undefined;\n const message = isRecord(error) ? error.message : undefined;\n return new RelumeError({\n status: response.status,\n code: typeof code === 'string' ? code : 'unexpected_response',\n message:\n typeof message === 'string'\n ? message\n : `The Relume API answered ${response.status}.`,\n requestId: response.requestId,\n });\n}\n\n/**\n * Which failures a request may retry. Every request retries a 429, which is\n * refused before any work. `unsafe` requests (they aren't idempotent and\n * have a lasting effect, like creating a site) retry nothing else: a 5xx or\n * a timeout may mean the work was done.\n */\nexport type RetryPolicy = 'safe' | 'unsafe';\n\nexport interface RequestSpec<T> {\n method: 'GET' | 'POST';\n path: string;\n query?: Query;\n body?: unknown;\n retry: RetryPolicy;\n /** A light check that the body is the object asked for. */\n isResult: (body: unknown) => body is T;\n}\n\n/**\n * A request with retries: 429s after Retry-After, and for `safe` requests\n * 5xx answers and failed connections with exponential backoff and jitter.\n * Throws RelumeError once retries run out or the answer is final.\n */\n/** A redirect from the API. Callers see a plain RelumeError. */\nclass RedirectedError extends RelumeError {}\n\nexport async function requestJson<T>(\n config: ApiConfig,\n spec: RequestSpec<T>,\n): Promise<T> {\n for (let attempt = 0; ; attempt += 1) {\n const canRetry = attempt < config.maxRetries;\n let response: ApiResponse;\n try {\n response = await apiRequest(config, spec.method, spec.path, {\n body: spec.body,\n query: spec.query,\n });\n } catch (error) {\n // A redirect isn't transient: retrying only delays the error.\n const transient = !(error instanceof RedirectedError);\n if (!canRetry || spec.retry !== 'safe' || !transient) throw error;\n await config.sleep(backoffMs(attempt));\n continue;\n }\n\n if (response.status >= 200 && response.status < 300) {\n if (spec.isResult(response.body)) return response.body;\n throw new RelumeError({\n status: response.status,\n code: 'unexpected_response',\n message: `${spec.method} ${spec.path} answered ${response.status} without the expected body.`,\n requestId: response.requestId,\n });\n }\n\n const retryable =\n response.status === 429 ||\n (spec.retry === 'safe' &&\n response.status >= 500 &&\n !NON_RETRYABLE_5XX.has(response.status));\n const delay =\n response.retryAfterMs === null\n ? backoffMs(attempt)\n : response.status === 429\n ? response.retryAfterMs\n : Math.min(response.retryAfterMs, RETRY_MAX_DELAY_MS);\n if (!canRetry || !retryable || delay > MAX_RETRY_AFTER_MS) {\n throw errorFromResponse(response);\n }\n await config.sleep(delay);\n }\n}\n\n/** Full jitter: anywhere up to 500ms, 1s, 2s, … capped at 8s. */\nfunction backoffMs(attempt: number): number {\n const ceiling = Math.min(\n RETRY_MAX_DELAY_MS,\n RETRY_BASE_DELAY_MS * 2 ** attempt,\n );\n return Math.round(Math.random() * ceiling);\n}\n\nexport function isSession(value: unknown): value is Session {\n return (\n isRecord(value) &&\n typeof value.clientSecret === 'string' &&\n typeof value.expiresAt === 'string'\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.\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 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\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: 'invalid_request' | 'unsupported_call_type';\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\nexport function parseCall(body: unknown): 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 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 { RelumeError } from './api.ts';\nimport { isRecord } from './guards.ts';\nimport type { Page } from './wire.ts';\n\nexport function isPage(value: unknown): value is Page<unknown> {\n return (\n isRecord(value) &&\n Array.isArray(value.data) &&\n typeof value.nextCursor === 'string' &&\n typeof value.hasMore === 'boolean'\n );\n}\n\n/**\n * A list endpoint, fetched a page at a time as you iterate:\n *\n * for await (const entry of relume.ledger.list({ after })) { … }\n *\n * Nothing is requested until iteration starts, and each iteration starts\n * again from the `after` the list was made with. Iterate `pages()` instead to\n * handle (and checkpoint) a page at a time. Iterate a list once at a time:\n * every iteration moves the same `nextCursor`, so two at once would leave it\n * wherever the last one got to.\n */\nexport class PagedList<T> implements AsyncIterable<T> {\n readonly #fetchPage: (after: string | undefined) => Promise<Page<T>>;\n readonly #after: string | undefined;\n #nextCursor: string | null;\n\n constructor(\n fetchPage: (after: string | undefined) => Promise<Page<T>>,\n after: string | undefined,\n ) {\n this.#fetchPage = fetchPage;\n this.#after = after;\n this.#nextCursor = after ?? null;\n }\n\n /**\n * Where to resume: the `nextCursor` of the last page whose items have all\n * been handed out, or the starting `after` (null when there was none)\n * before that. Once iteration ends, store it and pass it as `after` later\n * to get only what is new (on the ledger; site cursors don't poll).\n */\n get nextCursor(): string | null {\n return this.#nextCursor;\n }\n\n /** Every page in turn. The cursor advances when you ask for the next one. */\n async *pages(): AsyncGenerator<Page<T>, void, undefined> {\n let after = this.#after;\n for (;;) {\n const page = await this.#fetchPage(after);\n yield page;\n this.#nextCursor = page.nextCursor;\n if (!page.hasMore) return;\n if (page.nextCursor === after) {\n // A page that says there's more but doesn't move would loop forever.\n throw new RelumeError({\n status: 200,\n code: 'unexpected_response',\n message: 'The Relume API returned hasMore with the same cursor.',\n });\n }\n after = page.nextCursor;\n }\n }\n\n async *[Symbol.asyncIterator](): AsyncGenerator<T, void, undefined> {\n for await (const page of this.pages()) yield* page.data;\n }\n\n /** Every item in one array. Mind the size of what you're listing. */\n async toArray(): Promise<T[]> {\n const items: T[] = [];\n for await (const item of this) items.push(item);\n return items;\n }\n}\n","// A typed method for every partner API endpoint (protocol section 7).\n\nimport { isSession, requestJson, type ApiConfig, type Query } from './api.ts';\nimport { isRecord } from './guards.ts';\nimport { isPage, PagedList } from './pages.ts';\nimport type {\n CreatedSite,\n CreateSessionBody,\n CreateSiteBody,\n Export,\n LedgerEntry,\n LedgerEntryDetail,\n Page,\n PageQuery,\n Price,\n PriceList,\n Session,\n Site,\n UsageGroup,\n UsageQuery,\n UsageRow,\n User,\n} from './wire.ts';\n\nexport interface UsageParams extends Omit<UsageQuery, 'groupBy'> {\n /** One row per combination of these. Omitted: one row for the whole range. */\n groupBy?: readonly UsageGroup[];\n}\n\nexport interface PartnerApi {\n sessions: {\n /**\n * POST /sessions: a single-use client secret for this user's embed. The\n * first call for a userId creates their Relume account with user.get.\n */\n create(input: CreateSessionBody): Promise<Session>;\n };\n users: {\n /** GET /users/{userId}. `user_not_found` until their first session. */\n get(userId: string): Promise<User>;\n /**\n * POST /users/{userId}/deactivate: cuts off their sessions and export\n * links; data is kept. Idempotent, and retried on a 5xx.\n */\n deactivate(userId: string): Promise<User>;\n /** POST /users/{userId}/reactivate. `user_suspended` if Relume staff suspended them. */\n reactivate(userId: string): Promise<User>;\n };\n sites: {\n /**\n * GET /users/{userId}/sites, most recently updated first. Fetch from the\n * beginning each time: new and changed sites sort to the front, so a\n * stored cursor never shows them.\n */\n list(userId: string, params?: PageQuery): PagedList<Site>;\n /** POST /users/{userId}/sites: an empty site. Not idempotent, so a 5xx isn't retried. */\n create(userId: string, input?: CreateSiteBody): Promise<CreatedSite>;\n /** GET /sites/{siteId}. */\n get(siteId: string): Promise<Site>;\n };\n exports: {\n /**\n * GET /sites/{siteId}/exports/{exportId}. Not available yet: answers\n * `not_implemented` (501) until exports ship.\n */\n get(siteId: string, exportId: string): Promise<Export>;\n };\n ledger: {\n /**\n * GET /ledger, in change order: an entry appears again each time its\n * status changes, so upsert on `id`. Store the list's `nextCursor` when\n * you're done and pass it as `after` to get only what changed since.\n */\n list(params?: PageQuery): PagedList<LedgerEntry>;\n /** GET /ledger/{id}: one entry by chargeId or reversalId, with partnerResponse. */\n get(id: string): Promise<LedgerEntryDetail>;\n };\n usage: {\n /** GET /usage: credits totalled over [from, to), ordered by day, user, action. */\n get(params: UsageParams): PagedList<UsageRow>;\n };\n prices: {\n /** GET /prices: every billable action's price in this environment. */\n list(): Promise<Price[]>;\n };\n}\n\n// Light checks that the body is the object asked for, by its string ids.\n// The other fields are as the protocol says; unknown ones are kept, as it\n// requires.\nfunction hasStrings<T>(\n ...keys: (keyof T & string)[]\n): (value: unknown) => value is T {\n return (value): value is T =>\n isRecord(value) && keys.every((key) => typeof value[key] === 'string');\n}\n\nconst isUser = hasStrings<User>('userId', 'status');\nconst isSite = hasStrings<Site>('siteId', 'userId');\nconst isCreatedSite = hasStrings<CreatedSite>('siteId');\nconst isExport = hasStrings<Export>('exportId', 'status');\nconst isLedgerEntryDetail = hasStrings<LedgerEntryDetail>(\n 'id',\n 'type',\n 'status',\n);\n\nfunction isPriceList(value: unknown): value is PriceList {\n return isRecord(value) && Array.isArray(value.data);\n}\n\n/**\n * A path segment from a caller's id: never empty, always escaped. Throws, so\n * the Promise-returning methods call it inside an async function and reject.\n */\nfunction segment(name: string, value: unknown): string {\n if (typeof value !== 'string' || value === '') {\n throw new TypeError(`${name} must be a non-empty string.`);\n }\n return encodeURIComponent(value);\n}\n\nexport function partnerApi(config: ApiConfig): PartnerApi {\n const get = <T>(\n path: string,\n isResult: (body: unknown) => body is T,\n query?: Query,\n ) =>\n requestJson(config, {\n method: 'GET',\n path,\n query,\n retry: 'safe',\n isResult,\n });\n\n const list = <T>(\n path: string,\n { after, ...query }: Query & { after?: string },\n ) =>\n new PagedList<T>(\n (cursor) =>\n get(path, (body): body is Page<T> => isPage(body), {\n ...query,\n after: cursor,\n }),\n after,\n );\n\n return {\n sessions: {\n create: async ({ userId }) =>\n requestJson(config, {\n method: 'POST',\n path: '/sessions',\n body: { userId } satisfies CreateSessionBody,\n // Safe to retry: Relume provisions a new user once (later sessions\n // wait for or find the account), so a retry at most mints another\n // client secret.\n retry: 'safe',\n isResult: isSession,\n }),\n },\n users: {\n get: async (userId) => get(`/users/${segment('userId', userId)}`, isUser),\n deactivate: async (userId) =>\n requestJson(config, {\n method: 'POST',\n path: `/users/${segment('userId', userId)}/deactivate`,\n retry: 'safe',\n isResult: isUser,\n }),\n reactivate: async (userId) =>\n requestJson(config, {\n method: 'POST',\n path: `/users/${segment('userId', userId)}/reactivate`,\n retry: 'safe',\n isResult: isUser,\n }),\n },\n sites: {\n list: (userId, params = {}) =>\n list<Site>(`/users/${segment('userId', userId)}/sites`, { ...params }),\n create: async (userId, input = {}) =>\n requestJson(config, {\n method: 'POST',\n path: `/users/${segment('userId', userId)}/sites`,\n body: (input.name === undefined\n ? {}\n : { name: input.name }) satisfies CreateSiteBody,\n retry: 'unsafe',\n isResult: isCreatedSite,\n }),\n get: async (siteId) => get(`/sites/${segment('siteId', siteId)}`, isSite),\n },\n exports: {\n get: async (siteId, exportId) =>\n get(\n `/sites/${segment('siteId', siteId)}/exports/${segment('exportId', exportId)}`,\n isExport,\n ),\n },\n ledger: {\n list: (params = {}) => list<LedgerEntry>('/ledger', { ...params }),\n get: async (id) =>\n get(`/ledger/${segment('id', id)}`, isLedgerEntryDetail),\n },\n usage: {\n get: ({ groupBy, ...params }) =>\n list<UsageRow>('/usage', {\n ...params,\n groupBy: groupBy?.length ? groupBy.join(',') : undefined,\n }),\n },\n prices: {\n list: async () => (await get('/prices', isPriceList)).data,\n },\n };\n}\n","import { expressHandler, type NodeHandler } from './adapters/express.ts';\nimport { fetchHandler } from './adapters/fetch.ts';\nimport {\n apiRequest,\n DEFAULT_API_BASE_URL,\n DEFAULT_API_TIMEOUT_MS,\n DEFAULT_MAX_RETRIES,\n errorFromResponse,\n isSession,\n RelumeError,\n type ApiConfig,\n} from './api.ts';\nimport {\n checkChargeResult,\n checkCredits,\n checkReversalResult,\n checkUserProfile,\n parseCall,\n type IncomingCall,\n} from './calls.ts';\nimport { partnerApi, type PartnerApi } from './resources.ts';\nimport { errorResponse, jsonResponse, MAX_BODY_BYTES } from './response.ts';\nimport { refuseSessionRequest, type SessionRequest } from './session.ts';\nimport { verifySignature } from './signing.ts';\nimport type {\n CallContext,\n CallType,\n ChargeCreditsCallback,\n GetCreditsCallback,\n GetUserCallback,\n GetUserIdFromRequest,\n HandlerErrorInfo,\n RelumeResponse,\n ReverseChargeCallback,\n} from './types.ts';\n\nexport interface RelumeOptions {\n /** `sk_test_…` or `sk_live_…`. Server only: never send it to a browser. */\n secretKey: string;\n /**\n * Defaults to production. Relume gives you another URL for staging. Must\n * be https, since every request carries the secret key; http is allowed\n * only for localhost.\n */\n apiBaseUrl?: string;\n /** Per partner API request attempt. Default 15 seconds. */\n apiTimeoutMs?: number;\n /**\n * Retries after the first attempt. Default 2. A 429 is retried after its\n * Retry-After; a 5xx or a failed connection with backoff, except on\n * `sites.create`, which isn't idempotent. 0 turns retries off.\n */\n maxRetries?: number;\n /** Replace `fetch` for partner API requests, e.g. to add a proxy agent. */\n fetch?: typeof fetch;\n /**\n * Called when a callback throws or returns something the protocol doesn't\n * allow, or the session route fails. Defaults to console.error. The caller\n * always gets a 5xx; Relume never sees the error's message.\n */\n onHandlerError?: (error: unknown, info: HandlerErrorInfo) => void;\n /** The clock, in milliseconds. For tests. */\n now?: () => number;\n /** Waits between retries. For tests. */\n sleep?: (ms: number) => Promise<void>;\n}\n\ninterface Callbacks<TRequest> {\n getUserIdFromRequest?: GetUserIdFromRequest<TRequest>;\n onGetUser?: GetUserCallback;\n onGetCredits?: GetCreditsCallback;\n onChargeCredits?: ChargeCreditsCallback;\n onReverseCharge?: ReverseChargeCallback;\n}\n\nconst CALLBACK_FOR: Record<CallType, keyof Callbacks<unknown>> = {\n 'user.get': 'onGetUser',\n 'credits.get': 'onGetCredits',\n 'credits.charge': 'onChargeCredits',\n 'credits.reverse': 'onReverseCharge',\n};\n\nconst INTERNAL_ERROR = () =>\n errorResponse(\n 500,\n 'internal_error',\n 'The partner failed to handle the call.',\n );\n\nfunction defaultOnHandlerError(error: unknown, info: HandlerErrorInfo): void {\n const where =\n info.route === 'calls' ? `${info.type ?? 'call'}` : 'GET /relume/session';\n const requestId = info.requestId\n ? ` (Relume-Request-Id ${info.requestId})`\n : '';\n console.error(`[relume] ${where} failed${requestId}:`, error);\n}\n\n/**\n * A Relume partner client. Create one with `Relume.init`; there is no global\n * state, so test and live clients can sit side by side.\n *\n * `TRequest` is the request your `getUserIdFromRequest` receives: the\n * Express/Node request for `express()`, the `Request` for `fetchHandler`.\n */\nexport class Relume<TRequest extends SessionRequest = SessionRequest> {\n // The partner API. Each method throws RelumeError when the API refuses.\n readonly sessions: PartnerApi['sessions'];\n readonly users: PartnerApi['users'];\n readonly sites: PartnerApi['sites'];\n readonly exports: PartnerApi['exports'];\n readonly ledger: PartnerApi['ledger'];\n readonly usage: PartnerApi['usage'];\n readonly prices: PartnerApi['prices'];\n\n /**\n * A fetch-style handler for Next.js route handlers, Hono, Cloudflare\n * Workers, Bun and Deno. Answers `GET …/session` and `POST …/calls`.\n */\n readonly fetchHandler: (request: Request) => Promise<Response>;\n\n readonly #secretKey: string;\n readonly #api: ApiConfig;\n readonly #callbacks: Callbacks<TRequest> = {};\n readonly #onHandlerError: (error: unknown, info: HandlerErrorInfo) => void;\n readonly #now: () => number;\n\n static init<TRequest extends SessionRequest = SessionRequest>(\n options: RelumeOptions,\n ): Relume<TRequest> {\n return new Relume<TRequest>(options);\n }\n\n constructor(options: RelumeOptions) {\n // A key read from a file or .env often ends in a newline, which would\n // make every signature check and API request fail.\n const secretKey =\n typeof options?.secretKey === 'string' ? options.secretKey.trim() : '';\n if (secretKey === '') {\n throw new TypeError(\n 'Relume.init: secretKey is required (sk_test_… or sk_live_…). Is the environment variable set?',\n );\n }\n if (secretKey.startsWith('pk_')) {\n throw new TypeError(\n 'Relume.init: that is a publishable key. Pass the secret key (sk_…); the publishable key goes to the embed.',\n );\n }\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {\n throw new TypeError(\n 'Relume.init: maxRetries must be a whole number, 0 or more.',\n );\n }\n this.#secretKey = secretKey;\n this.#now = options.now ?? Date.now;\n this.#api = {\n secretKey,\n baseUrl: checkApiBaseUrl(options.apiBaseUrl ?? DEFAULT_API_BASE_URL),\n timeoutMs: options.apiTimeoutMs ?? DEFAULT_API_TIMEOUT_MS,\n fetch: options.fetch ?? ((...args) => fetch(...args)),\n maxRetries,\n sleep:\n options.sleep ??\n ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),\n now: this.#now,\n };\n this.#onHandlerError = options.onHandlerError ?? defaultOnHandlerError;\n\n const api = partnerApi(this.#api);\n this.sessions = api.sessions;\n this.users = api.users;\n this.sites = api.sites;\n this.exports = api.exports;\n this.ledger = api.ledger;\n this.usage = api.usage;\n this.prices = api.prices;\n this.fetchHandler = fetchHandler(this, MAX_BODY_BYTES);\n }\n\n /** Who is signed in, for the browser's `GET /relume/session`. */\n getUserIdFromRequest(callback: GetUserIdFromRequest<TRequest>): this {\n this.#callbacks.getUserIdFromRequest = assertFunction(\n callback,\n 'getUserIdFromRequest',\n );\n return this;\n }\n\n /** Answers `user.get`: the user's profile, or null if you don't know them. */\n onGetUser(callback: GetUserCallback): this {\n this.#callbacks.onGetUser = assertFunction(callback, 'onGetUser');\n return this;\n }\n\n /** Answers `credits.get`: `{ balance }` in your own credits. */\n onGetCredits(callback: GetCreditsCallback): this {\n this.#callbacks.onGetCredits = assertFunction(callback, 'onGetCredits');\n return this;\n }\n\n /**\n * Answers `credits.charge`. Debit `partnerCredits` (never `relumeCredits`),\n * at most once per `chargeId`, and return `{ ok, balance }`.\n */\n onChargeCredits(callback: ChargeCreditsCallback): this {\n this.#callbacks.onChargeCredits = assertFunction(\n callback,\n 'onChargeCredits',\n );\n return this;\n }\n\n /**\n * Answers `credits.reverse`. Credit back `partnerCredits` (never\n * `relumeCredits`), at most once per `reversalId`. If `chargeId` names a\n * debit you never made, change nothing and return normally.\n */\n onReverseCharge(callback: ReverseChargeCallback): this {\n this.#callbacks.onReverseCharge = assertFunction(\n callback,\n 'onReverseCharge',\n );\n return this;\n }\n\n /**\n * Express (or Connect, or plain `node:http`) middleware. Mount it before\n * any body parser: `app.use('/relume', relume.express())`.\n */\n express(): NodeHandler {\n return expressHandler(this, MAX_BODY_BYTES, (error, route) =>\n this.#onHandlerError(error, { route }),\n );\n }\n\n /**\n * `POST /relume/calls`, framework-agnostic: verifies the signature and the\n * 5-minute window, routes by `type`, runs your callback and validates what\n * it returns. Pass the raw body exactly as received.\n */\n async handleCall(input: {\n rawBody: string | Uint8Array;\n signatureHeader: string | null | undefined;\n requestId?: string | null;\n }): Promise<RelumeResponse> {\n const requestId = input.requestId ?? null;\n const verified = await verifySignature({\n secret: this.#secretKey,\n header: input.signatureHeader,\n rawBody: input.rawBody,\n nowSeconds: Math.floor(this.#now() / 1000),\n });\n if (!verified.valid) {\n const message =\n verified.reason === 'timestamp_out_of_tolerance'\n ? 'The signature timestamp is more than 5 minutes from this server’s clock.'\n : verified.reason === 'malformed_header'\n ? 'Missing or malformed Relume-Signature header.'\n : \"The signature doesn't match this server's secret key.\";\n return errorResponse(401, 'invalid_signature', message);\n }\n\n let body: unknown;\n try {\n body =\n typeof input.rawBody === 'string'\n ? JSON.parse(input.rawBody)\n : JSON.parse(new TextDecoder().decode(input.rawBody));\n } catch {\n return errorResponse(\n 400,\n 'invalid_request',\n 'The body is not valid JSON.',\n );\n }\n\n const parsed = parseCall(body);\n if (!parsed.ok) return errorResponse(400, parsed.code, parsed.message);\n\n const { call } = parsed;\n const context: CallContext = { type: call.type, requestId };\n const callbackName = CALLBACK_FOR[call.type];\n if (!this.#callbacks[callbackName]) {\n this.#onHandlerError(\n new Error(\n `No ${callbackName} callback is registered, so ${call.type} was answered 400.`,\n ),\n { route: 'calls', type: call.type, requestId },\n );\n return errorResponse(\n 400,\n 'unsupported_call_type',\n `This partner doesn't handle ${call.type}.`,\n );\n }\n\n try {\n return await this.#runCallback(call, context);\n } catch (error) {\n this.#onHandlerError(error, {\n route: 'calls',\n type: call.type,\n requestId,\n });\n return INTERNAL_ERROR();\n }\n }\n\n async #runCallback(\n call: IncomingCall,\n context: CallContext,\n ): Promise<RelumeResponse> {\n const callbacks = this.#callbacks;\n switch (call.type) {\n case 'user.get': {\n const profile = checkUserProfile(\n await callbacks.onGetUser!(call.userId, context),\n );\n return profile\n ? jsonResponse(200, profile)\n : errorResponse(404, 'user_not_found', 'Unknown user.');\n }\n case 'credits.get':\n return jsonResponse(\n 200,\n checkCredits(await callbacks.onGetCredits!(call.userId, context)),\n );\n case 'credits.charge': {\n const { ok, ...rest } = checkChargeResult(\n await callbacks.onChargeCredits!(call.charge, context),\n );\n return ok\n ? jsonResponse(200, rest)\n : jsonResponse(402, {\n ...rest,\n error: {\n code: 'insufficient_credits',\n message: \"The user can't afford this action.\",\n },\n });\n }\n case 'credits.reverse':\n return jsonResponse(\n 200,\n checkReversalResult(\n await callbacks.onReverseCharge!(call.reversal, context),\n ),\n );\n }\n }\n\n /**\n * `GET /relume/session`, framework-agnostic: refuses a request from\n * another site or without the embed's `Relume-Embed` header, works out the\n * signed-in user with `getUserIdFromRequest`, calls `POST /sessions` and\n * returns the client secret, or passes the API's error through unchanged.\n * `request` must carry its headers: the fetch Request, the Node request,\n * or an object with `headers`.\n */\n async handleSession(request: TRequest): Promise<RelumeResponse> {\n const refused = refuseSessionRequest(request);\n if (refused) {\n if (refused.error) {\n this.#onHandlerError(refused.error, { route: 'session' });\n }\n return refused.response;\n }\n\n const resolve = this.#callbacks.getUserIdFromRequest;\n if (!resolve) {\n this.#onHandlerError(\n new Error('No getUserIdFromRequest callback is registered.'),\n { route: 'session' },\n );\n return INTERNAL_ERROR();\n }\n\n let userId: string;\n try {\n const resolved = await resolve(request);\n if (resolved === null || resolved === undefined || resolved === '') {\n return errorResponse(401, 'not_signed_in', 'Nobody is signed in.');\n }\n if (typeof resolved === 'number' && Number.isSafeInteger(resolved)) {\n userId = String(resolved);\n } else if (\n typeof resolved === 'string' &&\n resolved.length >= 1 &&\n resolved.length <= 128\n ) {\n userId = resolved;\n } else {\n throw new TypeError(\n 'getUserIdFromRequest must return a string of 1 to 128 characters, a whole number, or null.',\n );\n }\n } catch (error) {\n this.#onHandlerError(error, { route: 'session' });\n return INTERNAL_ERROR();\n }\n\n let response;\n try {\n // One attempt, no retries: a browser is waiting on this route.\n response = await apiRequest(this.#api, 'POST', '/sessions', {\n body: { userId },\n });\n } catch (error) {\n this.#onHandlerError(error, { route: 'session' });\n const status =\n error instanceof RelumeError && error.code === 'timeout' ? 504 : 502;\n return errorResponse(status, 'session_failed', \"Couldn't reach Relume.\");\n }\n\n if (response.status >= 200 && response.status < 300) {\n if (isSession(response.body)) {\n return jsonResponse(200, {\n clientSecret: response.body.clientSecret,\n expiresAt: response.body.expiresAt,\n });\n }\n this.#onHandlerError(errorFromResponse(response), {\n route: 'session',\n requestId: response.requestId,\n });\n return errorResponse(\n 502,\n 'session_failed',\n 'Relume answered without a session.',\n );\n }\n\n // Pass Relume's error through, so the embed can tell user_deactivated\n // from a failure. A body without `error` gets a generic one.\n const hasError =\n typeof response.body === 'object' &&\n response.body !== null &&\n 'error' in response.body;\n const body = hasError\n ? response.body\n : {\n error: {\n code: 'session_failed',\n message: `Relume answered ${response.status}.`,\n },\n };\n if (response.status === 401) {\n // Our own key was refused: the partner's problem, worth logging.\n this.#onHandlerError(errorFromResponse(response), {\n route: 'session',\n requestId: response.requestId,\n });\n }\n return jsonResponse(response.status, body);\n }\n}\n\nconst LOOPBACK = /^(localhost|127(\\.\\d{1,3}){3}|\\[::1\\])$/;\n\n/** The base URL without trailing slashes, refusing one that would send the key in clear. */\nfunction checkApiBaseUrl(value: string): string {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new TypeError(`Relume.init: apiBaseUrl isn't a URL: ${value}`);\n }\n const local = url.protocol === 'http:' && LOOPBACK.test(url.hostname);\n if (url.protocol !== 'https:' && !local) {\n throw new TypeError(\n 'Relume.init: apiBaseUrl must be an https URL (http only for localhost): every request carries your secret key.',\n );\n }\n return value.replace(/\\/+$/, '');\n}\n\nfunction assertFunction<T>(callback: T, name: string): T {\n if (typeof callback !== 'function') {\n throw new TypeError(`relume.${name} expects a function.`);\n }\n return callback;\n}\n"],"mappings":"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","names":["header"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relume_io/partner-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Server SDK for Relume partners: answer Relume's signed calls and serve builder sessions without writing any cryptography.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"homepage": "https://apis.relume.io/nova/docs/partners",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./dist/index.cjs",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/signing.ts","../src/guards.ts"],"sourcesContent":["// 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","// 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"],"mappings":"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","names":[]}
|