@mesub/node 0.0.0-stage → 0.1.2

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +840 -2
  3. package/dist/chunk-43Q4PNF7.cjs +480 -0
  4. package/dist/chunk-43Q4PNF7.cjs.map +1 -0
  5. package/dist/chunk-4AAPR37Y.js +120 -0
  6. package/dist/chunk-4AAPR37Y.js.map +1 -0
  7. package/dist/chunk-4JSJOX35.cjs +2040 -0
  8. package/dist/chunk-4JSJOX35.cjs.map +1 -0
  9. package/dist/chunk-66VVEXMU.cjs +340 -0
  10. package/dist/chunk-66VVEXMU.cjs.map +1 -0
  11. package/dist/chunk-J2CE4QWV.js +340 -0
  12. package/dist/chunk-J2CE4QWV.js.map +1 -0
  13. package/dist/chunk-JJWHMB2X.js +480 -0
  14. package/dist/chunk-JJWHMB2X.js.map +1 -0
  15. package/dist/chunk-NOS4FWWQ.js +2040 -0
  16. package/dist/chunk-NOS4FWWQ.js.map +1 -0
  17. package/dist/chunk-SJRQDWTI.cjs +120 -0
  18. package/dist/chunk-SJRQDWTI.cjs.map +1 -0
  19. package/dist/client-BEuDegEY.d.cts +765 -0
  20. package/dist/client-CXQLkSAJ.d.ts +765 -0
  21. package/dist/errors-BN4ZWUfM.d.ts +112 -0
  22. package/dist/errors-DcLfunS-.d.cts +112 -0
  23. package/dist/express.cjs +129 -0
  24. package/dist/express.cjs.map +1 -0
  25. package/dist/express.d.cts +57 -0
  26. package/dist/express.d.ts +57 -0
  27. package/dist/express.js +129 -0
  28. package/dist/express.js.map +1 -0
  29. package/dist/guard-CaXiHMVF.d.ts +51 -0
  30. package/dist/guard-D4AB5HtN.d.cts +51 -0
  31. package/dist/index.cjs +39 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +44 -0
  34. package/dist/index.d.ts +44 -0
  35. package/dist/index.js +39 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/nest.cjs +62 -0
  38. package/dist/nest.cjs.map +1 -0
  39. package/dist/nest.d.cts +57 -0
  40. package/dist/nest.d.ts +57 -0
  41. package/dist/nest.js +62 -0
  42. package/dist/nest.js.map +1 -0
  43. package/dist/next.cjs +113 -0
  44. package/dist/next.cjs.map +1 -0
  45. package/dist/next.d.cts +63 -0
  46. package/dist/next.d.ts +63 -0
  47. package/dist/next.js +113 -0
  48. package/dist/next.js.map +1 -0
  49. package/dist/routes-B7wN4sby.d.ts +130 -0
  50. package/dist/routes-CMzB52P6.d.cts +130 -0
  51. package/dist/situations.cjs +15 -0
  52. package/dist/situations.cjs.map +1 -0
  53. package/dist/situations.d.cts +607 -0
  54. package/dist/situations.d.ts +607 -0
  55. package/dist/situations.js +15 -0
  56. package/dist/situations.js.map +1 -0
  57. package/dist/subscriptions-B6Lin6i_.d.cts +775 -0
  58. package/dist/subscriptions-B6Lin6i_.d.ts +775 -0
  59. package/dist/testing.cjs +1488 -0
  60. package/dist/testing.cjs.map +1 -0
  61. package/dist/testing.d.cts +331 -0
  62. package/dist/testing.d.ts +331 -0
  63. package/dist/testing.js +1488 -0
  64. package/dist/testing.js.map +1 -0
  65. package/package.json +158 -4
