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