@relume_io/partner-node 0.0.0-stage → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1275 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_API_BASE_URL: () => DEFAULT_API_BASE_URL,
24
+ EMBED_HEADER: () => EMBED_HEADER,
25
+ EMBED_HEADER_VALUE: () => EMBED_HEADER_VALUE,
26
+ InvalidCallbackResultError: () => InvalidCallbackResultError,
27
+ MAX_SIGNATURES: () => MAX_SIGNATURES,
28
+ PagedList: () => PagedList,
29
+ REQUEST_ID_HEADER: () => REQUEST_ID_HEADER,
30
+ Relume: () => Relume,
31
+ RelumeError: () => RelumeError,
32
+ SIGNATURE_HEADER: () => SIGNATURE_HEADER,
33
+ SIGNATURE_TOLERANCE_SECONDS: () => SIGNATURE_TOLERANCE_SECONDS,
34
+ computeSignature: () => computeSignature,
35
+ parseSignatureHeader: () => parseSignatureHeader,
36
+ signCall: () => signCall,
37
+ verifySignature: () => verifySignature
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/response.ts
42
+ var MAX_BODY_BYTES = 1024 * 1024;
43
+ var JSON_HEADERS = {
44
+ "Content-Type": "application/json; charset=utf-8",
45
+ "Cache-Control": "no-store"
46
+ };
47
+ function jsonResponse(status, body, headers = {}) {
48
+ return { status, headers: { ...JSON_HEADERS, ...headers }, body };
49
+ }
50
+ function errorResponse(status, code, message, headers) {
51
+ return jsonResponse(status, { error: { code, message } }, headers);
52
+ }
53
+ var METHOD_FOR = { session: "GET", calls: "POST" };
54
+ function routeOf(pathname) {
55
+ const last = pathname.replace(/\/+$/, "").split("/").pop();
56
+ return last === "session" || last === "calls" ? last : null;
57
+ }
58
+ function mountedRouteOf(pathname, mounted) {
59
+ const path = pathname.replace(/(.)\/+$/, "$1");
60
+ const prefix = mounted ? "" : "/relume";
61
+ if (path === `${prefix}/session`) return "session";
62
+ if (path === `${prefix}/calls`) return "calls";
63
+ return null;
64
+ }
65
+ function methodNotAllowed(route, method) {
66
+ const allowed = METHOD_FOR[route];
67
+ if (method === allowed) return null;
68
+ return errorResponse(
69
+ 405,
70
+ "method_not_allowed",
71
+ `Use ${allowed} for this route.`,
72
+ { Allow: allowed }
73
+ );
74
+ }
75
+ function payloadTooLarge() {
76
+ return errorResponse(413, "payload_too_large", "The body is too large.");
77
+ }
78
+ function preflight(route) {
79
+ return { status: 204, headers: { Allow: METHOD_FOR[route] }, body: null };
80
+ }
81
+
82
+ // src/signing.ts
83
+ var SIGNATURE_HEADER = "Relume-Signature";
84
+ var REQUEST_ID_HEADER = "Relume-Request-Id";
85
+ var SIGNATURE_SCHEME = "v1";
86
+ var SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
87
+ var MAX_SIGNATURES = 2;
88
+ var HEX_SHA256 = /^[0-9a-f]{64}$/i;
89
+ var UNIX_SECONDS = /^(0|[1-9]\d{0,11})$/;
90
+ var encoder = new TextEncoder();
91
+ function bodyBytes(rawBody) {
92
+ return typeof rawBody === "string" ? encoder.encode(rawBody) : rawBody;
93
+ }
94
+ function signedMessage(timestamp, rawBody) {
95
+ const prefix = encoder.encode(`${timestamp}.`);
96
+ const body = bodyBytes(rawBody);
97
+ const message = new Uint8Array(prefix.length + body.length);
98
+ message.set(prefix, 0);
99
+ message.set(body, prefix.length);
100
+ return message;
101
+ }
102
+ function hmacKey(secret, usage) {
103
+ return crypto.subtle.importKey(
104
+ "raw",
105
+ encoder.encode(secret),
106
+ { name: "HMAC", hash: "SHA-256" },
107
+ false,
108
+ [usage]
109
+ );
110
+ }
111
+ function toHex(bytes) {
112
+ return Array.from(
113
+ new Uint8Array(bytes),
114
+ (byte) => byte.toString(16).padStart(2, "0")
115
+ ).join("");
116
+ }
117
+ function fromHex(hex) {
118
+ const bytes = new Uint8Array(hex.length / 2);
119
+ for (let i = 0; i < bytes.length; i += 1) {
120
+ bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
121
+ }
122
+ return bytes;
123
+ }
124
+ function assertTimestamp(timestamp) {
125
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0) {
126
+ throw new TypeError("timestamp must be whole unix seconds");
127
+ }
128
+ }
129
+ async function computeSignature(secret, timestamp, rawBody) {
130
+ if (typeof secret !== "string" || secret.length === 0) {
131
+ throw new TypeError("secret must be a non-empty string");
132
+ }
133
+ assertTimestamp(timestamp);
134
+ const key = await hmacKey(secret, "sign");
135
+ return toHex(
136
+ await crypto.subtle.sign("HMAC", key, signedMessage(timestamp, rawBody))
137
+ );
138
+ }
139
+ async function signCall(secrets, timestamp, rawBody) {
140
+ const list = typeof secrets === "string" ? [secrets] : secrets;
141
+ if (list.length === 0 || list.length > MAX_SIGNATURES) {
142
+ throw new TypeError(
143
+ `expected 1 to ${MAX_SIGNATURES} secrets, got ${list.length}`
144
+ );
145
+ }
146
+ const signatures = await Promise.all(
147
+ list.map(async (secret) => {
148
+ const hex = await computeSignature(secret, timestamp, rawBody);
149
+ return `${SIGNATURE_SCHEME}=${hex}`;
150
+ })
151
+ );
152
+ return [`t=${timestamp}`, ...signatures].join(",");
153
+ }
154
+ function parseSignatureHeader(header2) {
155
+ let timestamp = null;
156
+ const signatures = [];
157
+ for (const part of header2.split(",")) {
158
+ const separator = part.indexOf("=");
159
+ if (separator === -1) continue;
160
+ const name = part.slice(0, separator).trim();
161
+ const value = part.slice(separator + 1).trim();
162
+ if (name === "t") {
163
+ if (timestamp !== null || !UNIX_SECONDS.test(value)) return null;
164
+ timestamp = Number(value);
165
+ } else if (name === SIGNATURE_SCHEME) {
166
+ if (signatures.length === MAX_SIGNATURES) return null;
167
+ signatures.push(value);
168
+ }
169
+ }
170
+ if (timestamp === null || signatures.length === 0) return null;
171
+ return { timestamp, signatures };
172
+ }
173
+ async function verifySignature(input) {
174
+ const parsed = input.header ? parseSignatureHeader(input.header) : null;
175
+ if (parsed === null) return { valid: false, reason: "malformed_header" };
176
+ if (typeof input.secret !== "string" || input.secret.length === 0) {
177
+ return { valid: false, reason: "signature_mismatch" };
178
+ }
179
+ const key = await hmacKey(input.secret, "verify");
180
+ const message = signedMessage(parsed.timestamp, input.rawBody);
181
+ const results = await Promise.all(
182
+ parsed.signatures.map(
183
+ (signature) => HEX_SHA256.test(signature) ? crypto.subtle.verify("HMAC", key, fromHex(signature), message) : Promise.resolve(false)
184
+ )
185
+ );
186
+ if (!results.includes(true)) {
187
+ return { valid: false, reason: "signature_mismatch" };
188
+ }
189
+ const now = input.nowSeconds ?? Math.floor(Date.now() / 1e3);
190
+ if (Math.abs(now - parsed.timestamp) > SIGNATURE_TOLERANCE_SECONDS) {
191
+ return { valid: false, reason: "timestamp_out_of_tolerance" };
192
+ }
193
+ return { valid: true, timestamp: parsed.timestamp };
194
+ }
195
+
196
+ // src/adapters/express.ts
197
+ var PayloadTooLargeError = class extends Error {
198
+ };
199
+ var BodyAlreadyReadError = class extends Error {
200
+ constructor() {
201
+ super(
202
+ "The request body was already read by a body parser, so the Relume signature can't be checked. Mount relume.express() before express.json(), or give it express.raw({ type: 'application/json' })."
203
+ );
204
+ this.name = "BodyAlreadyReadError";
205
+ }
206
+ };
207
+ function header(request, name) {
208
+ const value = request.headers[name.toLowerCase()];
209
+ if (Array.isArray(value)) return value[0] ?? null;
210
+ return value ?? null;
211
+ }
212
+ async function readRawBody(request, maxBodyBytes) {
213
+ if (request.body instanceof Uint8Array) return request.body;
214
+ if (typeof request.body === "string") return request.body;
215
+ if (request.readableEnded) throw new BodyAlreadyReadError();
216
+ if (Number(header(request, "content-length")) > maxBodyBytes) {
217
+ throw new PayloadTooLargeError();
218
+ }
219
+ return new Promise((resolve, reject) => {
220
+ const chunks = [];
221
+ let size = 0;
222
+ const cleanup = () => {
223
+ request.off("data", onData);
224
+ request.off("end", onEnd);
225
+ request.off("error", onError);
226
+ };
227
+ const onData = (chunk) => {
228
+ const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
229
+ size += bytes.byteLength;
230
+ if (size > maxBodyBytes) {
231
+ cleanup();
232
+ chunks.length = 0;
233
+ request.resume();
234
+ reject(new PayloadTooLargeError());
235
+ return;
236
+ }
237
+ chunks.push(bytes);
238
+ };
239
+ const onEnd = () => {
240
+ cleanup();
241
+ const body = new Uint8Array(size);
242
+ let offset = 0;
243
+ for (const chunk of chunks) {
244
+ body.set(chunk, offset);
245
+ offset += chunk.byteLength;
246
+ }
247
+ resolve(body);
248
+ };
249
+ const onError = (error) => {
250
+ cleanup();
251
+ reject(error);
252
+ };
253
+ request.on("data", onData);
254
+ request.on("end", onEnd);
255
+ request.on("error", onError);
256
+ });
257
+ }
258
+ function send(request, response, result) {
259
+ if (response.headersSent) return;
260
+ response.statusCode = result.status;
261
+ for (const [name, value] of Object.entries(result.headers)) {
262
+ response.setHeader(name, value);
263
+ }
264
+ response.end(
265
+ request.method === "HEAD" || result.status === 204 ? void 0 : JSON.stringify(result.body)
266
+ );
267
+ }
268
+ function expressHandler(client, maxBodyBytes, onError) {
269
+ return (request, response, next) => {
270
+ const route = mountedRouteOf(
271
+ new URL(request.url ?? "/", "http://localhost").pathname,
272
+ Boolean(request.baseUrl)
273
+ );
274
+ if (!route) {
275
+ if (next) next();
276
+ else
277
+ send(request, response, errorResponse(404, "not_found", "Not found."));
278
+ return;
279
+ }
280
+ if (request.method === "OPTIONS") {
281
+ if (next) next();
282
+ else send(request, response, preflight(route));
283
+ return;
284
+ }
285
+ const wrongMethod = methodNotAllowed(route, request.method);
286
+ if (wrongMethod) {
287
+ send(request, response, wrongMethod);
288
+ return;
289
+ }
290
+ const run = async () => {
291
+ if (route === "session") return client.handleSession(request);
292
+ let rawBody;
293
+ try {
294
+ rawBody = await readRawBody(request, maxBodyBytes);
295
+ } catch (error) {
296
+ if (error instanceof PayloadTooLargeError) return payloadTooLarge();
297
+ if (error instanceof BodyAlreadyReadError) {
298
+ onError(error, route);
299
+ return errorResponse(
300
+ 500,
301
+ "internal_error",
302
+ "The partner failed to handle the call."
303
+ );
304
+ }
305
+ throw error;
306
+ }
307
+ return client.handleCall({
308
+ rawBody,
309
+ signatureHeader: header(request, SIGNATURE_HEADER),
310
+ requestId: header(request, REQUEST_ID_HEADER)
311
+ });
312
+ };
313
+ run().then(
314
+ (result) => send(request, response, result),
315
+ (error) => {
316
+ if (next) next(error);
317
+ else {
318
+ onError(error, route);
319
+ send(
320
+ request,
321
+ response,
322
+ errorResponse(
323
+ 500,
324
+ "internal_error",
325
+ "The partner failed to handle the call."
326
+ )
327
+ );
328
+ }
329
+ }
330
+ );
331
+ };
332
+ }
333
+
334
+ // src/adapters/fetch.ts
335
+ function toResponse(response, head) {
336
+ const empty = head || response.status === 204;
337
+ return new Response(empty ? null : JSON.stringify(response.body), {
338
+ status: response.status,
339
+ headers: response.headers
340
+ });
341
+ }
342
+ async function readCapped(request, maxBodyBytes) {
343
+ if (Number(request.headers.get("content-length")) > maxBodyBytes) {
344
+ return null;
345
+ }
346
+ if (!request.body) return new Uint8Array(0);
347
+ const reader = request.body.getReader();
348
+ const chunks = [];
349
+ let size = 0;
350
+ for (; ; ) {
351
+ const { done, value } = await reader.read();
352
+ if (done) break;
353
+ size += value.byteLength;
354
+ if (size > maxBodyBytes) {
355
+ reader.releaseLock();
356
+ return null;
357
+ }
358
+ chunks.push(value);
359
+ }
360
+ const body = new Uint8Array(size);
361
+ let offset = 0;
362
+ for (const chunk of chunks) {
363
+ body.set(chunk, offset);
364
+ offset += chunk.byteLength;
365
+ }
366
+ return body;
367
+ }
368
+ function fetchHandler(client, maxBodyBytes) {
369
+ return async (request) => {
370
+ const head = request.method === "HEAD";
371
+ const route = routeOf(new URL(request.url).pathname);
372
+ if (!route) {
373
+ return toResponse(errorResponse(404, "not_found", "Not found."), head);
374
+ }
375
+ if (request.method === "OPTIONS") return toResponse(preflight(route), true);
376
+ const wrongMethod = methodNotAllowed(route, request.method);
377
+ if (wrongMethod) return toResponse(wrongMethod, head);
378
+ if (route === "session") {
379
+ return toResponse(await client.handleSession(request), head);
380
+ }
381
+ const rawBody = await readCapped(request, maxBodyBytes);
382
+ if (rawBody === null) return toResponse(payloadTooLarge(), false);
383
+ return toResponse(
384
+ await client.handleCall({
385
+ rawBody,
386
+ signatureHeader: request.headers.get(SIGNATURE_HEADER),
387
+ requestId: request.headers.get(REQUEST_ID_HEADER)
388
+ }),
389
+ false
390
+ );
391
+ };
392
+ }
393
+
394
+ // src/guards.ts
395
+ function isRecord(value) {
396
+ return typeof value === "object" && value !== null && !Array.isArray(value);
397
+ }
398
+ function isString(value, min, max) {
399
+ return typeof value === "string" && value.length >= min && value.length <= max;
400
+ }
401
+ function isWhole(value, min) {
402
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= min;
403
+ }
404
+ function absent(value) {
405
+ return value === void 0 || value === null || value === "";
406
+ }
407
+ var EMAIL = /^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/;
408
+ function isEmail(value) {
409
+ return typeof value === "string" && EMAIL.test(value);
410
+ }
411
+ function isHttpsUrl(value) {
412
+ if (typeof value !== "string") return false;
413
+ try {
414
+ return new URL(value).protocol === "https:";
415
+ } catch {
416
+ return false;
417
+ }
418
+ }
419
+
420
+ // src/api.ts
421
+ var DEFAULT_API_BASE_URL = "https://apis.relume.io/nova/partner/v1";
422
+ var SDK_VERSION = "2.0.0";
423
+ var USER_AGENT = `relume-partner-node/${SDK_VERSION}`;
424
+ var DEFAULT_API_TIMEOUT_MS = 15e3;
425
+ var DEFAULT_MAX_RETRIES = 2;
426
+ var RETRY_BASE_DELAY_MS = 500;
427
+ var RETRY_MAX_DELAY_MS = 8e3;
428
+ var MAX_RETRY_AFTER_MS = 6e4;
429
+ var NON_RETRYABLE_5XX = /* @__PURE__ */ new Set([501, 505]);
430
+ var RelumeError = class extends Error {
431
+ /** The HTTP status, or 0 when no response arrived. */
432
+ status;
433
+ /** The API's `error.code`, safe to switch on. */
434
+ code;
435
+ requestId;
436
+ constructor(input) {
437
+ super(input.message, { cause: input.cause });
438
+ this.name = "RelumeError";
439
+ this.status = input.status;
440
+ this.code = input.code;
441
+ this.requestId = input.requestId ?? null;
442
+ }
443
+ };
444
+ async function apiRequest(config, method, path, options = {}) {
445
+ const { body, query } = options;
446
+ let response;
447
+ try {
448
+ response = await config.fetch(
449
+ `${config.baseUrl}${path}${queryString(query)}`,
450
+ {
451
+ method,
452
+ headers: {
453
+ Authorization: `Bearer ${config.secretKey}`,
454
+ Accept: "application/json",
455
+ "User-Agent": USER_AGENT,
456
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
457
+ },
458
+ body: body === void 0 ? void 0 : JSON.stringify(body),
459
+ signal: AbortSignal.timeout(config.timeoutMs),
460
+ // Not 'error': Cloudflare Workers refuse it. A redirect is turned
461
+ // into a network error below instead.
462
+ redirect: "manual"
463
+ }
464
+ );
465
+ } catch (cause) {
466
+ const timedOut = cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
467
+ throw new RelumeError({
468
+ status: 0,
469
+ code: timedOut ? "timeout" : "network_error",
470
+ message: timedOut ? `The Relume API didn't answer within ${config.timeoutMs}ms.` : "Couldn't reach the Relume API.",
471
+ cause
472
+ });
473
+ }
474
+ if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
475
+ const status = response.status === 0 ? "" : ` (${response.status})`;
476
+ throw new RedirectedError({
477
+ status: 0,
478
+ code: "network_error",
479
+ message: `The Relume API answered with a redirect${status}; check apiBaseUrl.`
480
+ });
481
+ }
482
+ const text = await response.text().catch(() => "");
483
+ let parsed = null;
484
+ if (text) {
485
+ try {
486
+ parsed = JSON.parse(text);
487
+ } catch {
488
+ parsed = null;
489
+ }
490
+ }
491
+ return {
492
+ status: response.status,
493
+ body: parsed,
494
+ requestId: response.headers.get(REQUEST_ID_HEADER),
495
+ retryAfterMs: parseRetryAfter(
496
+ response.headers.get("Retry-After"),
497
+ config.now()
498
+ )
499
+ };
500
+ }
501
+ function queryString(query) {
502
+ if (!query) return "";
503
+ const params = new URLSearchParams();
504
+ for (const [key, value] of Object.entries(query)) {
505
+ if (value !== void 0) params.set(key, String(value));
506
+ }
507
+ const encoded = params.toString();
508
+ return encoded ? `?${encoded}` : "";
509
+ }
510
+ function parseRetryAfter(header2, nowMs) {
511
+ if (header2 === null) return null;
512
+ const value = header2.trim();
513
+ if (/^\d+$/.test(value)) return Number(value) * 1e3;
514
+ const date = Date.parse(value);
515
+ return Number.isNaN(date) ? null : Math.max(0, date - nowMs);
516
+ }
517
+ function errorFromResponse(response) {
518
+ const error = isRecord(response.body) ? response.body.error : void 0;
519
+ const code = isRecord(error) ? error.code : void 0;
520
+ const message = isRecord(error) ? error.message : void 0;
521
+ return new RelumeError({
522
+ status: response.status,
523
+ code: typeof code === "string" ? code : "unexpected_response",
524
+ message: typeof message === "string" ? message : `The Relume API answered ${response.status}.`,
525
+ requestId: response.requestId
526
+ });
527
+ }
528
+ var RedirectedError = class extends RelumeError {
529
+ };
530
+ async function requestJson(config, spec) {
531
+ for (let attempt = 0; ; attempt += 1) {
532
+ const canRetry = attempt < config.maxRetries;
533
+ let response;
534
+ try {
535
+ response = await apiRequest(config, spec.method, spec.path, {
536
+ body: spec.body,
537
+ query: spec.query
538
+ });
539
+ } catch (error) {
540
+ const transient = !(error instanceof RedirectedError);
541
+ if (!canRetry || spec.retry !== "safe" || !transient) throw error;
542
+ await config.sleep(backoffMs(attempt));
543
+ continue;
544
+ }
545
+ if (response.status >= 200 && response.status < 300) {
546
+ if (spec.isResult(response.body)) return response.body;
547
+ throw new RelumeError({
548
+ status: response.status,
549
+ code: "unexpected_response",
550
+ message: `${spec.method} ${spec.path} answered ${response.status} without the expected body.`,
551
+ requestId: response.requestId
552
+ });
553
+ }
554
+ const retryable = response.status === 429 || spec.retry === "safe" && response.status >= 500 && !NON_RETRYABLE_5XX.has(response.status);
555
+ const delay = response.retryAfterMs === null ? backoffMs(attempt) : response.status === 429 ? response.retryAfterMs : Math.min(response.retryAfterMs, RETRY_MAX_DELAY_MS);
556
+ if (!canRetry || !retryable || delay > MAX_RETRY_AFTER_MS) {
557
+ throw errorFromResponse(response);
558
+ }
559
+ await config.sleep(delay);
560
+ }
561
+ }
562
+ function backoffMs(attempt) {
563
+ const ceiling = Math.min(
564
+ RETRY_MAX_DELAY_MS,
565
+ RETRY_BASE_DELAY_MS * 2 ** attempt
566
+ );
567
+ return Math.round(Math.random() * ceiling);
568
+ }
569
+ function isSession(value) {
570
+ return isRecord(value) && typeof value.clientSecret === "string" && typeof value.expiresAt === "string";
571
+ }
572
+
573
+ // src/calls.ts
574
+ var CALL_TYPES = [
575
+ "user.get",
576
+ "credits.get",
577
+ "credits.charge",
578
+ "credits.reverse"
579
+ ];
580
+ var InvalidCallbackResultError = class extends Error {
581
+ constructor(message) {
582
+ super(message);
583
+ this.name = "InvalidCallbackResultError";
584
+ }
585
+ };
586
+ function optionalString(value, max) {
587
+ if (value === void 0 || value === null) {
588
+ return { ok: true, value: void 0 };
589
+ }
590
+ return isString(value, 1, max) ? { ok: true, value } : { ok: false };
591
+ }
592
+ function isCallType(value) {
593
+ return CALL_TYPES.some((type) => type === value);
594
+ }
595
+ function parseCall(body) {
596
+ if (!isRecord(body)) {
597
+ return {
598
+ ok: false,
599
+ code: "invalid_request",
600
+ message: "The body must be a JSON object."
601
+ };
602
+ }
603
+ const { type } = body;
604
+ if (!isCallType(type)) {
605
+ return {
606
+ ok: false,
607
+ code: "unsupported_call_type",
608
+ message: `Unsupported call type: ${JSON.stringify(type ?? null).slice(0, 64)}.`
609
+ };
610
+ }
611
+ const invalid = (field) => ({
612
+ ok: false,
613
+ code: "invalid_request",
614
+ message: `Invalid ${field} for ${type}.`,
615
+ type
616
+ });
617
+ const { userId } = body;
618
+ if (!isString(userId, 1, 128)) return invalid("userId");
619
+ switch (type) {
620
+ case "user.get":
621
+ case "credits.get":
622
+ return { ok: true, call: { type, userId } };
623
+ case "credits.charge": {
624
+ const { chargeId, partnerCredits, relumeCredits, action, description } = body;
625
+ if (!isString(chargeId, 1, 64)) return invalid("chargeId");
626
+ if (!isWhole(partnerCredits, 1)) return invalid("partnerCredits");
627
+ if (!isWhole(relumeCredits, 0)) return invalid("relumeCredits");
628
+ if (!isString(action, 1, 200)) return invalid("action");
629
+ if (!isString(description, 0, 200)) return invalid("description");
630
+ const siteId = optionalString(body.siteId, 200);
631
+ if (!siteId.ok) return invalid("siteId");
632
+ const pageId = optionalString(body.pageId, 200);
633
+ if (!pageId.ok) return invalid("pageId");
634
+ const charge = {
635
+ userId,
636
+ chargeId,
637
+ partnerCredits,
638
+ relumeCredits,
639
+ action,
640
+ description
641
+ };
642
+ if (siteId.value !== void 0) charge.siteId = siteId.value;
643
+ if (pageId.value !== void 0) charge.pageId = pageId.value;
644
+ return { ok: true, call: { type, charge } };
645
+ }
646
+ case "credits.reverse": {
647
+ const { reversalId, partnerCredits, relumeCredits, reason } = body;
648
+ if (!isString(reversalId, 1, 64)) return invalid("reversalId");
649
+ if (!isWhole(partnerCredits, 1)) return invalid("partnerCredits");
650
+ if (!isWhole(relumeCredits, 0)) return invalid("relumeCredits");
651
+ if (!isString(reason, 1, 200)) return invalid("reason");
652
+ const chargeId = optionalString(body.chargeId, 64);
653
+ if (!chargeId.ok) return invalid("chargeId");
654
+ const reversal = {
655
+ userId,
656
+ reversalId,
657
+ partnerCredits,
658
+ relumeCredits,
659
+ reason
660
+ };
661
+ if (chargeId.value !== void 0) reversal.chargeId = chargeId.value;
662
+ return { ok: true, call: { type, reversal } };
663
+ }
664
+ }
665
+ }
666
+ function balanceOrThrow(value, callback) {
667
+ if (!isWhole(value, 0)) {
668
+ throw new InvalidCallbackResultError(
669
+ `${callback} must return a balance that is a whole number, zero or more; got ${JSON.stringify(value)}.`
670
+ );
671
+ }
672
+ return value;
673
+ }
674
+ function checkUserProfile(value) {
675
+ if (value === null || value === void 0) return null;
676
+ if (!isRecord(value)) {
677
+ throw new InvalidCallbackResultError(
678
+ "onGetUser must return { name?, email?, avatarUrl? } or null."
679
+ );
680
+ }
681
+ const profile = {};
682
+ const { name, email, avatarUrl } = value;
683
+ if (!absent(name)) {
684
+ if (!isString(name, 1, 200)) {
685
+ throw new InvalidCallbackResultError(
686
+ "onGetUser: name must be a string of at most 200 characters."
687
+ );
688
+ }
689
+ profile.name = name;
690
+ }
691
+ if (!absent(email)) {
692
+ if (!isEmail(email)) {
693
+ throw new InvalidCallbackResultError(
694
+ "onGetUser: email must be a valid email address."
695
+ );
696
+ }
697
+ profile.email = email;
698
+ }
699
+ if (!absent(avatarUrl)) {
700
+ if (!isHttpsUrl(avatarUrl)) {
701
+ throw new InvalidCallbackResultError(
702
+ "onGetUser: avatarUrl must be an https URL."
703
+ );
704
+ }
705
+ profile.avatarUrl = avatarUrl;
706
+ }
707
+ return profile;
708
+ }
709
+ function checkCredits(value) {
710
+ if (!isRecord(value)) {
711
+ throw new InvalidCallbackResultError(
712
+ "onGetCredits must return { balance }."
713
+ );
714
+ }
715
+ return { balance: balanceOrThrow(value.balance, "onGetCredits") };
716
+ }
717
+ function optionalBalance(value, callback) {
718
+ return absent(value.balance) ? {} : { balance: balanceOrThrow(value.balance, callback) };
719
+ }
720
+ function checkChargeResult(value) {
721
+ if (!isRecord(value) || typeof value.ok !== "boolean") {
722
+ throw new InvalidCallbackResultError(
723
+ "onChargeCredits must return { ok: boolean, balance? }."
724
+ );
725
+ }
726
+ return { ok: value.ok, ...optionalBalance(value, "onChargeCredits") };
727
+ }
728
+ function checkReversalResult(value) {
729
+ if (value === void 0 || value === null) return {};
730
+ if (!isRecord(value)) {
731
+ throw new InvalidCallbackResultError(
732
+ "onReverseCharge must return { balance? } or nothing."
733
+ );
734
+ }
735
+ return optionalBalance(value, "onReverseCharge");
736
+ }
737
+
738
+ // src/pages.ts
739
+ function isPage(value) {
740
+ return isRecord(value) && Array.isArray(value.data) && typeof value.nextCursor === "string" && typeof value.hasMore === "boolean";
741
+ }
742
+ var PagedList = class {
743
+ #fetchPage;
744
+ #after;
745
+ #nextCursor;
746
+ constructor(fetchPage, after) {
747
+ this.#fetchPage = fetchPage;
748
+ this.#after = after;
749
+ this.#nextCursor = after ?? null;
750
+ }
751
+ /**
752
+ * Where to resume: the `nextCursor` of the last page whose items have all
753
+ * been handed out, or the starting `after` (null when there was none)
754
+ * before that. Once iteration ends, store it and pass it as `after` later
755
+ * to get only what is new (on the ledger; site cursors don't poll).
756
+ */
757
+ get nextCursor() {
758
+ return this.#nextCursor;
759
+ }
760
+ /** Every page in turn. The cursor advances when you ask for the next one. */
761
+ async *pages() {
762
+ let after = this.#after;
763
+ for (; ; ) {
764
+ const page = await this.#fetchPage(after);
765
+ yield page;
766
+ this.#nextCursor = page.nextCursor;
767
+ if (!page.hasMore) return;
768
+ if (page.nextCursor === after) {
769
+ throw new RelumeError({
770
+ status: 200,
771
+ code: "unexpected_response",
772
+ message: "The Relume API returned hasMore with the same cursor."
773
+ });
774
+ }
775
+ after = page.nextCursor;
776
+ }
777
+ }
778
+ async *[Symbol.asyncIterator]() {
779
+ for await (const page of this.pages()) yield* page.data;
780
+ }
781
+ /** Every item in one array. Mind the size of what you're listing. */
782
+ async toArray() {
783
+ const items = [];
784
+ for await (const item of this) items.push(item);
785
+ return items;
786
+ }
787
+ };
788
+
789
+ // src/resources.ts
790
+ function hasStrings(...keys) {
791
+ return (value) => isRecord(value) && keys.every((key) => typeof value[key] === "string");
792
+ }
793
+ var isUser = hasStrings("userId", "status");
794
+ var isSite = hasStrings("siteId", "userId");
795
+ var isCreatedSite = hasStrings("siteId");
796
+ var isExport = hasStrings("exportId", "status");
797
+ var isLedgerEntryDetail = hasStrings(
798
+ "id",
799
+ "type",
800
+ "status"
801
+ );
802
+ function isPriceList(value) {
803
+ return isRecord(value) && Array.isArray(value.data);
804
+ }
805
+ function segment(name, value) {
806
+ if (typeof value !== "string" || value === "") {
807
+ throw new TypeError(`${name} must be a non-empty string.`);
808
+ }
809
+ return encodeURIComponent(value);
810
+ }
811
+ function partnerApi(config) {
812
+ const get = (path, isResult, query) => requestJson(config, {
813
+ method: "GET",
814
+ path,
815
+ query,
816
+ retry: "safe",
817
+ isResult
818
+ });
819
+ const list = (path, { after, ...query }) => new PagedList(
820
+ (cursor) => get(path, (body) => isPage(body), {
821
+ ...query,
822
+ after: cursor
823
+ }),
824
+ after
825
+ );
826
+ return {
827
+ sessions: {
828
+ create: async ({ userId }) => requestJson(config, {
829
+ method: "POST",
830
+ path: "/sessions",
831
+ body: { userId },
832
+ // Safe to retry: Relume provisions a new user once (later sessions
833
+ // wait for or find the account), so a retry at most mints another
834
+ // client secret.
835
+ retry: "safe",
836
+ isResult: isSession
837
+ })
838
+ },
839
+ users: {
840
+ get: async (userId) => get(`/users/${segment("userId", userId)}`, isUser),
841
+ deactivate: async (userId) => requestJson(config, {
842
+ method: "POST",
843
+ path: `/users/${segment("userId", userId)}/deactivate`,
844
+ retry: "safe",
845
+ isResult: isUser
846
+ }),
847
+ reactivate: async (userId) => requestJson(config, {
848
+ method: "POST",
849
+ path: `/users/${segment("userId", userId)}/reactivate`,
850
+ retry: "safe",
851
+ isResult: isUser
852
+ })
853
+ },
854
+ sites: {
855
+ list: (userId, params = {}) => list(`/users/${segment("userId", userId)}/sites`, { ...params }),
856
+ create: async (userId, input = {}) => requestJson(config, {
857
+ method: "POST",
858
+ path: `/users/${segment("userId", userId)}/sites`,
859
+ body: input.name === void 0 ? {} : { name: input.name },
860
+ retry: "unsafe",
861
+ isResult: isCreatedSite
862
+ }),
863
+ get: async (siteId) => get(`/sites/${segment("siteId", siteId)}`, isSite)
864
+ },
865
+ exports: {
866
+ get: async (siteId, exportId) => get(
867
+ `/sites/${segment("siteId", siteId)}/exports/${segment("exportId", exportId)}`,
868
+ isExport
869
+ )
870
+ },
871
+ ledger: {
872
+ list: (params = {}) => list("/ledger", { ...params }),
873
+ get: async (id) => get(`/ledger/${segment("id", id)}`, isLedgerEntryDetail)
874
+ },
875
+ usage: {
876
+ get: ({ groupBy, ...params }) => list("/usage", {
877
+ ...params,
878
+ groupBy: groupBy?.length ? groupBy.join(",") : void 0
879
+ })
880
+ },
881
+ prices: {
882
+ list: async () => (await get("/prices", isPriceList)).data
883
+ }
884
+ };
885
+ }
886
+
887
+ // src/session.ts
888
+ var EMBED_HEADER = "Relume-Embed";
889
+ var EMBED_HEADER_VALUE = "1";
890
+ function isHeadersLike(value) {
891
+ return isRecord(value) && typeof value.get === "function";
892
+ }
893
+ function headerReader(request) {
894
+ const headers = isRecord(request) ? request.headers : void 0;
895
+ if (isHeadersLike(headers)) return (name) => headers.get(name);
896
+ if (!isRecord(headers)) return null;
897
+ return (name) => {
898
+ const wanted = name.toLowerCase();
899
+ for (const [key, value] of Object.entries(headers)) {
900
+ if (key.toLowerCase() !== wanted) continue;
901
+ const first = Array.isArray(value) ? value[0] : value;
902
+ return typeof first === "string" ? first : null;
903
+ }
904
+ return null;
905
+ };
906
+ }
907
+ function refuseSessionRequest(request) {
908
+ const header2 = headerReader(request);
909
+ if (!header2) {
910
+ return {
911
+ response: errorResponse(
912
+ 500,
913
+ "internal_error",
914
+ "The partner failed to handle the call."
915
+ ),
916
+ error: new TypeError(
917
+ "handleSession couldn't read the request's headers. Pass it the fetch Request, the Node request, or an object with headers."
918
+ )
919
+ };
920
+ }
921
+ if (header2("sec-fetch-site")?.trim().toLowerCase() === "cross-site") {
922
+ return {
923
+ response: errorResponse(
924
+ 403,
925
+ "cross_site_request",
926
+ "The session route only answers your own pages."
927
+ )
928
+ };
929
+ }
930
+ if (header2(EMBED_HEADER)?.trim() !== EMBED_HEADER_VALUE) {
931
+ return {
932
+ response: errorResponse(
933
+ 400,
934
+ "embed_header_missing",
935
+ `The session route needs the ${EMBED_HEADER} header that @relume_io/partner-embed sends.`
936
+ ),
937
+ error: new Error(
938
+ `GET /relume/session was refused: no ${EMBED_HEADER}: ${EMBED_HEADER_VALUE} header. @relume_io/partner-embed sends it from 2.0.0; update the script tag or package. On another origin, allow the header in the route's CORS preflight.`
939
+ )
940
+ };
941
+ }
942
+ return null;
943
+ }
944
+
945
+ // src/client.ts
946
+ var CALLBACK_FOR = {
947
+ "user.get": "onGetUser",
948
+ "credits.get": "onGetCredits",
949
+ "credits.charge": "onChargeCredits",
950
+ "credits.reverse": "onReverseCharge"
951
+ };
952
+ var INTERNAL_ERROR = () => errorResponse(
953
+ 500,
954
+ "internal_error",
955
+ "The partner failed to handle the call."
956
+ );
957
+ function defaultOnHandlerError(error, info) {
958
+ const where = info.route === "calls" ? `${info.type ?? "call"}` : "GET /relume/session";
959
+ const requestId = info.requestId ? ` (Relume-Request-Id ${info.requestId})` : "";
960
+ console.error(`[relume] ${where} failed${requestId}:`, error);
961
+ }
962
+ var Relume = class _Relume {
963
+ // The partner API. Each method throws RelumeError when the API refuses.
964
+ sessions;
965
+ users;
966
+ sites;
967
+ exports;
968
+ ledger;
969
+ usage;
970
+ prices;
971
+ /**
972
+ * A fetch-style handler for Next.js route handlers, Hono, Cloudflare
973
+ * Workers, Bun and Deno. Answers `GET …/session` and `POST …/calls`.
974
+ */
975
+ fetchHandler;
976
+ #secretKey;
977
+ #api;
978
+ #callbacks = {};
979
+ #onHandlerError;
980
+ #now;
981
+ static init(options) {
982
+ return new _Relume(options);
983
+ }
984
+ constructor(options) {
985
+ const secretKey = typeof options?.secretKey === "string" ? options.secretKey.trim() : "";
986
+ if (secretKey === "") {
987
+ throw new TypeError(
988
+ "Relume.init: secretKey is required (sk_test_\u2026 or sk_live_\u2026). Is the environment variable set?"
989
+ );
990
+ }
991
+ if (secretKey.startsWith("pk_")) {
992
+ throw new TypeError(
993
+ "Relume.init: that is a publishable key. Pass the secret key (sk_\u2026); the publishable key goes to the embed."
994
+ );
995
+ }
996
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
997
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {
998
+ throw new TypeError(
999
+ "Relume.init: maxRetries must be a whole number, 0 or more."
1000
+ );
1001
+ }
1002
+ this.#secretKey = secretKey;
1003
+ this.#now = options.now ?? Date.now;
1004
+ this.#api = {
1005
+ secretKey,
1006
+ baseUrl: checkApiBaseUrl(options.apiBaseUrl ?? DEFAULT_API_BASE_URL),
1007
+ timeoutMs: options.apiTimeoutMs ?? DEFAULT_API_TIMEOUT_MS,
1008
+ fetch: options.fetch ?? ((...args) => fetch(...args)),
1009
+ maxRetries,
1010
+ sleep: options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
1011
+ now: this.#now
1012
+ };
1013
+ this.#onHandlerError = options.onHandlerError ?? defaultOnHandlerError;
1014
+ const api = partnerApi(this.#api);
1015
+ this.sessions = api.sessions;
1016
+ this.users = api.users;
1017
+ this.sites = api.sites;
1018
+ this.exports = api.exports;
1019
+ this.ledger = api.ledger;
1020
+ this.usage = api.usage;
1021
+ this.prices = api.prices;
1022
+ this.fetchHandler = fetchHandler(this, MAX_BODY_BYTES);
1023
+ }
1024
+ /** Who is signed in, for the browser's `GET /relume/session`. */
1025
+ getUserIdFromRequest(callback) {
1026
+ this.#callbacks.getUserIdFromRequest = assertFunction(
1027
+ callback,
1028
+ "getUserIdFromRequest"
1029
+ );
1030
+ return this;
1031
+ }
1032
+ /** Answers `user.get`: the user's profile, or null if you don't know them. */
1033
+ onGetUser(callback) {
1034
+ this.#callbacks.onGetUser = assertFunction(callback, "onGetUser");
1035
+ return this;
1036
+ }
1037
+ /** Answers `credits.get`: `{ balance }` in your own credits. */
1038
+ onGetCredits(callback) {
1039
+ this.#callbacks.onGetCredits = assertFunction(callback, "onGetCredits");
1040
+ return this;
1041
+ }
1042
+ /**
1043
+ * Answers `credits.charge`. Debit `partnerCredits` (never `relumeCredits`),
1044
+ * at most once per `chargeId`, and return `{ ok, balance }`.
1045
+ */
1046
+ onChargeCredits(callback) {
1047
+ this.#callbacks.onChargeCredits = assertFunction(
1048
+ callback,
1049
+ "onChargeCredits"
1050
+ );
1051
+ return this;
1052
+ }
1053
+ /**
1054
+ * Answers `credits.reverse`. Credit back `partnerCredits` (never
1055
+ * `relumeCredits`), at most once per `reversalId`. If `chargeId` names a
1056
+ * debit you never made, change nothing and return normally.
1057
+ */
1058
+ onReverseCharge(callback) {
1059
+ this.#callbacks.onReverseCharge = assertFunction(
1060
+ callback,
1061
+ "onReverseCharge"
1062
+ );
1063
+ return this;
1064
+ }
1065
+ /**
1066
+ * Express (or Connect, or plain `node:http`) middleware. Mount it before
1067
+ * any body parser: `app.use('/relume', relume.express())`.
1068
+ */
1069
+ express() {
1070
+ return expressHandler(
1071
+ this,
1072
+ MAX_BODY_BYTES,
1073
+ (error, route) => this.#onHandlerError(error, { route })
1074
+ );
1075
+ }
1076
+ /**
1077
+ * `POST /relume/calls`, framework-agnostic: verifies the signature and the
1078
+ * 5-minute window, routes by `type`, runs your callback and validates what
1079
+ * it returns. Pass the raw body exactly as received.
1080
+ */
1081
+ async handleCall(input) {
1082
+ const requestId = input.requestId ?? null;
1083
+ const verified = await verifySignature({
1084
+ secret: this.#secretKey,
1085
+ header: input.signatureHeader,
1086
+ rawBody: input.rawBody,
1087
+ nowSeconds: Math.floor(this.#now() / 1e3)
1088
+ });
1089
+ if (!verified.valid) {
1090
+ const message = verified.reason === "timestamp_out_of_tolerance" ? "The signature timestamp is more than 5 minutes from this server\u2019s clock." : verified.reason === "malformed_header" ? "Missing or malformed Relume-Signature header." : "The signature doesn't match this server's secret key.";
1091
+ return errorResponse(401, "invalid_signature", message);
1092
+ }
1093
+ let body;
1094
+ try {
1095
+ body = typeof input.rawBody === "string" ? JSON.parse(input.rawBody) : JSON.parse(new TextDecoder().decode(input.rawBody));
1096
+ } catch {
1097
+ return errorResponse(
1098
+ 400,
1099
+ "invalid_request",
1100
+ "The body is not valid JSON."
1101
+ );
1102
+ }
1103
+ const parsed = parseCall(body);
1104
+ if (!parsed.ok) return errorResponse(400, parsed.code, parsed.message);
1105
+ const { call } = parsed;
1106
+ const context = { type: call.type, requestId };
1107
+ const callbackName = CALLBACK_FOR[call.type];
1108
+ if (!this.#callbacks[callbackName]) {
1109
+ this.#onHandlerError(
1110
+ new Error(
1111
+ `No ${callbackName} callback is registered, so ${call.type} was answered 400.`
1112
+ ),
1113
+ { route: "calls", type: call.type, requestId }
1114
+ );
1115
+ return errorResponse(
1116
+ 400,
1117
+ "unsupported_call_type",
1118
+ `This partner doesn't handle ${call.type}.`
1119
+ );
1120
+ }
1121
+ try {
1122
+ return await this.#runCallback(call, context);
1123
+ } catch (error) {
1124
+ this.#onHandlerError(error, {
1125
+ route: "calls",
1126
+ type: call.type,
1127
+ requestId
1128
+ });
1129
+ return INTERNAL_ERROR();
1130
+ }
1131
+ }
1132
+ async #runCallback(call, context) {
1133
+ const callbacks = this.#callbacks;
1134
+ switch (call.type) {
1135
+ case "user.get": {
1136
+ const profile = checkUserProfile(
1137
+ await callbacks.onGetUser(call.userId, context)
1138
+ );
1139
+ return profile ? jsonResponse(200, profile) : errorResponse(404, "user_not_found", "Unknown user.");
1140
+ }
1141
+ case "credits.get":
1142
+ return jsonResponse(
1143
+ 200,
1144
+ checkCredits(await callbacks.onGetCredits(call.userId, context))
1145
+ );
1146
+ case "credits.charge": {
1147
+ const { ok, ...rest } = checkChargeResult(
1148
+ await callbacks.onChargeCredits(call.charge, context)
1149
+ );
1150
+ return ok ? jsonResponse(200, rest) : jsonResponse(402, {
1151
+ ...rest,
1152
+ error: {
1153
+ code: "insufficient_credits",
1154
+ message: "The user can't afford this action."
1155
+ }
1156
+ });
1157
+ }
1158
+ case "credits.reverse":
1159
+ return jsonResponse(
1160
+ 200,
1161
+ checkReversalResult(
1162
+ await callbacks.onReverseCharge(call.reversal, context)
1163
+ )
1164
+ );
1165
+ }
1166
+ }
1167
+ /**
1168
+ * `GET /relume/session`, framework-agnostic: refuses a request from
1169
+ * another site or without the embed's `Relume-Embed` header, works out the
1170
+ * signed-in user with `getUserIdFromRequest`, calls `POST /sessions` and
1171
+ * returns the client secret, or passes the API's error through unchanged.
1172
+ * `request` must carry its headers: the fetch Request, the Node request,
1173
+ * or an object with `headers`.
1174
+ */
1175
+ async handleSession(request) {
1176
+ const refused = refuseSessionRequest(request);
1177
+ if (refused) {
1178
+ if (refused.error) {
1179
+ this.#onHandlerError(refused.error, { route: "session" });
1180
+ }
1181
+ return refused.response;
1182
+ }
1183
+ const resolve = this.#callbacks.getUserIdFromRequest;
1184
+ if (!resolve) {
1185
+ this.#onHandlerError(
1186
+ new Error("No getUserIdFromRequest callback is registered."),
1187
+ { route: "session" }
1188
+ );
1189
+ return INTERNAL_ERROR();
1190
+ }
1191
+ let userId;
1192
+ try {
1193
+ const resolved = await resolve(request);
1194
+ if (resolved === null || resolved === void 0 || resolved === "") {
1195
+ return errorResponse(401, "not_signed_in", "Nobody is signed in.");
1196
+ }
1197
+ if (typeof resolved === "number" && Number.isSafeInteger(resolved)) {
1198
+ userId = String(resolved);
1199
+ } else if (typeof resolved === "string" && resolved.length >= 1 && resolved.length <= 128) {
1200
+ userId = resolved;
1201
+ } else {
1202
+ throw new TypeError(
1203
+ "getUserIdFromRequest must return a string of 1 to 128 characters, a whole number, or null."
1204
+ );
1205
+ }
1206
+ } catch (error) {
1207
+ this.#onHandlerError(error, { route: "session" });
1208
+ return INTERNAL_ERROR();
1209
+ }
1210
+ let response;
1211
+ try {
1212
+ response = await apiRequest(this.#api, "POST", "/sessions", {
1213
+ body: { userId }
1214
+ });
1215
+ } catch (error) {
1216
+ this.#onHandlerError(error, { route: "session" });
1217
+ const status = error instanceof RelumeError && error.code === "timeout" ? 504 : 502;
1218
+ return errorResponse(status, "session_failed", "Couldn't reach Relume.");
1219
+ }
1220
+ if (response.status >= 200 && response.status < 300) {
1221
+ if (isSession(response.body)) {
1222
+ return jsonResponse(200, {
1223
+ clientSecret: response.body.clientSecret,
1224
+ expiresAt: response.body.expiresAt
1225
+ });
1226
+ }
1227
+ this.#onHandlerError(errorFromResponse(response), {
1228
+ route: "session",
1229
+ requestId: response.requestId
1230
+ });
1231
+ return errorResponse(
1232
+ 502,
1233
+ "session_failed",
1234
+ "Relume answered without a session."
1235
+ );
1236
+ }
1237
+ const hasError = typeof response.body === "object" && response.body !== null && "error" in response.body;
1238
+ const body = hasError ? response.body : {
1239
+ error: {
1240
+ code: "session_failed",
1241
+ message: `Relume answered ${response.status}.`
1242
+ }
1243
+ };
1244
+ if (response.status === 401) {
1245
+ this.#onHandlerError(errorFromResponse(response), {
1246
+ route: "session",
1247
+ requestId: response.requestId
1248
+ });
1249
+ }
1250
+ return jsonResponse(response.status, body);
1251
+ }
1252
+ };
1253
+ var LOOPBACK = /^(localhost|127(\.\d{1,3}){3}|\[::1\])$/;
1254
+ function checkApiBaseUrl(value) {
1255
+ let url;
1256
+ try {
1257
+ url = new URL(value);
1258
+ } catch {
1259
+ throw new TypeError(`Relume.init: apiBaseUrl isn't a URL: ${value}`);
1260
+ }
1261
+ const local = url.protocol === "http:" && LOOPBACK.test(url.hostname);
1262
+ if (url.protocol !== "https:" && !local) {
1263
+ throw new TypeError(
1264
+ "Relume.init: apiBaseUrl must be an https URL (http only for localhost): every request carries your secret key."
1265
+ );
1266
+ }
1267
+ return value.replace(/\/+$/, "");
1268
+ }
1269
+ function assertFunction(callback, name) {
1270
+ if (typeof callback !== "function") {
1271
+ throw new TypeError(`relume.${name} expects a function.`);
1272
+ }
1273
+ return callback;
1274
+ }
1275
+ //# sourceMappingURL=index.cjs.map