@@ -0,0 +1,2040 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;// src/cache/access-cache.ts
2
+ var DEFAULT_MAX_STALE_MS = 24 * 60 * 60 * 1e3;
3
+ var AccessCache = class _AccessCache {
4
+ constructor(store, options = {}) {
5
+ this.store = store;
6
+ this.maxStaleMs = _nullishCoalesce(options.maxStaleMs, () => ( DEFAULT_MAX_STALE_MS));
7
+ this.now = _nullishCoalesce(options.now, () => ( Date.now));
8
+ }
9
+
10
+
11
+
12
+ /**
13
+ * `mesub:access:<scope>:<plan>:<who>`, prefixed so it can share a Redis
14
+ * with anything. The scope (the client passes a hash of its API key) keeps
15
+ * two projects sharing one store apart; without one, `mesub:access:<plan>:<who>`.
16
+ *
17
+ * `who` is whatever names the customer, the client's being
18
+ * `<kind>:<wallet or hash>`. A null plan is the list of every plan they
19
+ * have, under its own prefix, `mesub:access-list:`: no plan slug can reach it.
20
+ */
21
+ static key(who, plan, scope) {
22
+ const prefix = plan === null ? "mesub:access-list" : "mesub:access";
23
+ const path = plan === null ? who : `${plan}:${who}`;
24
+ return scope === void 0 ? `${prefix}:${path}` : `${prefix}:${scope}:${path}`;
25
+ }
26
+ /** With a `deadline` (epoch ms), a store that has not answered by then is a miss. */
27
+ async read(who, plan, scope, deadline) {
28
+ let entry;
29
+ try {
30
+ const pending = this.store.get(_AccessCache.key(who, plan, scope));
31
+ entry = deadline === void 0 ? await pending : await this.within(pending, deadline);
32
+ } catch (e) {
33
+ return void 0;
34
+ }
35
+ const now = this.now();
36
+ if (!entry || now > entry.keepUntil) return void 0;
37
+ return { value: entry.value, fresh: now < entry.freshUntil };
38
+ }
39
+ /** What `pending` gives by `deadline`, or undefined, as a miss, when it has not answered. */
40
+ async within(pending, deadline) {
41
+ let timer;
42
+ const late = new Promise((resolve) => {
43
+ timer = setTimeout(resolve, Math.max(0, deadline - this.now()), void 0);
44
+ });
45
+ try {
46
+ return await Promise.race([pending, late]);
47
+ } finally {
48
+ clearTimeout(timer);
49
+ }
50
+ }
51
+ async write(who, plan, value, scope) {
52
+ if (!Number.isFinite(value.revalidate_after)) return;
53
+ const now = this.now();
54
+ const freshUntil = now + Math.max(0, value.revalidate_after) * 1e3;
55
+ const keepUntil = freshUntil + this.maxStaleMs;
56
+ try {
57
+ await this.store.set(
58
+ _AccessCache.key(who, plan, scope),
59
+ { value, freshUntil, keepUntil },
60
+ keepUntil - now
61
+ );
62
+ } catch (e2) {
63
+ }
64
+ }
65
+ /**
66
+ * Drops the cached answer when `outdated` says it no longer holds, so the
67
+ * next call asks Mesub. A store without `delete` gets it back as stale
68
+ * instead: never served as is again, only as the outage fallback.
69
+ */
70
+ async forget(who, plan, scope, outdated) {
71
+ const key = _AccessCache.key(who, plan, scope);
72
+ try {
73
+ const entry = await this.store.get(key);
74
+ if (!entry || !outdated(entry.value)) return;
75
+ if (this.store.delete) {
76
+ await this.store.delete(key);
77
+ return;
78
+ }
79
+ const now = this.now();
80
+ if (now < entry.keepUntil) {
81
+ await this.store.set(key, { ...entry, freshUntil: now }, entry.keepUntil - now);
82
+ }
83
+ } catch (e3) {
84
+ }
85
+ }
86
+ };
87
+
88
+ // src/version.ts
89
+ var VERSION = "0.1.2";
90
+ var API_VERSION = "2026-10-02";
91
+ var API_VERSION_HEADER = "Mesub-Version";
92
+
93
+ // src/options.ts
94
+ var MAX_DELAY = 2147483647;
95
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
96
+ function apiKeyOf(given) {
97
+ if (given !== void 0 && typeof given !== "string") {
98
+ throw new TypeError("apiKey must be a string.");
99
+ }
100
+ const apiKey = given || envKey();
101
+ if (!apiKey) {
102
+ throw new TypeError(
103
+ typeof process === "undefined" ? "Missing Mesub API key: there is no process.env here (an edge runtime such as Cloudflare Workers), so pass it: `new Mesub({ apiKey: env.MESUB_API_KEY })`." : "Missing Mesub API key: pass `new Mesub({ apiKey })` or set MESUB_API_KEY."
104
+ );
105
+ }
106
+ return apiKey;
107
+ }
108
+ function envKey() {
109
+ return typeof process === "undefined" ? void 0 : _optionalChain([process, 'access', _ => _.env, 'optionalAccess', _2 => _2["MESUB_API_KEY"]]);
110
+ }
111
+ function baseUrlOf(given, fallback) {
112
+ if (given === void 0) return fallback;
113
+ if (typeof given !== "string") throw new TypeError("baseUrl must be a string.");
114
+ let url;
115
+ try {
116
+ url = new URL(given);
117
+ } catch (e4) {
118
+ throw new TypeError(`baseUrl must be an absolute URL, not ${JSON.stringify(given)}.`);
119
+ }
120
+ if (url.search !== "" || url.hash !== "" || given.includes("?") || given.includes("#")) {
121
+ throw new TypeError(
122
+ `baseUrl must have no query nor fragment, not ${JSON.stringify(given)}.`
123
+ );
124
+ }
125
+ const local = url.protocol === "http:" && LOOPBACK.has(url.hostname);
126
+ if (url.protocol !== "https:" && !local) {
127
+ throw new TypeError(
128
+ `baseUrl must be https (http only to localhost or 127.0.0.1), not ${url.protocol}//${url.host}: every call carries the API key.`
129
+ );
130
+ }
131
+ return given.replace(/\/+$/, "");
132
+ }
133
+ function numberOf(name, given, fallback, rule = {}) {
134
+ if (given === void 0) return fallback;
135
+ const ok = typeof given === "number" && Number.isFinite(given) && (rule.zero ? given >= 0 : given > 0) && (!rule.integer || Number.isInteger(given)) && (!rule.delay || given <= MAX_DELAY);
136
+ if (!ok) {
137
+ const what = rule.integer ? "an integer" : "a number of milliseconds";
138
+ const range = rule.zero ? "0 or more" : "above 0";
139
+ const cap = rule.delay ? `, ${MAX_DELAY} at most` : "";
140
+ const shown = typeof given === "string" ? JSON.stringify(given) : String(given);
141
+ throw new TypeError(`${name} must be ${what}, ${range}${cap}, not ${shown}.`);
142
+ }
143
+ return given;
144
+ }
145
+ var OWN_HEADERS = /* @__PURE__ */ new Set([
146
+ "authorization",
147
+ "user-agent",
148
+ "accept",
149
+ "content-type",
150
+ API_VERSION_HEADER.toLowerCase()
151
+ ]);
152
+ function headersOf(given) {
153
+ if (given === void 0) return {};
154
+ if (typeof given !== "object" || given === null || Array.isArray(given)) {
155
+ throw new TypeError("headers must be an object of header names to string values.");
156
+ }
157
+ const headers = {};
158
+ for (const [name, value] of Object.entries(given)) {
159
+ if (typeof value !== "string") {
160
+ throw new TypeError(`headers.${name} must be a string.`);
161
+ }
162
+ if (OWN_HEADERS.has(name.toLowerCase())) {
163
+ throw new TypeError(`headers cannot set ${name}: the SDK sets it itself.`);
164
+ }
165
+ try {
166
+ new Headers([[name, value]]);
167
+ } catch (e5) {
168
+ throw new TypeError(`headers.${name} is not a valid header name and value.`);
169
+ }
170
+ headers[name] = value;
171
+ }
172
+ return headers;
173
+ }
174
+
175
+ // src/cache/memory-store.ts
176
+ var DEFAULT_MAX_ENTRIES = 1e4;
177
+ var MemoryStore = (_class = class {
178
+ __init() {this.entries = /* @__PURE__ */ new Map()}
179
+
180
+
181
+ constructor(options = {}) {;_class.prototype.__init.call(this);
182
+ this.maxEntries = numberOf("maxEntries", options.maxEntries, DEFAULT_MAX_ENTRIES, {
183
+ integer: true
184
+ });
185
+ this.now = _nullishCoalesce(options.now, () => ( Date.now));
186
+ }
187
+ /** How many entries are held, expired ones included until they are read. */
188
+ get size() {
189
+ return this.entries.size;
190
+ }
191
+ get(key) {
192
+ const result = this.entries.get(key);
193
+ if (!result) return void 0;
194
+ if (result.keepUntil < this.now()) {
195
+ this.entries.delete(key);
196
+ return void 0;
197
+ }
198
+ this.entries.delete(key);
199
+ this.entries.set(key, result);
200
+ return result;
201
+ }
202
+ set(key, entry, _ttlMs) {
203
+ this.entries.delete(key);
204
+ this.entries.set(key, entry);
205
+ while (this.entries.size > this.maxEntries) {
206
+ const oldest = this.entries.keys().next().value;
207
+ if (oldest === void 0) break;
208
+ this.entries.delete(oldest);
209
+ }
210
+ }
211
+ delete(key) {
212
+ this.entries.delete(key);
213
+ }
214
+ }, _class);
215
+
216
+ // src/customer.ts
217
+ var CUSTOMER_KINDS = ["wallet", "external_id", "email"];
218
+ function customerOf(customer) {
219
+ if (typeof customer === "string") return { kind: "wallet", value: customer };
220
+ const named = CUSTOMER_KINDS.filter(
221
+ (kind2) => _optionalChain([customer, 'optionalAccess', _3 => _3[kind2]]) !== void 0
222
+ );
223
+ const kind = named[0];
224
+ const value = kind && customer[kind];
225
+ if (named.length !== 1 || !kind || typeof value !== "string") {
226
+ throw new TypeError("Ask about exactly one of wallet, external_id or email, as a string.");
227
+ }
228
+ if (kind === "email") return { kind, value: value.trim().toLowerCase() };
229
+ if (kind === "external_id") return { kind, value: value.trim() };
230
+ return { kind, value };
231
+ }
232
+
233
+ // src/errors.ts
234
+ var MesubError = (_class2 = class extends Error {
235
+ __init2() {this.name = "MesubError"}
236
+ /**
237
+ * The HTTP status, or `null` when no response came back (network error,
238
+ * timeout). On the copy an Express middleware hands to `next(err)` for a
239
+ * 4xx of Mesub's, 500: Mesub's own is on `cause`.
240
+ */
241
+
242
+
243
+ /**
244
+ * The code Mesub's error body names (`subscription_not_found`,
245
+ * `pending_cap_reached`, ...), finer than `code` and as stable: never
246
+ * renamed nor reused, though new ones get added. Null when no Mesub error
247
+ * came back: a network error, a timeout, a body that is not Mesub's.
248
+ */
249
+
250
+ /**
251
+ * Whether the same call, sent again unchanged, may succeed later without
252
+ * anybody doing anything: Mesub's own `retryable` when it sent one. Not
253
+ * "at once": the SDK has already retried what it retries.
254
+ */
255
+
256
+ /**
257
+ * The body Mesub answered, parsed when it is JSON, the text otherwise;
258
+ * undefined when no response came back.
259
+ */
260
+
261
+ /**
262
+ * How long Mesub asked to wait before the same call is sent again, in
263
+ * milliseconds, from the response's `Retry-After`, as sent. It comes on
264
+ * a 429 (`rate_limited`, or `pending_cap_reached` on create: a quota,
265
+ * not a rate, and the wait may be most of an hour), on every 503 of a
266
+ * route that builds, sends or queues (`network_unavailable`, about 10 s;
267
+ * `unavailable` on `submit`, about 2 s, and on `retry`, about 20 s),
268
+ * and on two 409s:
269
+ * `retry_too_soon` on `retry`, and `comeback_period_rolling` on
270
+ * `create`, `transaction` and `submit`. Null when the response had none,
271
+ * or none came back.
272
+ */
273
+
274
+ /**
275
+ * The id Mesub gave the request (`req_` and 24 letters or digits), from
276
+ * the response's `Mesub-Request-Id` header, or the error body's
277
+ * `request_id` when a proxy dropped the header (Mesub-io/backend#472).
278
+ * Log it, and quote it to Mesub support: it finds the request in Mesub's
279
+ * logs. Null when no response came back, or one that is not Mesub's.
280
+ */
281
+
282
+ constructor(message, options) {
283
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });_class2.prototype.__init2.call(this);;
284
+ this.status = options.status;
285
+ this.code = options.code;
286
+ this.apiCode = _nullishCoalesce(options.apiCode, () => ( null));
287
+ this.retryable = _nullishCoalesce(options.retryable, () => ( (options.code === "unavailable" || options.code === "rate_limited")));
288
+ this.body = options.body;
289
+ this.retryAfter = _nullishCoalesce(options.retryAfter, () => ( null));
290
+ this.requestId = _nullishCoalesce(options.requestId, () => ( null));
291
+ }
292
+ }, _class2);
293
+ var MesubSubmitError = class extends MesubError {
294
+ /** The subscription as read back after the last send, or null if that read failed. */
295
+
296
+ /** How many times the submit was sent, the same request each time. */
297
+
298
+ constructor(message, options) {
299
+ super(message, options);
300
+ this.subscription = options.subscription;
301
+ this.sends = options.sends;
302
+ }
303
+ };
304
+ function codeFor(status, apiCode) {
305
+ if (status === 404 && apiCode === "plan_not_found") return "plan_not_found";
306
+ if (status === 400) return "invalid_request";
307
+ if (status === 401) return "unauthorized";
308
+ if (status === 403) return "forbidden";
309
+ if (status === 404) return "not_found";
310
+ if (status === 409) return "conflict";
311
+ if (status === 429) return "rate_limited";
312
+ if (status >= 500) return "unavailable";
313
+ return "unexpected";
314
+ }
315
+
316
+ // src/validate.ts
317
+ var STRING = { check: (value) => typeof value === "string", expected: "a string" };
318
+ var STRING_OR_NULL = {
319
+ check: (value) => value === null || typeof value === "string",
320
+ expected: "a string or null"
321
+ };
322
+ var OPTIONAL_STRING = { ...STRING, optional: true };
323
+ var COUNT = {
324
+ check: (value) => typeof value === "number" && Number.isInteger(value) && value >= 0,
325
+ expected: "a whole number"
326
+ };
327
+ var BOOLEAN = { check: (value) => typeof value === "boolean", expected: "a boolean" };
328
+ var OBJECT = { check: isObject, expected: "an object" };
329
+ var LIST_OF = { check: Array.isArray, expected: "a list" };
330
+ var DATE = { check: isDate, expected: "a date" };
331
+ var DATE_OR_NULL = {
332
+ check: (value) => value === null || isDate(value),
333
+ expected: "a date or null"
334
+ };
335
+ var RETRY_DEADLINE = { ...DATE_OR_NULL, absent: null };
336
+ var PAUSED = { ...BOOLEAN, absent: false };
337
+ var END_REASON = { ...STRING_OR_NULL, absent: null };
338
+ var BLOCKED_SINCE = { ...DATE_OR_NULL, absent: null };
339
+ var BLOCKED_REASON = { ...STRING_OR_NULL, absent: null };
340
+ var SUBSCRIPTION_ID = { ...STRING_OR_NULL, absent: null };
341
+ var STOP_REASON = { ...STRING_OR_NULL, absent: null };
342
+ var CLOSABLE = { ...BOOLEAN, absent: false };
343
+ var CLOSABLE_FROM = { ...DATE_OR_NULL, absent: null };
344
+ var TRANSACTION_EXPIRES_AT = { ...DATE_OR_NULL, absent: null };
345
+ var AMOUNT_USD = { ...STRING_OR_NULL, absent: null };
346
+ var AMOUNT_RECEIVED = { ...STRING_OR_NULL, absent: null };
347
+ var TOKEN = {
348
+ ...OBJECT,
349
+ absent: () => ({ symbol: null, name: null, logo_url: null, verified: false })
350
+ };
351
+ var RETRY_COUNT = {
352
+ check: (value) => value === null || COUNT.check(value),
353
+ expected: "a whole number or null",
354
+ absent: null
355
+ };
356
+ var SECONDS = {
357
+ check: (value) => typeof value === "number" && Number.isFinite(value),
358
+ expected: "a finite number of seconds"
359
+ };
360
+ var ANSWER = {
361
+ /** Null asked by external id or email, for a customer with nothing on that plan. */
362
+ wallet: STRING_OR_NULL,
363
+ plan: STRING,
364
+ subscription_id: SUBSCRIPTION_ID,
365
+ access: BOOLEAN,
366
+ status: STRING,
367
+ paused: PAUSED,
368
+ end_reason: END_REASON,
369
+ late_reason: STRING_OR_NULL,
370
+ stop_reason: STOP_REASON,
371
+ payment_status: STRING,
372
+ blocked_since: BLOCKED_SINCE,
373
+ blocked_reason: BLOCKED_REASON,
374
+ subscribed_since: DATE_OR_NULL,
375
+ first_subscribed_at: DATE_OR_NULL,
376
+ current_period_end: DATE_OR_NULL,
377
+ cancelled_at: DATE_OR_NULL,
378
+ access_until: DATE_OR_NULL,
379
+ next_charge_at: DATE_OR_NULL,
380
+ next_retry_at: DATE_OR_NULL,
381
+ retry_deadline: RETRY_DEADLINE,
382
+ closable: CLOSABLE,
383
+ closable_from: CLOSABLE_FROM,
384
+ revalidate_after: SECONDS
385
+ };
386
+ var ATTEMPT = {
387
+ outcome: STRING,
388
+ reason: STRING_OR_NULL,
389
+ amount: STRING,
390
+ attempted_at: DATE,
391
+ signature: STRING_OR_NULL
392
+ };
393
+ function accessAnswerFrom(body) {
394
+ const problem = answerProblem(body);
395
+ if (problem !== null) throw unreadable(problem, body);
396
+ return body;
397
+ }
398
+ function accessListFrom(body) {
399
+ const problem = _nullishCoalesce(problemWith(body, LIST), () => ( plansProblem(body)));
400
+ if (problem !== null) throw unreadable(problem, body);
401
+ return body;
402
+ }
403
+ var LIST = {
404
+ plans: LIST_OF,
405
+ revalidate_after: SECONDS
406
+ };
407
+ function answerProblem(body, label) {
408
+ return _nullishCoalesce(problemWith(body, ANSWER, label), () => ( attemptsProblem(body, label)));
409
+ }
410
+ function plansProblem(body) {
411
+ const { plans } = body;
412
+ for (const [index, answer] of plans.entries()) {
413
+ const problem = answerProblem(answer, `plans[${index}]`);
414
+ if (problem !== null) return problem;
415
+ }
416
+ return null;
417
+ }
418
+ var SUBSCRIPTION = {
419
+ id: STRING,
420
+ status: STRING,
421
+ paused: PAUSED,
422
+ end_reason: END_REASON,
423
+ late_reason: STRING_OR_NULL,
424
+ stop_reason: STOP_REASON,
425
+ access: BOOLEAN,
426
+ payment_status: STRING,
427
+ blocked_since: BLOCKED_SINCE,
428
+ blocked_reason: BLOCKED_REASON,
429
+ plan: STRING_OR_NULL,
430
+ wallet: STRING,
431
+ email: STRING_OR_NULL,
432
+ external_id: STRING_OR_NULL,
433
+ current_period_start: DATE_OR_NULL,
434
+ current_period_end: DATE_OR_NULL,
435
+ next_charge_at: DATE_OR_NULL,
436
+ next_retry_at: DATE_OR_NULL,
437
+ retry_deadline: RETRY_DEADLINE,
438
+ next_retry_number: RETRY_COUNT,
439
+ retries_allowed: RETRY_COUNT,
440
+ access_until: DATE_OR_NULL,
441
+ closable: CLOSABLE,
442
+ closable_from: CLOSABLE_FROM,
443
+ created_at: DATE,
444
+ confirmed_at: DATE_OR_NULL
445
+ };
446
+ var SUBSCRIPTION_LIST = {
447
+ data: LIST_OF,
448
+ has_more: BOOLEAN
449
+ };
450
+ var SUBSCRIPTION_ATTEMPT = {
451
+ id: STRING,
452
+ attempted_at: DATE,
453
+ outcome: STRING,
454
+ reason: STRING_OR_NULL,
455
+ amount: STRING,
456
+ amount_usd: AMOUNT_USD,
457
+ amount_received: AMOUNT_RECEIVED,
458
+ signature: STRING_OR_NULL,
459
+ retry: BOOLEAN,
460
+ retry_number: RETRY_COUNT,
461
+ retries_allowed: RETRY_COUNT,
462
+ period_start: DATE_OR_NULL
463
+ };
464
+ var SUBSCRIPTION_ATTEMPT_LIST = {
465
+ data: LIST_OF,
466
+ has_more: BOOLEAN,
467
+ paid: OBJECT
468
+ };
469
+ var BASE_UNITS = {
470
+ check: (value) => typeof value === "string" && /^\d+$/.test(value),
471
+ expected: "a whole number as a string"
472
+ };
473
+ var SUBSCRIBE_TRANSACTION = {
474
+ subscription: OBJECT,
475
+ transaction: STRING,
476
+ last_valid_block_height: STRING,
477
+ transaction_expires_at: TRANSACTION_EXPIRES_AT,
478
+ costs: OBJECT,
479
+ terms: OBJECT
480
+ };
481
+ var COSTS = {
482
+ rent: OBJECT,
483
+ fee: OBJECT,
484
+ total: BASE_UNITS
485
+ };
486
+ var RENT = {
487
+ subscription: BASE_UNITS,
488
+ // Null when the authority exists already: no rent for it.
489
+ authority: {
490
+ check: (value) => value === null || BASE_UNITS.check(value),
491
+ expected: "a whole number as a string or null"
492
+ },
493
+ total: BASE_UNITS
494
+ };
495
+ var FEE = {
496
+ signatures: COUNT,
497
+ per_signature: BASE_UNITS,
498
+ priority: BASE_UNITS,
499
+ total: BASE_UNITS
500
+ };
501
+ var PLAN = {
502
+ slug: STRING,
503
+ name: STRING,
504
+ description: STRING_OR_NULL,
505
+ project_name: STRING,
506
+ logo_url: STRING_OR_NULL,
507
+ amount: STRING,
508
+ amount_display: STRING,
509
+ amount_usd: AMOUNT_USD,
510
+ decimals: COUNT,
511
+ symbol: STRING_OR_NULL,
512
+ token: TOKEN,
513
+ mint: STRING,
514
+ period_hours: COUNT,
515
+ network: STRING,
516
+ // A string, not one of two: a status added later is handed back, not refused.
517
+ status: STRING,
518
+ available: BOOLEAN,
519
+ ends_at: DATE_OR_NULL
520
+ };
521
+ var PLAN_TOKEN = {
522
+ symbol: STRING_OR_NULL,
523
+ name: STRING_OR_NULL,
524
+ logo_url: STRING_OR_NULL,
525
+ verified: BOOLEAN
526
+ };
527
+ function planProblem(body, label) {
528
+ return _nullishCoalesce(problemWith(body, PLAN, label), () => ( problemWith(
529
+ body.token,
530
+ PLAN_TOKEN,
531
+ label === void 0 ? "token" : `${label}.token`
532
+ )));
533
+ }
534
+ function planFrom(body) {
535
+ const problem = planProblem(body);
536
+ if (problem !== null) throw unreadable(problem, body, "GET /v1/plans/:slug");
537
+ return body;
538
+ }
539
+ function planListFrom(body) {
540
+ const problem = _nullishCoalesce(problemWith(body, { plans: LIST_OF }), () => ( firstProblem(body.plans, "plans", planProblem)));
541
+ if (problem !== null) throw unreadable(problem, body, "GET /v1/plans");
542
+ return body.plans;
543
+ }
544
+ function serverSubscriptionFrom(body, route = "GET /v1/subscriptions/:id", status = 200) {
545
+ const problem = problemWith(body, SUBSCRIPTION);
546
+ if (problem !== null) throw unreadable(problem, body, route, status);
547
+ return body;
548
+ }
549
+ function serverSubscriptionListFrom(body) {
550
+ const problem = _nullishCoalesce(problemWith(body, SUBSCRIPTION_LIST), () => ( firstProblem(body.data, "data", SUBSCRIPTION)));
551
+ if (problem !== null) throw unreadable(problem, body, "GET /v1/subscriptions");
552
+ return body;
553
+ }
554
+ function subscriptionAttemptListFrom(body) {
555
+ const problem = _nullishCoalesce(_nullishCoalesce(problemWith(body, SUBSCRIPTION_ATTEMPT_LIST), () => ( firstProblem(body.data, "data", SUBSCRIPTION_ATTEMPT))), () => ( problemWith(
556
+ body.paid,
557
+ { count: COUNT, amount: BASE_UNITS },
558
+ "paid"
559
+ )));
560
+ if (problem !== null) throw unreadable(problem, body, "GET /v1/subscriptions/:id/attempts");
561
+ return body;
562
+ }
563
+ function subscribeTransactionFrom(body, route = "POST /v1/subscriptions") {
564
+ const problem = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(problemWith(body, SUBSCRIBE_TRANSACTION), () => ( problemWith(
565
+ body.subscription,
566
+ { id: STRING, status: STRING },
567
+ "subscription"
568
+ ))), () => ( problemWith(body.costs, COSTS, "costs"))), () => ( problemWith(body.costs.rent, RENT, "costs.rent"))), () => ( problemWith(body.costs.fee, FEE, "costs.fee"))), () => ( problemWith(
569
+ body.terms,
570
+ { message: STRING, expires_at: DATE },
571
+ "terms"
572
+ )));
573
+ if (problem !== null) throw unreadable(problem, body, route, 201);
574
+ return body;
575
+ }
576
+ function submitResultFrom(body) {
577
+ return confirmResultFrom(body, "POST /v1/subscriptions/:id/submit");
578
+ }
579
+ function confirmResultFrom(body, route) {
580
+ const problem = _nullishCoalesce(_nullishCoalesce(problemWith(body, { subscription: OBJECT }), () => ( problemWith(body.subscription, SUBSCRIPTION, "subscription"))), () => ( reasonProblem(body)));
581
+ if (problem !== null) throw unreadable(problem, body, route, 201);
582
+ return body;
583
+ }
584
+ var WALLET_TRANSACTION = {
585
+ transaction: STRING,
586
+ last_valid_block_height: STRING,
587
+ transaction_expires_at: TRANSACTION_EXPIRES_AT
588
+ };
589
+ function walletTransactionFrom(body, route) {
590
+ const problem = problemWith(body, WALLET_TRANSACTION);
591
+ if (problem !== null) throw unreadable(problem, body, route, 201);
592
+ return body;
593
+ }
594
+ function reasonProblem({ reason, reason_code }) {
595
+ if (reason !== void 0 && typeof reason !== "string") return "reason is not a string";
596
+ if (reason_code !== void 0 && typeof reason_code !== "string") {
597
+ return "reason_code is not a string";
598
+ }
599
+ return null;
600
+ }
601
+ var SEQUENCE = {
602
+ check: (value) => typeof value === "number" && Number.isInteger(value) && value >= 1,
603
+ expected: "a whole number from 1",
604
+ optional: true
605
+ };
606
+ var EVENT = {
607
+ id: STRING,
608
+ object: { check: (value) => value === "event", expected: '"event"' },
609
+ type: STRING,
610
+ created_at: DATE,
611
+ network: STRING,
612
+ api_version: STRING,
613
+ sequence: SEQUENCE,
614
+ data: OBJECT,
615
+ detail: OBJECT
616
+ };
617
+ var TEST = { check: (value) => value === true, expected: "true", optional: true };
618
+ var OLDER_WEBHOOK = { type: STRING, created_at: DATE, data: OBJECT, test: TEST };
619
+ var FIRST_PAYMENT = {
620
+ check: (value) => value === null || problemWith(value, { amount: STRING, mint: STRING_OR_NULL, signature: STRING_OR_NULL }) === null,
621
+ expected: "null or a payment (amount, mint, signature)",
622
+ optional: true
623
+ };
624
+ var ACCESS_CHANGED = {
625
+ changed_at: DATE,
626
+ blocked_reason: { ...STRING_OR_NULL, optional: true },
627
+ access_until: { ...DATE_OR_NULL, optional: true }
628
+ };
629
+ var DETAILS = {
630
+ "subscription.created": {
631
+ previous_id: OPTIONAL_STRING,
632
+ first_payment: FIRST_PAYMENT
633
+ },
634
+ "subscription.ended": {
635
+ changed_at: { ...DATE, optional: true },
636
+ // Only `cancelled` today: a newer word is let through, as a status is.
637
+ end_reason: OPTIONAL_STRING
638
+ },
639
+ "subscription.renewal_upcoming": {
640
+ can_pay: BOOLEAN,
641
+ renewal_issue: STRING_OR_NULL,
642
+ amount: STRING,
643
+ mint: STRING,
644
+ due_at: DATE
645
+ },
646
+ "subscription.renewed": {
647
+ amount: STRING,
648
+ mint: STRING,
649
+ period_start: DATE,
650
+ period_end: DATE,
651
+ signature: STRING
652
+ },
653
+ "subscription.payment_failed": {
654
+ reason: STRING,
655
+ amount: STRING,
656
+ mint: STRING,
657
+ period_start: DATE_OR_NULL,
658
+ period_end: DATE_OR_NULL,
659
+ next_retry_at: DATE_OR_NULL,
660
+ retry_deadline: DATE_OR_NULL,
661
+ retries_left: COUNT,
662
+ retry_mode: STRING_OR_NULL
663
+ },
664
+ "subscription.stopped": {
665
+ reason: STRING
666
+ },
667
+ "subscription.access_granted": ACCESS_CHANGED,
668
+ "subscription.access_revoked": ACCESS_CHANGED
669
+ };
670
+ function eventProblem(body, label) {
671
+ const prefix = label === void 0 ? "" : `${label}.`;
672
+ const { type, data, detail } = body;
673
+ const fields = DETAILS[type];
674
+ return _nullishCoalesce(_nullishCoalesce(problemWith(body, EVENT, label), () => ( problemWith(data, SUBSCRIPTION, `${prefix}data`))), () => ( (fields ? problemWith(detail, fields, `${prefix}detail`) : null)));
675
+ }
676
+ function eventFrom(body) {
677
+ const problem = eventProblem(body);
678
+ if (problem !== null) throw unreadable(problem, body, "GET /v1/events/:id");
679
+ return body;
680
+ }
681
+ function eventListFrom(body) {
682
+ const problem = _nullishCoalesce(problemWith(body, EVENT_LIST), () => ( firstProblem(body.data, "data", eventProblem)));
683
+ if (problem !== null) throw unreadable(problem, body, "GET /v1/events");
684
+ return body;
685
+ }
686
+ var EVENT_LIST = {
687
+ data: LIST_OF,
688
+ has_more: BOOLEAN,
689
+ available_since: DATE
690
+ };
691
+ function webhookEventFrom(body, deliveryId) {
692
+ const older = isObject(body) && !("id" in body) && !("object" in body);
693
+ const problem = older ? olderWebhookProblem(body) : _nullishCoalesce(eventProblem(body), () => ( problemWith(body, { test: TEST })));
694
+ if (problem !== null) {
695
+ throw new MesubError(`Mesub sent a webhook this SDK cannot read: ${problem}.`, {
696
+ status: null,
697
+ code: "unexpected",
698
+ body
699
+ });
700
+ }
701
+ if (!older) return { ...body, delivery_id: deliveryId };
702
+ const { type, created_at, data, test } = body;
703
+ const { detail, ...subscription } = data;
704
+ return {
705
+ id: null,
706
+ delivery_id: deliveryId,
707
+ object: "event",
708
+ type,
709
+ created_at,
710
+ network: null,
711
+ api_version: null,
712
+ data: subscription,
713
+ detail,
714
+ ...test !== void 0 && { test }
715
+ };
716
+ }
717
+ function olderWebhookProblem(body) {
718
+ const { type, data } = body;
719
+ const fields = DETAILS[type];
720
+ return _nullishCoalesce(_nullishCoalesce(problemWith(body, OLDER_WEBHOOK), () => ( problemWith(data, { ...SUBSCRIPTION, detail: OBJECT }, "data"))), () => ( (fields ? problemWith(data.detail, fields, "data.detail") : null)));
721
+ }
722
+ function firstProblem(items, label, fields) {
723
+ for (const [index, item] of items.entries()) {
724
+ const at = `${label}[${index}]`;
725
+ const problem = typeof fields === "function" ? fields(item, at) : problemWith(item, fields, at);
726
+ if (problem !== null) return problem;
727
+ }
728
+ return null;
729
+ }
730
+ function unreadable(problem, body, route = "/v1/access", status = 200) {
731
+ return new MesubError(
732
+ `Mesub answered ${route} with an answer this SDK cannot read: ${problem}.`,
733
+ { status, code: "unexpected", body }
734
+ );
735
+ }
736
+ function problemWith(body, fields, label) {
737
+ if (!isObject(body)) {
738
+ return `${_nullishCoalesce(label, () => ( "the body"))} is not an object`;
739
+ }
740
+ const prefix = label === void 0 ? "" : `${label}.`;
741
+ for (const [name, field] of Object.entries(fields)) {
742
+ const value = body[name];
743
+ if (value === void 0 && field.optional) continue;
744
+ if (value === void 0 && "absent" in field) {
745
+ body[name] = typeof field.absent === "function" ? field.absent() : field.absent;
746
+ continue;
747
+ }
748
+ if (value === void 0) return `${prefix}${name} is missing`;
749
+ if (!field.check(value)) return `${prefix}${name} is not ${field.expected}`;
750
+ }
751
+ return null;
752
+ }
753
+ function attemptsProblem(body, label) {
754
+ const { attempts } = body;
755
+ const prefix = label === void 0 ? "" : `${label}.`;
756
+ if (attempts === void 0) return null;
757
+ if (!Array.isArray(attempts)) return `${prefix}attempts is not a list`;
758
+ for (const [index, attempt] of attempts.entries()) {
759
+ const problem = problemWith(attempt, ATTEMPT, `${prefix}attempts[${index}]`);
760
+ if (problem !== null) return problem;
761
+ }
762
+ return null;
763
+ }
764
+ function isDate(value) {
765
+ return typeof value === "string" && !Number.isNaN(Date.parse(value));
766
+ }
767
+ function isObject(value) {
768
+ return typeof value === "object" && value !== null && !Array.isArray(value);
769
+ }
770
+
771
+ // src/webhooks.ts
772
+ var SECRET_PREFIX = "whsec_";
773
+ var DEFAULT_TOLERANCE = 5 * 60 * 1e3;
774
+ var TIMESTAMP = /^[0-9]{1,15}$/;
775
+ async function verifyWebhook(body, headers, options = {}) {
776
+ return verify(body, headers, options, void 0);
777
+ }
778
+ var Webhooks = class {
779
+ #secret;
780
+ #received;
781
+ /** @internal */
782
+ constructor(secret, received) {
783
+ this.#secret = secret;
784
+ this.#received = received;
785
+ }
786
+ /**
787
+ * `verifyWebhook`, with `webhookSecret` unless `options.secret` says
788
+ * otherwise. A verified event also drops every access answer cached for
789
+ * its customer, the yes and the no, so the next check asks Mesub.
790
+ * Three drop nothing: `subscription.renewal_upcoming`, which moves no
791
+ * access, the `test` event, and any body with `test: true`, whose
792
+ * subscription is made up.
793
+ */
794
+ async verify(body, headers, options = {}) {
795
+ const event = await verify(body, headers, options, this.#secret);
796
+ const moves = event.test !== true && event.type !== "test" && event.type !== "subscription.renewal_upcoming";
797
+ if (moves) await this.#received(event);
798
+ return event;
799
+ }
800
+ };
801
+ async function verify(body, headers, options, fallback) {
802
+ const key = options.secret !== void 0 || fallback !== void 0 ? keyOf(_nullishCoalesce(options.secret, () => ( fallback)), "secret") : keyOf(envSecret(), "MESUB_WEBHOOK_SECRET");
803
+ const tolerance = numberOf("tolerance", options.tolerance, DEFAULT_TOLERANCE);
804
+ const bytes = bytesOf(body);
805
+ if (typeof headers !== "object" || headers === null) {
806
+ throw new TypeError("headers must be the request headers: a Headers or a plain object.");
807
+ }
808
+ const id = headerIn(headers, "webhook-id");
809
+ const timestamp = headerIn(headers, "webhook-timestamp");
810
+ const signatures = headerIn(headers, "webhook-signature");
811
+ if (!id || !timestamp || !signatures) {
812
+ const missing = [
813
+ !id && "webhook-id",
814
+ !timestamp && "webhook-timestamp",
815
+ !signatures && "webhook-signature"
816
+ ].filter(Boolean);
817
+ throw invalid(`The webhook has no ${missing.join(", ")} header: it is not one of Mesub's.`);
818
+ }
819
+ if (!TIMESTAMP.test(timestamp)) {
820
+ throw invalid("The webhook-timestamp header is not a number of seconds.");
821
+ }
822
+ const expected = await signatureOf(key, id, timestamp, bytes);
823
+ let matched = false;
824
+ for (const candidate of signatures.split(" ")) {
825
+ const [version, encoded] = candidate.split(",");
826
+ const given = version === "v1" && encoded !== void 0 ? fromBase64(encoded) : null;
827
+ if (given !== null && sameBytes(given, expected)) matched = true;
828
+ }
829
+ if (!matched) {
830
+ throw invalid(
831
+ "No signature of the webhook matches the secret: not Mesub's, another endpoint's secret, or a body changed on its way (parsed then serialized again?). Verify the raw body."
832
+ );
833
+ }
834
+ const skew = Date.now() - Number(timestamp) * 1e3;
835
+ if (Math.abs(skew) > tolerance) {
836
+ throw invalid(
837
+ `The webhook was signed ${Math.round(Math.abs(skew) / 1e3)} s ${skew > 0 ? "ago" : "ahead of this clock"}, beyond the ${tolerance / 1e3} s tolerance: a replay, or this server's clock is off.`
838
+ );
839
+ }
840
+ return webhookEventFrom(parsed(body, bytes), id);
841
+ }
842
+ function bytesOf(body) {
843
+ if (typeof body === "string") return new TextEncoder().encode(body);
844
+ if (body instanceof Uint8Array) return new Uint8Array(body);
845
+ if (body instanceof ArrayBuffer) return new Uint8Array(body.slice(0));
846
+ throw new TypeError(
847
+ "body must be the raw body as received, a string or a Buffer, never parsed: the signature is over its exact bytes. With Express, read it with express.raw({ type: 'application/json' }); in a Next route, await request.text()."
848
+ );
849
+ }
850
+ function parsed(body, bytes) {
851
+ try {
852
+ const text = typeof body === "string" ? body : new TextDecoder("utf-8", { fatal: true }).decode(bytes);
853
+ return JSON.parse(text);
854
+ } catch (cause) {
855
+ throw new MesubError("Mesub sent a webhook whose body is not JSON.", {
856
+ status: null,
857
+ code: "unexpected",
858
+ cause
859
+ });
860
+ }
861
+ }
862
+ function keyOf(secret, name) {
863
+ if (secret === void 0 || secret === "") {
864
+ throw new TypeError(
865
+ typeof process === "undefined" ? "Missing Mesub webhook secret: there is no process.env here, so pass it: `{ secret: env.MESUB_WEBHOOK_SECRET }` or `new Mesub({ webhookSecret })`." : "Missing Mesub webhook secret: pass `{ secret }` or `new Mesub({ webhookSecret })`, or set MESUB_WEBHOOK_SECRET."
866
+ );
867
+ }
868
+ const key = typeof secret === "string" && secret.startsWith(SECRET_PREFIX) ? fromBase64(secret.slice(SECRET_PREFIX.length)) : null;
869
+ if (key === null || key.length === 0) {
870
+ throw new TypeError(
871
+ `${name} must be the endpoint's signing secret, whsec_..., from its page in the Mesub dashboard` + (typeof secret === "string" && /^(SUB|PUB)_/.test(secret) ? ", not an API key." : ".")
872
+ );
873
+ }
874
+ return key;
875
+ }
876
+ function webhookSecretOf(given) {
877
+ if (given !== void 0 && typeof given !== "string") {
878
+ throw new TypeError("webhookSecret must be a string.");
879
+ }
880
+ const secret = given || envSecret();
881
+ if (secret) keyOf(secret, given ? "webhookSecret" : "MESUB_WEBHOOK_SECRET");
882
+ return secret || void 0;
883
+ }
884
+ function envSecret() {
885
+ return typeof process === "undefined" ? void 0 : _optionalChain([process, 'access', _4 => _4.env, 'optionalAccess', _5 => _5["MESUB_WEBHOOK_SECRET"]]);
886
+ }
887
+ function headerIn(headers, name) {
888
+ if (typeof headers.get === "function") {
889
+ return _nullishCoalesce(headers.get(name), () => ( void 0));
890
+ }
891
+ for (const [key, value] of Object.entries(headers)) {
892
+ if (key.toLowerCase() === name) return Array.isArray(value) ? value[0] : value;
893
+ }
894
+ return void 0;
895
+ }
896
+ async function signatureOf(key, id, timestamp, body) {
897
+ const prefix = new TextEncoder().encode(`${id}.${timestamp}.`);
898
+ const signed = new Uint8Array(prefix.length + body.length);
899
+ signed.set(prefix);
900
+ signed.set(body, prefix.length);
901
+ const hmac = await crypto.subtle.importKey(
902
+ "raw",
903
+ key,
904
+ { name: "HMAC", hash: "SHA-256" },
905
+ false,
906
+ ["sign"]
907
+ );
908
+ return new Uint8Array(await crypto.subtle.sign("HMAC", hmac, signed));
909
+ }
910
+ async function signedHeaders(secret, id, timestamp, body) {
911
+ const key = keyOf(secret, "secret");
912
+ const signature = await signatureOf(key, id, String(timestamp), new TextEncoder().encode(body));
913
+ return {
914
+ "webhook-id": id,
915
+ "webhook-timestamp": String(timestamp),
916
+ "webhook-signature": `v1,${toBase64(signature)}`
917
+ };
918
+ }
919
+ function sameBytes(a, b) {
920
+ if (a.length !== b.length) return false;
921
+ let difference = 0;
922
+ for (let index = 0; index < a.length; index += 1) {
923
+ difference |= (_nullishCoalesce(a[index], () => ( 0))) ^ (_nullishCoalesce(b[index], () => ( 0)));
924
+ }
925
+ return difference === 0;
926
+ }
927
+ function fromBase64(text) {
928
+ try {
929
+ return Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
930
+ } catch (e6) {
931
+ return null;
932
+ }
933
+ }
934
+ function toBase64(bytes) {
935
+ return btoa(String.fromCharCode(...bytes));
936
+ }
937
+ function invalid(message) {
938
+ return new MesubError(message, { status: null, code: "invalid_webhook" });
939
+ }
940
+
941
+ // src/transport.ts
942
+ var REQUEST_ID_HEADER = "Mesub-Request-Id";
943
+ var INITIAL_RETRY_DELAY = 500;
944
+ var MAX_RETRY_DELAY = 8e3;
945
+ var MAX_RETRY_AFTER = 6e4;
946
+ var Transport = class {
947
+ #config;
948
+ constructor(config) {
949
+ this.#config = config;
950
+ }
951
+ async get(path, query = {}, options = {}) {
952
+ const url = endpoint(this.#config.baseUrl, path);
953
+ for (const [key, value] of Object.entries(query)) {
954
+ if (value !== void 0) url.searchParams.set(key, String(value));
955
+ }
956
+ return this.#send("GET", url, void 0, options);
957
+ }
958
+ /**
959
+ * A POST with a JSON body, sent once: never retried here, whatever the
960
+ * status, a timeout or a network error. A write that got no answer may
961
+ * still have been done: its caller decides, knowing the route, whether
962
+ * the same body may be sent again (`submit` does, Mesub-io/backend#190)
963
+ * or the state read back.
964
+ */
965
+ async post(path, body, options = {}) {
966
+ return this.#send("POST", endpoint(this.#config.baseUrl, path), body, options);
967
+ }
968
+ async #send(method, url, body, options) {
969
+ const maxRetries = method === "POST" ? 0 : _nullishCoalesce(options.maxRetries, () => ( this.#config.maxRetries));
970
+ const { deadline, signal } = options;
971
+ numberOf("timeout", options.timeout, this.#config.timeout, { delay: true });
972
+ for (let retry = 0; ; retry++) {
973
+ const attempt = await this.#attempt(method, url, body, options);
974
+ if (attempt.ok) return attempt.body;
975
+ if (!attempt.retry || retry >= maxRetries) throw attempt.error;
976
+ if (options.retryRateLimited === false && attempt.error.status === 429) {
977
+ throw attempt.error;
978
+ }
979
+ const wait = _nullishCoalesce(attempt.retryAfter, () => ( backoff(retry)));
980
+ if (deadline !== void 0 && Date.now() + wait >= deadline) {
981
+ throw attempt.error;
982
+ }
983
+ await sleep(wait, signal);
984
+ }
985
+ }
986
+ async #attempt(method, url, body, { deadline, signal, ...options }) {
987
+ _optionalChain([signal, 'optionalAccess', _6 => _6.throwIfAborted, 'call', _7 => _7()]);
988
+ const perAttempt = _nullishCoalesce(options.timeout, () => ( this.#config.timeout));
989
+ const left = deadline === void 0 ? Infinity : deadline - Date.now();
990
+ if (left <= 0) {
991
+ const error = new MesubError("No time was left to call Mesub.", {
992
+ status: null,
993
+ code: "unavailable"
994
+ });
995
+ return { ok: false, error, retry: false, retryAfter: null };
996
+ }
997
+ const timeout = Math.min(perAttempt, left);
998
+ const cutByDeadline = timeout < perAttempt;
999
+ const controller = new AbortController();
1000
+ const timer = setTimeout(() => controller.abort(), timeout);
1001
+ const stop = () => controller.abort(_optionalChain([signal, 'optionalAccess', _8 => _8.reason]));
1002
+ _optionalChain([signal, 'optionalAccess', _9 => _9.addEventListener, 'call', _10 => _10("abort", stop, { once: true })]);
1003
+ try {
1004
+ const response = await this.#config.fetch(url, {
1005
+ method,
1006
+ headers: {
1007
+ ...this.#config.headers,
1008
+ Authorization: `Bearer ${this.#config.apiKey}`,
1009
+ Accept: "application/json",
1010
+ "User-Agent": `@mesub/node/${VERSION}`,
1011
+ [API_VERSION_HEADER]: API_VERSION,
1012
+ ...body !== void 0 && { "Content-Type": "application/json" }
1013
+ },
1014
+ ...body !== void 0 && { body: JSON.stringify(body) },
1015
+ signal: controller.signal
1016
+ });
1017
+ const text = await response.text();
1018
+ const requestId = response.headers.get(REQUEST_ID_HEADER);
1019
+ if (response.ok) {
1020
+ try {
1021
+ return { ok: true, body: JSON.parse(text) };
1022
+ } catch (cause) {
1023
+ const error2 = new MesubError("Mesub answered with a body that is not JSON.", {
1024
+ status: response.status,
1025
+ code: "unexpected",
1026
+ body: text,
1027
+ requestId,
1028
+ cause
1029
+ });
1030
+ return { ok: false, error: error2, retry: false, retryAfter: null };
1031
+ }
1032
+ }
1033
+ const answered = parsed2(text);
1034
+ const asked = retryAfter(response.headers.get("retry-after"));
1035
+ const error = response.status === 404 && !isMesubErrorBody(answered) ? new MesubError(
1036
+ `${url.pathname} answered 404 with no Mesub error: is baseUrl (${this.#config.baseUrl}) the Mesub API?`,
1037
+ {
1038
+ status: 404,
1039
+ code: "unexpected",
1040
+ body: answered,
1041
+ retryAfter: asked,
1042
+ requestId
1043
+ }
1044
+ ) : errorFrom(response.status, answered, asked, requestId);
1045
+ return {
1046
+ ok: false,
1047
+ error,
1048
+ // Sent again (a GET only, a POST never is) when the error is `retryable`:
1049
+ // Mesub's flag when the body has one, the status's otherwise.
1050
+ retry: error.retryable,
1051
+ retryAfter: asked === null ? null : Math.min(asked, MAX_RETRY_AFTER)
1052
+ };
1053
+ } catch (cause) {
1054
+ if (_optionalChain([signal, 'optionalAccess', _11 => _11.aborted])) throw signal.reason;
1055
+ if (controller.signal.aborted && cutByDeadline) {
1056
+ const error2 = new MesubError(
1057
+ `Mesub did not answer within the ${Math.round(timeout)} ms left before the deadline.`,
1058
+ { status: null, code: "unavailable", cause }
1059
+ );
1060
+ return { ok: false, error: error2, retry: false, retryAfter: null };
1061
+ }
1062
+ const message = controller.signal.aborted ? `Mesub did not answer within ${perAttempt} ms.` : `Could not reach Mesub: ${cause instanceof Error ? cause.message : String(cause)}`;
1063
+ const error = new MesubError(message, { status: null, code: "unavailable", cause });
1064
+ return { ok: false, error, retry: true, retryAfter: null };
1065
+ } finally {
1066
+ clearTimeout(timer);
1067
+ _optionalChain([signal, 'optionalAccess', _12 => _12.removeEventListener, 'call', _13 => _13("abort", stop)]);
1068
+ }
1069
+ }
1070
+ };
1071
+ function endpoint(baseUrl, path) {
1072
+ return new URL(baseUrl + path);
1073
+ }
1074
+ function errorFrom(status, body, retryAfter2, header) {
1075
+ const { code, retryable, request_id } = isRecord(body) ? body : {};
1076
+ const apiCode = typeof code === "string" ? code : null;
1077
+ return new MesubError(messageFrom(body, status), {
1078
+ status,
1079
+ code: codeFor(status, apiCode),
1080
+ apiCode,
1081
+ retryable: typeof retryable === "boolean" ? retryable : retryableStatus(status),
1082
+ body,
1083
+ retryAfter: retryAfter2,
1084
+ // The header first: the body's says the same, and is the fallback
1085
+ // when a proxy in front drops the header (Mesub-io/backend#472).
1086
+ requestId: _nullishCoalesce(header, () => ( (typeof request_id === "string" ? request_id : null)))
1087
+ });
1088
+ }
1089
+ function retryableStatus(status) {
1090
+ return status === 408 || status === 429 || status >= 500;
1091
+ }
1092
+ function retryAfter(header) {
1093
+ if (header === null || header.trim() === "") return null;
1094
+ const seconds = Number(header);
1095
+ const ms = Number.isNaN(seconds) ? Date.parse(header) - Date.now() : seconds * 1e3;
1096
+ if (!Number.isFinite(ms) || ms < 0) return null;
1097
+ return ms;
1098
+ }
1099
+ function backoff(retry) {
1100
+ const delay = Math.min(INITIAL_RETRY_DELAY * 2 ** retry, MAX_RETRY_DELAY);
1101
+ return delay * (1 - Math.random() * 0.25);
1102
+ }
1103
+ function messageFrom(body, status) {
1104
+ const fallback = `Mesub answered with HTTP ${status}.`;
1105
+ if (!isRecord(body)) return fallback;
1106
+ const { message, error } = body;
1107
+ if (typeof message === "string" && message !== "") return message;
1108
+ if (Array.isArray(message)) {
1109
+ const parts = message.filter((part) => typeof part === "string");
1110
+ if (parts.length > 0) return parts.join("; ");
1111
+ }
1112
+ if (typeof error === "string" && error !== "") return error;
1113
+ return fallback;
1114
+ }
1115
+ function isMesubErrorBody(body) {
1116
+ if (!isRecord(body)) return false;
1117
+ const { code, statusCode, message } = body;
1118
+ if (typeof message === "string" && /^Cannot [A-Z]+ \//.test(message)) return false;
1119
+ return typeof code === "string" || typeof statusCode === "number";
1120
+ }
1121
+ function parsed2(text) {
1122
+ try {
1123
+ return JSON.parse(text);
1124
+ } catch (e7) {
1125
+ return text;
1126
+ }
1127
+ }
1128
+ function isRecord(value) {
1129
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1130
+ }
1131
+ function sleep(ms, signal) {
1132
+ return new Promise((resolve, reject) => {
1133
+ const stop = () => {
1134
+ clearTimeout(timer);
1135
+ reject(_optionalChain([signal, 'optionalAccess', _14 => _14.reason]));
1136
+ };
1137
+ const timer = setTimeout(() => {
1138
+ _optionalChain([signal, 'optionalAccess', _15 => _15.removeEventListener, 'call', _16 => _16("abort", stop)]);
1139
+ resolve();
1140
+ }, ms);
1141
+ _optionalChain([signal, 'optionalAccess', _17 => _17.addEventListener, 'call', _18 => _18("abort", stop, { once: true })]);
1142
+ });
1143
+ }
1144
+
1145
+ // src/subscriptions.ts
1146
+ var SUBMIT_TIMEOUT = 9e4;
1147
+ var REPLAY_ROOM = 3e4;
1148
+ var MAX_SENDS = 3;
1149
+ var REPLAY_WAIT = 1e4;
1150
+ var READ_BACK_TIME = 1e4;
1151
+ var CONFIRM_TIMEOUT = SUBMIT_TIMEOUT;
1152
+ var Subscriptions = class {
1153
+ #transport;
1154
+ /**
1155
+ * Told of every subscription `submit`, `retrieve` and `list` answer, so
1156
+ * the client drops the cached access answers one that landed outdates.
1157
+ */
1158
+ #seen;
1159
+ /**
1160
+ * Told of the subscription a confirm moved, so the client drops every
1161
+ * access answer cached for its customer, the yes as well as the no.
1162
+ */
1163
+ #changed;
1164
+ /** @internal */
1165
+ constructor(transport, seen = async () => {
1166
+ }, changed = seen) {
1167
+ this.#transport = transport;
1168
+ this.#seen = seen;
1169
+ this.#changed = changed;
1170
+ }
1171
+ /**
1172
+ * Reserves a subscription to that plan for that wallet, and builds what
1173
+ * it signs: `terms.message`, then `transaction`. Hand both to your front,
1174
+ * then their signatures to `submit`. Called again for the same plan,
1175
+ * wallet and customer (`external_id`, else `email`) while nothing landed,
1176
+ * it answers the same subscription with a fresh transaction; by its id,
1177
+ * `transaction` does. Another
1178
+ * customer naming that wallet gets a subscription of its own
1179
+ * (Mesub-io/backend#311): only the one the wallet signs for lands, and
1180
+ * the others then expire. Sent once, never retried.
1181
+ */
1182
+ async create(params, options = {}) {
1183
+ const { plan, wallet, email, external_id } = params;
1184
+ const body = { plan, wallet, email, external_id };
1185
+ return subscribeTransactionFrom(
1186
+ await this.#transport.post("/v1/subscriptions", body, options)
1187
+ );
1188
+ }
1189
+ /**
1190
+ * Relays what the wallet signed. Mesub checks it, co-signs, sends it and
1191
+ * waits for the chain, so one send takes up to a minute or so: 90 s
1192
+ * unless `options.timeout` says otherwise.
1193
+ *
1194
+ * When the send gets no answer that says what became of it (a timeout,
1195
+ * a network error, a 5xx), or an error Mesub marks `retryable`, the same
1196
+ * request, the same body, is sent again, up to twice, after the
1197
+ * `Retry-After` Mesub asked for or 10 s, within `options.budget` (120 s
1198
+ * by default). That is safe since Mesub-io/backend#190 and #202: Mesub
1199
+ * recognises the transaction and the terms signature it already
1200
+ * co-signed, signs nothing again, and answers from the chain or from the
1201
+ * subscription it settled. Its answer is then returned as if the first
1202
+ * send had got it, a `pending` with Mesub's own `reason` included.
1203
+ *
1204
+ * When no send got an answer, the subscription is read back once, within
1205
+ * 10 s: it is returned if `active` or `cancelled` (it landed), and
1206
+ * otherwise a `MesubSubmitError` `unavailable` is thrown with it attached
1207
+ * as `subscription` (null if that read failed too): the wallet may have
1208
+ * paid and the transaction may still land, so read it again with
1209
+ * `retrieve` rather than create anew. A 2xx this SDK cannot read is read
1210
+ * back the same way, and thrown as `unexpected`; so is a replay refused
1211
+ * with `not_awaiting_signature` after a send that got no answer, thrown
1212
+ * as that `conflict`.
1213
+ *
1214
+ * A 503 `unavailable` (too many submits waiting on the project,
1215
+ * Mesub-io/backend#505) says nothing was signed or sent: it is replayed
1216
+ * after its `Retry-After` (2 s) like any retryable refusal, and when the
1217
+ * replays run out it is thrown as is, `retryable`, with no read back,
1218
+ * unless an earlier send got no answer.
1219
+ *
1220
+ * Any other refusal (`terms_expired`, `transaction_expired`, ...) is
1221
+ * thrown as is. After `transaction_expired` nothing was sent and the same
1222
+ * submit cannot land: call `transaction(id)` for a new one, never
1223
+ * `create` again.
1224
+ * An abort through `options.signal` stops everything, sends, waits and
1225
+ * the read back, and rejects with the signal's reason; a send already out
1226
+ * may have been co-signed, so read the subscription back.
1227
+ *
1228
+ * A subscription returned with `access` (`active`, or `cancelled` before
1229
+ * its end) drops the `/v1/access` answers cached for that customer (its
1230
+ * wallet, external id and email) that still say no: `hasAccess` right
1231
+ * after asks Mesub again.
1232
+ */
1233
+ async submit(id, params, options = {}) {
1234
+ const result = await this.#submit(id, params, options);
1235
+ await this.#seen(result.subscription);
1236
+ return result;
1237
+ }
1238
+ /**
1239
+ * A fresh transaction for a subscription still `pending`, with the terms
1240
+ * it names (Mesub-io/backend#427). A transaction lives about half a
1241
+ * minute, the terms five: have the terms signed first, call this when the
1242
+ * wallet is ready, have it sign the transaction at once, then `submit`.
1243
+ * Always sign the transaction of the last call. It is also what follows
1244
+ * a submit refused with `transaction_expired`.
1245
+ *
1246
+ * A terms signature you hold is still good when `terms.message` is, to
1247
+ * the letter, the message the wallet signed. Another text is new terms:
1248
+ * have the wallet sign it, the old signature is refused
1249
+ * (`invalid_terms_signature`).
1250
+ *
1251
+ * Reserves nothing and sends no body: the wallet, the plan and the
1252
+ * customer are the subscription's own. Refused with `apiCode`
1253
+ * `not_awaiting_signature` once it is signed, expired or ended (call
1254
+ * `create` again for an expired one), and with what `create` refuses of
1255
+ * a plan or a wallet (`plan_ended`, `insufficient_balance`,
1256
+ * `already_subscribed`, ...). Sent once, never retried: asking twice is
1257
+ * safe, only the last transaction is to be signed.
1258
+ */
1259
+ async transaction(id, options = {}) {
1260
+ return subscribeTransactionFrom(
1261
+ await this.#transport.post(`${pathOf(id)}/transaction`, void 0, options),
1262
+ "POST /v1/subscriptions/:id/transaction"
1263
+ );
1264
+ }
1265
+ async #submit(id, params, options) {
1266
+ const path = `${pathOf(id)}/submit`;
1267
+ const body = { transaction: params.transaction, terms_signature: params.terms_signature };
1268
+ const { signal } = options;
1269
+ const timeout = numberOf("timeout", options.timeout, SUBMIT_TIMEOUT, { delay: true });
1270
+ const budget = _nullishCoalesce(options.budget, () => ( timeout + REPLAY_ROOM));
1271
+ if (!Number.isFinite(budget) || budget <= 0) {
1272
+ throw new TypeError(`budget must be a positive number of ms, not ${budget}.`);
1273
+ }
1274
+ const deadline = Date.now() + budget;
1275
+ let lost = false;
1276
+ let responded = null;
1277
+ for (let sent = 1; ; sent++) {
1278
+ let error;
1279
+ try {
1280
+ const answer = await this.#transport.post(path, body, {
1281
+ timeout,
1282
+ deadline,
1283
+ ...signal !== void 0 && { signal }
1284
+ });
1285
+ return submitResultFrom(answer);
1286
+ } catch (caught) {
1287
+ if (!(caught instanceof MesubError)) throw caught;
1288
+ error = caught;
1289
+ }
1290
+ const lostBefore = lost;
1291
+ if (unanswered(error)) lost = true;
1292
+ if (error.status !== null) responded = error;
1293
+ const unsettled = { sends: sent, cause: error, from: error, lead: error.message };
1294
+ if (error.status !== null && error.status < 300) {
1295
+ return this.#settle(id, signal, {
1296
+ ...unsettled,
1297
+ code: "unexpected",
1298
+ retryable: false
1299
+ });
1300
+ }
1301
+ if (lostBefore && error.apiCode === "not_awaiting_signature") {
1302
+ return this.#settle(id, signal, {
1303
+ ...unsettled,
1304
+ code: error.code,
1305
+ retryable: error.retryable
1306
+ });
1307
+ }
1308
+ const rebuild = error.apiCode === "comeback_period_rolling";
1309
+ const replay = !rebuild && (error.status === null || error.retryable);
1310
+ const wait = _nullishCoalesce(error.retryAfter, () => ( REPLAY_WAIT));
1311
+ if (!replay || sent >= MAX_SENDS || Date.now() + wait >= deadline) {
1312
+ if (!lost || !unanswered(error) && !error.retryable) throw error;
1313
+ return this.#settle(id, signal, {
1314
+ ...unsettled,
1315
+ from: _nullishCoalesce(responded, () => ( error)),
1316
+ code: "unavailable",
1317
+ retryable: true,
1318
+ lead: `Mesub never said what became of the submit, sent ${sent} ${sent === 1 ? "time" : "times"}: ${error.message}`
1319
+ });
1320
+ }
1321
+ await sleep(wait, signal);
1322
+ }
1323
+ }
1324
+ /**
1325
+ * A submit whose outcome is unknown, settled from the subscription read
1326
+ * back: returned if it landed, thrown with it otherwise.
1327
+ */
1328
+ async #settle(id, signal, { sends, cause, from, code, retryable, lead }) {
1329
+ const subscription = await this.#readBack(id, signal);
1330
+ if (_optionalChain([subscription, 'optionalAccess', _19 => _19.status]) === "active" || _optionalChain([subscription, 'optionalAccess', _20 => _20.status]) === "cancelled") {
1331
+ return { subscription };
1332
+ }
1333
+ const then = subscription === null ? "Reading the subscription back failed too" : `The subscription read back is ${subscription.status}`;
1334
+ throw new MesubSubmitError(
1335
+ `${lead} ${then}: read it again with retrieve before creating anew.`,
1336
+ {
1337
+ status: from.status,
1338
+ code,
1339
+ apiCode: from.apiCode,
1340
+ retryable,
1341
+ body: from.body,
1342
+ retryAfter: from.retryAfter,
1343
+ requestId: from.requestId,
1344
+ cause,
1345
+ subscription,
1346
+ sends
1347
+ }
1348
+ );
1349
+ }
1350
+ /**
1351
+ * The subscription after a submit, within `READ_BACK_TIME`: no attempt
1352
+ * nor retry wait goes past it. Null when it cannot be read; the caller's
1353
+ * abort is thrown as is.
1354
+ */
1355
+ async #readBack(id, signal) {
1356
+ try {
1357
+ const body = await this.#transport.get(
1358
+ pathOf(id),
1359
+ {},
1360
+ {
1361
+ deadline: Date.now() + READ_BACK_TIME,
1362
+ ...signal !== void 0 && { signal }
1363
+ }
1364
+ );
1365
+ return serverSubscriptionFrom(body);
1366
+ } catch (error) {
1367
+ if (_optionalChain([signal, 'optionalAccess', _21 => _21.aborted])) throw error;
1368
+ return null;
1369
+ }
1370
+ }
1371
+ /**
1372
+ * Builds the transaction that cancels an `active`, `unpaid` or `stopped`
1373
+ * subscription. Hand it to your front: the subscription's wallet signs
1374
+ * and sends it, then `confirmCancel` takes the signature. Nothing is
1375
+ * cancelled until then. Sent once, never retried.
1376
+ *
1377
+ * One paid up keeps its access to the end of its period; a late or
1378
+ * stopped one has none, and its missed period is never collected.
1379
+ */
1380
+ async cancel(id, options = {}) {
1381
+ return this.#build("cancel", id, options);
1382
+ }
1383
+ /**
1384
+ * Settles a cancellation the wallet sent, by the signature it answered:
1385
+ * `cancelled`, or the subscription as it was with a `reason` when nothing
1386
+ * landed. Mesub waits for the chain, so up to a minute or so: 90 s unless
1387
+ * `options.timeout` says otherwise. Sent once, never retried; the same
1388
+ * confirm sent again is answered the same.
1389
+ *
1390
+ * Settled without a `reason`, it drops every `/v1/access` answer cached
1391
+ * for that customer on that plan: `access` right after asks Mesub again.
1392
+ */
1393
+ async confirmCancel(id, params, options = {}) {
1394
+ return this.#confirm("cancel", id, params, options);
1395
+ }
1396
+ /**
1397
+ * Builds the transaction that takes a `cancelled` subscription back,
1398
+ * before the period it paid for is over. Hand it to your front: the
1399
+ * wallet signs and sends it, then `confirmResume` takes the signature.
1400
+ * Sent once, never retried.
1401
+ */
1402
+ async resume(id, options = {}) {
1403
+ return this.#build("resume", id, options);
1404
+ }
1405
+ /** As `confirmCancel`, for a resume: `active` again, or a `reason`. */
1406
+ async confirmResume(id, params, options = {}) {
1407
+ return this.#confirm("resume", id, params, options);
1408
+ }
1409
+ /**
1410
+ * Builds the transaction that closes the wallet's authorisation on a
1411
+ * subscription that is over: its rent goes back to the wallet. Mesub
1412
+ * builds it for a cancelled one past its end; for a stopped one
1413
+ * cancelled on chain and past its end, or whose authority is closed for
1414
+ * good; and for one `ended` with its plan (`end_reason` `plan_removed`,
1415
+ * `plan_replaced` or `plan_ended`). A stopped one that nothing cancelled
1416
+ * is refused with `subscription_not_cancelled`: cancel it first.
1417
+ * Hand the transaction to your front: the wallet signs and sends it,
1418
+ * then `confirmClose` takes the signature. Sent once, never retried.
1419
+ */
1420
+ async close(id, options = {}) {
1421
+ return this.#build("close", id, options);
1422
+ }
1423
+ /** As `confirmCancel`, for a close: `ended` with `end_reason` `closed`, or a `reason`. */
1424
+ async confirmClose(id, params, options = {}) {
1425
+ return this.#confirm("close", id, params, options);
1426
+ }
1427
+ /**
1428
+ * Pays a late payment now (Mesub-io/backend#354): the pull of the missed
1429
+ * period runs at once rather than at its next retry, with nothing for the
1430
+ * wallet to sign, since it approved the plan when it subscribed. Answers
1431
+ * the subscription, still `unpaid`: the outcome comes with the pull, as
1432
+ * the `subscription.renewed` or `subscription.payment_failed` webhook.
1433
+ *
1434
+ * The merchant's hand retry from the dashboard follows the same rules: on
1435
+ * Free it is one of the missed period's three, whoever fires it; on Dev
1436
+ * and Business it runs beside the schedule, the wait between two doubling.
1437
+ * Mesub refuses it first when it cannot succeed, so it never spends a
1438
+ * retry for nothing: `apiCode` `insufficient_balance` (top up, then pay
1439
+ * again), `retry_cannot_succeed` (the wallet no longer approves it, its
1440
+ * token account is frozen, or its authority is closed), `retry_too_soon`
1441
+ * (`retryAfter` says when), `retries_spent`, `retry_deadline_passed`,
1442
+ * `subscription_not_late`, `subscription_paused`, `plan_ended`. Two more
1443
+ * are `retryable`: `subscription_changed` (two retries met, or a pull
1444
+ * moved the subscription on) and, on Free, `pull_running` (a pull runs
1445
+ * now). Read the subscription back before you ask again. A 503 is
1446
+ * `retryable` too, and nothing changed: `unavailable` (the charge could
1447
+ * not be queued) or `network_unavailable`, with `retryAfter` saying when
1448
+ * to ask again (20 s and 10 s). Sent once, never retried: a second one is
1449
+ * a second retry.
1450
+ */
1451
+ async retry(id, options = {}) {
1452
+ return serverSubscriptionFrom(
1453
+ await this.#transport.post(`${pathOf(id)}/retry`, void 0, options),
1454
+ "POST /v1/subscriptions/:id/retry",
1455
+ 202
1456
+ );
1457
+ }
1458
+ async #build(action, id, options) {
1459
+ const path = `${pathOf(id)}/${action}`;
1460
+ return walletTransactionFrom(
1461
+ await this.#transport.post(path, void 0, options),
1462
+ `POST /v1/subscriptions/:id/${action}`
1463
+ );
1464
+ }
1465
+ async #confirm(action, id, params, options) {
1466
+ const path = `${pathOf(id)}/${action}/confirm`;
1467
+ const result = confirmResultFrom(
1468
+ await this.#transport.post(
1469
+ path,
1470
+ { signature: params.signature },
1471
+ { ...options, timeout: _nullishCoalesce(options.timeout, () => ( CONFIRM_TIMEOUT)) }
1472
+ ),
1473
+ `POST /v1/subscriptions/:id/${action}/confirm`
1474
+ );
1475
+ await (result.reason === void 0 ? this.#changed : this.#seen)(result.subscription);
1476
+ return result;
1477
+ }
1478
+ /**
1479
+ * One subscription of your project, by id. Found with `access`, it drops
1480
+ * the access answers cached that still say no, as `submit` does.
1481
+ */
1482
+ async retrieve(id, options = {}) {
1483
+ const subscription = serverSubscriptionFrom(
1484
+ await this.#transport.get(pathOf(id), {}, options)
1485
+ );
1486
+ await this.#seen(subscription);
1487
+ return subscription;
1488
+ }
1489
+ /**
1490
+ * A customer's subscriptions, newest first, one page at a time; expired
1491
+ * checkouts included. `listAll` walks every page.
1492
+ *
1493
+ * The customer is named as for `access`, and normalised the same way;
1494
+ * none or two of `wallet`, `external_id` and `email` throws a TypeError.
1495
+ * Each one with `access` drops the access answers cached that still say
1496
+ * no, as `submit` does.
1497
+ */
1498
+ async list(params, options = {}) {
1499
+ const { kind, value } = customerOf(params);
1500
+ const { plan, limit, starting_after } = params;
1501
+ const query = {
1502
+ [kind]: value,
1503
+ plan,
1504
+ limit,
1505
+ starting_after
1506
+ };
1507
+ const page = serverSubscriptionListFrom(
1508
+ await this.#transport.get("/v1/subscriptions", query, options)
1509
+ );
1510
+ for (const subscription of page.data) await this.#seen(subscription);
1511
+ return page;
1512
+ }
1513
+ /**
1514
+ * Every subscription `list` would answer, page after page, for `for await`.
1515
+ * Each page is one call, made when the previous one is used up.
1516
+ */
1517
+ async *listAll(params, options = {}) {
1518
+ let page = await this.list(params, options);
1519
+ yield* page.data;
1520
+ while (page.has_more && page.data.length > 0) {
1521
+ page = await this.list({ ...params, starting_after: page.data.at(-1).id }, options);
1522
+ yield* page.data;
1523
+ }
1524
+ }
1525
+ /**
1526
+ * That subscription's own pull attempts, newest first, one page at a
1527
+ * time, and `paid`: what it paid since it began, whatever the page.
1528
+ * `allAttempts` walks every page.
1529
+ *
1530
+ * The id asked and no other: a subscription the wallet came back over
1531
+ * (`superseded`) keeps its own payments, and the one that replaced it
1532
+ * starts from its first pull. Needs a Mesub that serves the route
1533
+ * (Mesub-io/backend#289): from an older one it throws a `not_found`
1534
+ * whose `apiCode` is null, where an id it does not hold is a `not_found`
1535
+ * whose `apiCode` is `subscription_not_found`.
1536
+ */
1537
+ async attempts(id, params = {}, options = {}) {
1538
+ const { limit, starting_after } = params;
1539
+ const path = `${pathOf(id)}/attempts`;
1540
+ try {
1541
+ return subscriptionAttemptListFrom(
1542
+ await this.#transport.get(path, { limit, starting_after }, options)
1543
+ );
1544
+ } catch (error) {
1545
+ if (!(error instanceof MesubError) || !routeMissing(error)) throw error;
1546
+ throw new MesubError(
1547
+ "This Mesub does not serve GET /v1/subscriptions/:id/attempts yet.",
1548
+ {
1549
+ status: 404,
1550
+ code: "not_found",
1551
+ body: error.body,
1552
+ requestId: error.requestId,
1553
+ cause: error
1554
+ }
1555
+ );
1556
+ }
1557
+ }
1558
+ /**
1559
+ * Every attempt `attempts` would answer, page after page, for `for await`.
1560
+ * Each page is one call, made when the previous one is used up.
1561
+ */
1562
+ async *allAttempts(id, params = {}, options = {}) {
1563
+ let page = await this.attempts(id, params, options);
1564
+ yield* page.data;
1565
+ while (page.has_more && page.data.length > 0) {
1566
+ page = await this.attempts(
1567
+ id,
1568
+ { ...params, starting_after: page.data.at(-1).id },
1569
+ options
1570
+ );
1571
+ yield* page.data;
1572
+ }
1573
+ }
1574
+ };
1575
+ var ID = /^[A-Za-z0-9_-]{1,64}$/;
1576
+ function pathOf(id) {
1577
+ if (typeof id !== "string" || id === "") {
1578
+ throw new MesubError("A subscription id is required.", {
1579
+ status: null,
1580
+ code: "invalid_request"
1581
+ });
1582
+ }
1583
+ if (!ID.test(id)) {
1584
+ throw new MesubError(
1585
+ "A subscription id is letters, digits, hyphens and underscores, 64 at most.",
1586
+ { status: null, code: "invalid_request" }
1587
+ );
1588
+ }
1589
+ return `/v1/subscriptions/${id}`;
1590
+ }
1591
+ function routeMissing(error) {
1592
+ const { message } = _nullishCoalesce(error.body, () => ( {}));
1593
+ return error.status === 404 && error.apiCode === null && typeof message === "string" && message.startsWith("Cannot GET /");
1594
+ }
1595
+ function unanswered(error) {
1596
+ if (error.status === 503 && error.apiCode === "unavailable") return false;
1597
+ return error.status === null || error.status >= 500;
1598
+ }
1599
+
1600
+ // src/events.ts
1601
+ var Events = class {
1602
+ #transport;
1603
+ /** @internal */
1604
+ constructor(transport) {
1605
+ this.#transport = transport;
1606
+ }
1607
+ /**
1608
+ * Your project's events of the last 30 days, newest first, one page at a
1609
+ * time: each the very body its webhooks carried, the same `id`, `data`
1610
+ * and `detail`. Read it to catch up on what an endpoint missed while it
1611
+ * was down or turned off. Events before `available_since` are never
1612
+ * served, whatever the window.
1613
+ *
1614
+ * An event is listed a moment after its `created_at`, so one can appear
1615
+ * below one already read: when polling for what is new, page on until
1616
+ * `created_at` is a minute older than the newest you held, and skip the
1617
+ * ids you have seen.
1618
+ *
1619
+ * Needs a Mesub that serves the route (Mesub-io/backend#482): from an
1620
+ * older one it throws a `not_found` whose `apiCode` is null.
1621
+ */
1622
+ async list(params = {}, options = {}) {
1623
+ const { type, subscription, limit, starting_after } = params;
1624
+ return eventListFrom(
1625
+ await this.#get("/v1/events", { type, subscription, limit, starting_after }, options)
1626
+ );
1627
+ }
1628
+ /**
1629
+ * One event, by the `id` its webhook body carries, as `list` serves it.
1630
+ * A `not_found` whose `apiCode` is `event_not_found` for an id older than
1631
+ * 30 days or than `available_since`, or of another project. A delivery's
1632
+ * `webhook-id` is not an event id: read `event.id`, not `delivery_id`.
1633
+ */
1634
+ async retrieve(id, options = {}) {
1635
+ return eventFrom(await this.#get(pathOf2(id), {}, options));
1636
+ }
1637
+ async #get(path, query, options) {
1638
+ try {
1639
+ return await this.#transport.get(path, query, options);
1640
+ } catch (error) {
1641
+ if (!(error instanceof MesubError) || !routeMissing(error)) throw error;
1642
+ throw new MesubError("This Mesub does not serve GET /v1/events yet.", {
1643
+ status: 404,
1644
+ code: "not_found",
1645
+ body: error.body,
1646
+ requestId: error.requestId,
1647
+ cause: error
1648
+ });
1649
+ }
1650
+ }
1651
+ };
1652
+ var ID2 = /^[A-Za-z0-9_-]{1,64}$/;
1653
+ function pathOf2(id) {
1654
+ if (typeof id !== "string" || !ID2.test(id)) {
1655
+ throw new MesubError(
1656
+ "An event id is letters, digits, hyphens and underscores, 64 at most.",
1657
+ { status: null, code: "invalid_request" }
1658
+ );
1659
+ }
1660
+ return `/v1/events/${id}`;
1661
+ }
1662
+
1663
+ // src/plans.ts
1664
+ var Plans = class {
1665
+ #transport;
1666
+ /** @internal */
1667
+ constructor(transport) {
1668
+ this.#transport = transport;
1669
+ }
1670
+ /**
1671
+ * Every plan of your project a subscriber can hold, sorted by slug: the
1672
+ * active ones, and the ones ending (`sunset`, `available: false`). Never
1673
+ * cached: call it where you render, and cache it your way.
1674
+ */
1675
+ async list(options = {}) {
1676
+ return planListFrom(await this.#transport.get("/v1/plans", {}, options));
1677
+ }
1678
+ /** One plan by its slug. A slug your project lacks throws `plan_not_found`. */
1679
+ async retrieve(slug, options = {}) {
1680
+ return planFrom(await this.#transport.get(pathOf3(slug), {}, options));
1681
+ }
1682
+ };
1683
+ var SLUG = /^(?=.*[a-z])[a-z0-9]+(?:-[a-z0-9]+)*$/;
1684
+ function pathOf3(slug) {
1685
+ if (typeof slug !== "string" || !SLUG.test(slug)) {
1686
+ throw new MesubError(
1687
+ "A plan slug is lowercase letters, digits and hyphens, with at least one letter.",
1688
+ { status: null, code: "invalid_request" }
1689
+ );
1690
+ }
1691
+ return `/v1/plans/${slug}`;
1692
+ }
1693
+
1694
+ // src/client.ts
1695
+ var DEFAULT_BASE_URL = "https://api.mesub.io";
1696
+ var DEFAULT_GUARD_TIMEOUT = 2e3;
1697
+ var Mesub = (_class3 = class {
1698
+ /** Subscribing from your server: create, submit, retrieve and list. */
1699
+
1700
+ /** The plans of your project, for a pricing page: `list` and `retrieve`. */
1701
+
1702
+ /** Checking the webhooks Mesub sends: `verify`. */
1703
+
1704
+ /** The events of the last 30 days, as their webhooks carried them: `list` and `retrieve`. */
1705
+
1706
+ /** @internal */
1707
+
1708
+ /** @internal */
1709
+
1710
+ /** The lists of `accessList`, in the same store, under their own keys. */
1711
+
1712
+ /** What the cache keys are scoped by: a hash of the API key, computed once. */
1713
+
1714
+ /** What hashes emails and external ids in the cache keys: the API key, imported once. */
1715
+
1716
+
1717
+ /**
1718
+ * The `/v1/access` requests in flight, by cache key: a call for the same
1719
+ * answer waits for that one rather than send its own (#34).
1720
+ */
1721
+ __init3() {this.flights = /* @__PURE__ */ new Map()}
1722
+ constructor(options = {}) {;_class3.prototype.__init3.call(this);
1723
+ const apiKey = apiKeyOf(options.apiKey);
1724
+ const baseUrl = baseUrlOf(options.baseUrl, DEFAULT_BASE_URL);
1725
+ const headers = headersOf(options.headers);
1726
+ const webhookSecret = webhookSecretOf(options.webhookSecret);
1727
+ if (options.fetch !== void 0 && typeof options.fetch !== "function") {
1728
+ throw new TypeError("fetch must be a function.");
1729
+ }
1730
+ const fetch = _nullishCoalesce(options.fetch, () => ( ((input, init) => globalThis.fetch(input, init))));
1731
+ this.guardTimeout = numberOf("guardTimeout", options.guardTimeout, DEFAULT_GUARD_TIMEOUT, {
1732
+ delay: true
1733
+ });
1734
+ this.transport = new Transport({
1735
+ apiKey,
1736
+ baseUrl,
1737
+ headers,
1738
+ fetch,
1739
+ timeout: numberOf("timeout", options.timeout, 5e3, { delay: true }),
1740
+ maxRetries: numberOf("maxRetries", options.maxRetries, 2, {
1741
+ zero: true,
1742
+ integer: true
1743
+ })
1744
+ });
1745
+ this.subscriptions = new Subscriptions(
1746
+ this.transport,
1747
+ (subscription) => this.landed(subscription),
1748
+ (subscription) => this.landed(subscription, true)
1749
+ );
1750
+ this.plans = new Plans(this.transport);
1751
+ this.events = new Events(this.transport);
1752
+ this.webhooks = new Webhooks(webhookSecret, (event) => this.landed(event.data, true));
1753
+ const store = _nullishCoalesce(options.cache, () => ( new MemoryStore()));
1754
+ const staleness = options.maxStaleMs === void 0 ? {} : { maxStaleMs: numberOf("maxStaleMs", options.maxStaleMs, 0, { zero: true }) };
1755
+ this.cache = new AccessCache(store, staleness);
1756
+ this.lists = new AccessCache(store, staleness);
1757
+ let cacheScope;
1758
+ this.cacheScope = () => cacheScope ??= hashScope(apiKey);
1759
+ let cacheSecret;
1760
+ this.cacheSecret = () => cacheSecret ??= importSecret(apiKey);
1761
+ }
1762
+ /**
1763
+ * Everything Mesub knows about that customer on that plan: whether they
1764
+ * have access, their status, their dates. Served from the cache while fresh.
1765
+ *
1766
+ * The customer is `{ wallet }`, `{ external_id }` or `{ email }`, or a
1767
+ * wallet as a string. Asked by external id or email, the answer is about
1768
+ * the wallet that grants access, if any: `wallet` is null when they have
1769
+ * nothing on that plan.
1770
+ *
1771
+ * Throws a `MesubError` whenever Mesub cannot answer: it is for screens.
1772
+ * A guard calls `hasAccess`, which falls back instead.
1773
+ */
1774
+ async access(customer, plan, options = {}) {
1775
+ const asked = customerOf(customer);
1776
+ const slug = planOf(plan);
1777
+ if (options.attempts === true) return this.ask(asked, slug, null, boundOf(options));
1778
+ const slot = await this.slotOf(asked);
1779
+ const cached = await this.cache.read(slot.who, slug, slot.scope);
1780
+ if (_optionalChain([cached, 'optionalAccess', _22 => _22.fresh])) return cached.value;
1781
+ return this.ask(asked, slug, slot);
1782
+ }
1783
+ /**
1784
+ * Every plan of the project that customer has anything on, each answered
1785
+ * like `access`, for a page listing their entitlements in one call. Cached
1786
+ * apart from the answers by plan, for its own `revalidate_after`.
1787
+ *
1788
+ * Throws a `MesubError` whenever Mesub cannot answer, like `access`.
1789
+ */
1790
+ async accessList(customer, options = {}) {
1791
+ const asked = customerOf(customer);
1792
+ const attempts = options.attempts === true;
1793
+ const slot = attempts ? null : await this.slotOf(asked);
1794
+ if (slot) {
1795
+ const cached = await this.lists.read(slot.who, null, slot.scope);
1796
+ if (_optionalChain([cached, 'optionalAccess', _23 => _23.fresh])) return cached.value;
1797
+ }
1798
+ const request = async () => accessListFrom(
1799
+ await this.transport.get("/v1/access", {
1800
+ [asked.kind]: asked.value,
1801
+ attempts: attempts || void 0
1802
+ })
1803
+ );
1804
+ if (!slot) return request();
1805
+ return this.shared(
1806
+ flightKey("call", slot, null),
1807
+ request,
1808
+ (list) => this.lists.write(slot.who, null, list, slot.scope)
1809
+ );
1810
+ }
1811
+ /**
1812
+ * `access` from Mesub, cached in the slot its caller computed once and
1813
+ * already read, or without the cache at all (`null`) for an answer with
1814
+ * its attempts.
1815
+ */
1816
+ async ask(asked, plan, slot, call = {}) {
1817
+ const attempts = slot === null;
1818
+ const request = async () => accessAnswerFrom(
1819
+ await this.transport.get(
1820
+ "/v1/access",
1821
+ {
1822
+ [asked.kind]: asked.value,
1823
+ plan,
1824
+ // Left out when not asked: the transport drops undefined values.
1825
+ attempts: attempts || void 0
1826
+ },
1827
+ call
1828
+ )
1829
+ );
1830
+ if (!slot) return request();
1831
+ const mode = call.deadline === void 0 ? "call" : "guard";
1832
+ return this.shared(
1833
+ flightKey(mode, slot, plan),
1834
+ request,
1835
+ (answer) => this.cache.write(slot.who, plan, answer, slot.scope)
1836
+ );
1837
+ }
1838
+ /**
1839
+ * One request in flight per key (#34): fifty calls for a customer not in
1840
+ * the cache made fifty requests to Mesub, and as many 429s to retry. The
1841
+ * first sends it, the others wait for its answer or its error. Its answer
1842
+ * is cached once, unless a subscription that landed dropped the flight
1843
+ * meanwhile: then it is answered, not cached.
1844
+ */
1845
+ shared(key, request, keep) {
1846
+ const pending = this.flights.get(key);
1847
+ if (pending) return pending;
1848
+ const flight = request().then((value) => {
1849
+ if (this.flights.get(key) === flight) void keep(value);
1850
+ return value;
1851
+ });
1852
+ const land = () => {
1853
+ if (this.flights.get(key) === flight) this.flights.delete(key);
1854
+ };
1855
+ this.flights.set(key, flight);
1856
+ void flight.then(land, land);
1857
+ return flight;
1858
+ }
1859
+ /**
1860
+ * Where a customer's answers are cached: the project's scope, and who
1861
+ * they are as the key reads it. A wallet is public on chain and stays
1862
+ * readable; an email or an external id is never written in clear, only
1863
+ * as an HMAC under the API key, which a leaked Redis alone cannot reverse
1864
+ * by guessing.
1865
+ */
1866
+ async slotOf(asked) {
1867
+ const scope = await this.cacheScope();
1868
+ if (asked.kind === "wallet") return { scope, who: `wallet:${asked.value}` };
1869
+ const mac = await crypto.subtle.sign(
1870
+ "HMAC",
1871
+ await this.cacheSecret(),
1872
+ new TextEncoder().encode(`${asked.kind}:${asked.value}`)
1873
+ );
1874
+ return { scope, who: `${asked.kind}:${hex(mac)}` };
1875
+ }
1876
+ /**
1877
+ * A subscription `submit`, `retrieve` or `list` answered (#33): once it
1878
+ * grants access (`active`, or `cancelled` before its end), the answers
1879
+ * cached for its customer that say no are dropped, by wallet, external
1880
+ * id and email, for its plan and in the list of every plan. Without it, a
1881
+ * `hasAccess` right after a submit kept answering the cached no until
1882
+ * `revalidate_after` ran out. A store that fails costs nothing more than
1883
+ * that: it never throws.
1884
+ *
1885
+ * With `moved`, one a confirm just cancelled, resumed or closed, or one a
1886
+ * verified webhook carried: every answer cached for its customer is
1887
+ * dropped, the yes too, since its status and dates changed whatever its
1888
+ * access. A `stopped` or `ended` event must not leave a yes behind.
1889
+ */
1890
+ async landed(subscription, moved = false) {
1891
+ const { plan } = subscription;
1892
+ if (!moved && !subscription.access) return;
1893
+ try {
1894
+ for (const asked of customersOf(subscription)) {
1895
+ const slot = await this.slotOf(asked);
1896
+ for (const mode of ["call", "guard"]) {
1897
+ if (plan !== null) this.flights.delete(flightKey(mode, slot, plan));
1898
+ this.flights.delete(flightKey(mode, slot, null));
1899
+ }
1900
+ if (plan !== null) {
1901
+ await this.cache.forget(
1902
+ slot.who,
1903
+ plan,
1904
+ slot.scope,
1905
+ (answer) => moved || !answer.access
1906
+ );
1907
+ }
1908
+ await this.lists.forget(
1909
+ slot.who,
1910
+ null,
1911
+ slot.scope,
1912
+ (list) => moved || !list.plans.some((answer) => answer.plan === plan && answer.access)
1913
+ );
1914
+ }
1915
+ } catch (e8) {
1916
+ }
1917
+ }
1918
+ /**
1919
+ * Whether that customer has access to that plan, for a guard. The
1920
+ * customer is the same as for `access`, a wallet string included.
1921
+ *
1922
+ * When Mesub is unavailable after the retries (`unavailable` or
1923
+ * `rate_limited`), it answers the last answer it knew, even stale, and
1924
+ * `false` for a customer it never saw. Any other error is thrown, never
1925
+ * turned into `false`: a bad key or an unknown plan is a broken
1926
+ * integration, not a denial.
1927
+ *
1928
+ * No time budget: it waits out the client's timeout and retries, and a
1929
+ * 429's `Retry-After`. A guard answers within `guardTimeout` instead.
1930
+ */
1931
+ async hasAccess(customer, plan) {
1932
+ return (await this.decideBy(customer, plan)).access;
1933
+ }
1934
+ /**
1935
+ * `hasAccess`, with the answer it decided on, within `guardTimeout`: what
1936
+ * the middlewares hand the route. `answer` is null only for a customer
1937
+ * never seen during an outage, which is then `unavailable`; `stale` is
1938
+ * true when the answer came from the fallback.
1939
+ *
1940
+ * @internal
1941
+ */
1942
+ async decide(customer, plan) {
1943
+ return this.decideBy(customer, plan, Date.now() + this.guardTimeout);
1944
+ }
1945
+ /**
1946
+ * The decision itself. With a deadline (the guards), every call to Mesub
1947
+ * fits before it: attempts are cut at it and a Retry-After that would
1948
+ * outlast it is not waited. Without one (`hasAccess` called directly),
1949
+ * the client's timeout and retries, as configured.
1950
+ */
1951
+ async decideBy(customer, plan, deadline) {
1952
+ const call = deadline === void 0 ? {} : { deadline, retryRateLimited: false };
1953
+ const asked = customerOf(customer);
1954
+ const slug = planOf(plan);
1955
+ const slot = await this.slotOf(asked);
1956
+ const cached = await this.cache.read(slot.who, slug, slot.scope, deadline);
1957
+ if (_optionalChain([cached, 'optionalAccess', _24 => _24.fresh])) {
1958
+ return { access: cached.value.access, answer: cached.value, stale: false };
1959
+ }
1960
+ try {
1961
+ const answer = await this.ask(asked, slug, slot, call);
1962
+ return { access: answer.access, answer, stale: false };
1963
+ } catch (error) {
1964
+ const unreachable = error instanceof MesubError && (error.code === "unavailable" || error.code === "rate_limited");
1965
+ if (!unreachable) throw error;
1966
+ if (cached) {
1967
+ return { access: stillGrants(cached.value), answer: cached.value, stale: true };
1968
+ }
1969
+ return {
1970
+ access: false,
1971
+ answer: null,
1972
+ stale: true,
1973
+ unavailable: true,
1974
+ ...error.retryAfter !== null && { retryAfter: error.retryAfter }
1975
+ };
1976
+ }
1977
+ }
1978
+ }, _class3);
1979
+ function boundOf(options) {
1980
+ const { deadline, retryRateLimited } = options;
1981
+ return {
1982
+ ...deadline !== void 0 && { deadline },
1983
+ ...retryRateLimited !== void 0 && { retryRateLimited }
1984
+ };
1985
+ }
1986
+ function flightKey(mode, slot, plan) {
1987
+ return `${mode}|${AccessCache.key(slot.who, plan, slot.scope)}`;
1988
+ }
1989
+ function customersOf(subscription) {
1990
+ const { wallet, external_id, email } = subscription;
1991
+ const customers = [{ wallet }];
1992
+ if (external_id) customers.push({ external_id });
1993
+ if (email) customers.push({ email });
1994
+ return customers.map(customerOf);
1995
+ }
1996
+ function planOf(plan) {
1997
+ if (typeof plan !== "string" || plan === "") {
1998
+ throw new TypeError("A plan is needed: accessList(customer) lists every plan instead.");
1999
+ }
2000
+ return plan;
2001
+ }
2002
+ async function hashScope(apiKey) {
2003
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
2004
+ return `key-${hex(digest).slice(0, 16)}`;
2005
+ }
2006
+ function importSecret(apiKey) {
2007
+ return crypto.subtle.importKey(
2008
+ "raw",
2009
+ new TextEncoder().encode(apiKey),
2010
+ { name: "HMAC", hash: "SHA-256" },
2011
+ false,
2012
+ ["sign"]
2013
+ );
2014
+ }
2015
+ function hex(bytes) {
2016
+ return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
2017
+ }
2018
+ function stillGrants(answer, now = Date.now()) {
2019
+ if (!answer.access || !answer.access_until) return answer.access;
2020
+ const renewing = Boolean(answer.next_charge_at || answer.next_retry_at);
2021
+ if (renewing && !answer.blocked_since) return true;
2022
+ return Date.parse(answer.access_until) > now;
2023
+ }
2024
+
2025
+
2026
+
2027
+
2028
+
2029
+
2030
+
2031
+
2032
+
2033
+
2034
+
2035
+
2036
+
2037
+
2038
+
2039
+ exports.DEFAULT_MAX_STALE_MS = DEFAULT_MAX_STALE_MS; exports.AccessCache = AccessCache; exports.VERSION = VERSION; exports.API_VERSION = API_VERSION; exports.API_VERSION_HEADER = API_VERSION_HEADER; exports.DEFAULT_MAX_ENTRIES = DEFAULT_MAX_ENTRIES; exports.MemoryStore = MemoryStore; exports.customerOf = customerOf; exports.MesubError = MesubError; exports.MesubSubmitError = MesubSubmitError; exports.verifyWebhook = verifyWebhook; exports.signedHeaders = signedHeaders; exports.Mesub = Mesub;
2040
+ //# sourceMappingURL=chunk-4JSJOX35.cjs.map