@okxweb3/app-x402-core 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cjs/OKXFacilitatorClient-Bqyw9fzj.d.ts +69 -0
  2. package/dist/cjs/client/index.d.ts +1 -1
  3. package/dist/cjs/client/index.js +34 -0
  4. package/dist/cjs/client/index.js.map +1 -1
  5. package/dist/cjs/facilitator/index.d.ts +2 -2
  6. package/dist/cjs/facilitator/index.js +166 -4
  7. package/dist/cjs/facilitator/index.js.map +1 -1
  8. package/dist/cjs/http/index.d.ts +5 -3
  9. package/dist/cjs/http/index.js +1241 -7
  10. package/dist/cjs/http/index.js.map +1 -1
  11. package/dist/cjs/index-2gWfiUbK.d.ts +713 -0
  12. package/dist/cjs/index.d.ts +2 -2
  13. package/dist/cjs/index.js +166 -4
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/{mechanisms-sojpSwWW.d.ts → mechanisms-LhI9qkRo.d.ts} +509 -1
  16. package/dist/cjs/server/index.d.ts +4 -2
  17. package/dist/cjs/server/index.js +1256 -7
  18. package/dist/cjs/server/index.js.map +1 -1
  19. package/dist/cjs/subscription/index.d.ts +3 -0
  20. package/dist/cjs/subscription/index.js +600 -0
  21. package/dist/cjs/subscription/index.js.map +1 -0
  22. package/dist/cjs/types/index.d.ts +1 -1
  23. package/dist/cjs/utils/index.d.ts +1 -1
  24. package/dist/cjs/{x402HTTPResourceServer-CcsAkcgI.d.ts → x402HTTPResourceServer-B0mXzV8r.d.ts} +114 -1
  25. package/dist/esm/OKXFacilitatorClient-z-cCE5Db.d.mts +69 -0
  26. package/dist/esm/chunk-4KASWSSY.mjs +257 -0
  27. package/dist/esm/chunk-4KASWSSY.mjs.map +1 -0
  28. package/dist/esm/chunk-CKXR4QVD.mjs +274 -0
  29. package/dist/esm/chunk-CKXR4QVD.mjs.map +1 -0
  30. package/dist/esm/{chunk-XBQG2CDV.mjs → chunk-EYS4TWVA.mjs} +617 -9
  31. package/dist/esm/chunk-EYS4TWVA.mjs.map +1 -0
  32. package/dist/esm/client/index.d.mts +1 -1
  33. package/dist/esm/client/index.mjs +3 -2
  34. package/dist/esm/client/index.mjs.map +1 -1
  35. package/dist/esm/facilitator/index.d.mts +2 -2
  36. package/dist/esm/facilitator/index.mjs +2 -1
  37. package/dist/esm/facilitator/index.mjs.map +1 -1
  38. package/dist/esm/http/index.d.mts +5 -3
  39. package/dist/esm/http/index.mjs +3 -2
  40. package/dist/esm/index-DKbqlTu_.d.mts +713 -0
  41. package/dist/esm/index.d.mts +2 -2
  42. package/dist/esm/index.mjs +2 -1
  43. package/dist/esm/{mechanisms-sojpSwWW.d.mts → mechanisms-LhI9qkRo.d.mts} +509 -1
  44. package/dist/esm/server/index.d.mts +4 -2
  45. package/dist/esm/server/index.mjs +3 -2
  46. package/dist/esm/subscription/index.d.mts +3 -0
  47. package/dist/esm/subscription/index.mjs +309 -0
  48. package/dist/esm/subscription/index.mjs.map +1 -0
  49. package/dist/esm/types/index.d.mts +1 -1
  50. package/dist/esm/utils/index.d.mts +1 -1
  51. package/dist/esm/{x402HTTPResourceServer-DBeutKxq.d.mts → x402HTTPResourceServer-56Tq3Jup.d.mts} +114 -1
  52. package/package.json +12 -1
  53. package/dist/cjs/OKXFacilitatorClient-BvyQB1QM.d.ts +0 -59
  54. package/dist/esm/OKXFacilitatorClient-D5E3LX50.d.mts +0 -59
  55. package/dist/esm/chunk-O3IYMTNT.mjs +0 -118
  56. package/dist/esm/chunk-O3IYMTNT.mjs.map +0 -1
  57. package/dist/esm/chunk-XBQG2CDV.mjs.map +0 -1
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,642 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/subscription/codec/typed-data.ts
34
+ function getSubscriptionExtra(req) {
35
+ const extra = req.extra;
36
+ if (!extra || !extra.contracts || !extra.plan || !extra.domain) {
37
+ throw new Error(
38
+ "subscription codec: PaymentRequirements.extra is missing contracts/plan/domain"
39
+ );
40
+ }
41
+ return extra;
42
+ }
43
+ function defaultPermitAmount(extra) {
44
+ const initialChargePeriods = BigInt(extra.initialCharge?.periodCount ?? 0);
45
+ const initialChargeAmount = BigInt(extra.initialCharge?.totalAmount ?? "0");
46
+ const remainingPeriods = BigInt(extra.maxPeriods) - initialChargePeriods;
47
+ const remainingAmount = remainingPeriods > 0n ? remainingPeriods * BigInt(extra.amountPerPeriod) : 0n;
48
+ return (initialChargeAmount + remainingAmount).toString();
49
+ }
50
+ function buildPermit2TypedData(input) {
51
+ const extra = getSubscriptionExtra(input.selected);
52
+ const amount = input.amount ?? defaultPermitAmount(extra);
53
+ const chainId = parseChainIdFromNetwork(input.selected.network);
54
+ return {
55
+ domain: {
56
+ name: "Permit2",
57
+ chainId,
58
+ verifyingContract: extra.contracts.permit2
59
+ },
60
+ types: PERMIT2_TYPES,
61
+ primaryType: "PermitSingle",
62
+ message: {
63
+ details: {
64
+ token: input.selected.asset,
65
+ amount,
66
+ expiration: input.expiration,
67
+ nonce: input.nonce
68
+ },
69
+ spender: extra.contracts.subscription,
70
+ sigDeadline: input.sigDeadline
71
+ }
72
+ };
73
+ }
74
+ function computePermitSingleStructHash(permit) {
75
+ const PERMIT_DETAILS_TYPEHASH = (0, import_viem.keccak256)(
76
+ new TextEncoder().encode(
77
+ "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
78
+ )
79
+ );
80
+ const detailsHash = (0, import_viem.keccak256)(
81
+ (0, import_viem.encodeAbiParameters)(
82
+ [
83
+ { type: "bytes32" },
84
+ { type: "address" },
85
+ { type: "uint160" },
86
+ { type: "uint48" },
87
+ { type: "uint48" }
88
+ ],
89
+ [
90
+ PERMIT_DETAILS_TYPEHASH,
91
+ permit.details.token,
92
+ BigInt(permit.details.amount),
93
+ Number(permit.details.expiration),
94
+ Number(permit.details.nonce)
95
+ ]
96
+ )
97
+ );
98
+ const PERMIT_SINGLE_TYPEHASH = (0, import_viem.keccak256)(
99
+ new TextEncoder().encode(
100
+ "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
101
+ )
102
+ );
103
+ return (0, import_viem.keccak256)(
104
+ (0, import_viem.encodeAbiParameters)(
105
+ [{ type: "bytes32" }, { type: "bytes32" }, { type: "address" }, { type: "uint256" }],
106
+ [PERMIT_SINGLE_TYPEHASH, detailsHash, permit.spender, BigInt(permit.sigDeadline)]
107
+ )
108
+ );
109
+ }
110
+ function buildSubscriptionTermsTypedData(input) {
111
+ const extra = getSubscriptionExtra(input.selected);
112
+ const domain = input.domain ?? extra.domain;
113
+ let changeEffectiveAt = 0;
114
+ if (input.changeFrom?.effectiveAt === "immediate") changeEffectiveAt = 1;
115
+ else if (input.changeFrom?.effectiveAt === "period_end") changeEffectiveAt = 2;
116
+ const message = {
117
+ payer: input.payer,
118
+ merchant: input.selected.payTo,
119
+ facilitator: extra.facilitator,
120
+ token: input.selected.asset,
121
+ amountPerPeriod: extra.amountPerPeriod,
122
+ periodSec: extra.periodSec,
123
+ maxPeriods: extra.maxPeriods,
124
+ startAt: input.startAt,
125
+ initialChargePeriods: extra.initialCharge?.periodCount ?? 0,
126
+ initialChargeAmount: extra.initialCharge?.totalAmount ?? "0",
127
+ termsDeadline: input.termsDeadline,
128
+ permitHash: input.permitHash,
129
+ salt: input.salt,
130
+ planTier: extra.plan.tier,
131
+ changeFromSubId: input.changeFrom?.fromSubId ?? ZERO_BYTES32,
132
+ changeEffectiveAt,
133
+ periodMode: extra.periodMode ?? 0
134
+ };
135
+ return {
136
+ domain,
137
+ types: SUBSCRIPTION_TERMS_TYPES,
138
+ primaryType: "SubscriptionTerms",
139
+ message
140
+ };
141
+ }
142
+ function buildCancelAuthTypedData(input) {
143
+ return {
144
+ domain: input.domain,
145
+ types: CANCEL_AUTH_TYPES,
146
+ primaryType: "CancelAuth",
147
+ message: {
148
+ action: 0,
149
+ subId: input.subId,
150
+ initiator: CANCEL_INITIATOR_TO_ENUM[input.initiator],
151
+ nonce: input.nonce,
152
+ deadline: input.deadline
153
+ }
154
+ };
155
+ }
156
+ function buildPendingChangeCancelAuthTypedData(input) {
157
+ return {
158
+ domain: input.domain,
159
+ types: PENDING_CHANGE_CANCEL_AUTH_TYPES,
160
+ primaryType: "PendingChangeCancelAuth",
161
+ message: {
162
+ subId: input.subId,
163
+ newSubId: input.newSubId,
164
+ nonce: input.nonce,
165
+ deadline: input.deadline
166
+ }
167
+ };
168
+ }
169
+ function parseChainIdFromNetwork(network) {
170
+ const parts = network.split(":");
171
+ if (parts.length !== 2 || parts[0] !== "eip155") {
172
+ throw new Error(`parseChainIdFromNetwork: expected "eip155:<chainId>", got "${network}"`);
173
+ }
174
+ const id = Number(parts[1]);
175
+ if (!Number.isInteger(id) || id <= 0) {
176
+ throw new Error(`parseChainIdFromNetwork: invalid chainId "${parts[1]}"`);
177
+ }
178
+ return id;
179
+ }
180
+ var import_viem, ZERO_BYTES32, PERMIT2_TYPES, SUBSCRIPTION_TERMS_TYPES, CANCEL_AUTH_TYPES, PENDING_CHANGE_CANCEL_AUTH_TYPES, CANCEL_INITIATOR_TO_ENUM;
181
+ var init_typed_data = __esm({
182
+ "src/subscription/codec/typed-data.ts"() {
183
+ "use strict";
184
+ import_viem = require("viem");
185
+ ZERO_BYTES32 = `0x${"0".repeat(64)}`;
186
+ PERMIT2_TYPES = {
187
+ PermitSingle: [
188
+ { name: "details", type: "PermitDetails" },
189
+ { name: "spender", type: "address" },
190
+ { name: "sigDeadline", type: "uint256" }
191
+ ],
192
+ PermitDetails: [
193
+ { name: "token", type: "address" },
194
+ { name: "amount", type: "uint160" },
195
+ { name: "expiration", type: "uint48" },
196
+ { name: "nonce", type: "uint48" }
197
+ ]
198
+ };
199
+ SUBSCRIPTION_TERMS_TYPES = {
200
+ SubscriptionTerms: [
201
+ { name: "payer", type: "address" },
202
+ { name: "merchant", type: "address" },
203
+ { name: "facilitator", type: "address" },
204
+ { name: "token", type: "address" },
205
+ { name: "amountPerPeriod", type: "uint160" },
206
+ { name: "periodSec", type: "uint64" },
207
+ { name: "maxPeriods", type: "uint32" },
208
+ { name: "startAt", type: "uint64" },
209
+ { name: "initialChargePeriods", type: "uint32" },
210
+ { name: "initialChargeAmount", type: "uint160" },
211
+ { name: "termsDeadline", type: "uint64" },
212
+ { name: "permitHash", type: "bytes32" },
213
+ { name: "salt", type: "bytes32" },
214
+ { name: "planTier", type: "uint8" },
215
+ { name: "changeFromSubId", type: "bytes32" },
216
+ { name: "changeEffectiveAt", type: "uint8" },
217
+ { name: "periodMode", type: "uint8" }
218
+ ]
219
+ };
220
+ CANCEL_AUTH_TYPES = {
221
+ CancelAuth: [
222
+ { name: "action", type: "uint8" },
223
+ { name: "subId", type: "bytes32" },
224
+ { name: "initiator", type: "uint8" },
225
+ { name: "nonce", type: "bytes32" },
226
+ { name: "deadline", type: "uint64" }
227
+ ]
228
+ };
229
+ PENDING_CHANGE_CANCEL_AUTH_TYPES = {
230
+ PendingChangeCancelAuth: [
231
+ { name: "subId", type: "bytes32" },
232
+ { name: "newSubId", type: "bytes32" },
233
+ { name: "nonce", type: "bytes32" },
234
+ { name: "deadline", type: "uint64" }
235
+ ]
236
+ };
237
+ CANCEL_INITIATOR_TO_ENUM = {
238
+ payer: 0,
239
+ merchant: 1
240
+ };
241
+ }
242
+ });
243
+
244
+ // src/subscription/codec/base64.ts
245
+ function base64EncodeUtf8(value) {
246
+ if (hasBuffer) return Buffer.from(value, "utf8").toString("base64");
247
+ const binary = unescape(encodeURIComponent(value));
248
+ return globalThis.btoa(binary);
249
+ }
250
+ function base64DecodeUtf8(value) {
251
+ if (hasBuffer) return Buffer.from(value, "base64").toString("utf8");
252
+ const binary = globalThis.atob(value);
253
+ return decodeURIComponent(escape(binary));
254
+ }
255
+ var hasBuffer;
256
+ var init_base64 = __esm({
257
+ "src/subscription/codec/base64.ts"() {
258
+ "use strict";
259
+ hasBuffer = typeof Buffer !== "undefined";
260
+ }
261
+ });
262
+
263
+ // src/subscription/codec/payload.ts
264
+ function parsePaymentRequired(headerValue) {
265
+ const json = base64DecodeUtf8(headerValue);
266
+ const parsed = JSON.parse(json);
267
+ if (!parsed || !Array.isArray(parsed.accepts)) {
268
+ throw new Error("parsePaymentRequired: missing or invalid `accepts` array");
269
+ }
270
+ return parsed.accepts;
271
+ }
272
+ function encodePaymentPayload(input) {
273
+ const payload = {
274
+ x402Version: 2,
275
+ accepted: input.selected,
276
+ payload: {
277
+ permitSingle: input.permitSingle,
278
+ permitSingleSignature: input.permitSingleSignature,
279
+ terms: input.terms,
280
+ termsSignature: input.termsSignature
281
+ }
282
+ };
283
+ return base64EncodeUtf8(JSON.stringify(payload));
284
+ }
285
+ function decodePaymentPayload(headerValue) {
286
+ const json = base64DecodeUtf8(headerValue);
287
+ return JSON.parse(json);
288
+ }
289
+ function asSubscriptionPaymentInner(payload) {
290
+ const inner = payload.payload;
291
+ if (!inner || !inner.permitSingle || !inner.terms || !inner.permitSingleSignature || !inner.termsSignature) {
292
+ throw new Error(
293
+ "asSubscriptionPaymentInner: payload.payload is missing required permitSingle/terms fields"
294
+ );
295
+ }
296
+ return inner;
297
+ }
298
+ var init_payload = __esm({
299
+ "src/subscription/codec/payload.ts"() {
300
+ "use strict";
301
+ init_base64();
302
+ }
303
+ });
304
+
305
+ // src/subscription/types.ts
306
+ function hasSubscriptionCapability(scheme) {
307
+ return typeof scheme === "object" && scheme !== null && "verifyAccess" in scheme && "settlementMode" in scheme && scheme.settlementMode === "pre";
308
+ }
309
+ var init_types = __esm({
310
+ "src/subscription/types.ts"() {
311
+ "use strict";
312
+ }
313
+ });
314
+
315
+ // src/subscription/errors.ts
316
+ var ErrorCode, ChargeErrorCode, ChargeError;
317
+ var init_errors = __esm({
318
+ "src/subscription/errors.ts"() {
319
+ "use strict";
320
+ ErrorCode = {
321
+ // subscribe / change
322
+ TermsBindingInvalid: "terms_binding_invalid",
323
+ AllowanceInsufficient: "allowance_insufficient",
324
+ AllowanceExpired: "allowance_expired",
325
+ // charge
326
+ PeriodNotDue: "period_not_due",
327
+ InsufficientBalance: "insufficient_balance",
328
+ // charge / cancel / access
329
+ SubscriptionNotActive: "subscription_not_active",
330
+ /**
331
+ * SDK-local code. Surfaced by `verifyAccess` when local period math
332
+ * yields `currentCalculatePeriod === 0` — subscription exists but
333
+ * `nowSec < startAt`, i.e. has not yet entered its first chargeable
334
+ * period.
335
+ */
336
+ SubscriptionNotYetActive: "subscription_not_yet_active",
337
+ UnauthorizedCaller: "unauthorized_caller",
338
+ // cancel
339
+ CancelSignatureInvalid: "cancel_signature_invalid",
340
+ CancelNonceUsed: "cancel_nonce_used",
341
+ // change
342
+ TierSame: "tier_same",
343
+ ChangeEffectiveAtMismatch: "change_effective_at_mismatch",
344
+ MerchantMismatch: "merchant_mismatch",
345
+ PayerMismatch: "payer_mismatch",
346
+ PendingChangeExists: "pending_change_exists",
347
+ SubNotActiveForChange: "sub_not_active_for_change",
348
+ // cancel-pending-change
349
+ NoPendingChange: "no_pending_change",
350
+ // all writes
351
+ ConfirmationTimeout: "confirmation_timeout"
352
+ };
353
+ ChargeErrorCode = {
354
+ PeriodNotDue: ErrorCode.PeriodNotDue,
355
+ SubscriptionNotActive: ErrorCode.SubscriptionNotActive,
356
+ InsufficientBalance: ErrorCode.InsufficientBalance,
357
+ AllowanceExpired: ErrorCode.AllowanceExpired,
358
+ UnauthorizedCaller: ErrorCode.UnauthorizedCaller,
359
+ ConfirmationTimeout: ErrorCode.ConfirmationTimeout
360
+ };
361
+ ChargeError = class extends Error {
362
+ constructor(code, subId, txHash) {
363
+ super(`charge failed: ${code} (sub=${subId})`);
364
+ this.name = "ChargeError";
365
+ this.code = code;
366
+ this.subId = subId;
367
+ this.txHash = txHash;
368
+ }
369
+ };
370
+ }
371
+ });
372
+
373
+ // src/subscription/store.ts
374
+ var InMemoryStore;
375
+ var init_store = __esm({
376
+ "src/subscription/store.ts"() {
377
+ "use strict";
378
+ InMemoryStore = class {
379
+ constructor() {
380
+ this.data = /* @__PURE__ */ new Map();
381
+ }
382
+ async get(subId) {
383
+ const sub = this.data.get(subId);
384
+ return sub ? { ...sub } : null;
385
+ }
386
+ async put(sub) {
387
+ this.data.set(sub.subId, { ...sub });
388
+ }
389
+ async delete(subId) {
390
+ this.data.delete(subId);
391
+ }
392
+ /**
393
+ * Return all subscriptions, ordered by `startAt` ascending. Not part of
394
+ * the SubscriptionStore interface — admin/debug helper, not used by the
395
+ * scheme. Production backends should expose paginated equivalents.
396
+ */
397
+ async list() {
398
+ return Array.from(this.data.values()).map((s) => ({ ...s })).sort((a, b) => a.startAt - b.startAt);
399
+ }
400
+ };
401
+ }
402
+ });
403
+
404
+ // src/subscription/client.ts
405
+ var SubscriptionClient;
406
+ var init_client = __esm({
407
+ "src/subscription/client.ts"() {
408
+ "use strict";
409
+ SubscriptionClient = class {
410
+ constructor(config) {
411
+ this.scheme = config.scheme;
412
+ this.store = config.store;
413
+ }
414
+ /**
415
+ * Run one charge period for a subscription. Throws `ChargeError` (one of 6
416
+ * codes) on facilitator-side failure. Internally `scheme.charge` already
417
+ * updates the store on success (and on `planChangeTriggered`); the client is
418
+ * a pass-through.
419
+ */
420
+ async charge(subId) {
421
+ return this.scheme.charge(subId);
422
+ }
423
+ /**
424
+ * Seller-initiated cancel (e.g. ToS violation, fraud, business reason).
425
+ *
426
+ * The SDK does NOT hold the Seller's merchant private key; the Seller must
427
+ * construct + sign a `CancelAuth` with `by=1 (MERCHANT)` outside and pass
428
+ * it in. SDK runs verifyCancel (sanity check on the auth) then settleCancel
429
+ * (facilitator + store mark canceled).
430
+ *
431
+ * Throws on either verify or settle failure.
432
+ */
433
+ async cancelBySeller(subId, auth, _reason) {
434
+ const v = await this.scheme.verifyCancel(auth, subId);
435
+ if (!v.ok) {
436
+ throw new Error(`cancelBySeller.verify failed: ${v.error}`);
437
+ }
438
+ const r = await this.scheme.settleCancel(auth, subId);
439
+ if (!r.success) {
440
+ throw new Error(`cancelBySeller.settle failed: ${r.error}`);
441
+ }
442
+ }
443
+ /**
444
+ * Re-sync a subscription from chain and repair the store. Use when:
445
+ * - `charge` threw `SubscriptionNotActive` (buyer may have cancelled
446
+ * directly via the facilitator or contract)
447
+ * - `charge` threw `ConfirmationTimeout` (network-level failure; chain
448
+ * may or may not have written)
449
+ * - periodic reconciliation
450
+ *
451
+ * If the synced sub is in `"changed"` state, the downstream `changedToSubId`
452
+ * is also fetched and persisted, so the Seller's `dueIndex` can switch over
453
+ * to the new sub without manual intervention.
454
+ */
455
+ async syncFromChain(subId) {
456
+ const latest = await this.scheme.getSubscription(subId);
457
+ if (!latest) return null;
458
+ await this.store.put(latest);
459
+ if (latest.state === "changed" && latest.changedToSubId) {
460
+ const newSub = await this.scheme.getSubscription(latest.changedToSubId);
461
+ if (newSub) await this.store.put(newSub);
462
+ }
463
+ return latest;
464
+ }
465
+ /**
466
+ * Direct store read. Cheap; does NOT touch the chain. Use this for hot-path
467
+ * lookups (e.g. resolving subId to plan/tier for business logic). For chain
468
+ * state of record, use `syncFromChain`.
469
+ */
470
+ async getSubscription(subId) {
471
+ return this.store.get(subId);
472
+ }
473
+ };
474
+ }
475
+ });
476
+
477
+ // src/subscription/codec/verify-terms.ts
478
+ function addrEq(a, b) {
479
+ return a.toLowerCase() === b.toLowerCase();
480
+ }
481
+ function hexEq(a, b) {
482
+ return !!a && a.toLowerCase() === b.toLowerCase();
483
+ }
484
+ function verifyTermsBindRequirements(terms, requirements) {
485
+ const extra = requirements.extra ?? {};
486
+ if (!extra.plan || extra.amountPerPeriod === void 0 || extra.facilitator === void 0) {
487
+ return ErrorCode.TermsBindingInvalid;
488
+ }
489
+ if (!addrEq(terms.merchant, requirements.payTo)) return ErrorCode.MerchantMismatch;
490
+ if (!addrEq(terms.token, requirements.asset)) return ErrorCode.TermsBindingInvalid;
491
+ if (!addrEq(terms.facilitator, extra.facilitator)) return ErrorCode.TermsBindingInvalid;
492
+ if (terms.amountPerPeriod !== extra.amountPerPeriod) return ErrorCode.TermsBindingInvalid;
493
+ if (terms.periodSec !== extra.periodSec) return ErrorCode.TermsBindingInvalid;
494
+ if (terms.maxPeriods !== extra.maxPeriods) return ErrorCode.TermsBindingInvalid;
495
+ if (terms.periodMode !== (extra.periodMode ?? 0)) return ErrorCode.TermsBindingInvalid;
496
+ if (terms.planTier !== extra.plan.tier) return ErrorCode.TermsBindingInvalid;
497
+ if (extra.startAt !== void 0 && terms.startAt !== extra.startAt) {
498
+ return ErrorCode.TermsBindingInvalid;
499
+ }
500
+ const expectedInitPeriods = extra.initialCharge?.periodCount ?? 0;
501
+ const expectedInitAmount = extra.initialCharge?.totalAmount ?? "0";
502
+ if (terms.initialChargePeriods !== expectedInitPeriods) return ErrorCode.TermsBindingInvalid;
503
+ if (terms.initialChargeAmount !== expectedInitAmount) return ErrorCode.TermsBindingInvalid;
504
+ if (extra.changeFrom) {
505
+ if (!hexEq(terms.changeFromSubId, extra.changeFrom.fromSubId)) {
506
+ return ErrorCode.TermsBindingInvalid;
507
+ }
508
+ const expectedEff = extra.changeFrom.effectiveAt === "immediate" ? 1 : extra.changeFrom.effectiveAt === "period_end" ? 2 : 0;
509
+ if (terms.changeEffectiveAt !== expectedEff) return ErrorCode.TermsBindingInvalid;
510
+ } else {
511
+ if (terms.changeFromSubId !== ZERO_BYTES32) return ErrorCode.TermsBindingInvalid;
512
+ if (terms.changeEffectiveAt !== 0) return ErrorCode.TermsBindingInvalid;
513
+ }
514
+ return null;
515
+ }
516
+ var init_verify_terms = __esm({
517
+ "src/subscription/codec/verify-terms.ts"() {
518
+ "use strict";
519
+ init_errors();
520
+ init_typed_data();
521
+ }
522
+ });
523
+
524
+ // src/subscription/codec/period-math.ts
525
+ function computeElapsedPeriods(periodMode, startAt, billingAnchorAt, periodSec, nowSec) {
526
+ if (nowSec < startAt) return 0;
527
+ if (periodMode === PERIOD_MODE_CALENDAR_MONTH) {
528
+ const anchor = billingAnchorAt > 0 ? billingAnchorAt : startAt;
529
+ const startOffset = elapsedCalendarMonths(anchor, startAt);
530
+ return elapsedCalendarMonths(anchor, nowSec) - startOffset + 1;
531
+ }
532
+ if (periodSec <= 0) return 0;
533
+ return Math.floor((nowSec - startAt) / periodSec) + 1;
534
+ }
535
+ function elapsedCalendarMonths(anchorSec, tsSec) {
536
+ if (tsSec <= anchorSec) return 0;
537
+ const anchor = new Date(anchorSec * 1e3);
538
+ const ts = new Date(tsSec * 1e3);
539
+ let diff = (ts.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (ts.getUTCMonth() - anchor.getUTCMonth());
540
+ if (diff < 0) return 0;
541
+ if (addCalendarMonths(anchorSec, diff) > tsSec) diff--;
542
+ return Math.max(diff, 0);
543
+ }
544
+ function addCalendarMonths(anchorSec, n) {
545
+ const anchor = new Date(anchorSec * 1e3);
546
+ const targetYear = anchor.getUTCFullYear() + Math.floor((anchor.getUTCMonth() + n) / 12);
547
+ const targetMonth = ((anchor.getUTCMonth() + n) % 12 + 12) % 12;
548
+ const daysInTargetMonth = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate();
549
+ const day = Math.min(anchor.getUTCDate(), daysInTargetMonth);
550
+ const ts = Date.UTC(
551
+ targetYear,
552
+ targetMonth,
553
+ day,
554
+ anchor.getUTCHours(),
555
+ anchor.getUTCMinutes(),
556
+ anchor.getUTCSeconds(),
557
+ anchor.getUTCMilliseconds()
558
+ );
559
+ return Math.floor(ts / 1e3);
560
+ }
561
+ var PERIOD_MODE_CALENDAR_MONTH;
562
+ var init_period_math = __esm({
563
+ "src/subscription/codec/period-math.ts"() {
564
+ "use strict";
565
+ PERIOD_MODE_CALENDAR_MONTH = 1;
566
+ }
567
+ });
568
+
569
+ // src/subscription/codec/access-proof.ts
570
+ function buildAccessProofMessage(input) {
571
+ return (0, import_viem2.keccak256)(
572
+ (0, import_viem2.encodePacked)(
573
+ ["bytes32", "address", "uint256"],
574
+ [input.subId, input.payer, BigInt(input.timestamp)]
575
+ )
576
+ );
577
+ }
578
+ function encodeAccessProof(proof) {
579
+ return base64EncodeUtf8(JSON.stringify(proof));
580
+ }
581
+ function decodeAccessProof(headerValue) {
582
+ const json = base64DecodeUtf8(headerValue);
583
+ const parsed = JSON.parse(json);
584
+ if (!parsed || parsed.kind !== "subscription-id") {
585
+ throw new Error(`decodeAccessProof: expected kind="subscription-id", got "${parsed?.kind}"`);
586
+ }
587
+ return parsed;
588
+ }
589
+ var import_viem2;
590
+ var init_access_proof = __esm({
591
+ "src/subscription/codec/access-proof.ts"() {
592
+ "use strict";
593
+ import_viem2 = require("viem");
594
+ init_base64();
595
+ }
596
+ });
597
+
598
+ // src/subscription/codec/index.ts
599
+ var init_codec = __esm({
600
+ "src/subscription/codec/index.ts"() {
601
+ "use strict";
602
+ init_verify_terms();
603
+ init_period_math();
604
+ init_base64();
605
+ init_payload();
606
+ init_access_proof();
607
+ init_typed_data();
608
+ }
609
+ });
610
+
611
+ // src/subscription/facilitator-client.ts
612
+ function supportsSubscription(client) {
613
+ const c = client;
614
+ return typeof c.subscribe === "function" && typeof c.changeSubscription === "function" && typeof c.cancelSubscription === "function" && typeof c.cancelPendingChange === "function" && typeof c.chargeSubscription === "function" && typeof c.finalizeExpired === "function" && typeof c.getCharges === "function" && typeof c.getPendingChange === "function" && typeof c.getSubscription === "function";
615
+ }
616
+ var init_facilitator_client = __esm({
617
+ "src/subscription/facilitator-client.ts"() {
618
+ "use strict";
619
+ }
620
+ });
621
+
622
+ // src/subscription/index.ts
623
+ var subscription_exports = {};
624
+ __export(subscription_exports, {
625
+ CANCEL_AUTH_TYPES: () => CANCEL_AUTH_TYPES,
626
+ ChargeError: () => ChargeError,
627
+ ChargeErrorCode: () => ChargeErrorCode,
628
+ ErrorCode: () => ErrorCode,
629
+ InMemoryStore: () => InMemoryStore,
630
+ PENDING_CHANGE_CANCEL_AUTH_TYPES: () => PENDING_CHANGE_CANCEL_AUTH_TYPES,
631
+ PERMIT2_TYPES: () => PERMIT2_TYPES,
632
+ SUBSCRIPTION_TERMS_TYPES: () => SUBSCRIPTION_TERMS_TYPES,
633
+ SubscriptionClient: () => SubscriptionClient,
634
+ ZERO_BYTES32: () => ZERO_BYTES32,
635
+ addCalendarMonths: () => addCalendarMonths,
636
+ asSubscriptionPaymentInner: () => asSubscriptionPaymentInner,
637
+ base64DecodeUtf8: () => base64DecodeUtf8,
638
+ base64EncodeUtf8: () => base64EncodeUtf8,
639
+ buildAccessProofMessage: () => buildAccessProofMessage,
640
+ buildCancelAuthTypedData: () => buildCancelAuthTypedData,
641
+ buildPendingChangeCancelAuthTypedData: () => buildPendingChangeCancelAuthTypedData,
642
+ buildPermit2TypedData: () => buildPermit2TypedData,
643
+ buildSubscriptionTermsTypedData: () => buildSubscriptionTermsTypedData,
644
+ computeElapsedPeriods: () => computeElapsedPeriods,
645
+ computePermitSingleStructHash: () => computePermitSingleStructHash,
646
+ decodeAccessProof: () => decodeAccessProof,
647
+ decodePaymentPayload: () => decodePaymentPayload,
648
+ elapsedCalendarMonths: () => elapsedCalendarMonths,
649
+ encodeAccessProof: () => encodeAccessProof,
650
+ encodePaymentPayload: () => encodePaymentPayload,
651
+ hasSubscriptionCapability: () => hasSubscriptionCapability,
652
+ parseChainIdFromNetwork: () => parseChainIdFromNetwork,
653
+ parsePaymentRequired: () => parsePaymentRequired,
654
+ supportsSubscription: () => supportsSubscription,
655
+ verifyTermsBindRequirements: () => verifyTermsBindRequirements
656
+ });
657
+ var init_subscription = __esm({
658
+ "src/subscription/index.ts"() {
659
+ "use strict";
660
+ init_types();
661
+ init_errors();
662
+ init_store();
663
+ init_client();
664
+ init_codec();
665
+ init_facilitator_client();
666
+ }
667
+ });
668
+
30
669
  // src/server/index.ts
31
670
  var server_exports = {};
32
671
  __export(server_exports, {
@@ -167,6 +806,10 @@ function deepEqual(obj1, obj2) {
167
806
  }
168
807
  }
169
808
 
809
+ // src/http/httpFacilitatorClient.ts
810
+ init_typed_data();
811
+ init_payload();
812
+
170
813
  // src/schemas/index.ts
171
814
  var import_zod = require("zod");
172
815
  var import_zod2 = require("zod");
@@ -278,6 +921,8 @@ var HTTPFacilitatorClient = class {
278
921
  constructor(config) {
279
922
  this.url = config?.url || DEFAULT_FACILITATOR_URL;
280
923
  this._createAuthHeaders = config?.createAuthHeaders;
924
+ this._createSubscriptionAuthHeaders = config?.createSubscriptionAuthHeaders;
925
+ this._fetchFn = config?.fetchFn ?? fetch;
281
926
  }
282
927
  /**
283
928
  * Verify a payment with the facilitator
@@ -294,7 +939,7 @@ var HTTPFacilitatorClient = class {
294
939
  const authHeaders = await this.createAuthHeaders("verify");
295
940
  headers = { ...headers, ...authHeaders.headers };
296
941
  }
297
- const response = await fetch(`${this.url}/verify`, {
942
+ const response = await this._fetchFn(`${this.url}/verify`, {
298
943
  method: "POST",
299
944
  headers,
300
945
  body: JSON.stringify({
@@ -335,7 +980,7 @@ var HTTPFacilitatorClient = class {
335
980
  const authHeaders = await this.createAuthHeaders("settle");
336
981
  headers = { ...headers, ...authHeaders.headers };
337
982
  }
338
- const response = await fetch(`${this.url}/settle`, {
983
+ const response = await this._fetchFn(`${this.url}/settle`, {
339
984
  method: "POST",
340
985
  headers,
341
986
  body: JSON.stringify({
@@ -377,7 +1022,7 @@ var HTTPFacilitatorClient = class {
377
1022
  }
378
1023
  let lastError = null;
379
1024
  for (let attempt = 0; attempt < GET_SUPPORTED_RETRIES; attempt++) {
380
- const response = await fetch(`${this.url}/supported`, {
1025
+ const response = await this._fetchFn(`${this.url}/supported`, {
381
1026
  method: "GET",
382
1027
  headers
383
1028
  });
@@ -411,10 +1056,13 @@ var HTTPFacilitatorClient = class {
411
1056
  const authHeaders = await this.createAuthHeaders("settle/status");
412
1057
  headers = { ...headers, ...authHeaders.headers };
413
1058
  }
414
- const response = await fetch(`${this.url}/settle/status?txHash=${encodeURIComponent(txHash)}`, {
415
- method: "GET",
416
- headers
417
- });
1059
+ const response = await this._fetchFn(
1060
+ `${this.url}/settle/status?txHash=${encodeURIComponent(txHash)}`,
1061
+ {
1062
+ method: "GET",
1063
+ headers
1064
+ }
1065
+ );
418
1066
  if (!response.ok) {
419
1067
  const text = await response.text().catch(() => response.statusText);
420
1068
  throw new Error(
@@ -453,10 +1101,129 @@ var HTTPFacilitatorClient = class {
453
1101
  JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() : value)
454
1102
  );
455
1103
  }
1104
+ // ── SubscriptionFacilitatorClient (period) ─────────────
1105
+ //
1106
+ // Generic JSON POST / GET helpers parameterized by `op` so the same code
1107
+ // path covers all five subscription endpoints. The standard OKX envelope
1108
+ // `{ code, msg?, data? }` is returned to the caller unparsed (the
1109
+ // subscription scheme reads `code === "0"` and `data` directly).
1110
+ async subscriptionAuthHeaders(op) {
1111
+ if (!this._createSubscriptionAuthHeaders) return {};
1112
+ return this._createSubscriptionAuthHeaders(op);
1113
+ }
1114
+ async subscriptionPost(op, path, body) {
1115
+ const headers = {
1116
+ "Content-Type": "application/json",
1117
+ ...await this.subscriptionAuthHeaders(op)
1118
+ };
1119
+ const resp = await this._fetchFn(`${this.url}${path}`, {
1120
+ method: "POST",
1121
+ headers,
1122
+ body: JSON.stringify(this.toJsonSafe(body))
1123
+ });
1124
+ if (!resp.ok) {
1125
+ throw new Error(`facilitator ${op} returned HTTP ${resp.status}: ${await resp.text()}`);
1126
+ }
1127
+ return await resp.json();
1128
+ }
1129
+ async subscriptionGet(op, path) {
1130
+ const headers = await this.subscriptionAuthHeaders(op);
1131
+ const resp = await this._fetchFn(`${this.url}${path}`, { method: "GET", headers });
1132
+ if (!resp.ok) {
1133
+ throw new Error(`facilitator ${op} returned HTTP ${resp.status}: ${await resp.text()}`);
1134
+ }
1135
+ return await resp.json();
1136
+ }
1137
+ /**
1138
+ * Build the {chainIndex, terms, permit, termsSig, permitSig, syncSettle}
1139
+ * request body shared by subscribe / change endpoints.
1140
+ */
1141
+ buildWriteBody(payload, requirements, syncSettle) {
1142
+ const inner = asSubscriptionPaymentInner(payload);
1143
+ return {
1144
+ chainIndex: parseChainIdFromNetwork(requirements.network),
1145
+ terms: inner.terms,
1146
+ permit: inner.permitSingle,
1147
+ termsSig: inner.termsSignature,
1148
+ permitSig: inner.permitSingleSignature,
1149
+ syncSettle: syncSettle ?? true
1150
+ };
1151
+ }
1152
+ async subscribe(paymentPayload, paymentRequirements, syncSettle) {
1153
+ return this.subscriptionPost(
1154
+ "subscribe",
1155
+ "/api/v6/pay/x402/subscriptions",
1156
+ this.buildWriteBody(paymentPayload, paymentRequirements, syncSettle)
1157
+ );
1158
+ }
1159
+ async changeSubscription(paymentPayload, paymentRequirements, oldSubId, syncSettle) {
1160
+ return this.subscriptionPost(
1161
+ "change",
1162
+ "/api/v6/pay/x402/subscriptions/change",
1163
+ {
1164
+ ...this.buildWriteBody(paymentPayload, paymentRequirements, syncSettle),
1165
+ // `oldSubId` is informational — server reads
1166
+ // newTerms.changeFromSubId for the authoritative value.
1167
+ oldSubId,
1168
+ // change body uses `newTerms` not `terms`.
1169
+ newTerms: asSubscriptionPaymentInner(paymentPayload).terms,
1170
+ terms: void 0
1171
+ }
1172
+ );
1173
+ }
1174
+ async cancelSubscription(subId, cancelAuth, syncSettle) {
1175
+ return this.subscriptionPost(
1176
+ "cancel",
1177
+ "/api/v6/pay/x402/subscriptions/cancel",
1178
+ { subId, cancelAuth, syncSettle: syncSettle ?? true }
1179
+ );
1180
+ }
1181
+ async cancelPendingChange(subId, cancelAuth, syncSettle) {
1182
+ return this.subscriptionPost(
1183
+ "cancel-pending-change",
1184
+ "/api/v6/pay/x402/subscriptions/cancel-pending-change",
1185
+ { subId, cancelAuth, syncSettle: syncSettle ?? true }
1186
+ );
1187
+ }
1188
+ async chargeSubscription(subId, syncSettle) {
1189
+ return this.subscriptionPost(
1190
+ "charge",
1191
+ "/api/v6/pay/x402/subscriptions/charge",
1192
+ { subId, syncSettle: syncSettle ?? true }
1193
+ );
1194
+ }
1195
+ async finalizeExpired(subId, syncSettle) {
1196
+ return this.subscriptionPost(
1197
+ "finalize-expired",
1198
+ "/api/v6/pay/x402/subscriptions/finalize-expired",
1199
+ { subId, syncSettle: syncSettle ?? true }
1200
+ );
1201
+ }
1202
+ async getCharges(subId, limit = 50, offset = 0) {
1203
+ const q = new URLSearchParams({ subId, limit: String(limit), offset: String(offset) });
1204
+ return this.subscriptionGet(
1205
+ "getCharges",
1206
+ `/api/v6/pay/x402/subscriptions/charges?${q.toString()}`
1207
+ );
1208
+ }
1209
+ async getPendingChange(subId) {
1210
+ return this.subscriptionGet(
1211
+ "getPendingChange",
1212
+ `/api/v6/pay/x402/subscriptions/pending?subId=${encodeURIComponent(subId)}`
1213
+ );
1214
+ }
1215
+ async getSubscription(subId) {
1216
+ return this.subscriptionGet(
1217
+ "getSubscription",
1218
+ `/api/v6/pay/x402/subscriptions/detail?subId=${encodeURIComponent(subId)}`
1219
+ );
1220
+ }
456
1221
  };
457
1222
 
458
1223
  // src/facilitator/OKXFacilitatorClient.ts
459
1224
  var import_node_crypto = __toESM(require("crypto"));
1225
+ init_typed_data();
1226
+ init_payload();
460
1227
 
461
1228
  // src/index.ts
462
1229
  var x402Version = 2;
@@ -532,6 +1299,21 @@ var x402ResourceServer = class {
532
1299
  hasRegisteredScheme(network, scheme) {
533
1300
  return !!findByNetworkAndScheme(this.registeredServerSchemes, scheme, network);
534
1301
  }
1302
+ /**
1303
+ * Look up the registered SchemeNetworkServer for a given network + scheme.
1304
+ * Exposed so the HTTP dispatch layer can perform capability detection
1305
+ * (e.g. `hasSubscriptionCapability(scheme)`) on the actual instance.
1306
+ *
1307
+ * Pattern matching follows the same CAIP-style rules as `verifyPayment`:
1308
+ * registered keys may use wildcards like `eip155:*`.
1309
+ *
1310
+ * @param network - The network identifier
1311
+ * @param scheme - The payment scheme name
1312
+ * @returns The registered scheme server, or undefined if none matches.
1313
+ */
1314
+ findScheme(network, scheme) {
1315
+ return findByNetworkAndScheme(this.registeredServerSchemes, scheme, network);
1316
+ }
535
1317
  /**
536
1318
  * Registers a resource service extension that can enrich extension declarations.
537
1319
  *
@@ -1224,6 +2006,7 @@ var x402HTTPResourceServer = class {
1224
2006
  constructor(ResourceServer, routes) {
1225
2007
  this.compiledRoutes = [];
1226
2008
  this.protectedRequestHooks = [];
2009
+ this.beforeAccessHooks = [];
1227
2010
  this.pollDeadlineMs = DEFAULT_POLL_DEADLINE_MS;
1228
2011
  this.ResourceServer = ResourceServer;
1229
2012
  this.routesConfig = routes;
@@ -1298,6 +2081,22 @@ var x402HTTPResourceServer = class {
1298
2081
  this.protectedRequestHooks.push(hook);
1299
2082
  return this;
1300
2083
  }
2084
+ /**
2085
+ * Register a seller-global `onBeforeAccess` hook fired on every access-
2086
+ * verified subscription request, AFTER `verifyAccess` (signature + payer
2087
+ * + plan allowlist + period math) but BEFORE the handler runs. Seller
2088
+ * uses it for cross-cutting access policy (quota / ban list / feature
2089
+ * gating). Hooks are executed in order of registration; the first one
2090
+ * to return `{ ok: false }` denies (→ 402). Route-level
2091
+ * `RouteConfig.onBeforeAccess` runs AFTER all global hooks.
2092
+ *
2093
+ * @param hook - The hook function
2094
+ * @returns The x402HTTPResourceServer instance for chaining
2095
+ */
2096
+ onBeforeAccess(hook) {
2097
+ this.beforeAccessHooks.push(hook);
2098
+ return this;
2099
+ }
1301
2100
  /**
1302
2101
  * Register a hook to call when the facilitator returns status="timeout".
1303
2102
  * The hook should verify the tx on-chain and return { confirmed: boolean }.
@@ -1355,6 +2154,14 @@ var x402HTTPResourceServer = class {
1355
2154
  }
1356
2155
  const paymentOptions = this.normalizePaymentOptions(routeConfig);
1357
2156
  const paymentPayload = this.extractPayment(adapter);
2157
+ if (routeConfig.operation === "cancel") {
2158
+ const cancelResult = await this.tryDispatchCancelFlow(adapter, routeConfig, paymentOptions);
2159
+ if (cancelResult) return cancelResult;
2160
+ }
2161
+ if (routeConfig.operation === "cancel-pending-change") {
2162
+ const r = await this.tryDispatchCancelPendingChangeFlow(adapter, routeConfig, paymentOptions);
2163
+ if (r) return r;
2164
+ }
1358
2165
  const resourceInfo = {
1359
2166
  url: routeConfig.resource || enrichedContext.adapter.getUrl(),
1360
2167
  description: routeConfig.description || "",
@@ -1364,6 +2171,89 @@ var x402HTTPResourceServer = class {
1364
2171
  paymentOptions,
1365
2172
  enrichedContext
1366
2173
  );
2174
+ if (routeConfig.operation === "change") {
2175
+ let scheme = null;
2176
+ for (const opt of paymentOptions) {
2177
+ if (!opt.network || !opt.scheme) continue;
2178
+ scheme = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
2179
+ if (scheme) break;
2180
+ }
2181
+ if (!scheme) {
2182
+ return {
2183
+ type: "payment-error",
2184
+ response: {
2185
+ status: 500,
2186
+ headers: { "Content-Type": "application/json" },
2187
+ body: { error: "change route: no subscription scheme registered" }
2188
+ }
2189
+ };
2190
+ }
2191
+ let currentSubId;
2192
+ if (paymentPayload) {
2193
+ const innerTerms = paymentPayload.payload?.terms;
2194
+ currentSubId = innerTerms?.changeFromSubId;
2195
+ } else {
2196
+ const accessHeader = this.extractAccessProofHeader(enrichedContext.adapter);
2197
+ if (!accessHeader) {
2198
+ return {
2199
+ type: "payment-error",
2200
+ response: {
2201
+ status: 401,
2202
+ headers: { "Content-Type": "application/json" },
2203
+ body: { error: "change route: missing APP-Access header" }
2204
+ }
2205
+ };
2206
+ }
2207
+ const { decodeAccessProof: decodeAccessProof2 } = await this.loadSubscriptionModule();
2208
+ let proof;
2209
+ try {
2210
+ proof = decodeAccessProof2(accessHeader);
2211
+ } catch {
2212
+ return {
2213
+ type: "payment-error",
2214
+ response: {
2215
+ status: 400,
2216
+ headers: { "Content-Type": "application/json" },
2217
+ body: { error: "change route: invalid APP-Access header" }
2218
+ }
2219
+ };
2220
+ }
2221
+ const verify = await scheme.verifyOwnership(proof);
2222
+ if (!verify.ok) {
2223
+ return {
2224
+ type: "payment-error",
2225
+ response: {
2226
+ status: 401,
2227
+ headers: { "Content-Type": "application/json" },
2228
+ body: { error: verify.error }
2229
+ }
2230
+ };
2231
+ }
2232
+ currentSubId = verify.subId;
2233
+ }
2234
+ if (!currentSubId) {
2235
+ return {
2236
+ type: "payment-error",
2237
+ response: {
2238
+ status: 400,
2239
+ headers: { "Content-Type": "application/json" },
2240
+ body: { error: "change route: cannot resolve currentSubId" }
2241
+ }
2242
+ };
2243
+ }
2244
+ const enriched = await scheme.enrichAcceptsForChange(requirements, currentSubId);
2245
+ if (enriched === null) {
2246
+ return {
2247
+ type: "payment-error",
2248
+ response: {
2249
+ status: 404,
2250
+ headers: { "Content-Type": "application/json" },
2251
+ body: { error: "sub_not_active_for_change" }
2252
+ }
2253
+ };
2254
+ }
2255
+ requirements = enriched;
2256
+ }
1367
2257
  let extensions = routeConfig.extensions;
1368
2258
  if (extensions) {
1369
2259
  extensions = this.ResourceServer.enrichExtensions(extensions, enrichedContext);
@@ -1376,6 +2266,23 @@ var x402HTTPResourceServer = class {
1376
2266
  extensions,
1377
2267
  transportContext
1378
2268
  );
2269
+ if (routeConfig.operation !== "change") {
2270
+ const accessResult = await this.tryDispatchAccessFlow(
2271
+ adapter,
2272
+ routeConfig,
2273
+ paymentOptions,
2274
+ paymentRequired
2275
+ );
2276
+ if (accessResult) return accessResult;
2277
+ }
2278
+ if (paymentPayload) {
2279
+ const subResult = await this.tryDispatchSubscriptionPresettle(
2280
+ paymentPayload,
2281
+ paymentRequired.accepts,
2282
+ routeConfig.operation === "change" ? "change" : "subscribe"
2283
+ );
2284
+ if (subResult) return subResult;
2285
+ }
1379
2286
  if (!paymentPayload) {
1380
2287
  const unpaidBody = routeConfig.unpaidResponseBody ? await routeConfig.unpaidResponseBody(enrichedContext) : void 0;
1381
2288
  return {
@@ -1597,6 +2504,322 @@ var x402HTTPResourceServer = class {
1597
2504
  requiresPayment(context) {
1598
2505
  return this.getRouteConfig(context.path, context.method) !== void 0;
1599
2506
  }
2507
+ /**
2508
+ * Lazy loader for the subscription submodule. The `import()` cache makes
2509
+ * this effectively free after the first hit; isolating it in one place
2510
+ * keeps dispatch helpers free of dynamic-import boilerplate and lets
2511
+ * bundlers tree-shake the entire subscription path when no caller touches
2512
+ * it.
2513
+ */
2514
+ loadSubscriptionModule() {
2515
+ return Promise.resolve().then(() => (init_subscription(), subscription_exports));
2516
+ }
2517
+ /**
2518
+ * Single chokepoint for "is this (network, scheme) backed by a
2519
+ * SubscriptionCapability-implementing scheme?". Returns the narrowed
2520
+ * capability (so callers get full typing on `verifyAccess` / `verifySubscribe`
2521
+ * / etc.) or null if not registered or not a subscription scheme.
2522
+ */
2523
+ async resolveSubscriptionScheme(network, schemeName) {
2524
+ const registered = this.ResourceServer.findScheme(network, schemeName);
2525
+ if (!registered) return null;
2526
+ const { hasSubscriptionCapability: hasSubscriptionCapability2 } = await this.loadSubscriptionModule();
2527
+ return hasSubscriptionCapability2(registered) ? registered : null;
2528
+ }
2529
+ /**
2530
+ * period dispatch helper — Access flow.
2531
+ *
2532
+ * Returns an `access-verified` (or `payment-error`) HTTPProcessResult when
2533
+ * the request carries `APP-Access` AND a subscription-capable scheme is
2534
+ * registered for one of the route's accepted (scheme, network) pairs.
2535
+ * Returns `null` to indicate the dispatcher should fall through to classic
2536
+ * pay-per-request handling.
2537
+ */
2538
+ async tryDispatchAccessFlow(adapter, routeConfig, paymentOptions, paymentRequired) {
2539
+ const headerB64 = this.extractAccessProofHeader(adapter);
2540
+ if (!headerB64) return null;
2541
+ const { decodeAccessProof: decodeAccessProof2 } = await this.loadSubscriptionModule();
2542
+ let proof;
2543
+ try {
2544
+ proof = decodeAccessProof2(headerB64);
2545
+ } catch (err) {
2546
+ return {
2547
+ type: "payment-error",
2548
+ response: {
2549
+ status: 401,
2550
+ headers: { "Content-Type": "application/json" },
2551
+ body: { error: `invalid APP-Access: ${err.message}` }
2552
+ }
2553
+ };
2554
+ }
2555
+ const acceptedPlanIds = collectAcceptedPlanIds(paymentOptions);
2556
+ for (const opt of paymentOptions) {
2557
+ if (!opt.network || !opt.scheme) continue;
2558
+ const scheme = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
2559
+ if (!scheme) continue;
2560
+ const result = await scheme.verifyAccess(proof, { acceptedPlanIds });
2561
+ if (!result.ok) {
2562
+ return {
2563
+ type: "payment-error",
2564
+ response: {
2565
+ status: 402,
2566
+ headers: {
2567
+ "Content-Type": "application/json",
2568
+ "PAYMENT-REQUIRED": encodePaymentRequiredHeader(paymentRequired)
2569
+ },
2570
+ body: { error: result.error }
2571
+ }
2572
+ };
2573
+ }
2574
+ const hooks = [
2575
+ ...this.beforeAccessHooks,
2576
+ ...routeConfig.onBeforeAccess ? [routeConfig.onBeforeAccess] : []
2577
+ ];
2578
+ for (const hook of hooks) {
2579
+ const decision = await hook({
2580
+ subscription: result.subscription,
2581
+ request: {
2582
+ path: adapter.getPath(),
2583
+ method: adapter.getMethod(),
2584
+ headers: adapter.getHeaders?.() ?? {}
2585
+ },
2586
+ route: { acceptedPlanIds, accepts: paymentRequired.accepts }
2587
+ });
2588
+ if (!decision.ok) {
2589
+ return {
2590
+ type: "payment-error",
2591
+ response: {
2592
+ status: 402,
2593
+ headers: { "Content-Type": "application/json" },
2594
+ body: {
2595
+ error: decision.error ?? "access_denied",
2596
+ retryAfter: decision.retryAfter,
2597
+ upgradeOffers: decision.upgradeOffers
2598
+ }
2599
+ }
2600
+ };
2601
+ }
2602
+ }
2603
+ return {
2604
+ type: "access-verified",
2605
+ subscription: result.subscription,
2606
+ headers: {}
2607
+ };
2608
+ }
2609
+ return {
2610
+ type: "payment-error",
2611
+ response: {
2612
+ status: 401,
2613
+ headers: { "Content-Type": "application/json" },
2614
+ body: { error: "no subscription scheme registered for this route" }
2615
+ }
2616
+ };
2617
+ }
2618
+ /**
2619
+ * period dispatch helper — Subscribe presettle flow.
2620
+ *
2621
+ * When the buyer presents a PaymentPayload whose `accepted.scheme` is a
2622
+ * subscription scheme with `settlementMode === "pre"`, this runs verify +
2623
+ * (settle on demand) and returns `payment-presettle`. The middleware is
2624
+ * expected to call `result.settle()` AFTER decision-time but BEFORE
2625
+ * `next()` so handler only runs when the chain creation succeeded.
2626
+ *
2627
+ * Returns `null` to fall through to classic post-settle path-verified flow.
2628
+ */
2629
+ async tryDispatchSubscriptionPresettle(paymentPayload, serverAccepts, operation) {
2630
+ const { accepted } = paymentPayload;
2631
+ const scheme = await this.resolveSubscriptionScheme(accepted.network, accepted.scheme);
2632
+ if (!scheme) return null;
2633
+ const serverReq = this.ResourceServer.findMatchingRequirements(serverAccepts, paymentPayload);
2634
+ if (!serverReq) {
2635
+ return {
2636
+ type: "payment-error",
2637
+ response: {
2638
+ status: 402,
2639
+ headers: { "Content-Type": "application/json" },
2640
+ body: { error: "no_matching_requirements" }
2641
+ }
2642
+ };
2643
+ }
2644
+ if (operation === "change") {
2645
+ const verifyResult2 = await scheme.verifyChange(paymentPayload, serverReq);
2646
+ if (!verifyResult2.ok) {
2647
+ return {
2648
+ type: "payment-error",
2649
+ response: {
2650
+ status: 402,
2651
+ headers: { "Content-Type": "application/json" },
2652
+ body: { error: verifyResult2.error }
2653
+ }
2654
+ };
2655
+ }
2656
+ return {
2657
+ type: "payment-presettle",
2658
+ paymentPayload,
2659
+ paymentRequirements: serverReq,
2660
+ operation: "change",
2661
+ settle: async () => {
2662
+ const r = await scheme.settleChange(paymentPayload, serverReq);
2663
+ return r.success ? {
2664
+ success: true,
2665
+ headers: r.headers,
2666
+ data: {
2667
+ newSubId: r.newSubId,
2668
+ oldSubId: r.oldSubId,
2669
+ operationType: r.operationType,
2670
+ scheduledFromPeriod: r.scheduledFromPeriod
2671
+ }
2672
+ } : { success: false, error: r.error };
2673
+ }
2674
+ };
2675
+ }
2676
+ const verifyResult = await scheme.verifySubscribe(paymentPayload, serverReq);
2677
+ if (!verifyResult.ok) {
2678
+ return {
2679
+ type: "payment-error",
2680
+ response: {
2681
+ status: 402,
2682
+ headers: { "Content-Type": "application/json" },
2683
+ body: { error: verifyResult.error }
2684
+ }
2685
+ };
2686
+ }
2687
+ return {
2688
+ type: "payment-presettle",
2689
+ paymentPayload,
2690
+ paymentRequirements: serverReq,
2691
+ operation: "subscribe",
2692
+ settle: async () => {
2693
+ const r = await scheme.settleSubscribe(paymentPayload, serverReq);
2694
+ return r.success ? {
2695
+ success: true,
2696
+ headers: r.headers,
2697
+ data: { subId: r.subId, subscription: r.subscription }
2698
+ } : { success: false, error: r.error };
2699
+ }
2700
+ };
2701
+ }
2702
+ /**
2703
+ * period dispatch helper — Cancel flow.
2704
+ *
2705
+ * Reads JSON body { auth: CancelAuth, subId: string }, runs verifyCancel
2706
+ * then wraps settleCancel as a payment-presettle (settle-before-handler so
2707
+ * the cancelation is on-chain before the seller's response).
2708
+ */
2709
+ async tryDispatchCancelFlow(adapter, routeConfig, paymentOptions) {
2710
+ let scheme = null;
2711
+ for (const opt of paymentOptions) {
2712
+ if (!opt.network || !opt.scheme) continue;
2713
+ const resolved = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
2714
+ if (resolved) {
2715
+ scheme = resolved;
2716
+ break;
2717
+ }
2718
+ }
2719
+ if (!scheme) return null;
2720
+ const body = adapter.getBody?.() ?? {};
2721
+ if (!body.auth || !body.subId) {
2722
+ return {
2723
+ type: "payment-error",
2724
+ response: {
2725
+ status: 400,
2726
+ headers: { "Content-Type": "application/json" },
2727
+ body: { error: "cancel: body must include auth and subId" }
2728
+ }
2729
+ };
2730
+ }
2731
+ const verifyResult = await scheme.verifyCancel(body.auth, body.subId);
2732
+ if (!verifyResult.ok) {
2733
+ return {
2734
+ type: "payment-error",
2735
+ response: {
2736
+ status: 402,
2737
+ headers: { "Content-Type": "application/json" },
2738
+ body: { error: verifyResult.error }
2739
+ }
2740
+ };
2741
+ }
2742
+ void routeConfig;
2743
+ const settleScheme = scheme;
2744
+ const auth = body.auth;
2745
+ const subId = body.subId;
2746
+ return {
2747
+ type: "payment-presettle",
2748
+ paymentPayload: { x402Version: 2, accepted: null, payload: {} },
2749
+ paymentRequirements: null,
2750
+ operation: "cancel",
2751
+ settle: async () => {
2752
+ const r = await settleScheme.settleCancel(auth, subId);
2753
+ return r.success ? { success: true, headers: r.headers, data: { subId } } : { success: false, error: r.error };
2754
+ }
2755
+ };
2756
+ }
2757
+ /**
2758
+ * period dispatch helper — Cancel-Pending-Change flow.
2759
+ *
2760
+ * Reads JSON body `{ auth: PendingChangeCancelAuth, subId: string }`. The
2761
+ * auth must carry `newSubId` (matches the currently PENDING downgrade
2762
+ * target). Runs verifyCancelPendingChange then wraps
2763
+ * settleCancelPendingChange as a payment-presettle.
2764
+ */
2765
+ async tryDispatchCancelPendingChangeFlow(adapter, routeConfig, paymentOptions) {
2766
+ let scheme = null;
2767
+ for (const opt of paymentOptions) {
2768
+ if (!opt.network || !opt.scheme) continue;
2769
+ const resolved = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
2770
+ if (resolved) {
2771
+ scheme = resolved;
2772
+ break;
2773
+ }
2774
+ }
2775
+ if (!scheme) return null;
2776
+ const body = adapter.getBody?.() ?? {};
2777
+ if (!body.auth || !body.subId) {
2778
+ return {
2779
+ type: "payment-error",
2780
+ response: {
2781
+ status: 400,
2782
+ headers: { "Content-Type": "application/json" },
2783
+ body: { error: "cancel-pending-change: body must include auth and subId" }
2784
+ }
2785
+ };
2786
+ }
2787
+ if (!body.auth.newSubId) {
2788
+ return {
2789
+ type: "payment-error",
2790
+ response: {
2791
+ status: 400,
2792
+ headers: { "Content-Type": "application/json" },
2793
+ body: { error: "cancel-pending-change: auth.newSubId is required" }
2794
+ }
2795
+ };
2796
+ }
2797
+ const verifyResult = await scheme.verifyCancelPendingChange(body.auth, body.subId);
2798
+ if (!verifyResult.ok) {
2799
+ return {
2800
+ type: "payment-error",
2801
+ response: {
2802
+ status: 402,
2803
+ headers: { "Content-Type": "application/json" },
2804
+ body: { error: verifyResult.error }
2805
+ }
2806
+ };
2807
+ }
2808
+ void routeConfig;
2809
+ const settleScheme = scheme;
2810
+ const auth = body.auth;
2811
+ const subId = body.subId;
2812
+ return {
2813
+ type: "payment-presettle",
2814
+ paymentPayload: { x402Version: 2, accepted: null, payload: {} },
2815
+ paymentRequirements: null,
2816
+ operation: "cancel-pending-change",
2817
+ settle: async () => {
2818
+ const r = await settleScheme.settleCancelPendingChange(auth, subId);
2819
+ return r.success ? { success: true, headers: r.headers, data: { subId: r.subId } } : { success: false, error: r.error };
2820
+ }
2821
+ };
2822
+ }
1600
2823
  /**
1601
2824
  * Build HTTPResponseInstructions for settlement failure.
1602
2825
  * Uses settlementFailedResponseBody hook if configured, otherwise defaults to empty body.
@@ -1708,8 +2931,25 @@ var x402HTTPResourceServer = class {
1708
2931
  console.warn("Failed to decode PAYMENT-SIGNATURE header:", error);
1709
2932
  }
1710
2933
  }
2934
+ const subHeader = adapter.getHeader("app-payment") || adapter.getHeader("APP-PAYMENT");
2935
+ if (subHeader) {
2936
+ try {
2937
+ const json = Buffer.from(subHeader, "base64").toString("utf8");
2938
+ return JSON.parse(json);
2939
+ } catch (error) {
2940
+ console.warn("Failed to decode APP-PAYMENT header:", error);
2941
+ }
2942
+ }
1711
2943
  return null;
1712
2944
  }
2945
+ /**
2946
+ * Extract `APP-Access` header (subscription access-flow). Returns the raw
2947
+ * base64 string so callers can pass it through to `decodeAccessProof` in
2948
+ * the subscription codec.
2949
+ */
2950
+ extractAccessProofHeader(adapter) {
2951
+ return adapter.getHeader("app-access") || adapter.getHeader("APP-Access") || null;
2952
+ }
1713
2953
  /**
1714
2954
  * Check if request is from a web browser
1715
2955
  *
@@ -1865,6 +3105,15 @@ var x402HTTPResourceServer = class {
1865
3105
  return 0;
1866
3106
  }
1867
3107
  };
3108
+ function collectAcceptedPlanIds(options) {
3109
+ const seen = /* @__PURE__ */ new Set();
3110
+ for (const opt of options) {
3111
+ const extra = opt.extra;
3112
+ const id = extra?.plan?.id;
3113
+ if (typeof id === "string" && id.length > 0) seen.add(id);
3114
+ }
3115
+ return Array.from(seen);
3116
+ }
1868
3117
  // Annotate the CommonJS export names for ESM import in node:
1869
3118
  0 && (module.exports = {
1870
3119
  DEFAULT_POLL_DEADLINE_MS,