@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
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  x402Version
3
- } from "./chunk-O3IYMTNT.mjs";
3
+ } from "./chunk-4KASWSSY.mjs";
4
+ import {
5
+ asSubscriptionPaymentInner,
6
+ parseChainIdFromNetwork
7
+ } from "./chunk-CKXR4QVD.mjs";
4
8
  import {
5
9
  FacilitatorResponseError,
6
10
  SettleError,
@@ -90,6 +94,8 @@ var HTTPFacilitatorClient = class {
90
94
  constructor(config) {
91
95
  this.url = config?.url || DEFAULT_FACILITATOR_URL;
92
96
  this._createAuthHeaders = config?.createAuthHeaders;
97
+ this._createSubscriptionAuthHeaders = config?.createSubscriptionAuthHeaders;
98
+ this._fetchFn = config?.fetchFn ?? fetch;
93
99
  }
94
100
  /**
95
101
  * Verify a payment with the facilitator
@@ -106,7 +112,7 @@ var HTTPFacilitatorClient = class {
106
112
  const authHeaders = await this.createAuthHeaders("verify");
107
113
  headers = { ...headers, ...authHeaders.headers };
108
114
  }
109
- const response = await fetch(`${this.url}/verify`, {
115
+ const response = await this._fetchFn(`${this.url}/verify`, {
110
116
  method: "POST",
111
117
  headers,
112
118
  body: JSON.stringify({
@@ -147,7 +153,7 @@ var HTTPFacilitatorClient = class {
147
153
  const authHeaders = await this.createAuthHeaders("settle");
148
154
  headers = { ...headers, ...authHeaders.headers };
149
155
  }
150
- const response = await fetch(`${this.url}/settle`, {
156
+ const response = await this._fetchFn(`${this.url}/settle`, {
151
157
  method: "POST",
152
158
  headers,
153
159
  body: JSON.stringify({
@@ -189,7 +195,7 @@ var HTTPFacilitatorClient = class {
189
195
  }
190
196
  let lastError = null;
191
197
  for (let attempt = 0; attempt < GET_SUPPORTED_RETRIES; attempt++) {
192
- const response = await fetch(`${this.url}/supported`, {
198
+ const response = await this._fetchFn(`${this.url}/supported`, {
193
199
  method: "GET",
194
200
  headers
195
201
  });
@@ -223,10 +229,13 @@ var HTTPFacilitatorClient = class {
223
229
  const authHeaders = await this.createAuthHeaders("settle/status");
224
230
  headers = { ...headers, ...authHeaders.headers };
225
231
  }
226
- const response = await fetch(`${this.url}/settle/status?txHash=${encodeURIComponent(txHash)}`, {
227
- method: "GET",
228
- headers
229
- });
232
+ const response = await this._fetchFn(
233
+ `${this.url}/settle/status?txHash=${encodeURIComponent(txHash)}`,
234
+ {
235
+ method: "GET",
236
+ headers
237
+ }
238
+ );
230
239
  if (!response.ok) {
231
240
  const text = await response.text().catch(() => response.statusText);
232
241
  throw new Error(
@@ -265,6 +274,123 @@ var HTTPFacilitatorClient = class {
265
274
  JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() : value)
266
275
  );
267
276
  }
277
+ // ── SubscriptionFacilitatorClient (period) ─────────────
278
+ //
279
+ // Generic JSON POST / GET helpers parameterized by `op` so the same code
280
+ // path covers all five subscription endpoints. The standard OKX envelope
281
+ // `{ code, msg?, data? }` is returned to the caller unparsed (the
282
+ // subscription scheme reads `code === "0"` and `data` directly).
283
+ async subscriptionAuthHeaders(op) {
284
+ if (!this._createSubscriptionAuthHeaders) return {};
285
+ return this._createSubscriptionAuthHeaders(op);
286
+ }
287
+ async subscriptionPost(op, path, body) {
288
+ const headers = {
289
+ "Content-Type": "application/json",
290
+ ...await this.subscriptionAuthHeaders(op)
291
+ };
292
+ const resp = await this._fetchFn(`${this.url}${path}`, {
293
+ method: "POST",
294
+ headers,
295
+ body: JSON.stringify(this.toJsonSafe(body))
296
+ });
297
+ if (!resp.ok) {
298
+ throw new Error(`facilitator ${op} returned HTTP ${resp.status}: ${await resp.text()}`);
299
+ }
300
+ return await resp.json();
301
+ }
302
+ async subscriptionGet(op, path) {
303
+ const headers = await this.subscriptionAuthHeaders(op);
304
+ const resp = await this._fetchFn(`${this.url}${path}`, { method: "GET", headers });
305
+ if (!resp.ok) {
306
+ throw new Error(`facilitator ${op} returned HTTP ${resp.status}: ${await resp.text()}`);
307
+ }
308
+ return await resp.json();
309
+ }
310
+ /**
311
+ * Build the {chainIndex, terms, permit, termsSig, permitSig, syncSettle}
312
+ * request body shared by subscribe / change endpoints.
313
+ */
314
+ buildWriteBody(payload, requirements, syncSettle) {
315
+ const inner = asSubscriptionPaymentInner(payload);
316
+ return {
317
+ chainIndex: parseChainIdFromNetwork(requirements.network),
318
+ terms: inner.terms,
319
+ permit: inner.permitSingle,
320
+ termsSig: inner.termsSignature,
321
+ permitSig: inner.permitSingleSignature,
322
+ syncSettle: syncSettle ?? true
323
+ };
324
+ }
325
+ async subscribe(paymentPayload, paymentRequirements, syncSettle) {
326
+ return this.subscriptionPost(
327
+ "subscribe",
328
+ "/api/v6/pay/x402/subscriptions",
329
+ this.buildWriteBody(paymentPayload, paymentRequirements, syncSettle)
330
+ );
331
+ }
332
+ async changeSubscription(paymentPayload, paymentRequirements, oldSubId, syncSettle) {
333
+ return this.subscriptionPost(
334
+ "change",
335
+ "/api/v6/pay/x402/subscriptions/change",
336
+ {
337
+ ...this.buildWriteBody(paymentPayload, paymentRequirements, syncSettle),
338
+ // `oldSubId` is informational — server reads
339
+ // newTerms.changeFromSubId for the authoritative value.
340
+ oldSubId,
341
+ // change body uses `newTerms` not `terms`.
342
+ newTerms: asSubscriptionPaymentInner(paymentPayload).terms,
343
+ terms: void 0
344
+ }
345
+ );
346
+ }
347
+ async cancelSubscription(subId, cancelAuth, syncSettle) {
348
+ return this.subscriptionPost(
349
+ "cancel",
350
+ "/api/v6/pay/x402/subscriptions/cancel",
351
+ { subId, cancelAuth, syncSettle: syncSettle ?? true }
352
+ );
353
+ }
354
+ async cancelPendingChange(subId, cancelAuth, syncSettle) {
355
+ return this.subscriptionPost(
356
+ "cancel-pending-change",
357
+ "/api/v6/pay/x402/subscriptions/cancel-pending-change",
358
+ { subId, cancelAuth, syncSettle: syncSettle ?? true }
359
+ );
360
+ }
361
+ async chargeSubscription(subId, syncSettle) {
362
+ return this.subscriptionPost(
363
+ "charge",
364
+ "/api/v6/pay/x402/subscriptions/charge",
365
+ { subId, syncSettle: syncSettle ?? true }
366
+ );
367
+ }
368
+ async finalizeExpired(subId, syncSettle) {
369
+ return this.subscriptionPost(
370
+ "finalize-expired",
371
+ "/api/v6/pay/x402/subscriptions/finalize-expired",
372
+ { subId, syncSettle: syncSettle ?? true }
373
+ );
374
+ }
375
+ async getCharges(subId, limit = 50, offset = 0) {
376
+ const q = new URLSearchParams({ subId, limit: String(limit), offset: String(offset) });
377
+ return this.subscriptionGet(
378
+ "getCharges",
379
+ `/api/v6/pay/x402/subscriptions/charges?${q.toString()}`
380
+ );
381
+ }
382
+ async getPendingChange(subId) {
383
+ return this.subscriptionGet(
384
+ "getPendingChange",
385
+ `/api/v6/pay/x402/subscriptions/pending?subId=${encodeURIComponent(subId)}`
386
+ );
387
+ }
388
+ async getSubscription(subId) {
389
+ return this.subscriptionGet(
390
+ "getSubscription",
391
+ `/api/v6/pay/x402/subscriptions/detail?subId=${encodeURIComponent(subId)}`
392
+ );
393
+ }
268
394
  };
269
395
 
270
396
  // src/server/x402ResourceServer.ts
@@ -338,6 +464,21 @@ var x402ResourceServer = class {
338
464
  hasRegisteredScheme(network, scheme) {
339
465
  return !!findByNetworkAndScheme(this.registeredServerSchemes, scheme, network);
340
466
  }
467
+ /**
468
+ * Look up the registered SchemeNetworkServer for a given network + scheme.
469
+ * Exposed so the HTTP dispatch layer can perform capability detection
470
+ * (e.g. `hasSubscriptionCapability(scheme)`) on the actual instance.
471
+ *
472
+ * Pattern matching follows the same CAIP-style rules as `verifyPayment`:
473
+ * registered keys may use wildcards like `eip155:*`.
474
+ *
475
+ * @param network - The network identifier
476
+ * @param scheme - The payment scheme name
477
+ * @returns The registered scheme server, or undefined if none matches.
478
+ */
479
+ findScheme(network, scheme) {
480
+ return findByNetworkAndScheme(this.registeredServerSchemes, scheme, network);
481
+ }
341
482
  /**
342
483
  * Registers a resource service extension that can enrich extension declarations.
343
484
  *
@@ -1131,6 +1272,7 @@ var x402HTTPResourceServer = class {
1131
1272
  constructor(ResourceServer, routes) {
1132
1273
  this.compiledRoutes = [];
1133
1274
  this.protectedRequestHooks = [];
1275
+ this.beforeAccessHooks = [];
1134
1276
  this.pollDeadlineMs = DEFAULT_POLL_DEADLINE_MS;
1135
1277
  this.ResourceServer = ResourceServer;
1136
1278
  this.routesConfig = routes;
@@ -1205,6 +1347,22 @@ var x402HTTPResourceServer = class {
1205
1347
  this.protectedRequestHooks.push(hook);
1206
1348
  return this;
1207
1349
  }
1350
+ /**
1351
+ * Register a seller-global `onBeforeAccess` hook fired on every access-
1352
+ * verified subscription request, AFTER `verifyAccess` (signature + payer
1353
+ * + plan allowlist + period math) but BEFORE the handler runs. Seller
1354
+ * uses it for cross-cutting access policy (quota / ban list / feature
1355
+ * gating). Hooks are executed in order of registration; the first one
1356
+ * to return `{ ok: false }` denies (→ 402). Route-level
1357
+ * `RouteConfig.onBeforeAccess` runs AFTER all global hooks.
1358
+ *
1359
+ * @param hook - The hook function
1360
+ * @returns The x402HTTPResourceServer instance for chaining
1361
+ */
1362
+ onBeforeAccess(hook) {
1363
+ this.beforeAccessHooks.push(hook);
1364
+ return this;
1365
+ }
1208
1366
  /**
1209
1367
  * Register a hook to call when the facilitator returns status="timeout".
1210
1368
  * The hook should verify the tx on-chain and return { confirmed: boolean }.
@@ -1262,6 +1420,14 @@ var x402HTTPResourceServer = class {
1262
1420
  }
1263
1421
  const paymentOptions = this.normalizePaymentOptions(routeConfig);
1264
1422
  const paymentPayload = this.extractPayment(adapter);
1423
+ if (routeConfig.operation === "cancel") {
1424
+ const cancelResult = await this.tryDispatchCancelFlow(adapter, routeConfig, paymentOptions);
1425
+ if (cancelResult) return cancelResult;
1426
+ }
1427
+ if (routeConfig.operation === "cancel-pending-change") {
1428
+ const r = await this.tryDispatchCancelPendingChangeFlow(adapter, routeConfig, paymentOptions);
1429
+ if (r) return r;
1430
+ }
1265
1431
  const resourceInfo = {
1266
1432
  url: routeConfig.resource || enrichedContext.adapter.getUrl(),
1267
1433
  description: routeConfig.description || "",
@@ -1271,6 +1437,89 @@ var x402HTTPResourceServer = class {
1271
1437
  paymentOptions,
1272
1438
  enrichedContext
1273
1439
  );
1440
+ if (routeConfig.operation === "change") {
1441
+ let scheme = null;
1442
+ for (const opt of paymentOptions) {
1443
+ if (!opt.network || !opt.scheme) continue;
1444
+ scheme = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
1445
+ if (scheme) break;
1446
+ }
1447
+ if (!scheme) {
1448
+ return {
1449
+ type: "payment-error",
1450
+ response: {
1451
+ status: 500,
1452
+ headers: { "Content-Type": "application/json" },
1453
+ body: { error: "change route: no subscription scheme registered" }
1454
+ }
1455
+ };
1456
+ }
1457
+ let currentSubId;
1458
+ if (paymentPayload) {
1459
+ const innerTerms = paymentPayload.payload?.terms;
1460
+ currentSubId = innerTerms?.changeFromSubId;
1461
+ } else {
1462
+ const accessHeader = this.extractAccessProofHeader(enrichedContext.adapter);
1463
+ if (!accessHeader) {
1464
+ return {
1465
+ type: "payment-error",
1466
+ response: {
1467
+ status: 401,
1468
+ headers: { "Content-Type": "application/json" },
1469
+ body: { error: "change route: missing APP-Access header" }
1470
+ }
1471
+ };
1472
+ }
1473
+ const { decodeAccessProof } = await this.loadSubscriptionModule();
1474
+ let proof;
1475
+ try {
1476
+ proof = decodeAccessProof(accessHeader);
1477
+ } catch {
1478
+ return {
1479
+ type: "payment-error",
1480
+ response: {
1481
+ status: 400,
1482
+ headers: { "Content-Type": "application/json" },
1483
+ body: { error: "change route: invalid APP-Access header" }
1484
+ }
1485
+ };
1486
+ }
1487
+ const verify = await scheme.verifyOwnership(proof);
1488
+ if (!verify.ok) {
1489
+ return {
1490
+ type: "payment-error",
1491
+ response: {
1492
+ status: 401,
1493
+ headers: { "Content-Type": "application/json" },
1494
+ body: { error: verify.error }
1495
+ }
1496
+ };
1497
+ }
1498
+ currentSubId = verify.subId;
1499
+ }
1500
+ if (!currentSubId) {
1501
+ return {
1502
+ type: "payment-error",
1503
+ response: {
1504
+ status: 400,
1505
+ headers: { "Content-Type": "application/json" },
1506
+ body: { error: "change route: cannot resolve currentSubId" }
1507
+ }
1508
+ };
1509
+ }
1510
+ const enriched = await scheme.enrichAcceptsForChange(requirements, currentSubId);
1511
+ if (enriched === null) {
1512
+ return {
1513
+ type: "payment-error",
1514
+ response: {
1515
+ status: 404,
1516
+ headers: { "Content-Type": "application/json" },
1517
+ body: { error: "sub_not_active_for_change" }
1518
+ }
1519
+ };
1520
+ }
1521
+ requirements = enriched;
1522
+ }
1274
1523
  let extensions = routeConfig.extensions;
1275
1524
  if (extensions) {
1276
1525
  extensions = this.ResourceServer.enrichExtensions(extensions, enrichedContext);
@@ -1283,6 +1532,23 @@ var x402HTTPResourceServer = class {
1283
1532
  extensions,
1284
1533
  transportContext
1285
1534
  );
1535
+ if (routeConfig.operation !== "change") {
1536
+ const accessResult = await this.tryDispatchAccessFlow(
1537
+ adapter,
1538
+ routeConfig,
1539
+ paymentOptions,
1540
+ paymentRequired
1541
+ );
1542
+ if (accessResult) return accessResult;
1543
+ }
1544
+ if (paymentPayload) {
1545
+ const subResult = await this.tryDispatchSubscriptionPresettle(
1546
+ paymentPayload,
1547
+ paymentRequired.accepts,
1548
+ routeConfig.operation === "change" ? "change" : "subscribe"
1549
+ );
1550
+ if (subResult) return subResult;
1551
+ }
1286
1552
  if (!paymentPayload) {
1287
1553
  const unpaidBody = routeConfig.unpaidResponseBody ? await routeConfig.unpaidResponseBody(enrichedContext) : void 0;
1288
1554
  return {
@@ -1504,6 +1770,322 @@ var x402HTTPResourceServer = class {
1504
1770
  requiresPayment(context) {
1505
1771
  return this.getRouteConfig(context.path, context.method) !== void 0;
1506
1772
  }
1773
+ /**
1774
+ * Lazy loader for the subscription submodule. The `import()` cache makes
1775
+ * this effectively free after the first hit; isolating it in one place
1776
+ * keeps dispatch helpers free of dynamic-import boilerplate and lets
1777
+ * bundlers tree-shake the entire subscription path when no caller touches
1778
+ * it.
1779
+ */
1780
+ loadSubscriptionModule() {
1781
+ return import("./subscription/index.mjs");
1782
+ }
1783
+ /**
1784
+ * Single chokepoint for "is this (network, scheme) backed by a
1785
+ * SubscriptionCapability-implementing scheme?". Returns the narrowed
1786
+ * capability (so callers get full typing on `verifyAccess` / `verifySubscribe`
1787
+ * / etc.) or null if not registered or not a subscription scheme.
1788
+ */
1789
+ async resolveSubscriptionScheme(network, schemeName) {
1790
+ const registered = this.ResourceServer.findScheme(network, schemeName);
1791
+ if (!registered) return null;
1792
+ const { hasSubscriptionCapability } = await this.loadSubscriptionModule();
1793
+ return hasSubscriptionCapability(registered) ? registered : null;
1794
+ }
1795
+ /**
1796
+ * period dispatch helper — Access flow.
1797
+ *
1798
+ * Returns an `access-verified` (or `payment-error`) HTTPProcessResult when
1799
+ * the request carries `APP-Access` AND a subscription-capable scheme is
1800
+ * registered for one of the route's accepted (scheme, network) pairs.
1801
+ * Returns `null` to indicate the dispatcher should fall through to classic
1802
+ * pay-per-request handling.
1803
+ */
1804
+ async tryDispatchAccessFlow(adapter, routeConfig, paymentOptions, paymentRequired) {
1805
+ const headerB64 = this.extractAccessProofHeader(adapter);
1806
+ if (!headerB64) return null;
1807
+ const { decodeAccessProof } = await this.loadSubscriptionModule();
1808
+ let proof;
1809
+ try {
1810
+ proof = decodeAccessProof(headerB64);
1811
+ } catch (err) {
1812
+ return {
1813
+ type: "payment-error",
1814
+ response: {
1815
+ status: 401,
1816
+ headers: { "Content-Type": "application/json" },
1817
+ body: { error: `invalid APP-Access: ${err.message}` }
1818
+ }
1819
+ };
1820
+ }
1821
+ const acceptedPlanIds = collectAcceptedPlanIds(paymentOptions);
1822
+ for (const opt of paymentOptions) {
1823
+ if (!opt.network || !opt.scheme) continue;
1824
+ const scheme = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
1825
+ if (!scheme) continue;
1826
+ const result = await scheme.verifyAccess(proof, { acceptedPlanIds });
1827
+ if (!result.ok) {
1828
+ return {
1829
+ type: "payment-error",
1830
+ response: {
1831
+ status: 402,
1832
+ headers: {
1833
+ "Content-Type": "application/json",
1834
+ "PAYMENT-REQUIRED": encodePaymentRequiredHeader(paymentRequired)
1835
+ },
1836
+ body: { error: result.error }
1837
+ }
1838
+ };
1839
+ }
1840
+ const hooks = [
1841
+ ...this.beforeAccessHooks,
1842
+ ...routeConfig.onBeforeAccess ? [routeConfig.onBeforeAccess] : []
1843
+ ];
1844
+ for (const hook of hooks) {
1845
+ const decision = await hook({
1846
+ subscription: result.subscription,
1847
+ request: {
1848
+ path: adapter.getPath(),
1849
+ method: adapter.getMethod(),
1850
+ headers: adapter.getHeaders?.() ?? {}
1851
+ },
1852
+ route: { acceptedPlanIds, accepts: paymentRequired.accepts }
1853
+ });
1854
+ if (!decision.ok) {
1855
+ return {
1856
+ type: "payment-error",
1857
+ response: {
1858
+ status: 402,
1859
+ headers: { "Content-Type": "application/json" },
1860
+ body: {
1861
+ error: decision.error ?? "access_denied",
1862
+ retryAfter: decision.retryAfter,
1863
+ upgradeOffers: decision.upgradeOffers
1864
+ }
1865
+ }
1866
+ };
1867
+ }
1868
+ }
1869
+ return {
1870
+ type: "access-verified",
1871
+ subscription: result.subscription,
1872
+ headers: {}
1873
+ };
1874
+ }
1875
+ return {
1876
+ type: "payment-error",
1877
+ response: {
1878
+ status: 401,
1879
+ headers: { "Content-Type": "application/json" },
1880
+ body: { error: "no subscription scheme registered for this route" }
1881
+ }
1882
+ };
1883
+ }
1884
+ /**
1885
+ * period dispatch helper — Subscribe presettle flow.
1886
+ *
1887
+ * When the buyer presents a PaymentPayload whose `accepted.scheme` is a
1888
+ * subscription scheme with `settlementMode === "pre"`, this runs verify +
1889
+ * (settle on demand) and returns `payment-presettle`. The middleware is
1890
+ * expected to call `result.settle()` AFTER decision-time but BEFORE
1891
+ * `next()` so handler only runs when the chain creation succeeded.
1892
+ *
1893
+ * Returns `null` to fall through to classic post-settle path-verified flow.
1894
+ */
1895
+ async tryDispatchSubscriptionPresettle(paymentPayload, serverAccepts, operation) {
1896
+ const { accepted } = paymentPayload;
1897
+ const scheme = await this.resolveSubscriptionScheme(accepted.network, accepted.scheme);
1898
+ if (!scheme) return null;
1899
+ const serverReq = this.ResourceServer.findMatchingRequirements(serverAccepts, paymentPayload);
1900
+ if (!serverReq) {
1901
+ return {
1902
+ type: "payment-error",
1903
+ response: {
1904
+ status: 402,
1905
+ headers: { "Content-Type": "application/json" },
1906
+ body: { error: "no_matching_requirements" }
1907
+ }
1908
+ };
1909
+ }
1910
+ if (operation === "change") {
1911
+ const verifyResult2 = await scheme.verifyChange(paymentPayload, serverReq);
1912
+ if (!verifyResult2.ok) {
1913
+ return {
1914
+ type: "payment-error",
1915
+ response: {
1916
+ status: 402,
1917
+ headers: { "Content-Type": "application/json" },
1918
+ body: { error: verifyResult2.error }
1919
+ }
1920
+ };
1921
+ }
1922
+ return {
1923
+ type: "payment-presettle",
1924
+ paymentPayload,
1925
+ paymentRequirements: serverReq,
1926
+ operation: "change",
1927
+ settle: async () => {
1928
+ const r = await scheme.settleChange(paymentPayload, serverReq);
1929
+ return r.success ? {
1930
+ success: true,
1931
+ headers: r.headers,
1932
+ data: {
1933
+ newSubId: r.newSubId,
1934
+ oldSubId: r.oldSubId,
1935
+ operationType: r.operationType,
1936
+ scheduledFromPeriod: r.scheduledFromPeriod
1937
+ }
1938
+ } : { success: false, error: r.error };
1939
+ }
1940
+ };
1941
+ }
1942
+ const verifyResult = await scheme.verifySubscribe(paymentPayload, serverReq);
1943
+ if (!verifyResult.ok) {
1944
+ return {
1945
+ type: "payment-error",
1946
+ response: {
1947
+ status: 402,
1948
+ headers: { "Content-Type": "application/json" },
1949
+ body: { error: verifyResult.error }
1950
+ }
1951
+ };
1952
+ }
1953
+ return {
1954
+ type: "payment-presettle",
1955
+ paymentPayload,
1956
+ paymentRequirements: serverReq,
1957
+ operation: "subscribe",
1958
+ settle: async () => {
1959
+ const r = await scheme.settleSubscribe(paymentPayload, serverReq);
1960
+ return r.success ? {
1961
+ success: true,
1962
+ headers: r.headers,
1963
+ data: { subId: r.subId, subscription: r.subscription }
1964
+ } : { success: false, error: r.error };
1965
+ }
1966
+ };
1967
+ }
1968
+ /**
1969
+ * period dispatch helper — Cancel flow.
1970
+ *
1971
+ * Reads JSON body { auth: CancelAuth, subId: string }, runs verifyCancel
1972
+ * then wraps settleCancel as a payment-presettle (settle-before-handler so
1973
+ * the cancelation is on-chain before the seller's response).
1974
+ */
1975
+ async tryDispatchCancelFlow(adapter, routeConfig, paymentOptions) {
1976
+ let scheme = null;
1977
+ for (const opt of paymentOptions) {
1978
+ if (!opt.network || !opt.scheme) continue;
1979
+ const resolved = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
1980
+ if (resolved) {
1981
+ scheme = resolved;
1982
+ break;
1983
+ }
1984
+ }
1985
+ if (!scheme) return null;
1986
+ const body = adapter.getBody?.() ?? {};
1987
+ if (!body.auth || !body.subId) {
1988
+ return {
1989
+ type: "payment-error",
1990
+ response: {
1991
+ status: 400,
1992
+ headers: { "Content-Type": "application/json" },
1993
+ body: { error: "cancel: body must include auth and subId" }
1994
+ }
1995
+ };
1996
+ }
1997
+ const verifyResult = await scheme.verifyCancel(body.auth, body.subId);
1998
+ if (!verifyResult.ok) {
1999
+ return {
2000
+ type: "payment-error",
2001
+ response: {
2002
+ status: 402,
2003
+ headers: { "Content-Type": "application/json" },
2004
+ body: { error: verifyResult.error }
2005
+ }
2006
+ };
2007
+ }
2008
+ void routeConfig;
2009
+ const settleScheme = scheme;
2010
+ const auth = body.auth;
2011
+ const subId = body.subId;
2012
+ return {
2013
+ type: "payment-presettle",
2014
+ paymentPayload: { x402Version: 2, accepted: null, payload: {} },
2015
+ paymentRequirements: null,
2016
+ operation: "cancel",
2017
+ settle: async () => {
2018
+ const r = await settleScheme.settleCancel(auth, subId);
2019
+ return r.success ? { success: true, headers: r.headers, data: { subId } } : { success: false, error: r.error };
2020
+ }
2021
+ };
2022
+ }
2023
+ /**
2024
+ * period dispatch helper — Cancel-Pending-Change flow.
2025
+ *
2026
+ * Reads JSON body `{ auth: PendingChangeCancelAuth, subId: string }`. The
2027
+ * auth must carry `newSubId` (matches the currently PENDING downgrade
2028
+ * target). Runs verifyCancelPendingChange then wraps
2029
+ * settleCancelPendingChange as a payment-presettle.
2030
+ */
2031
+ async tryDispatchCancelPendingChangeFlow(adapter, routeConfig, paymentOptions) {
2032
+ let scheme = null;
2033
+ for (const opt of paymentOptions) {
2034
+ if (!opt.network || !opt.scheme) continue;
2035
+ const resolved = await this.resolveSubscriptionScheme(opt.network, opt.scheme);
2036
+ if (resolved) {
2037
+ scheme = resolved;
2038
+ break;
2039
+ }
2040
+ }
2041
+ if (!scheme) return null;
2042
+ const body = adapter.getBody?.() ?? {};
2043
+ if (!body.auth || !body.subId) {
2044
+ return {
2045
+ type: "payment-error",
2046
+ response: {
2047
+ status: 400,
2048
+ headers: { "Content-Type": "application/json" },
2049
+ body: { error: "cancel-pending-change: body must include auth and subId" }
2050
+ }
2051
+ };
2052
+ }
2053
+ if (!body.auth.newSubId) {
2054
+ return {
2055
+ type: "payment-error",
2056
+ response: {
2057
+ status: 400,
2058
+ headers: { "Content-Type": "application/json" },
2059
+ body: { error: "cancel-pending-change: auth.newSubId is required" }
2060
+ }
2061
+ };
2062
+ }
2063
+ const verifyResult = await scheme.verifyCancelPendingChange(body.auth, body.subId);
2064
+ if (!verifyResult.ok) {
2065
+ return {
2066
+ type: "payment-error",
2067
+ response: {
2068
+ status: 402,
2069
+ headers: { "Content-Type": "application/json" },
2070
+ body: { error: verifyResult.error }
2071
+ }
2072
+ };
2073
+ }
2074
+ void routeConfig;
2075
+ const settleScheme = scheme;
2076
+ const auth = body.auth;
2077
+ const subId = body.subId;
2078
+ return {
2079
+ type: "payment-presettle",
2080
+ paymentPayload: { x402Version: 2, accepted: null, payload: {} },
2081
+ paymentRequirements: null,
2082
+ operation: "cancel-pending-change",
2083
+ settle: async () => {
2084
+ const r = await settleScheme.settleCancelPendingChange(auth, subId);
2085
+ return r.success ? { success: true, headers: r.headers, data: { subId: r.subId } } : { success: false, error: r.error };
2086
+ }
2087
+ };
2088
+ }
1507
2089
  /**
1508
2090
  * Build HTTPResponseInstructions for settlement failure.
1509
2091
  * Uses settlementFailedResponseBody hook if configured, otherwise defaults to empty body.
@@ -1615,8 +2197,25 @@ var x402HTTPResourceServer = class {
1615
2197
  console.warn("Failed to decode PAYMENT-SIGNATURE header:", error);
1616
2198
  }
1617
2199
  }
2200
+ const subHeader = adapter.getHeader("app-payment") || adapter.getHeader("APP-PAYMENT");
2201
+ if (subHeader) {
2202
+ try {
2203
+ const json = Buffer.from(subHeader, "base64").toString("utf8");
2204
+ return JSON.parse(json);
2205
+ } catch (error) {
2206
+ console.warn("Failed to decode APP-PAYMENT header:", error);
2207
+ }
2208
+ }
1618
2209
  return null;
1619
2210
  }
2211
+ /**
2212
+ * Extract `APP-Access` header (subscription access-flow). Returns the raw
2213
+ * base64 string so callers can pass it through to `decodeAccessProof` in
2214
+ * the subscription codec.
2215
+ */
2216
+ extractAccessProofHeader(adapter) {
2217
+ return adapter.getHeader("app-access") || adapter.getHeader("APP-Access") || null;
2218
+ }
1620
2219
  /**
1621
2220
  * Check if request is from a web browser
1622
2221
  *
@@ -1772,6 +2371,15 @@ var x402HTTPResourceServer = class {
1772
2371
  return 0;
1773
2372
  }
1774
2373
  };
2374
+ function collectAcceptedPlanIds(options) {
2375
+ const seen = /* @__PURE__ */ new Set();
2376
+ for (const opt of options) {
2377
+ const extra = opt.extra;
2378
+ const id = extra?.plan?.id;
2379
+ if (typeof id === "string" && id.length > 0) seen.add(id);
2380
+ }
2381
+ return Array.from(seen);
2382
+ }
1775
2383
 
1776
2384
  export {
1777
2385
  HTTPFacilitatorClient,
@@ -1789,4 +2397,4 @@ export {
1789
2397
  decodePaymentResponseHeader,
1790
2398
  x402HTTPClient
1791
2399
  };
1792
- //# sourceMappingURL=chunk-XBQG2CDV.mjs.map
2400
+ //# sourceMappingURL=chunk-EYS4TWVA.mjs.map