@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/http/index.ts
31
670
  var http_exports = {};
32
671
  __export(http_exports, {
@@ -129,6 +768,10 @@ function getFacilitatorResponseError(error) {
129
768
  return null;
130
769
  }
131
770
 
771
+ // src/http/httpFacilitatorClient.ts
772
+ init_typed_data();
773
+ init_payload();
774
+
132
775
  // src/schemas/index.ts
133
776
  var import_zod = require("zod");
134
777
  var import_zod2 = require("zod");
@@ -240,6 +883,8 @@ var HTTPFacilitatorClient = class {
240
883
  constructor(config) {
241
884
  this.url = config?.url || DEFAULT_FACILITATOR_URL;
242
885
  this._createAuthHeaders = config?.createAuthHeaders;
886
+ this._createSubscriptionAuthHeaders = config?.createSubscriptionAuthHeaders;
887
+ this._fetchFn = config?.fetchFn ?? fetch;
243
888
  }
244
889
  /**
245
890
  * Verify a payment with the facilitator
@@ -256,7 +901,7 @@ var HTTPFacilitatorClient = class {
256
901
  const authHeaders = await this.createAuthHeaders("verify");
257
902
  headers = { ...headers, ...authHeaders.headers };
258
903
  }
259
- const response = await fetch(`${this.url}/verify`, {
904
+ const response = await this._fetchFn(`${this.url}/verify`, {
260
905
  method: "POST",
261
906
  headers,
262
907
  body: JSON.stringify({
@@ -297,7 +942,7 @@ var HTTPFacilitatorClient = class {
297
942
  const authHeaders = await this.createAuthHeaders("settle");
298
943
  headers = { ...headers, ...authHeaders.headers };
299
944
  }
300
- const response = await fetch(`${this.url}/settle`, {
945
+ const response = await this._fetchFn(`${this.url}/settle`, {
301
946
  method: "POST",
302
947
  headers,
303
948
  body: JSON.stringify({
@@ -339,7 +984,7 @@ var HTTPFacilitatorClient = class {
339
984
  }
340
985
  let lastError = null;
341
986
  for (let attempt = 0; attempt < GET_SUPPORTED_RETRIES; attempt++) {
342
- const response = await fetch(`${this.url}/supported`, {
987
+ const response = await this._fetchFn(`${this.url}/supported`, {
343
988
  method: "GET",
344
989
  headers
345
990
  });
@@ -373,10 +1018,13 @@ var HTTPFacilitatorClient = class {
373
1018
  const authHeaders = await this.createAuthHeaders("settle/status");
374
1019
  headers = { ...headers, ...authHeaders.headers };
375
1020
  }
376
- const response = await fetch(`${this.url}/settle/status?txHash=${encodeURIComponent(txHash)}`, {
377
- method: "GET",
378
- headers
379
- });
1021
+ const response = await this._fetchFn(
1022
+ `${this.url}/settle/status?txHash=${encodeURIComponent(txHash)}`,
1023
+ {
1024
+ method: "GET",
1025
+ headers
1026
+ }
1027
+ );
380
1028
  if (!response.ok) {
381
1029
  const text = await response.text().catch(() => response.statusText);
382
1030
  throw new Error(
@@ -415,10 +1063,129 @@ var HTTPFacilitatorClient = class {
415
1063
  JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() : value)
416
1064
  );
417
1065
  }
1066
+ // ── SubscriptionFacilitatorClient (period) ─────────────
1067
+ //
1068
+ // Generic JSON POST / GET helpers parameterized by `op` so the same code
1069
+ // path covers all five subscription endpoints. The standard OKX envelope
1070
+ // `{ code, msg?, data? }` is returned to the caller unparsed (the
1071
+ // subscription scheme reads `code === "0"` and `data` directly).
1072
+ async subscriptionAuthHeaders(op) {
1073
+ if (!this._createSubscriptionAuthHeaders) return {};
1074
+ return this._createSubscriptionAuthHeaders(op);
1075
+ }
1076
+ async subscriptionPost(op, path, body) {
1077
+ const headers = {
1078
+ "Content-Type": "application/json",
1079
+ ...await this.subscriptionAuthHeaders(op)
1080
+ };
1081
+ const resp = await this._fetchFn(`${this.url}${path}`, {
1082
+ method: "POST",
1083
+ headers,
1084
+ body: JSON.stringify(this.toJsonSafe(body))
1085
+ });
1086
+ if (!resp.ok) {
1087
+ throw new Error(`facilitator ${op} returned HTTP ${resp.status}: ${await resp.text()}`);
1088
+ }
1089
+ return await resp.json();
1090
+ }
1091
+ async subscriptionGet(op, path) {
1092
+ const headers = await this.subscriptionAuthHeaders(op);
1093
+ const resp = await this._fetchFn(`${this.url}${path}`, { method: "GET", headers });
1094
+ if (!resp.ok) {
1095
+ throw new Error(`facilitator ${op} returned HTTP ${resp.status}: ${await resp.text()}`);
1096
+ }
1097
+ return await resp.json();
1098
+ }
1099
+ /**
1100
+ * Build the {chainIndex, terms, permit, termsSig, permitSig, syncSettle}
1101
+ * request body shared by subscribe / change endpoints.
1102
+ */
1103
+ buildWriteBody(payload, requirements, syncSettle) {
1104
+ const inner = asSubscriptionPaymentInner(payload);
1105
+ return {
1106
+ chainIndex: parseChainIdFromNetwork(requirements.network),
1107
+ terms: inner.terms,
1108
+ permit: inner.permitSingle,
1109
+ termsSig: inner.termsSignature,
1110
+ permitSig: inner.permitSingleSignature,
1111
+ syncSettle: syncSettle ?? true
1112
+ };
1113
+ }
1114
+ async subscribe(paymentPayload, paymentRequirements, syncSettle) {
1115
+ return this.subscriptionPost(
1116
+ "subscribe",
1117
+ "/api/v6/pay/x402/subscriptions",
1118
+ this.buildWriteBody(paymentPayload, paymentRequirements, syncSettle)
1119
+ );
1120
+ }
1121
+ async changeSubscription(paymentPayload, paymentRequirements, oldSubId, syncSettle) {
1122
+ return this.subscriptionPost(
1123
+ "change",
1124
+ "/api/v6/pay/x402/subscriptions/change",
1125
+ {
1126
+ ...this.buildWriteBody(paymentPayload, paymentRequirements, syncSettle),
1127
+ // `oldSubId` is informational — server reads
1128
+ // newTerms.changeFromSubId for the authoritative value.
1129
+ oldSubId,
1130
+ // change body uses `newTerms` not `terms`.
1131
+ newTerms: asSubscriptionPaymentInner(paymentPayload).terms,
1132
+ terms: void 0
1133
+ }
1134
+ );
1135
+ }
1136
+ async cancelSubscription(subId, cancelAuth, syncSettle) {
1137
+ return this.subscriptionPost(
1138
+ "cancel",
1139
+ "/api/v6/pay/x402/subscriptions/cancel",
1140
+ { subId, cancelAuth, syncSettle: syncSettle ?? true }
1141
+ );
1142
+ }
1143
+ async cancelPendingChange(subId, cancelAuth, syncSettle) {
1144
+ return this.subscriptionPost(
1145
+ "cancel-pending-change",
1146
+ "/api/v6/pay/x402/subscriptions/cancel-pending-change",
1147
+ { subId, cancelAuth, syncSettle: syncSettle ?? true }
1148
+ );
1149
+ }
1150
+ async chargeSubscription(subId, syncSettle) {
1151
+ return this.subscriptionPost(
1152
+ "charge",
1153
+ "/api/v6/pay/x402/subscriptions/charge",
1154
+ { subId, syncSettle: syncSettle ?? true }
1155
+ );
1156
+ }
1157
+ async finalizeExpired(subId, syncSettle) {
1158
+ return this.subscriptionPost(
1159
+ "finalize-expired",
1160
+ "/api/v6/pay/x402/subscriptions/finalize-expired",
1161
+ { subId, syncSettle: syncSettle ?? true }
1162
+ );
1163
+ }
1164
+ async getCharges(subId, limit = 50, offset = 0) {
1165
+ const q = new URLSearchParams({ subId, limit: String(limit), offset: String(offset) });
1166
+ return this.subscriptionGet(
1167
+ "getCharges",
1168
+ `/api/v6/pay/x402/subscriptions/charges?${q.toString()}`
1169
+ );
1170
+ }
1171
+ async getPendingChange(subId) {
1172
+ return this.subscriptionGet(
1173
+ "getPendingChange",
1174
+ `/api/v6/pay/x402/subscriptions/pending?subId=${encodeURIComponent(subId)}`
1175
+ );
1176
+ }
1177
+ async getSubscription(subId) {
1178
+ return this.subscriptionGet(
1179
+ "getSubscription",
1180
+ `/api/v6/pay/x402/subscriptions/detail?subId=${encodeURIComponent(subId)}`
1181
+ );
1182
+ }
418
1183
  };
419
1184
 
420
1185
  // src/facilitator/OKXFacilitatorClient.ts
421
1186
  var import_node_crypto = __toESM(require("crypto"));
1187
+ init_typed_data();
1188
+ init_payload();
422
1189
 
423
1190
  // src/index.ts
424
1191
  var x402Version = 2;
@@ -452,6 +1219,7 @@ var x402HTTPResourceServer = class {
452
1219
  constructor(ResourceServer, routes) {
453
1220
  this.compiledRoutes = [];
454
1221
  this.protectedRequestHooks = [];
1222
+ this.beforeAccessHooks = [];
455
1223
  this.pollDeadlineMs = DEFAULT_POLL_DEADLINE_MS;
456
1224
  this.ResourceServer = ResourceServer;
457
1225
  this.routesConfig = routes;
@@ -526,6 +1294,22 @@ var x402HTTPResourceServer = class {
526
1294
  this.protectedRequestHooks.push(hook);
527
1295
  return this;
528
1296
  }
1297
+ /**
1298
+ * Register a seller-global `onBeforeAccess` hook fired on every access-
1299
+ * verified subscription request, AFTER `verifyAccess` (signature + payer
1300
+ * + plan allowlist + period math) but BEFORE the handler runs. Seller
1301
+ * uses it for cross-cutting access policy (quota / ban list / feature
1302
+ * gating). Hooks are executed in order of registration; the first one
1303
+ * to return `{ ok: false }` denies (→ 402). Route-level
1304
+ * `RouteConfig.onBeforeAccess` runs AFTER all global hooks.
1305
+ *
1306
+ * @param hook - The hook function
1307
+ * @returns The x402HTTPResourceServer instance for chaining
1308
+ */
1309
+ onBeforeAccess(hook) {
1310
+ this.beforeAccessHooks.push(hook);
1311
+ return this;
1312
+ }
529
1313
  /**
530
1314
  * Register a hook to call when the facilitator returns status="timeout".
531
1315
  * The hook should verify the tx on-chain and return { confirmed: boolean }.
@@ -583,6 +1367,14 @@ var x402HTTPResourceServer = class {
583
1367
  }
584
1368
  const paymentOptions = this.normalizePaymentOptions(routeConfig);
585
1369
  const paymentPayload = this.extractPayment(adapter);
1370
+ if (routeConfig.operation === "cancel") {
1371
+ const cancelResult = await this.tryDispatchCancelFlow(adapter, routeConfig, paymentOptions);
1372
+ if (cancelResult) return cancelResult;
1373
+ }
1374
+ if (routeConfig.operation === "cancel-pending-change") {
1375
+ const r = await this.tryDispatchCancelPendingChangeFlow(adapter, routeConfig, paymentOptions);
1376
+ if (r) return r;
1377
+ }
586
1378
  const resourceInfo = {
587
1379
  url: routeConfig.resource || enrichedContext.adapter.getUrl(),
588
1380
  description: routeConfig.description || "",
@@ -592,6 +1384,89 @@ var x402HTTPResourceServer = class {
592
1384
  paymentOptions,
593
1385
  enrichedContext
594
1386
  );
1387
+ if (routeConfig.operation === "change") {
1388
+ let scheme = null;
1389
+ for (const opt of paymentOptions) {
1390
+ if (!opt.network || !opt.scheme) continue;
1391
+ scheme = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
1392
+ if (scheme) break;
1393
+ }
1394
+ if (!scheme) {
1395
+ return {
1396
+ type: "payment-error",
1397
+ response: {
1398
+ status: 500,
1399
+ headers: { "Content-Type": "application/json" },
1400
+ body: { error: "change route: no subscription scheme registered" }
1401
+ }
1402
+ };
1403
+ }
1404
+ let currentSubId;
1405
+ if (paymentPayload) {
1406
+ const innerTerms = paymentPayload.payload?.terms;
1407
+ currentSubId = innerTerms?.changeFromSubId;
1408
+ } else {
1409
+ const accessHeader = this.extractAccessProofHeader(enrichedContext.adapter);
1410
+ if (!accessHeader) {
1411
+ return {
1412
+ type: "payment-error",
1413
+ response: {
1414
+ status: 401,
1415
+ headers: { "Content-Type": "application/json" },
1416
+ body: { error: "change route: missing APP-Access header" }
1417
+ }
1418
+ };
1419
+ }
1420
+ const { decodeAccessProof: decodeAccessProof2 } = await this.loadSubscriptionModule();
1421
+ let proof;
1422
+ try {
1423
+ proof = decodeAccessProof2(accessHeader);
1424
+ } catch {
1425
+ return {
1426
+ type: "payment-error",
1427
+ response: {
1428
+ status: 400,
1429
+ headers: { "Content-Type": "application/json" },
1430
+ body: { error: "change route: invalid APP-Access header" }
1431
+ }
1432
+ };
1433
+ }
1434
+ const verify = await scheme.verifyOwnership(proof);
1435
+ if (!verify.ok) {
1436
+ return {
1437
+ type: "payment-error",
1438
+ response: {
1439
+ status: 401,
1440
+ headers: { "Content-Type": "application/json" },
1441
+ body: { error: verify.error }
1442
+ }
1443
+ };
1444
+ }
1445
+ currentSubId = verify.subId;
1446
+ }
1447
+ if (!currentSubId) {
1448
+ return {
1449
+ type: "payment-error",
1450
+ response: {
1451
+ status: 400,
1452
+ headers: { "Content-Type": "application/json" },
1453
+ body: { error: "change route: cannot resolve currentSubId" }
1454
+ }
1455
+ };
1456
+ }
1457
+ const enriched = await scheme.enrichAcceptsForChange(requirements, currentSubId);
1458
+ if (enriched === null) {
1459
+ return {
1460
+ type: "payment-error",
1461
+ response: {
1462
+ status: 404,
1463
+ headers: { "Content-Type": "application/json" },
1464
+ body: { error: "sub_not_active_for_change" }
1465
+ }
1466
+ };
1467
+ }
1468
+ requirements = enriched;
1469
+ }
595
1470
  let extensions = routeConfig.extensions;
596
1471
  if (extensions) {
597
1472
  extensions = this.ResourceServer.enrichExtensions(extensions, enrichedContext);
@@ -604,6 +1479,23 @@ var x402HTTPResourceServer = class {
604
1479
  extensions,
605
1480
  transportContext
606
1481
  );
1482
+ if (routeConfig.operation !== "change") {
1483
+ const accessResult = await this.tryDispatchAccessFlow(
1484
+ adapter,
1485
+ routeConfig,
1486
+ paymentOptions,
1487
+ paymentRequired
1488
+ );
1489
+ if (accessResult) return accessResult;
1490
+ }
1491
+ if (paymentPayload) {
1492
+ const subResult = await this.tryDispatchSubscriptionPresettle(
1493
+ paymentPayload,
1494
+ paymentRequired.accepts,
1495
+ routeConfig.operation === "change" ? "change" : "subscribe"
1496
+ );
1497
+ if (subResult) return subResult;
1498
+ }
607
1499
  if (!paymentPayload) {
608
1500
  const unpaidBody = routeConfig.unpaidResponseBody ? await routeConfig.unpaidResponseBody(enrichedContext) : void 0;
609
1501
  return {
@@ -825,6 +1717,322 @@ var x402HTTPResourceServer = class {
825
1717
  requiresPayment(context) {
826
1718
  return this.getRouteConfig(context.path, context.method) !== void 0;
827
1719
  }
1720
+ /**
1721
+ * Lazy loader for the subscription submodule. The `import()` cache makes
1722
+ * this effectively free after the first hit; isolating it in one place
1723
+ * keeps dispatch helpers free of dynamic-import boilerplate and lets
1724
+ * bundlers tree-shake the entire subscription path when no caller touches
1725
+ * it.
1726
+ */
1727
+ loadSubscriptionModule() {
1728
+ return Promise.resolve().then(() => (init_subscription(), subscription_exports));
1729
+ }
1730
+ /**
1731
+ * Single chokepoint for "is this (network, scheme) backed by a
1732
+ * SubscriptionCapability-implementing scheme?". Returns the narrowed
1733
+ * capability (so callers get full typing on `verifyAccess` / `verifySubscribe`
1734
+ * / etc.) or null if not registered or not a subscription scheme.
1735
+ */
1736
+ async resolveSubscriptionScheme(network, schemeName) {
1737
+ const registered = this.ResourceServer.findScheme(network, schemeName);
1738
+ if (!registered) return null;
1739
+ const { hasSubscriptionCapability: hasSubscriptionCapability2 } = await this.loadSubscriptionModule();
1740
+ return hasSubscriptionCapability2(registered) ? registered : null;
1741
+ }
1742
+ /**
1743
+ * period dispatch helper — Access flow.
1744
+ *
1745
+ * Returns an `access-verified` (or `payment-error`) HTTPProcessResult when
1746
+ * the request carries `APP-Access` AND a subscription-capable scheme is
1747
+ * registered for one of the route's accepted (scheme, network) pairs.
1748
+ * Returns `null` to indicate the dispatcher should fall through to classic
1749
+ * pay-per-request handling.
1750
+ */
1751
+ async tryDispatchAccessFlow(adapter, routeConfig, paymentOptions, paymentRequired) {
1752
+ const headerB64 = this.extractAccessProofHeader(adapter);
1753
+ if (!headerB64) return null;
1754
+ const { decodeAccessProof: decodeAccessProof2 } = await this.loadSubscriptionModule();
1755
+ let proof;
1756
+ try {
1757
+ proof = decodeAccessProof2(headerB64);
1758
+ } catch (err) {
1759
+ return {
1760
+ type: "payment-error",
1761
+ response: {
1762
+ status: 401,
1763
+ headers: { "Content-Type": "application/json" },
1764
+ body: { error: `invalid APP-Access: ${err.message}` }
1765
+ }
1766
+ };
1767
+ }
1768
+ const acceptedPlanIds = collectAcceptedPlanIds(paymentOptions);
1769
+ for (const opt of paymentOptions) {
1770
+ if (!opt.network || !opt.scheme) continue;
1771
+ const scheme = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
1772
+ if (!scheme) continue;
1773
+ const result = await scheme.verifyAccess(proof, { acceptedPlanIds });
1774
+ if (!result.ok) {
1775
+ return {
1776
+ type: "payment-error",
1777
+ response: {
1778
+ status: 402,
1779
+ headers: {
1780
+ "Content-Type": "application/json",
1781
+ "PAYMENT-REQUIRED": encodePaymentRequiredHeader(paymentRequired)
1782
+ },
1783
+ body: { error: result.error }
1784
+ }
1785
+ };
1786
+ }
1787
+ const hooks = [
1788
+ ...this.beforeAccessHooks,
1789
+ ...routeConfig.onBeforeAccess ? [routeConfig.onBeforeAccess] : []
1790
+ ];
1791
+ for (const hook of hooks) {
1792
+ const decision = await hook({
1793
+ subscription: result.subscription,
1794
+ request: {
1795
+ path: adapter.getPath(),
1796
+ method: adapter.getMethod(),
1797
+ headers: adapter.getHeaders?.() ?? {}
1798
+ },
1799
+ route: { acceptedPlanIds, accepts: paymentRequired.accepts }
1800
+ });
1801
+ if (!decision.ok) {
1802
+ return {
1803
+ type: "payment-error",
1804
+ response: {
1805
+ status: 402,
1806
+ headers: { "Content-Type": "application/json" },
1807
+ body: {
1808
+ error: decision.error ?? "access_denied",
1809
+ retryAfter: decision.retryAfter,
1810
+ upgradeOffers: decision.upgradeOffers
1811
+ }
1812
+ }
1813
+ };
1814
+ }
1815
+ }
1816
+ return {
1817
+ type: "access-verified",
1818
+ subscription: result.subscription,
1819
+ headers: {}
1820
+ };
1821
+ }
1822
+ return {
1823
+ type: "payment-error",
1824
+ response: {
1825
+ status: 401,
1826
+ headers: { "Content-Type": "application/json" },
1827
+ body: { error: "no subscription scheme registered for this route" }
1828
+ }
1829
+ };
1830
+ }
1831
+ /**
1832
+ * period dispatch helper — Subscribe presettle flow.
1833
+ *
1834
+ * When the buyer presents a PaymentPayload whose `accepted.scheme` is a
1835
+ * subscription scheme with `settlementMode === "pre"`, this runs verify +
1836
+ * (settle on demand) and returns `payment-presettle`. The middleware is
1837
+ * expected to call `result.settle()` AFTER decision-time but BEFORE
1838
+ * `next()` so handler only runs when the chain creation succeeded.
1839
+ *
1840
+ * Returns `null` to fall through to classic post-settle path-verified flow.
1841
+ */
1842
+ async tryDispatchSubscriptionPresettle(paymentPayload, serverAccepts, operation) {
1843
+ const { accepted } = paymentPayload;
1844
+ const scheme = await this.resolveSubscriptionScheme(accepted.network, accepted.scheme);
1845
+ if (!scheme) return null;
1846
+ const serverReq = this.ResourceServer.findMatchingRequirements(serverAccepts, paymentPayload);
1847
+ if (!serverReq) {
1848
+ return {
1849
+ type: "payment-error",
1850
+ response: {
1851
+ status: 402,
1852
+ headers: { "Content-Type": "application/json" },
1853
+ body: { error: "no_matching_requirements" }
1854
+ }
1855
+ };
1856
+ }
1857
+ if (operation === "change") {
1858
+ const verifyResult2 = await scheme.verifyChange(paymentPayload, serverReq);
1859
+ if (!verifyResult2.ok) {
1860
+ return {
1861
+ type: "payment-error",
1862
+ response: {
1863
+ status: 402,
1864
+ headers: { "Content-Type": "application/json" },
1865
+ body: { error: verifyResult2.error }
1866
+ }
1867
+ };
1868
+ }
1869
+ return {
1870
+ type: "payment-presettle",
1871
+ paymentPayload,
1872
+ paymentRequirements: serverReq,
1873
+ operation: "change",
1874
+ settle: async () => {
1875
+ const r = await scheme.settleChange(paymentPayload, serverReq);
1876
+ return r.success ? {
1877
+ success: true,
1878
+ headers: r.headers,
1879
+ data: {
1880
+ newSubId: r.newSubId,
1881
+ oldSubId: r.oldSubId,
1882
+ operationType: r.operationType,
1883
+ scheduledFromPeriod: r.scheduledFromPeriod
1884
+ }
1885
+ } : { success: false, error: r.error };
1886
+ }
1887
+ };
1888
+ }
1889
+ const verifyResult = await scheme.verifySubscribe(paymentPayload, serverReq);
1890
+ if (!verifyResult.ok) {
1891
+ return {
1892
+ type: "payment-error",
1893
+ response: {
1894
+ status: 402,
1895
+ headers: { "Content-Type": "application/json" },
1896
+ body: { error: verifyResult.error }
1897
+ }
1898
+ };
1899
+ }
1900
+ return {
1901
+ type: "payment-presettle",
1902
+ paymentPayload,
1903
+ paymentRequirements: serverReq,
1904
+ operation: "subscribe",
1905
+ settle: async () => {
1906
+ const r = await scheme.settleSubscribe(paymentPayload, serverReq);
1907
+ return r.success ? {
1908
+ success: true,
1909
+ headers: r.headers,
1910
+ data: { subId: r.subId, subscription: r.subscription }
1911
+ } : { success: false, error: r.error };
1912
+ }
1913
+ };
1914
+ }
1915
+ /**
1916
+ * period dispatch helper — Cancel flow.
1917
+ *
1918
+ * Reads JSON body { auth: CancelAuth, subId: string }, runs verifyCancel
1919
+ * then wraps settleCancel as a payment-presettle (settle-before-handler so
1920
+ * the cancelation is on-chain before the seller's response).
1921
+ */
1922
+ async tryDispatchCancelFlow(adapter, routeConfig, paymentOptions) {
1923
+ let scheme = null;
1924
+ for (const opt of paymentOptions) {
1925
+ if (!opt.network || !opt.scheme) continue;
1926
+ const resolved = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
1927
+ if (resolved) {
1928
+ scheme = resolved;
1929
+ break;
1930
+ }
1931
+ }
1932
+ if (!scheme) return null;
1933
+ const body = adapter.getBody?.() ?? {};
1934
+ if (!body.auth || !body.subId) {
1935
+ return {
1936
+ type: "payment-error",
1937
+ response: {
1938
+ status: 400,
1939
+ headers: { "Content-Type": "application/json" },
1940
+ body: { error: "cancel: body must include auth and subId" }
1941
+ }
1942
+ };
1943
+ }
1944
+ const verifyResult = await scheme.verifyCancel(body.auth, body.subId);
1945
+ if (!verifyResult.ok) {
1946
+ return {
1947
+ type: "payment-error",
1948
+ response: {
1949
+ status: 402,
1950
+ headers: { "Content-Type": "application/json" },
1951
+ body: { error: verifyResult.error }
1952
+ }
1953
+ };
1954
+ }
1955
+ void routeConfig;
1956
+ const settleScheme = scheme;
1957
+ const auth = body.auth;
1958
+ const subId = body.subId;
1959
+ return {
1960
+ type: "payment-presettle",
1961
+ paymentPayload: { x402Version: 2, accepted: null, payload: {} },
1962
+ paymentRequirements: null,
1963
+ operation: "cancel",
1964
+ settle: async () => {
1965
+ const r = await settleScheme.settleCancel(auth, subId);
1966
+ return r.success ? { success: true, headers: r.headers, data: { subId } } : { success: false, error: r.error };
1967
+ }
1968
+ };
1969
+ }
1970
+ /**
1971
+ * period dispatch helper — Cancel-Pending-Change flow.
1972
+ *
1973
+ * Reads JSON body `{ auth: PendingChangeCancelAuth, subId: string }`. The
1974
+ * auth must carry `newSubId` (matches the currently PENDING downgrade
1975
+ * target). Runs verifyCancelPendingChange then wraps
1976
+ * settleCancelPendingChange as a payment-presettle.
1977
+ */
1978
+ async tryDispatchCancelPendingChangeFlow(adapter, routeConfig, paymentOptions) {
1979
+ let scheme = null;
1980
+ for (const opt of paymentOptions) {
1981
+ if (!opt.network || !opt.scheme) continue;
1982
+ const resolved = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
1983
+ if (resolved) {
1984
+ scheme = resolved;
1985
+ break;
1986
+ }
1987
+ }
1988
+ if (!scheme) return null;
1989
+ const body = adapter.getBody?.() ?? {};
1990
+ if (!body.auth || !body.subId) {
1991
+ return {
1992
+ type: "payment-error",
1993
+ response: {
1994
+ status: 400,
1995
+ headers: { "Content-Type": "application/json" },
1996
+ body: { error: "cancel-pending-change: body must include auth and subId" }
1997
+ }
1998
+ };
1999
+ }
2000
+ if (!body.auth.newSubId) {
2001
+ return {
2002
+ type: "payment-error",
2003
+ response: {
2004
+ status: 400,
2005
+ headers: { "Content-Type": "application/json" },
2006
+ body: { error: "cancel-pending-change: auth.newSubId is required" }
2007
+ }
2008
+ };
2009
+ }
2010
+ const verifyResult = await scheme.verifyCancelPendingChange(body.auth, body.subId);
2011
+ if (!verifyResult.ok) {
2012
+ return {
2013
+ type: "payment-error",
2014
+ response: {
2015
+ status: 402,
2016
+ headers: { "Content-Type": "application/json" },
2017
+ body: { error: verifyResult.error }
2018
+ }
2019
+ };
2020
+ }
2021
+ void routeConfig;
2022
+ const settleScheme = scheme;
2023
+ const auth = body.auth;
2024
+ const subId = body.subId;
2025
+ return {
2026
+ type: "payment-presettle",
2027
+ paymentPayload: { x402Version: 2, accepted: null, payload: {} },
2028
+ paymentRequirements: null,
2029
+ operation: "cancel-pending-change",
2030
+ settle: async () => {
2031
+ const r = await settleScheme.settleCancelPendingChange(auth, subId);
2032
+ return r.success ? { success: true, headers: r.headers, data: { subId: r.subId } } : { success: false, error: r.error };
2033
+ }
2034
+ };
2035
+ }
828
2036
  /**
829
2037
  * Build HTTPResponseInstructions for settlement failure.
830
2038
  * Uses settlementFailedResponseBody hook if configured, otherwise defaults to empty body.
@@ -936,8 +2144,25 @@ var x402HTTPResourceServer = class {
936
2144
  console.warn("Failed to decode PAYMENT-SIGNATURE header:", error);
937
2145
  }
938
2146
  }
2147
+ const subHeader = adapter.getHeader("app-payment") || adapter.getHeader("APP-PAYMENT");
2148
+ if (subHeader) {
2149
+ try {
2150
+ const json = Buffer.from(subHeader, "base64").toString("utf8");
2151
+ return JSON.parse(json);
2152
+ } catch (error) {
2153
+ console.warn("Failed to decode APP-PAYMENT header:", error);
2154
+ }
2155
+ }
939
2156
  return null;
940
2157
  }
2158
+ /**
2159
+ * Extract `APP-Access` header (subscription access-flow). Returns the raw
2160
+ * base64 string so callers can pass it through to `decodeAccessProof` in
2161
+ * the subscription codec.
2162
+ */
2163
+ extractAccessProofHeader(adapter) {
2164
+ return adapter.getHeader("app-access") || adapter.getHeader("APP-Access") || null;
2165
+ }
941
2166
  /**
942
2167
  * Check if request is from a web browser
943
2168
  *
@@ -1093,6 +2318,15 @@ var x402HTTPResourceServer = class {
1093
2318
  return 0;
1094
2319
  }
1095
2320
  };
2321
+ function collectAcceptedPlanIds(options) {
2322
+ const seen = /* @__PURE__ */ new Set();
2323
+ for (const opt of options) {
2324
+ const extra = opt.extra;
2325
+ const id = extra?.plan?.id;
2326
+ if (typeof id === "string" && id.length > 0) seen.add(id);
2327
+ }
2328
+ return Array.from(seen);
2329
+ }
1096
2330
 
1097
2331
  // src/http/x402HTTPClient.ts
1098
2332
  var x402HTTPClient = class {