@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,1488 @@
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;
2
+
3
+
4
+
5
+
6
+
7
+ var _chunk4JSJOX35cjs = require('./chunk-4JSJOX35.cjs');
8
+
9
+ // src/testing.ts
10
+ var ACCESS_ATTEMPTS = 5;
11
+ var NOTHING = {
12
+ subscription_id: null,
13
+ access: false,
14
+ status: "none",
15
+ paused: false,
16
+ end_reason: null,
17
+ late_reason: null,
18
+ stop_reason: null,
19
+ payment_status: "none",
20
+ blocked_since: null,
21
+ blocked_reason: null,
22
+ subscribed_since: null,
23
+ first_subscribed_at: null,
24
+ current_period_end: null,
25
+ cancelled_at: null,
26
+ access_until: null,
27
+ next_charge_at: null,
28
+ next_retry_at: null,
29
+ retry_deadline: null
30
+ };
31
+ var REVALIDATE_MIN_S = 10;
32
+ var REVALIDATE_MAX_S = 300;
33
+ var OUTAGE = {
34
+ status: 503,
35
+ code: "unavailable",
36
+ message: "Mesub is unavailable."
37
+ };
38
+ var FAKE_WEBHOOK_SECRET = "whsec_bWVzdWItZmFrZS13ZWJob29rLWtleS0x";
39
+ var FAKE_WALLET = "11111111111111111111111111111111";
40
+ var TEST_EVENT_ID = "evt_test";
41
+ var EVENTS_WINDOW_MS = 30 * 24 * 3600 * 1e3;
42
+ var EVENTS_LIMIT = 25;
43
+ var EVENTS_LIMIT_MAX = 100;
44
+ var EVENT_TYPES = [
45
+ "subscription.created",
46
+ "subscription.renewal_upcoming",
47
+ "subscription.renewed",
48
+ "subscription.payment_failed",
49
+ "subscription.stopped",
50
+ "subscription.cancelled",
51
+ "subscription.resumed",
52
+ "subscription.ended",
53
+ "subscription.expired",
54
+ "subscription.access_granted",
55
+ "subscription.access_revoked",
56
+ "subscription.paused",
57
+ "subscription.unpaused"
58
+ ];
59
+ var RESOURCE_ID = /^[A-Za-z0-9_-]{1,64}$/;
60
+ var REQUEST_ID_HEADER = "Mesub-Request-Id";
61
+ var USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
62
+ var PERIOD_MS = 30 * 24 * 3600 * 1e3;
63
+ var DAY_MS = 24 * 3600 * 1e3;
64
+ var TEST_SUBSCRIPTION = {
65
+ id: "sub_test",
66
+ status: "active",
67
+ paused: false,
68
+ end_reason: null,
69
+ late_reason: null,
70
+ stop_reason: null,
71
+ access: true,
72
+ payment_status: "paid",
73
+ blocked_since: null,
74
+ blocked_reason: null,
75
+ plan: "pro",
76
+ wallet: FAKE_WALLET,
77
+ email: "subscriber@example.com",
78
+ external_id: "user_123",
79
+ current_period_start: "2026-01-01T00:00:00.000Z",
80
+ current_period_end: "2026-01-31T00:00:00.000Z",
81
+ next_charge_at: "2026-01-31T00:00:00.000Z",
82
+ next_retry_at: null,
83
+ retry_deadline: null,
84
+ next_retry_number: null,
85
+ retries_allowed: null,
86
+ access_until: "2026-01-31T00:00:00.000Z",
87
+ closable: false,
88
+ closable_from: null,
89
+ created_at: "2026-01-01T00:00:00.000Z",
90
+ confirmed_at: "2026-01-01T00:00:00.000Z"
91
+ };
92
+ var FREE_RETRY_WAIT_MS = 10 * 60 * 1e3;
93
+ var FREE_HAND_RETRIES = 3;
94
+ var PAID_RETRY_WAIT_MS = 20 * 60 * 1e3;
95
+ var CLOCK_MARGIN_MS = 2 * 60 * 1e3;
96
+ var TRANSACTION_LIFE_MS = 3e4;
97
+ var REASONS = {
98
+ 400: "Bad Request",
99
+ 401: "Unauthorized",
100
+ 403: "Forbidden",
101
+ 404: "Not Found",
102
+ 409: "Conflict",
103
+ 429: "Too Many Requests",
104
+ 500: "Internal Server Error",
105
+ 503: "Service Unavailable"
106
+ };
107
+ var FakeMesub = (_class = class {
108
+
109
+
110
+
111
+ /** Every call received, oldest first. */
112
+ __init() {this.requests = []}
113
+ /** Hand it to `new Mesub({ fetch })`, or use `client()`. */
114
+
115
+ #plans;
116
+ #planList;
117
+ #answers = /* @__PURE__ */ new Map();
118
+ /** The ends `grantLastPeriod` and `endPlan` gave a plan, by slug: they win over `plans`. */
119
+ #ends = /* @__PURE__ */ new Map();
120
+ #subscriptions = [];
121
+ /** The subscriptions a cancel, resume or close transaction was built for, by id. */
122
+ #built = /* @__PURE__ */ new Map();
123
+ /** The rows ended with their plan whose close landed: nothing of them is left on chain. */
124
+ #gone = /* @__PURE__ */ new Set();
125
+ /** The attempts `setAttempts` gave each subscription, newest first, by id. */
126
+ #attempts = /* @__PURE__ */ new Map();
127
+ #attemptsRoute;
128
+ #revalidateAfter;
129
+ #attemptIds = 0;
130
+ #eventIds = 0;
131
+ /** The last `sequence` given to each subscription's events, by id. */
132
+ #sequences = /* @__PURE__ */ new Map();
133
+ #accessEvents;
134
+ #requestIds = 0;
135
+ /** Every event raised or added, as `/v1/events` serves it. */
136
+ #events = [];
137
+ #eventsSince;
138
+ #failure = null;
139
+ #ids = 0;
140
+ /** The made-up subscriptions of `webhook` and `addEvent`, apart: they take no `sub_fake_` id. */
141
+ #madeUpIds = 0;
142
+ #deliveryIds = 0;
143
+ #tier;
144
+ /** The hand retries fired on each late subscription, and when the last one was, by id. */
145
+ #handRetries = /* @__PURE__ */ new Map();
146
+ constructor(options = {}) {;_class.prototype.__init.call(this);
147
+ this.baseUrl = (_nullishCoalesce(options.baseUrl, () => ( "https://api.mesub.test"))).replace(/\/+$/, "");
148
+ this.apiKey = _nullishCoalesce(options.apiKey, () => ( "SUB_test_fake"));
149
+ const plans = _optionalChain([options, 'access', _ => _.plans, 'optionalAccess', _2 => _2.map, 'call', _3 => _3(
150
+ (plan) => typeof plan === "string" ? fakePlan(plan) : fakePlan(plan.slug, plan)
151
+ )]);
152
+ this.#plans = plans ? new Set(plans.map((plan) => plan.slug)) : null;
153
+ this.#planList = (_nullishCoalesce(plans, () => ( []))).sort((a, b) => a.slug.localeCompare(b.slug));
154
+ this.webhookSecret = _nullishCoalesce(options.webhookSecret, () => ( FAKE_WEBHOOK_SECRET));
155
+ this.#attemptsRoute = _nullishCoalesce(options.attemptsRoute, () => ( true));
156
+ this.#revalidateAfter = options.revalidateAfter;
157
+ this.#accessEvents = _nullishCoalesce(options.accessEvents, () => ( false));
158
+ this.#tier = _nullishCoalesce(options.tier, () => ( "dev"));
159
+ this.#eventsSince = new Date(
160
+ _nullishCoalesce(options.eventsSince, () => ( Date.now() - EVENTS_WINDOW_MS))
161
+ ).toISOString();
162
+ this.fetch = (input, init) => this.#handle(input, init);
163
+ }
164
+ /**
165
+ * A client wired to this fake: its key, base URL and fetch, and no retry
166
+ * unless asked, so a failure shows at once. Any other option is passed on.
167
+ */
168
+ client(options = {}) {
169
+ return new (0, _chunk4JSJOX35cjs.Mesub)({
170
+ maxRetries: 0,
171
+ ...options,
172
+ apiKey: this.apiKey,
173
+ baseUrl: this.baseUrl,
174
+ fetch: this.fetch,
175
+ webhookSecret: this.webhookSecret
176
+ });
177
+ }
178
+ /**
179
+ * That customer has access to that plan: active and paid, unless `fields`
180
+ * say otherwise. The customer is named as `access` names them, and only
181
+ * that way answers: a wallet granted is not found by its external id.
182
+ */
183
+ grant(customer, plan, fields = {}) {
184
+ return this.setAccess(customer, plan, {
185
+ access: true,
186
+ status: "active",
187
+ payment_status: "paid",
188
+ ...fields
189
+ });
190
+ }
191
+ /** That customer has no access to that plan: `status` says why, `none` by default. */
192
+ deny(customer, plan, fields = {}) {
193
+ return this.setAccess(customer, plan, { ...fields, access: false });
194
+ }
195
+ /**
196
+ * That customer is in the last period of a plan that ends at `endsAt`:
197
+ * active and paid, access until the end and no charge ahead, unless
198
+ * `fields` say otherwise. The plan gets that `ends_at`, and from then on
199
+ * the fake refuses everyone on it, as Mesub does, whatever `status` reads.
200
+ */
201
+ grantLastPeriod(customer, plan, endsAt, fields = {}) {
202
+ const end = new Date(endsAt).toISOString();
203
+ this.#ends.set(plan, end);
204
+ return this.grant(customer, plan, { access_until: end, ...fields });
205
+ }
206
+ /**
207
+ * That plan reached its end, at `at` or now: it takes nobody new, and
208
+ * what the fake holds on it is ended as Mesub ends it, `plan_ended` for a
209
+ * running or late one, `cancelled` for one that had cancelled. Before
210
+ * this is called, a plan past its `ends_at` already refuses everyone,
211
+ * with `status` unchanged: the minutes Mesub takes to end the rows.
212
+ */
213
+ endPlan(plan, at = /* @__PURE__ */ new Date()) {
214
+ this.#ends.set(plan, new Date(at).toISOString());
215
+ for (const [key, answer] of this.#answers) {
216
+ if (answer.plan === plan) this.#answers.set(key, { ...answer, ...endedBy(answer) });
217
+ }
218
+ for (const subscription of this.#subscriptions) {
219
+ if (subscription.plan !== plan) continue;
220
+ const ended = endedBy(subscription);
221
+ Object.assign(subscription, ended, "status" in ended && NO_RETRY);
222
+ }
223
+ }
224
+ /**
225
+ * Parks that subscription's seat, as Mesub does when a project holds more
226
+ * subscribers than its tier allows: `paused`, no next charge, access and
227
+ * status unchanged (the fake moves no clock, so the paid period never
228
+ * runs out). With `accessEvents`, raises `subscription.paused`
229
+ * (Mesub-io/backend#509), sent behind the same flag. Throws for an id the
230
+ * fake does not hold.
231
+ */
232
+ park(id) {
233
+ return this.#seat(id, true);
234
+ }
235
+ /**
236
+ * Brings a parked seat back: not `paused`, charged again at the end of
237
+ * its period. With `accessEvents`, raises `subscription.unpaused`.
238
+ */
239
+ unpark(id) {
240
+ return this.#seat(id, false);
241
+ }
242
+ #seat(id, paused) {
243
+ const subscription = this.#subscriptions.find((each) => each.id === id);
244
+ if (!subscription) throw new Error(`The fake holds no subscription ${id}.`);
245
+ if (subscription.paused === paused) return this.#view(subscription);
246
+ const before = this.#view(subscription).access;
247
+ const next_charge_at = paused ? null : subscription.current_period_end;
248
+ Object.assign(subscription, { paused, next_charge_at });
249
+ this.#answer(subscription, { paused, next_charge_at });
250
+ if (this.#accessEvents) {
251
+ this.#raise(
252
+ paused ? "subscription.paused" : "subscription.unpaused",
253
+ subscription,
254
+ before
255
+ );
256
+ }
257
+ return this.#view(subscription);
258
+ }
259
+ /**
260
+ * What `/v1/access` answers that customer on that plan, on top of the
261
+ * answer for a customer with nothing. `wallet` defaults to the wallet
262
+ * asked about, and to null for an external id or an email.
263
+ */
264
+ setAccess(customer, plan, fields) {
265
+ const asked = _chunk4JSJOX35cjs.customerOf.call(void 0, customer);
266
+ const answer = { ...nothing(asked, plan), ...fields, plan };
267
+ this.#answers.set(keyOf(asked, plan), answer);
268
+ return this.#fresh(answer);
269
+ }
270
+ /**
271
+ * A subscription `retrieve` and `list` answer, active by default, dated
272
+ * as Mesub dates a row of that `status` unless `fields` say otherwise:
273
+ * active, paid for a period ending in 30 days and charged then; unpaid,
274
+ * that period over, late, with access and a retry tomorrow, or on `free`
275
+ * no access and hand retries until `retry_deadline`; cancelled, access to
276
+ * the end of the period; stopped (`out_of_retries`) or ended (`closed`),
277
+ * no access. Its access answer is left as it is: set it with `grant`.
278
+ * Throws for a wallet Mesub would refuse. Its plan may be one `plans`
279
+ * leaves out: Mesub serves the rows of a plan it no longer has.
280
+ */
281
+ addSubscription(fields) {
282
+ if (!BASE58_ADDRESS.test(fields.wallet)) {
283
+ throw new Error(`${fields.wallet} is not a wallet: a base58 Solana address.`);
284
+ }
285
+ const subscription = this.#subscription(fields);
286
+ this.#subscriptions.push(subscription);
287
+ return subscription;
288
+ }
289
+ /**
290
+ * The pull attempts of that subscription, every one it ever had: what
291
+ * `subscriptions.attempts` answers for it, with their `paid` total, and
292
+ * the five newest through `/v1/access` with `attempts`, whose answer
293
+ * becomes about that subscription. Each is a paid one of 9.99 USDC now,
294
+ * priced 9.99 dollars, not a retry, unless told otherwise; one given
295
+ * another `amount` and no `amount_usd` is not priced. Newest first.
296
+ * Throws for an id the fake does not hold.
297
+ */
298
+ setAttempts(id, attempts) {
299
+ const subscription = this.#subscriptions.find((each) => each.id === id);
300
+ if (!subscription) throw new Error(`The fake holds no subscription ${id}.`);
301
+ const now = (/* @__PURE__ */ new Date()).toISOString();
302
+ const filled = attempts.map((attempt) => ({
303
+ id: `att_fake_${this.#attemptIds += 1}`,
304
+ attempted_at: now,
305
+ outcome: "paid",
306
+ reason: null,
307
+ amount: "9990000",
308
+ // The default amount's worth only: no price is guessed for another.
309
+ amount_usd: attempt.amount === void 0 ? "9.99" : null,
310
+ // A mint that withholds nothing: the receiver got it all.
311
+ amount_received: _nullishCoalesce(attempt.amount, () => ( "9990000")),
312
+ signature: "fake_signature",
313
+ retry: false,
314
+ retry_number: null,
315
+ retries_allowed: null,
316
+ period_start: null,
317
+ ...attempt
318
+ })).sort((a, b) => Date.parse(b.attempted_at) - Date.parse(a.attempted_at));
319
+ this.#attempts.set(id, filled);
320
+ this.#answer(subscription, {
321
+ subscribed_since: subscription.confirmed_at,
322
+ attempts: filled.slice(0, ACCESS_ATTEMPTS).map(({ outcome, reason, amount, attempted_at, signature }) => ({
323
+ outcome,
324
+ reason,
325
+ amount,
326
+ attempted_at,
327
+ signature
328
+ }))
329
+ });
330
+ return filled;
331
+ }
332
+ /**
333
+ * A webhook of that type, as Mesub posts it to an endpoint: the event's
334
+ * body (Mesub-io/backend#482), on a made-up subscription and detail
335
+ * unless `fields` say otherwise, the subscription the fake holds under
336
+ * `fields.subscription.id` when it holds one, signed with
337
+ * `webhookSecret`. The `test` event is Mesub's test delivery: `test:
338
+ * true`, `evt_test`, on its sample subscription (`sub_test`,
339
+ * `user_123`). Post it to your own handler, or hand it to
340
+ * `webhooks.verify` of a `client()`. Nothing is added to the fake.
341
+ */
342
+ async webhook(type, fields = {}) {
343
+ const now = /* @__PURE__ */ new Date();
344
+ const test = type === "test";
345
+ const data = test ? { ...TEST_SUBSCRIPTION, ...fields.subscription } : withClosing(this.#madeUp(type, fields.subscription));
346
+ const sequence = fields.sequence === void 0 ? test ? 1 : this.#nextSequence(data.id) : fields.sequence;
347
+ const payload = {
348
+ id: _nullishCoalesce(fields.id, () => ( (test ? TEST_EVENT_ID : this.#nextEventId()))),
349
+ object: "event",
350
+ type,
351
+ created_at: _nullishCoalesce(fields.created_at, () => ( now.toISOString())),
352
+ network: "mainnet-beta",
353
+ api_version: _chunk4JSJOX35cjs.API_VERSION,
354
+ ...sequence !== null && { sequence },
355
+ data,
356
+ detail: _nullishCoalesce(fields.detail, () => ( detailOf(type, now, data))),
357
+ ...test && { test: true }
358
+ };
359
+ return signWebhook(payload, {
360
+ secret: this.webhookSecret,
361
+ id: _nullishCoalesce(fields.delivery_id, () => ( `msg_fake_${this.#deliveryIds += 1}`)),
362
+ ...fields.timestamp !== void 0 && { timestamp: fields.timestamp }
363
+ });
364
+ }
365
+ /**
366
+ * The subscription an event carries: the one the fake holds under
367
+ * `fields.id` when it holds one, else a made-up one, active and paid on
368
+ * `pro` unless the type says otherwise; `fields` on top.
369
+ */
370
+ #madeUp(type, fields = {}) {
371
+ const held = this.#subscriptions.find((each) => each.id === fields.id);
372
+ const base = held ? this.#view(held) : {
373
+ id: _nullishCoalesce(fields.id, () => ( `sub_fake_event_${this.#madeUpIds += 1}`)),
374
+ wallet: FAKE_WALLET,
375
+ plan: "pro",
376
+ ...subscriptionOf(type)
377
+ };
378
+ return this.#subscription({ ...base, ...fields });
379
+ }
380
+ /**
381
+ * An event `/v1/events` serves from now on, as Mesub would have raised
382
+ * it: a made-up subscription and detail unless `fields` say otherwise,
383
+ * the subscription the fake holds under `fields.subscription.id` when it
384
+ * holds one. Nothing else changes: what it says happened is not done.
385
+ */
386
+ addEvent(type, fields = {}) {
387
+ const now = /* @__PURE__ */ new Date();
388
+ const data = withClosing(this.#madeUp(type, fields.subscription));
389
+ const sequence = fields.sequence === void 0 ? this.#nextSequence(data.id) : fields.sequence;
390
+ const event = {
391
+ id: _nullishCoalesce(fields.id, () => ( this.#nextEventId())),
392
+ object: "event",
393
+ type,
394
+ created_at: _nullishCoalesce(fields.created_at, () => ( now.toISOString())),
395
+ network: "mainnet-beta",
396
+ api_version: _chunk4JSJOX35cjs.API_VERSION,
397
+ ...sequence !== null && { sequence },
398
+ data,
399
+ detail: _nullishCoalesce(fields.detail, () => ( detailOf(type, now, data)))
400
+ };
401
+ this.#events.push(event);
402
+ return structuredClone(event);
403
+ }
404
+ /**
405
+ * Every call answers that error from now on, `'outage'` a 503, until
406
+ * `fail(null)`.
407
+ */
408
+ fail(failure) {
409
+ this.#failure = failure === "outage" ? OUTAGE : failure;
410
+ }
411
+ /**
412
+ * Forgets every answer, subscription, failure and request, and starts
413
+ * every id over: the same test makes the same ids, whatever ran before.
414
+ */
415
+ reset() {
416
+ this.#answers.clear();
417
+ this.#ends.clear();
418
+ this.#subscriptions = [];
419
+ this.#built.clear();
420
+ this.#gone.clear();
421
+ this.#attempts.clear();
422
+ this.#events = [];
423
+ this.#sequences.clear();
424
+ this.#handRetries.clear();
425
+ this.#failure = null;
426
+ this.requests.length = 0;
427
+ this.#ids = 0;
428
+ this.#madeUpIds = 0;
429
+ this.#eventIds = 0;
430
+ this.#attemptIds = 0;
431
+ this.#requestIds = 0;
432
+ this.#deliveryIds = 0;
433
+ }
434
+ /** A subscription with every field, dated as Mesub dates one of its `status`. */
435
+ #subscription(fields) {
436
+ const now = Date.now();
437
+ const iso = (ms) => new Date(ms).toISOString();
438
+ return {
439
+ id: _nullishCoalesce(fields.id, () => ( this.#nextId())),
440
+ status: "active",
441
+ paused: false,
442
+ end_reason: null,
443
+ late_reason: null,
444
+ stop_reason: null,
445
+ blocked_since: null,
446
+ blocked_reason: null,
447
+ email: null,
448
+ external_id: null,
449
+ next_retry_at: null,
450
+ retry_deadline: null,
451
+ next_retry_number: null,
452
+ retries_allowed: null,
453
+ closable: false,
454
+ closable_from: null,
455
+ created_at: iso(now),
456
+ confirmed_at: iso(now),
457
+ ...datesOf(_nullishCoalesce(fields.status, () => ( "active")), this.#tier, now),
458
+ ...fields
459
+ };
460
+ }
461
+ /**
462
+ * An answer as `/v1/access` serves it now: bounded by its plan's end, and
463
+ * fresh for as long as Mesub keeps it, unless a test said otherwise.
464
+ */
465
+ #fresh(answer) {
466
+ const end = this.#endOf(answer.plan);
467
+ const served = bounded(answer, end);
468
+ const closes = closingOf(served);
469
+ const closed = {
470
+ ...served,
471
+ closable: _nullishCoalesce(served.closable, () => ( closes.closable)),
472
+ closable_from: served.closable_from === void 0 ? closes.closable_from : served.closable_from
473
+ };
474
+ return {
475
+ ...closed,
476
+ revalidate_after: _nullishCoalesce(_nullishCoalesce(served.revalidate_after, () => ( this.#revalidateAfter)), () => ( revalidateAfter(closed, end)))
477
+ };
478
+ }
479
+ /**
480
+ * A subscription as Mesub serves it now: bounded by its plan's end, and
481
+ * `closable` as the fake's close would take it.
482
+ */
483
+ #view(subscription) {
484
+ const served = bounded(subscription, this.#endOf(subscription.plan));
485
+ return { ...served, ...closingOf(served, this.#gone.has(subscription.id)) };
486
+ }
487
+ /**
488
+ * Records the event Mesub raises for a change to that subscription, as
489
+ * it stands now, then, with `accessEvents`, the access event of the same
490
+ * change when access flipped from `before`: the same `data`, one
491
+ * `sequence` later.
492
+ */
493
+ #raise(type, subscription, before, detail = {}) {
494
+ const created_at = (/* @__PURE__ */ new Date()).toISOString();
495
+ const data = this.#view(subscription);
496
+ const push = (each, eachDetail) => this.#events.push({
497
+ id: this.#nextEventId(),
498
+ object: "event",
499
+ type: each,
500
+ created_at,
501
+ network: "mainnet-beta",
502
+ api_version: _chunk4JSJOX35cjs.API_VERSION,
503
+ sequence: this.#nextSequence(subscription.id),
504
+ data: structuredClone(data),
505
+ detail: eachDetail
506
+ });
507
+ push(type, detail);
508
+ if (this.#accessEvents && data.access !== before) {
509
+ const flip = data.access ? "subscription.access_granted" : "subscription.access_revoked";
510
+ push(flip, accessDetailOf(data, created_at));
511
+ }
512
+ }
513
+ /** The next `sequence` of that subscription's events: 1, 2, 3... */
514
+ #nextSequence(id) {
515
+ const next = (_nullishCoalesce(this.#sequences.get(id), () => ( 0))) + 1;
516
+ this.#sequences.set(id, next);
517
+ return next;
518
+ }
519
+ /** A plan's end: the one a test gave it, else its `ends_at` in `plans`. */
520
+ #endOf(slug) {
521
+ if (slug === null) return null;
522
+ return _nullishCoalesce(_nullishCoalesce(this.#ends.get(slug), () => ( _optionalChain([this, 'access', _4 => _4.#planList, 'access', _5 => _5.find, 'call', _6 => _6((each) => each.slug === slug), 'optionalAccess', _7 => _7.ends_at]))), () => ( null));
523
+ }
524
+ /** Whether that plan is past its end, where it takes nobody new. */
525
+ #over(slug) {
526
+ const end = this.#endOf(slug);
527
+ return end !== null && Date.parse(end) <= Date.now();
528
+ }
529
+ /** A plan as served: with its end, and taking nobody new once past it. */
530
+ #served(plan) {
531
+ const ends_at = this.#endOf(plan.slug);
532
+ return { ...plan, ends_at, available: plan.available && !this.#over(plan.slug) };
533
+ }
534
+ #nextId() {
535
+ this.#ids += 1;
536
+ return `sub_fake_${this.#ids}`;
537
+ }
538
+ #nextEventId() {
539
+ this.#eventIds += 1;
540
+ return `evt_fake_${this.#eventIds}`;
541
+ }
542
+ async #handle(input, init = {}) {
543
+ const url = new URL(input instanceof Request ? input.url : String(input));
544
+ const base = new URL(this.baseUrl);
545
+ const prefix = base.pathname.replace(/\/+$/, "");
546
+ if (url.origin !== base.origin || !url.pathname.startsWith(`${prefix}/`)) {
547
+ return new Response("Not Found", { status: 404 });
548
+ }
549
+ this.#requestIds += 1;
550
+ const id = `req_fake${String(this.#requestIds).padStart(20, "0")}`;
551
+ return stamped(await this.#route(url, url.pathname.slice(prefix.length), init), id);
552
+ }
553
+ async #route(url, path, init) {
554
+ const method = (_nullishCoalesce(init.method, () => ( "GET"))).toUpperCase();
555
+ const headers = new Headers(init.headers);
556
+ const query = Object.fromEntries(url.searchParams);
557
+ const body = typeof init.body === "string" ? JSON.parse(init.body) : void 0;
558
+ this.requests.push({ method, path, query, headers, body });
559
+ const [scheme, key] = (_nullishCoalesce(headers.get("authorization"), () => ( ""))).split(" ");
560
+ if (_optionalChain([scheme, 'optionalAccess', _8 => _8.toLowerCase, 'call', _9 => _9()]) !== "bearer" || !key) {
561
+ return error(401, "missing_api_key", "Send your API key as a bearer token.");
562
+ }
563
+ if (key !== this.apiKey) {
564
+ return error(401, "invalid_api_key", "That API key is not valid.");
565
+ }
566
+ const version = headers.get(_chunk4JSJOX35cjs.API_VERSION_HEADER);
567
+ if (version !== null && version !== _chunk4JSJOX35cjs.API_VERSION) {
568
+ return error(
569
+ 400,
570
+ "unsupported_version",
571
+ `${_chunk4JSJOX35cjs.API_VERSION_HEADER} must be one of the versions this API serves: ${_chunk4JSJOX35cjs.API_VERSION}.`
572
+ );
573
+ }
574
+ if (this.#failure) {
575
+ const { status, code, message, retryable } = this.#failure;
576
+ const retryAfter = _nullishCoalesce(this.#failure.retryAfter, () => ( waitOf(method, path, status, code, this.#tier)));
577
+ return error(status, code, _nullishCoalesce(message, () => ( `Mesub answered ${status}.`)), {
578
+ ...retryable !== void 0 && { retryable },
579
+ ...retryAfter !== void 0 && { retryAfter }
580
+ });
581
+ }
582
+ if (method === "GET" && path === "/v1/plans") {
583
+ return Response.json({ plans: this.#planList.map((plan) => this.#served(plan)) });
584
+ }
585
+ const slug = _optionalChain([/^\/v1\/plans\/([^/]+)$/, 'access', _10 => _10.exec, 'call', _11 => _11(path), 'optionalAccess', _12 => _12[1]]);
586
+ if (method === "GET" && slug !== void 0) {
587
+ const plan = _nullishCoalesce(this.#planList.find((each) => each.slug === slug), () => ( // No list given: every slug exists, as for access.
588
+ (this.#plans === null ? fakePlan(slug) : void 0)));
589
+ return plan ? Response.json(this.#served(plan)) : error(404, "plan_not_found", `No plan under slug ${slug}`);
590
+ }
591
+ if (method === "GET" && path === "/v1/access") return this.#access(query);
592
+ if (method === "GET" && path === "/v1/events") return this.#eventList(query);
593
+ const eventId = _optionalChain([/^\/v1\/events\/([^/]+)$/, 'access', _13 => _13.exec, 'call', _14 => _14(path), 'optionalAccess', _15 => _15[1]]);
594
+ if (method === "GET" && eventId !== void 0) {
595
+ const event = this.#servedEvents().find(
596
+ (each) => each.id === decodeURIComponent(eventId)
597
+ );
598
+ return event ? Response.json(event) : error(
599
+ 404,
600
+ "event_not_found",
601
+ "No event of yours under that id in the last 30 days."
602
+ );
603
+ }
604
+ if (method === "GET" && path === "/v1/subscriptions") return this.#list(query);
605
+ if (method === "POST" && path === "/v1/subscriptions") return this.#create(body);
606
+ const [, id, step, confirm] = _nullishCoalesce(/^\/v1\/subscriptions\/([^/]+)(?:\/(submit|transaction|cancel|resume|close|attempts|retry)(\/confirm)?)?$/.exec(
607
+ path
608
+ ), () => ( []));
609
+ if (step === "attempts" && (confirm || !this.#attemptsRoute)) {
610
+ return error(404, "not_found", `Cannot ${method} ${path}`);
611
+ }
612
+ if (method === "POST" && step === "submit" && !confirm) {
613
+ const invalid = invalidSubmit(body);
614
+ if (invalid) return invalid;
615
+ }
616
+ if (id !== void 0) {
617
+ const subscription = this.#subscriptions.find((s) => s.id === decodeURIComponent(id));
618
+ if (!subscription) {
619
+ return error(404, "subscription_not_found", "No subscription under that id.");
620
+ }
621
+ if (method === "GET" && !step) return Response.json(this.#view(subscription));
622
+ if (step === "attempts") {
623
+ return method === "GET" ? this.#attemptsOf(subscription, query) : error(404, "not_found", `Cannot ${method} ${path}`);
624
+ }
625
+ if (method === "POST" && step === "submit" && !confirm) {
626
+ return this.#submit(subscription);
627
+ }
628
+ if (step === "transaction") {
629
+ return method === "POST" && !confirm ? this.#transaction(subscription) : error(404, "not_found", `Cannot ${method} ${path}`);
630
+ }
631
+ if (step === "retry") {
632
+ return method === "POST" && !confirm ? this.#retry(subscription) : error(404, "not_found", `Cannot ${method} ${path}`);
633
+ }
634
+ if (method === "POST" && step && step !== "submit") {
635
+ return confirm ? this.#confirm(step, subscription, body) : this.#build(step, subscription);
636
+ }
637
+ }
638
+ return error(404, "not_found", `Cannot ${method} ${path}`);
639
+ }
640
+ #access(query) {
641
+ const invalid = invalidContact(query);
642
+ if (invalid) return invalid;
643
+ const asked = askedIn(query);
644
+ if (!asked)
645
+ return error(400, "invalid_request", "Name one of wallet, external_id or email.");
646
+ const { plan } = query;
647
+ const attempts = query["attempts"] === "true";
648
+ if (plan === void 0) {
649
+ const plans = [...this.#answers.entries()].filter(([key]) => key.startsWith(keyOf(asked, ""))).map(([, answer2]) => withAttempts(this.#fresh(answer2), attempts));
650
+ const granted = plans.filter((answer2) => answer2.access);
651
+ return Response.json({
652
+ plans,
653
+ revalidate_after: granted.length ? Math.min(...granted.map((answer2) => answer2.revalidate_after)) : _nullishCoalesce(this.#revalidateAfter, () => ( REVALIDATE_MIN_S))
654
+ });
655
+ }
656
+ if (this.#plans && !this.#plans.has(plan)) {
657
+ return error(404, "plan_not_found", `No plan under slug ${plan}`);
658
+ }
659
+ const answer = _nullishCoalesce(this.#answers.get(keyOf(asked, plan)), () => ( nothing(asked, plan)));
660
+ return Response.json(withAttempts(this.#fresh(answer), attempts));
661
+ }
662
+ #list(query) {
663
+ const invalid = invalidContact(query);
664
+ if (invalid) return invalid;
665
+ const asked = askedIn(query);
666
+ if (!asked)
667
+ return error(400, "invalid_request", "Name one of wallet, external_id or email.");
668
+ const limit = query["limit"] === void 0 ? 20 : limitOf(query["limit"]);
669
+ if (limit === null) return error(400, "invalid_request", [LIMIT_MESSAGE]);
670
+ const all = [...this.#subscriptions].reverse();
671
+ const after = query["starting_after"];
672
+ const from = after === void 0 ? 0 : all.findIndex((s) => s.id === after) + 1;
673
+ if (after !== void 0 && from === 0) {
674
+ return error(400, "invalid_request", "starting_after is not a subscription of yours.");
675
+ }
676
+ const data = all.slice(from).filter((s) => s.status !== "superseded").filter((s) => s[asked.kind] === asked.value).filter((s) => query["plan"] === void 0 || s.plan === query["plan"]);
677
+ const page = {
678
+ data: data.slice(0, limit).map((s) => this.#view(s)),
679
+ has_more: data.length > limit
680
+ };
681
+ return Response.json(page);
682
+ }
683
+ /** The events Mesub serves now: within the window and from `available_since`, newest first. */
684
+ #servedEvents() {
685
+ const floor = Math.max(Date.now() - EVENTS_WINDOW_MS, Date.parse(this.#eventsSince));
686
+ return this.#events.filter((event) => Date.parse(event.created_at) >= floor).sort(
687
+ (a, b) => Date.parse(b.created_at) - Date.parse(a.created_at) || // Within a millisecond, by `sequence`, highest first, as Mesub.
688
+ (_nullishCoalesce(b.sequence, () => ( 0))) - (_nullishCoalesce(a.sequence, () => ( 0))) || (a.id < b.id ? 1 : a.id > b.id ? -1 : 0)
689
+ );
690
+ }
691
+ #eventList(query) {
692
+ const { type, subscription, starting_after: after } = query;
693
+ const limit = Number(_nullishCoalesce(query["limit"], () => ( EVENTS_LIMIT)));
694
+ const reasons = [
695
+ !Number.isInteger(limit) || limit < 1 || limit > EVENTS_LIMIT_MAX ? `limit must be a whole number from 1 to ${EVENTS_LIMIT_MAX}` : null,
696
+ type !== void 0 && !EVENT_TYPES.includes(type) ? `type is one of ${EVENT_TYPES.join(", ")}` : null,
697
+ subscription !== void 0 && !RESOURCE_ID.test(subscription) ? "subscription must be a subscription id" : null,
698
+ after !== void 0 && !RESOURCE_ID.test(after) ? "starting_after must be an event id" : null
699
+ ].filter((reason) => reason !== null);
700
+ if (reasons.length) return error(400, "invalid_request", reasons);
701
+ const all = this.#servedEvents();
702
+ const from = after === void 0 ? 0 : all.findIndex((each) => each.id === after) + 1;
703
+ if (after !== void 0 && from === 0) {
704
+ return error(400, "invalid_request", "starting_after is not an event of yours.");
705
+ }
706
+ const data = all.slice(from).filter((each) => type === void 0 || each.type === type).filter((each) => subscription === void 0 || each.data.id === subscription);
707
+ const page = {
708
+ data: data.slice(0, limit),
709
+ has_more: data.length > limit,
710
+ available_since: this.#eventsSince
711
+ };
712
+ return Response.json(page);
713
+ }
714
+ #attemptsOf(subscription, query) {
715
+ const all = _nullishCoalesce(this.#attempts.get(subscription.id), () => ( []));
716
+ const limit = Number(_nullishCoalesce(query["limit"], () => ( 20)));
717
+ const after = query["starting_after"];
718
+ const from = after === void 0 ? 0 : all.findIndex((each) => each.id === after) + 1;
719
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
720
+ return error(400, "invalid_request", "limit must be a whole number from 1 to 100");
721
+ }
722
+ if (after !== void 0 && from === 0) {
723
+ return error(
724
+ 400,
725
+ "invalid_request",
726
+ "starting_after is not an attempt of this subscription."
727
+ );
728
+ }
729
+ const paid = all.filter((each) => each.outcome === "paid");
730
+ const page = {
731
+ data: all.slice(from, from + limit),
732
+ has_more: all.length > from + limit,
733
+ // Over every attempt, not the page; an amount that is no whole number counts for 0.
734
+ paid: {
735
+ count: paid.length,
736
+ amount: paid.reduce(
737
+ (sum, each) => sum + (/^\d+$/.test(each.amount) ? BigInt(each.amount) : 0n),
738
+ 0n
739
+ ).toString()
740
+ }
741
+ };
742
+ return Response.json(page);
743
+ }
744
+ #create(body) {
745
+ const { plan, wallet, email, external_id } = _nullishCoalesce(body, () => ( {}));
746
+ if (typeof plan !== "string" || typeof wallet !== "string") {
747
+ return error(400, "invalid_request", "plan and wallet are required.");
748
+ }
749
+ const invalid = invalidContact({ wallet, email });
750
+ if (invalid) return invalid;
751
+ if (this.#plans && !this.#plans.has(plan)) {
752
+ return error(404, "plan_not_found", `No plan under slug ${plan}`);
753
+ }
754
+ if (this.#over(plan)) return error(409, "plan_ended", "This plan has ended.");
755
+ const contact = {
756
+ email: typeof email === "string" ? email.trim().toLowerCase() : null,
757
+ external_id: typeof external_id === "string" ? external_id.trim() : null
758
+ };
759
+ const held = this.#subscriptions.filter((s) => s.plan === plan && s.wallet === wallet);
760
+ const refusal = liveRefusal(held);
761
+ if (refusal) return refusal;
762
+ const waiting = held.find(
763
+ (s) => s.status === "pending" && holderOf(s) === holderOf(contact)
764
+ );
765
+ if (waiting && contact.email !== null) waiting.email = contact.email;
766
+ const subscription = _nullishCoalesce(waiting, () => ( this.addSubscription({
767
+ plan,
768
+ wallet,
769
+ ...contact,
770
+ status: "pending",
771
+ access: false,
772
+ payment_status: "none",
773
+ current_period_start: null,
774
+ confirmed_at: null
775
+ })));
776
+ return Response.json(handedOut(subscription), { status: 201 });
777
+ }
778
+ /**
779
+ * A fresh transaction for a subscription still pending, with the same
780
+ * terms message as `create` gave it: a signature held stays good. Refused
781
+ * as Mesub refuses it otherwise.
782
+ */
783
+ #transaction(subscription) {
784
+ if (subscription.status !== "pending") {
785
+ return error(409, "not_awaiting_signature", "That subscription awaits no signature.");
786
+ }
787
+ if (this.#over(subscription.plan)) return error(409, "plan_ended", "This plan has ended.");
788
+ const refusal = liveRefusal(
789
+ this.#subscriptions.filter(
790
+ (s) => s.plan === subscription.plan && s.wallet === subscription.wallet
791
+ )
792
+ );
793
+ if (refusal) return refusal;
794
+ return Response.json(handedOut(subscription), { status: 201 });
795
+ }
796
+ /**
797
+ * Lands at once: the subscription turns active and its customer is granted
798
+ * the plan. The other checkouts waiting on that wallet expire, as on Mesub.
799
+ */
800
+ #submit(subscription) {
801
+ if (subscription.status !== "pending") {
802
+ return error(409, "not_awaiting_signature", "That subscription awaits no signature.");
803
+ }
804
+ if (this.#over(subscription.plan)) return error(409, "plan_ended", "This plan has ended.");
805
+ for (const other of this.#subscriptions) {
806
+ const beside = other.plan === subscription.plan && other.wallet === subscription.wallet;
807
+ if (beside && other !== subscription && other.status === "pending") {
808
+ other.status = "expired";
809
+ }
810
+ }
811
+ const now = /* @__PURE__ */ new Date();
812
+ const end = new Date(now.getTime() + PERIOD_MS).toISOString();
813
+ const dates = bounded(
814
+ { access: true, access_until: end, next_charge_at: end, next_retry_at: null },
815
+ this.#endOf(subscription.plan)
816
+ );
817
+ Object.assign(subscription, {
818
+ status: "active",
819
+ payment_status: "paid",
820
+ current_period_start: now.toISOString(),
821
+ current_period_end: end,
822
+ confirmed_at: now.toISOString(),
823
+ ...dates
824
+ });
825
+ const plan = _nullishCoalesce(subscription.plan, () => ( ""));
826
+ const fields = {
827
+ wallet: subscription.wallet,
828
+ subscription_id: subscription.id,
829
+ subscribed_since: now.toISOString(),
830
+ first_subscribed_at: now.toISOString(),
831
+ current_period_end: end,
832
+ access_until: dates.access_until,
833
+ next_charge_at: dates.next_charge_at
834
+ };
835
+ this.grant({ wallet: subscription.wallet }, plan, fields);
836
+ if (subscription.external_id) {
837
+ this.grant({ external_id: subscription.external_id }, plan, fields);
838
+ }
839
+ if (subscription.email) this.grant({ email: subscription.email }, plan, fields);
840
+ const terms = this.#planList.find((each) => each.slug === subscription.plan);
841
+ this.#raise("subscription.created", subscription, false, {
842
+ first_payment: {
843
+ amount: _nullishCoalesce(_optionalChain([terms, 'optionalAccess', _16 => _16.amount]), () => ( "9990000")),
844
+ mint: _nullishCoalesce(_optionalChain([terms, 'optionalAccess', _17 => _17.mint]), () => ( USDC)),
845
+ signature: "fake_signature"
846
+ }
847
+ });
848
+ return Response.json({ subscription: this.#view(subscription) }, { status: 201 });
849
+ }
850
+ /**
851
+ * A hand retry, refused as Mesub's `RetryNowService` refuses it from what
852
+ * the fake holds, in its order: a parked late seat, a row not late, a
853
+ * plan past its end, the wait since the last one (by `tier`, with
854
+ * `Retry-After`), and on Free a `retry_deadline` passed or the three
855
+ * retries spent. Taken, it pulls nothing and the row stays `unpaid`: the
856
+ * fake counts it as one more that failed. What only the chain or a race
857
+ * decides (`insufficient_balance`, `retry_cannot_succeed`,
858
+ * `pull_running`, `subscription_changed`) is answered with `fail()`.
859
+ */
860
+ #retry(subscription) {
861
+ if (subscription.status === "unpaid" && subscription.paused) {
862
+ return error(
863
+ 409,
864
+ "subscription_paused",
865
+ "This subscription is paused over the merchant's tier limit, so it cannot be retried."
866
+ );
867
+ }
868
+ if (subscription.status !== "unpaid") {
869
+ return error(
870
+ 409,
871
+ "subscription_not_late",
872
+ "This subscription is not behind on its payment."
873
+ );
874
+ }
875
+ if (this.#over(subscription.plan)) {
876
+ return error(
877
+ 409,
878
+ "plan_ended",
879
+ "This plan has ended, so this payment can no longer be collected."
880
+ );
881
+ }
882
+ const now = Date.now();
883
+ const free = this.#tier === "free";
884
+ const fired = _nullishCoalesce(this.#handRetries.get(subscription.id), () => ( { count: 0, at: 0 }));
885
+ const openAt = fired.count === 0 ? 0 : fired.at + (free ? FREE_RETRY_WAIT_MS : PAID_RETRY_WAIT_MS * 2 ** (fired.count - 1));
886
+ if (openAt > now) {
887
+ const minutes = Math.ceil((openAt - now) / 6e4);
888
+ return error(
889
+ 409,
890
+ "retry_too_soon",
891
+ `Try again in ${minutes} ${minutes === 1 ? "minute" : "minutes"}.`,
892
+ { retryAfter: Math.ceil((openAt - now) / 1e3) }
893
+ );
894
+ }
895
+ if (free) {
896
+ const deadline = subscription.retry_deadline;
897
+ if (deadline !== null && Date.parse(deadline) <= now) {
898
+ return error(
899
+ 409,
900
+ "retry_deadline_passed",
901
+ "The missed period ends too soon to retry."
902
+ );
903
+ }
904
+ if (fired.count >= FREE_HAND_RETRIES) {
905
+ return error(409, "retries_spent", "This subscription has no retries left.");
906
+ }
907
+ }
908
+ this.#handRetries.set(subscription.id, { count: fired.count + 1, at: now });
909
+ return Response.json(this.#view(subscription), { status: 202 });
910
+ }
911
+ /** The transaction of a cancel, resume or close, or Mesub's refusal of it. */
912
+ #build(action, subscription) {
913
+ const refusal = refusalOf(action, subscription, this.#gone.has(subscription.id));
914
+ if (refusal) return refusal;
915
+ const built = _nullishCoalesce(this.#built.get(subscription.id), () => ( /* @__PURE__ */ new Set()));
916
+ this.#built.set(subscription.id, built.add(action));
917
+ const answer = {
918
+ transaction: btoa(`fake ${action} transaction`),
919
+ last_valid_block_height: "0",
920
+ transaction_expires_at: expiresAt()
921
+ };
922
+ return Response.json(answer, { status: 201 });
923
+ }
924
+ /** Lands at once: the subscription moves, and so do its customer's access answers. */
925
+ #confirm(action, subscription, body) {
926
+ const { signature } = _nullishCoalesce(body, () => ( {}));
927
+ if (typeof signature !== "string" || signature === "") {
928
+ return error(400, "invalid_request", "signature must be a transaction signature.");
929
+ }
930
+ const gone = this.#gone.has(subscription.id);
931
+ const done = subscription.status === DONE[action] && !(endedWithPlan(subscription) && !gone);
932
+ const refusal = done ? null : refusalOf(action, subscription, gone);
933
+ if (refusal) return refusal;
934
+ if (!_optionalChain([this, 'access', _18 => _18.#built, 'access', _19 => _19.get, 'call', _20 => _20(subscription.id), 'optionalAccess', _21 => _21.has, 'call', _22 => _22(action)])) {
935
+ return error(400, "nothing_to_confirm", "There is no transaction to confirm yet.");
936
+ }
937
+ if (done) return Response.json({ subscription: this.#view(subscription) }, { status: 201 });
938
+ this.#built.set(subscription.id, /* @__PURE__ */ new Set([action]));
939
+ const before = this.#view(subscription).access;
940
+ const now = (/* @__PURE__ */ new Date()).toISOString();
941
+ const end = subscription.current_period_end;
942
+ if (action === "close" && endedWithPlan(subscription)) {
943
+ this.#gone.add(subscription.id);
944
+ this.#answer(subscription, {});
945
+ return Response.json({ subscription: this.#view(subscription) }, { status: 201 });
946
+ }
947
+ if (action === "cancel") {
948
+ Object.assign(subscription, {
949
+ status: "cancelled",
950
+ stop_reason: null,
951
+ ...NO_BLOCK,
952
+ next_charge_at: null,
953
+ next_retry_at: null,
954
+ retry_deadline: null,
955
+ next_retry_number: null,
956
+ retries_allowed: null,
957
+ access_until: subscription.access ? _nullishCoalesce(subscription.access_until, () => ( end)) : null
958
+ });
959
+ this.#answer(subscription, {
960
+ status: "cancelled",
961
+ stop_reason: null,
962
+ ...NO_BLOCK,
963
+ // The access a cancellation keeps, also where no grant dated it.
964
+ access_until: subscription.access_until,
965
+ cancelled_at: now,
966
+ next_charge_at: null,
967
+ next_retry_at: null,
968
+ retry_deadline: null
969
+ });
970
+ } else if (action === "resume") {
971
+ Object.assign(subscription, {
972
+ status: "active",
973
+ next_charge_at: end
974
+ });
975
+ this.#answer(subscription, {
976
+ status: "active",
977
+ cancelled_at: null,
978
+ next_charge_at: end
979
+ });
980
+ } else {
981
+ Object.assign(subscription, {
982
+ status: "ended",
983
+ end_reason: "closed",
984
+ stop_reason: null,
985
+ ...NO_BLOCK,
986
+ access: false,
987
+ payment_status: "none",
988
+ access_until: null,
989
+ next_charge_at: null,
990
+ next_retry_at: null,
991
+ retry_deadline: null,
992
+ next_retry_number: null,
993
+ retries_allowed: null
994
+ });
995
+ this.#answer(subscription, {
996
+ access: false,
997
+ status: "ended",
998
+ end_reason: "closed",
999
+ stop_reason: null,
1000
+ ...NO_BLOCK,
1001
+ payment_status: "none",
1002
+ access_until: null,
1003
+ next_charge_at: null,
1004
+ next_retry_at: null,
1005
+ retry_deadline: null
1006
+ });
1007
+ }
1008
+ this.#raise(EVENT_OF[action], subscription, before);
1009
+ return Response.json({ subscription: this.#view(subscription) }, { status: 201 });
1010
+ }
1011
+ /**
1012
+ * Changes what `/v1/access` answers a subscription's customer, by wallet,
1013
+ * external id and email, on top of what each was answered before.
1014
+ */
1015
+ #answer(subscription, fields) {
1016
+ const plan = _nullishCoalesce(subscription.plan, () => ( ""));
1017
+ const customers = [{ wallet: subscription.wallet }];
1018
+ if (subscription.external_id) customers.push({ external_id: subscription.external_id });
1019
+ if (subscription.email) customers.push({ email: subscription.email });
1020
+ for (const customer of customers) {
1021
+ const before = this.#answers.get(keyOf(_chunk4JSJOX35cjs.customerOf.call(void 0, customer), plan));
1022
+ this.setAccess(customer, plan, {
1023
+ wallet: subscription.wallet,
1024
+ access: subscription.access,
1025
+ payment_status: subscription.payment_status,
1026
+ ...before,
1027
+ subscription_id: subscription.id,
1028
+ // A row whose close landed has nothing left to close; any
1029
+ // other is worked out when served.
1030
+ ...this.#gone.has(subscription.id) && { closable: false, closable_from: null },
1031
+ ...fields
1032
+ });
1033
+ }
1034
+ }
1035
+ }, _class);
1036
+ function revalidateAfter(answer, planEnd) {
1037
+ if (!answer.access) return REVALIDATE_MIN_S;
1038
+ const next = soonest(
1039
+ _nullishCoalesce(_nullishCoalesce(answer.next_retry_at, () => ( answer.next_charge_at)), () => ( soonest(
1040
+ answer.paused ? answer.current_period_end : null,
1041
+ answer.status === "cancelled" ? answer.access_until : null
1042
+ ))),
1043
+ planEnd,
1044
+ answer.blocked_since === null ? null : answer.access_until
1045
+ );
1046
+ if (next === null) return REVALIDATE_MAX_S;
1047
+ const seconds = Math.floor((Date.parse(next) - Date.now()) / 1e3);
1048
+ return Math.min(REVALIDATE_MAX_S, Math.max(REVALIDATE_MIN_S, seconds));
1049
+ }
1050
+ function soonest(...dates) {
1051
+ return dates.reduce(
1052
+ (first, date) => date === null || first !== null && Date.parse(first) <= Date.parse(date) ? first : date,
1053
+ null
1054
+ );
1055
+ }
1056
+ function bounded(row, end) {
1057
+ if (end === null || !row.access) return row;
1058
+ const at = Date.parse(end);
1059
+ const none = { next_charge_at: null, next_retry_at: null };
1060
+ if (at <= Date.now()) return { ...row, ...none, access: false, access_until: null };
1061
+ const ahead = (date) => date !== null && Date.parse(date) < at ? date : null;
1062
+ return {
1063
+ ...row,
1064
+ access_until: row.access_until !== null && Date.parse(row.access_until) > at ? end : row.access_until,
1065
+ next_charge_at: ahead(row.next_charge_at),
1066
+ next_retry_at: ahead(row.next_retry_at)
1067
+ };
1068
+ }
1069
+ function datesOf(status, tier, now) {
1070
+ const iso = (ms) => new Date(ms).toISOString();
1071
+ const running = { current_period_start: iso(now), current_period_end: iso(now + PERIOD_MS) };
1072
+ const behind = { current_period_start: iso(now - PERIOD_MS), current_period_end: iso(now) };
1073
+ const nothing2 = {
1074
+ access: false,
1075
+ payment_status: "none",
1076
+ next_charge_at: null,
1077
+ access_until: null
1078
+ };
1079
+ switch (status) {
1080
+ case "active":
1081
+ return {
1082
+ ...running,
1083
+ access: true,
1084
+ payment_status: "paid",
1085
+ next_charge_at: running.current_period_end,
1086
+ access_until: running.current_period_end
1087
+ };
1088
+ case "cancelled":
1089
+ return {
1090
+ ...running,
1091
+ access: true,
1092
+ payment_status: "paid",
1093
+ next_charge_at: null,
1094
+ access_until: running.current_period_end
1095
+ };
1096
+ case "unpaid": {
1097
+ const late = {
1098
+ ...behind,
1099
+ late_reason: "insufficient_balance",
1100
+ payment_status: "late",
1101
+ next_charge_at: null
1102
+ };
1103
+ if (tier === "free") {
1104
+ const deadline = iso(now + PERIOD_MS - CLOCK_MARGIN_MS);
1105
+ return { ...late, access: false, access_until: null, retry_deadline: deadline };
1106
+ }
1107
+ const retry = iso(now + DAY_MS);
1108
+ return {
1109
+ ...late,
1110
+ access: true,
1111
+ // Its last retry: access stops there if that one fails too.
1112
+ access_until: retry,
1113
+ next_retry_at: retry,
1114
+ next_retry_number: 1,
1115
+ retries_allowed: 1
1116
+ };
1117
+ }
1118
+ case "stopped":
1119
+ return { ...behind, ...nothing2, stop_reason: "out_of_retries" };
1120
+ case "ended":
1121
+ return { ...behind, ...nothing2, end_reason: "closed" };
1122
+ case "superseded":
1123
+ return { ...behind, ...nothing2 };
1124
+ default:
1125
+ return {
1126
+ ...nothing2,
1127
+ current_period_start: null,
1128
+ current_period_end: null,
1129
+ confirmed_at: null
1130
+ };
1131
+ }
1132
+ }
1133
+ function expiresAt() {
1134
+ return new Date(Date.now() + TRANSACTION_LIFE_MS).toISOString();
1135
+ }
1136
+ function handedOut(subscription) {
1137
+ const zero = "0";
1138
+ return {
1139
+ subscription: { id: subscription.id, status: "pending" },
1140
+ transaction: btoa("fake transaction"),
1141
+ last_valid_block_height: zero,
1142
+ transaction_expires_at: expiresAt(),
1143
+ costs: {
1144
+ rent: { subscription: zero, authority: null, total: zero },
1145
+ fee: { signatures: 2, per_signature: zero, priority: zero, total: zero },
1146
+ total: zero
1147
+ },
1148
+ terms: {
1149
+ message: `Fake Mesub terms: ${subscription.wallet} subscribes to ${subscription.plan}.`,
1150
+ expires_at: new Date(Date.now() + 5 * 6e4).toISOString()
1151
+ }
1152
+ };
1153
+ }
1154
+ var NO_BLOCK = { blocked_since: null, blocked_reason: null };
1155
+ var NO_RETRY = { next_retry_number: null, retries_allowed: null };
1156
+ function endedBy(row) {
1157
+ const running = row.status === "active" || row.status === "unpaid";
1158
+ if (!running && row.status !== "cancelled") return {};
1159
+ return {
1160
+ status: "ended",
1161
+ end_reason: running ? "plan_ended" : "cancelled",
1162
+ late_reason: null,
1163
+ ...NO_BLOCK,
1164
+ paused: false,
1165
+ access: false,
1166
+ payment_status: "none",
1167
+ access_until: null,
1168
+ next_charge_at: null,
1169
+ next_retry_at: null,
1170
+ retry_deadline: null
1171
+ };
1172
+ }
1173
+ var EVENT_OF = {
1174
+ cancel: "subscription.cancelled",
1175
+ resume: "subscription.resumed",
1176
+ close: "subscription.ended"
1177
+ };
1178
+ function closingOf(row, gone = false) {
1179
+ const closable = refusalOf("close", row, gone) === null;
1180
+ const ahead = row.access_until !== null && Date.parse(row.access_until) > Date.now();
1181
+ return {
1182
+ closable,
1183
+ closable_from: !closable && row.status === "cancelled" && ahead ? row.access_until : null
1184
+ };
1185
+ }
1186
+ function withClosing(subscription) {
1187
+ return { ...subscription, ...closingOf(subscription) };
1188
+ }
1189
+ var DONE = {
1190
+ cancel: "cancelled",
1191
+ resume: "active",
1192
+ close: "ended"
1193
+ };
1194
+ function refusalOf(action, subscription, gone = false) {
1195
+ const { status, access_until } = subscription;
1196
+ const over = access_until === null || Date.parse(access_until) <= Date.now();
1197
+ const cancelled = status === "cancelled" || status === "ended" && subscription.end_reason === "cancelled";
1198
+ if (action === "cancel") {
1199
+ if (status === "cancelled") {
1200
+ return error(409, "subscription_cancelled", "This subscription is cancelled already.");
1201
+ }
1202
+ return ["active", "unpaid", "stopped"].includes(status) ? null : error(409, "subscription_not_active", "This subscription is not running.");
1203
+ }
1204
+ if (action === "resume") {
1205
+ if (status !== "cancelled") {
1206
+ return error(409, "subscription_not_cancelled", "This subscription is not cancelled.");
1207
+ }
1208
+ return over ? error(409, "subscription_ended", "The period paid for is over: subscribe again.") : null;
1209
+ }
1210
+ if (endedWithPlan(subscription)) {
1211
+ return gone ? error(
1212
+ 409,
1213
+ "subscription_not_on_chain",
1214
+ "This subscription was closed outside Mesub, so it has ended."
1215
+ ) : null;
1216
+ }
1217
+ if (isClosed(subscription)) {
1218
+ return error(409, "subscription_not_on_chain", "This subscription is closed already.");
1219
+ }
1220
+ if (status === "ended" && !cancelled) {
1221
+ return error(
1222
+ 409,
1223
+ "subscription_not_on_chain",
1224
+ "This subscription has ended and cannot be closed through Mesub."
1225
+ );
1226
+ }
1227
+ if (status === "stopped" && subscription.stop_reason === "authority_closed") return null;
1228
+ if (!cancelled) {
1229
+ return error(
1230
+ 409,
1231
+ "subscription_not_cancelled",
1232
+ "Cancel this subscription before closing it."
1233
+ );
1234
+ }
1235
+ return over ? null : error(409, "close_too_early", `This subscription runs until ${access_until}.`);
1236
+ }
1237
+ function liveRefusal(held) {
1238
+ const live = held.find(
1239
+ (s) => ["active", "cancelled", "unpaid"].includes(s.status) || s.status === "ended" && s.end_reason === "cancelled"
1240
+ );
1241
+ const dead = held.find((s) => s.status === "stopped" && s.stop_reason === "authority_closed");
1242
+ if (dead && !live) {
1243
+ return error(
1244
+ 409,
1245
+ "subscription_stopped",
1246
+ "Your wallet's approval for this token was closed for good, so this subscription cannot restart. Close it, then subscribe again."
1247
+ );
1248
+ }
1249
+ if (!live) return null;
1250
+ if (live.status === "active") {
1251
+ return error(409, "already_subscribed", "You are already subscribed to this plan.");
1252
+ }
1253
+ if (live.status === "unpaid") {
1254
+ return error(
1255
+ 409,
1256
+ "payment_pending",
1257
+ "You already have a subscription to this plan, and its last payment is late. Open it in your subscriptions to see how to fix it, or cancel it."
1258
+ );
1259
+ }
1260
+ const until = live.status === "cancelled" ? live.access_until : null;
1261
+ return error(
1262
+ 409,
1263
+ "subscription_cancelled",
1264
+ until !== null && Date.parse(until) > Date.now() ? `Your subscription to this plan is cancelled but runs until ${until}: resume it instead of subscribing again.` : "Your cancelled subscription to this plan must be closed first: close it, then subscribe again."
1265
+ );
1266
+ }
1267
+ function holderOf(contact) {
1268
+ if (contact.external_id) return `x:${contact.external_id}`;
1269
+ return contact.email ? `e:${contact.email}` : "-";
1270
+ }
1271
+ var WITH_ITS_PLAN = [
1272
+ "plan_removed",
1273
+ "plan_replaced",
1274
+ "plan_ended"
1275
+ ];
1276
+ function endedWithPlan(subscription) {
1277
+ return subscription.status === "ended" && WITH_ITS_PLAN.includes(subscription.end_reason);
1278
+ }
1279
+ function isClosed(subscription) {
1280
+ return subscription.status === "ended" && subscription.end_reason === "closed";
1281
+ }
1282
+ function keyOf(asked, plan) {
1283
+ return `${asked.kind}:${asked.value}
1284
+ ${plan}`;
1285
+ }
1286
+ var signedIds = 0;
1287
+ async function signWebhook(payload, options) {
1288
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
1289
+ const id = _nullishCoalesce(options.id, () => ( `msg_signed_${signedIds += 1}`));
1290
+ const timestamp = _nullishCoalesce(options.timestamp, () => ( Math.floor(Date.now() / 1e3)));
1291
+ return { body, headers: await _chunk4JSJOX35cjs.signedHeaders.call(void 0, options.secret, id, timestamp, body) };
1292
+ }
1293
+ function detailOf(type, now, data) {
1294
+ const start = now.toISOString();
1295
+ const end = new Date(now.getTime() + PERIOD_MS).toISOString();
1296
+ const paid = { amount: "9990000", mint: USDC, period_start: start, period_end: end };
1297
+ switch (type) {
1298
+ case "subscription.renewal_upcoming":
1299
+ return {
1300
+ can_pay: true,
1301
+ renewal_issue: null,
1302
+ amount: paid.amount,
1303
+ mint: USDC,
1304
+ // The check's window opens three days before the charge at most.
1305
+ due_at: new Date(now.getTime() + 3 * 24 * 3600 * 1e3).toISOString()
1306
+ };
1307
+ case "subscription.renewed":
1308
+ return { ...paid, signature: "fake_signature" };
1309
+ case "subscription.payment_failed":
1310
+ return {
1311
+ ...paid,
1312
+ reason: "insufficient_balance",
1313
+ next_retry_at: new Date(now.getTime() + 24 * 3600 * 1e3).toISOString(),
1314
+ retry_deadline: null,
1315
+ retries_left: 1,
1316
+ retry_mode: "scheduled"
1317
+ };
1318
+ case "subscription.stopped":
1319
+ return { reason: "insufficient_balance" };
1320
+ case "subscription.access_granted":
1321
+ case "subscription.access_revoked":
1322
+ return accessDetailOf(data, start);
1323
+ case "subscription.created":
1324
+ return {
1325
+ first_payment: { amount: paid.amount, mint: USDC, signature: "fake_signature" }
1326
+ };
1327
+ case "subscription.ended":
1328
+ return data.end_reason === "cancelled" ? { changed_at: start, end_reason: "cancelled" } : {};
1329
+ default:
1330
+ return {};
1331
+ }
1332
+ }
1333
+ function accessDetailOf(data, changedAt) {
1334
+ const { access, blocked_reason, access_until, status, end_reason } = data;
1335
+ const dated = blocked_reason !== null || status === "ended" && (end_reason === "cancelled" || end_reason === "plan_ended");
1336
+ return {
1337
+ changed_at: changedAt,
1338
+ blocked_reason,
1339
+ access_until: access ? blocked_reason === null ? null : access_until : _nullishCoalesce(access_until, () => ( (dated ? changedAt : null)))
1340
+ };
1341
+ }
1342
+ function subscriptionOf(type) {
1343
+ if (type === "subscription.paused") return { paused: true, next_charge_at: null };
1344
+ if (type !== "subscription.access_revoked") return {};
1345
+ return {
1346
+ status: "ended",
1347
+ end_reason: "cancelled",
1348
+ access: false,
1349
+ payment_status: "none",
1350
+ next_charge_at: null,
1351
+ access_until: null
1352
+ };
1353
+ }
1354
+ function nothing(asked, plan) {
1355
+ return { wallet: asked.kind === "wallet" ? asked.value : null, plan, ...NOTHING };
1356
+ }
1357
+ var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
1358
+ var EMAIL = /^[^\s@]+@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
1359
+ function invalidContact({ wallet, email }) {
1360
+ const reasons = [];
1361
+ if (typeof wallet === "string" && !BASE58_ADDRESS.test(wallet)) {
1362
+ reasons.push("wallet must be a base58 Solana address");
1363
+ }
1364
+ if (typeof email === "string") {
1365
+ const normalised = email.trim().toLowerCase();
1366
+ if (!EMAIL.test(normalised)) reasons.push("email must be an email address");
1367
+ if (normalised.length > 254) {
1368
+ reasons.push("email must be shorter than or equal to 254 characters");
1369
+ }
1370
+ }
1371
+ return reasons.length ? error(400, "invalid_request", reasons) : null;
1372
+ }
1373
+ var LIMIT_MESSAGE = "limit must be a whole number from 1 to 100";
1374
+ function limitOf(value) {
1375
+ const limit = /^\d{1,4}$/.test(value) ? Number(value) : NaN;
1376
+ return limit >= 1 && limit <= 100 ? limit : null;
1377
+ }
1378
+ function invalidSubmit(body) {
1379
+ const { transaction, terms_signature } = _nullishCoalesce(body, () => ( {}));
1380
+ const reasons = [];
1381
+ if (typeof transaction !== "string" || transaction.length > 2048) {
1382
+ reasons.push("transaction must be shorter than or equal to 2048 characters");
1383
+ }
1384
+ if (typeof transaction !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(transaction)) {
1385
+ reasons.push("transaction must be base64");
1386
+ }
1387
+ if (typeof terms_signature !== "string" || terms_signature === "") {
1388
+ reasons.push("terms_signature must be a base58 signature");
1389
+ }
1390
+ return reasons.length ? error(400, "invalid_request", reasons) : null;
1391
+ }
1392
+ function askedIn(query) {
1393
+ try {
1394
+ return _chunk4JSJOX35cjs.customerOf.call(void 0, {
1395
+ wallet: query["wallet"],
1396
+ external_id: query["external_id"],
1397
+ email: query["email"]
1398
+ });
1399
+ } catch (e) {
1400
+ return null;
1401
+ }
1402
+ }
1403
+ function withAttempts(answer, attempts) {
1404
+ if (attempts) return { ...answer, attempts: _nullishCoalesce(answer.attempts, () => ( [])) };
1405
+ const light = { ...answer };
1406
+ delete light.attempts;
1407
+ return light;
1408
+ }
1409
+ function fakePlan(slug, { token, ...over } = {}) {
1410
+ const symbol = over.symbol === void 0 ? token ? null : "USDC" : over.symbol;
1411
+ const vouched = token === void 0 && symbol !== null;
1412
+ return {
1413
+ slug,
1414
+ name: slug,
1415
+ description: null,
1416
+ project_name: "Test project",
1417
+ logo_url: null,
1418
+ amount: "9990000",
1419
+ amount_display: "9.99",
1420
+ amount_usd: vouched ? _nullishCoalesce(over.amount_display, () => ( "9.99")) : null,
1421
+ decimals: 6,
1422
+ symbol,
1423
+ token: {
1424
+ symbol: token ? null : symbol,
1425
+ name: vouched && symbol === "USDC" ? "USD Coin" : null,
1426
+ logo_url: null,
1427
+ verified: vouched,
1428
+ ...token
1429
+ },
1430
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1431
+ period_hours: 720,
1432
+ network: "mainnet-beta",
1433
+ status: "active",
1434
+ available: true,
1435
+ ends_at: null,
1436
+ ...over
1437
+ };
1438
+ }
1439
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
1440
+ "rate_limited",
1441
+ "internal_error",
1442
+ "unavailable",
1443
+ "comeback_in_flight",
1444
+ "comeback_period_rolling",
1445
+ "pending_cap_reached",
1446
+ "subscription_changed",
1447
+ "retry_too_soon",
1448
+ "pull_running",
1449
+ "network_unavailable"
1450
+ ]);
1451
+ function waitOf(method, path, status, code, tier) {
1452
+ if (code === "retry_too_soon")
1453
+ return (tier === "free" ? FREE_RETRY_WAIT_MS : PAID_RETRY_WAIT_MS) / 1e3;
1454
+ if (status !== 503 || method !== "POST" || !path.startsWith("/v1/subscriptions")) {
1455
+ return void 0;
1456
+ }
1457
+ if (code === "unavailable" && path.endsWith("/submit")) return 2;
1458
+ if (code === "unavailable" && path.endsWith("/retry")) return 20;
1459
+ return 10;
1460
+ }
1461
+ function error(status, code, message, { retryable, retryAfter } = {}) {
1462
+ return Response.json(
1463
+ {
1464
+ statusCode: status,
1465
+ error: _nullishCoalesce(REASONS[status], () => ( "Error")),
1466
+ message,
1467
+ code,
1468
+ retryable: _nullishCoalesce(retryable, () => ( (RETRYABLE_CODES.has(code) || status === 408 || status === 429 || status >= 500)))
1469
+ },
1470
+ {
1471
+ status,
1472
+ ...retryAfter !== void 0 && { headers: { "Retry-After": String(retryAfter) } }
1473
+ }
1474
+ );
1475
+ }
1476
+ async function stamped(response, id) {
1477
+ const headers = new Headers(response.headers);
1478
+ headers.set(REQUEST_ID_HEADER, id);
1479
+ headers.set(_chunk4JSJOX35cjs.API_VERSION_HEADER, _chunk4JSJOX35cjs.API_VERSION);
1480
+ if (response.ok) return new Response(response.body, { status: response.status, headers });
1481
+ const body = await response.json();
1482
+ return Response.json({ ...body, request_id: id }, { status: response.status, headers });
1483
+ }
1484
+
1485
+
1486
+
1487
+ exports.FakeMesub = FakeMesub; exports.signWebhook = signWebhook;
1488
+ //# sourceMappingURL=testing.cjs.map