@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.
- package/dist/cjs/OKXFacilitatorClient-Bqyw9fzj.d.ts +69 -0
- package/dist/cjs/client/index.d.ts +1 -1
- package/dist/cjs/client/index.js +34 -0
- package/dist/cjs/client/index.js.map +1 -1
- package/dist/cjs/facilitator/index.d.ts +2 -2
- package/dist/cjs/facilitator/index.js +166 -4
- package/dist/cjs/facilitator/index.js.map +1 -1
- package/dist/cjs/http/index.d.ts +5 -3
- package/dist/cjs/http/index.js +1241 -7
- package/dist/cjs/http/index.js.map +1 -1
- package/dist/cjs/index-2gWfiUbK.d.ts +713 -0
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +166 -4
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/{mechanisms-sojpSwWW.d.ts → mechanisms-LhI9qkRo.d.ts} +509 -1
- package/dist/cjs/server/index.d.ts +4 -2
- package/dist/cjs/server/index.js +1256 -7
- package/dist/cjs/server/index.js.map +1 -1
- package/dist/cjs/subscription/index.d.ts +3 -0
- package/dist/cjs/subscription/index.js +600 -0
- package/dist/cjs/subscription/index.js.map +1 -0
- package/dist/cjs/types/index.d.ts +1 -1
- package/dist/cjs/utils/index.d.ts +1 -1
- package/dist/cjs/{x402HTTPResourceServer-CcsAkcgI.d.ts → x402HTTPResourceServer-B0mXzV8r.d.ts} +114 -1
- package/dist/esm/OKXFacilitatorClient-z-cCE5Db.d.mts +69 -0
- package/dist/esm/chunk-4KASWSSY.mjs +257 -0
- package/dist/esm/chunk-4KASWSSY.mjs.map +1 -0
- package/dist/esm/chunk-CKXR4QVD.mjs +274 -0
- package/dist/esm/chunk-CKXR4QVD.mjs.map +1 -0
- package/dist/esm/{chunk-XBQG2CDV.mjs → chunk-EYS4TWVA.mjs} +617 -9
- package/dist/esm/chunk-EYS4TWVA.mjs.map +1 -0
- package/dist/esm/client/index.d.mts +1 -1
- package/dist/esm/client/index.mjs +3 -2
- package/dist/esm/client/index.mjs.map +1 -1
- package/dist/esm/facilitator/index.d.mts +2 -2
- package/dist/esm/facilitator/index.mjs +2 -1
- package/dist/esm/facilitator/index.mjs.map +1 -1
- package/dist/esm/http/index.d.mts +5 -3
- package/dist/esm/http/index.mjs +3 -2
- package/dist/esm/index-DKbqlTu_.d.mts +713 -0
- package/dist/esm/index.d.mts +2 -2
- package/dist/esm/index.mjs +2 -1
- package/dist/esm/{mechanisms-sojpSwWW.d.mts → mechanisms-LhI9qkRo.d.mts} +509 -1
- package/dist/esm/server/index.d.mts +4 -2
- package/dist/esm/server/index.mjs +3 -2
- package/dist/esm/subscription/index.d.mts +3 -0
- package/dist/esm/subscription/index.mjs +309 -0
- package/dist/esm/subscription/index.mjs.map +1 -0
- package/dist/esm/types/index.d.mts +1 -1
- package/dist/esm/utils/index.d.mts +1 -1
- package/dist/esm/{x402HTTPResourceServer-DBeutKxq.d.mts → x402HTTPResourceServer-56Tq3Jup.d.mts} +114 -1
- package/package.json +12 -1
- package/dist/cjs/OKXFacilitatorClient-BvyQB1QM.d.ts +0 -59
- package/dist/esm/OKXFacilitatorClient-D5E3LX50.d.mts +0 -59
- package/dist/esm/chunk-O3IYMTNT.mjs +0 -118
- package/dist/esm/chunk-O3IYMTNT.mjs.map +0 -1
- package/dist/esm/chunk-XBQG2CDV.mjs.map +0 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
x402Version
|
|
3
|
-
} from "./chunk-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
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-
|
|
2400
|
+
//# sourceMappingURL=chunk-EYS4TWVA.mjs.map
|