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