@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
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CANCEL_AUTH_TYPES,
|
|
3
|
+
PENDING_CHANGE_CANCEL_AUTH_TYPES,
|
|
4
|
+
PERMIT2_TYPES,
|
|
5
|
+
SUBSCRIPTION_TERMS_TYPES,
|
|
6
|
+
ZERO_BYTES32,
|
|
7
|
+
asSubscriptionPaymentInner,
|
|
8
|
+
base64DecodeUtf8,
|
|
9
|
+
base64EncodeUtf8,
|
|
10
|
+
buildCancelAuthTypedData,
|
|
11
|
+
buildPendingChangeCancelAuthTypedData,
|
|
12
|
+
buildPermit2TypedData,
|
|
13
|
+
buildSubscriptionTermsTypedData,
|
|
14
|
+
computePermitSingleStructHash,
|
|
15
|
+
decodePaymentPayload,
|
|
16
|
+
encodePaymentPayload,
|
|
17
|
+
parseChainIdFromNetwork,
|
|
18
|
+
parsePaymentRequired
|
|
19
|
+
} from "../chunk-CKXR4QVD.mjs";
|
|
20
|
+
|
|
21
|
+
// src/subscription/types.ts
|
|
22
|
+
function hasSubscriptionCapability(scheme) {
|
|
23
|
+
return typeof scheme === "object" && scheme !== null && "verifyAccess" in scheme && "settlementMode" in scheme && scheme.settlementMode === "pre";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/subscription/errors.ts
|
|
27
|
+
var ErrorCode = {
|
|
28
|
+
// subscribe / change
|
|
29
|
+
TermsBindingInvalid: "terms_binding_invalid",
|
|
30
|
+
AllowanceInsufficient: "allowance_insufficient",
|
|
31
|
+
AllowanceExpired: "allowance_expired",
|
|
32
|
+
// charge
|
|
33
|
+
PeriodNotDue: "period_not_due",
|
|
34
|
+
InsufficientBalance: "insufficient_balance",
|
|
35
|
+
// charge / cancel / access
|
|
36
|
+
SubscriptionNotActive: "subscription_not_active",
|
|
37
|
+
/**
|
|
38
|
+
* SDK-local code. Surfaced by `verifyAccess` when local period math
|
|
39
|
+
* yields `currentCalculatePeriod === 0` — subscription exists but
|
|
40
|
+
* `nowSec < startAt`, i.e. has not yet entered its first chargeable
|
|
41
|
+
* period.
|
|
42
|
+
*/
|
|
43
|
+
SubscriptionNotYetActive: "subscription_not_yet_active",
|
|
44
|
+
UnauthorizedCaller: "unauthorized_caller",
|
|
45
|
+
// cancel
|
|
46
|
+
CancelSignatureInvalid: "cancel_signature_invalid",
|
|
47
|
+
CancelNonceUsed: "cancel_nonce_used",
|
|
48
|
+
// change
|
|
49
|
+
TierSame: "tier_same",
|
|
50
|
+
ChangeEffectiveAtMismatch: "change_effective_at_mismatch",
|
|
51
|
+
MerchantMismatch: "merchant_mismatch",
|
|
52
|
+
PayerMismatch: "payer_mismatch",
|
|
53
|
+
PendingChangeExists: "pending_change_exists",
|
|
54
|
+
SubNotActiveForChange: "sub_not_active_for_change",
|
|
55
|
+
// cancel-pending-change
|
|
56
|
+
NoPendingChange: "no_pending_change",
|
|
57
|
+
// all writes
|
|
58
|
+
ConfirmationTimeout: "confirmation_timeout"
|
|
59
|
+
};
|
|
60
|
+
var ChargeErrorCode = {
|
|
61
|
+
PeriodNotDue: ErrorCode.PeriodNotDue,
|
|
62
|
+
SubscriptionNotActive: ErrorCode.SubscriptionNotActive,
|
|
63
|
+
InsufficientBalance: ErrorCode.InsufficientBalance,
|
|
64
|
+
AllowanceExpired: ErrorCode.AllowanceExpired,
|
|
65
|
+
UnauthorizedCaller: ErrorCode.UnauthorizedCaller,
|
|
66
|
+
ConfirmationTimeout: ErrorCode.ConfirmationTimeout
|
|
67
|
+
};
|
|
68
|
+
var ChargeError = class extends Error {
|
|
69
|
+
constructor(code, subId, txHash) {
|
|
70
|
+
super(`charge failed: ${code} (sub=${subId})`);
|
|
71
|
+
this.name = "ChargeError";
|
|
72
|
+
this.code = code;
|
|
73
|
+
this.subId = subId;
|
|
74
|
+
this.txHash = txHash;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/subscription/store.ts
|
|
79
|
+
var InMemoryStore = class {
|
|
80
|
+
constructor() {
|
|
81
|
+
this.data = /* @__PURE__ */ new Map();
|
|
82
|
+
}
|
|
83
|
+
async get(subId) {
|
|
84
|
+
const sub = this.data.get(subId);
|
|
85
|
+
return sub ? { ...sub } : null;
|
|
86
|
+
}
|
|
87
|
+
async put(sub) {
|
|
88
|
+
this.data.set(sub.subId, { ...sub });
|
|
89
|
+
}
|
|
90
|
+
async delete(subId) {
|
|
91
|
+
this.data.delete(subId);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Return all subscriptions, ordered by `startAt` ascending. Not part of
|
|
95
|
+
* the SubscriptionStore interface — admin/debug helper, not used by the
|
|
96
|
+
* scheme. Production backends should expose paginated equivalents.
|
|
97
|
+
*/
|
|
98
|
+
async list() {
|
|
99
|
+
return Array.from(this.data.values()).map((s) => ({ ...s })).sort((a, b) => a.startAt - b.startAt);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/subscription/client.ts
|
|
104
|
+
var SubscriptionClient = class {
|
|
105
|
+
constructor(config) {
|
|
106
|
+
this.scheme = config.scheme;
|
|
107
|
+
this.store = config.store;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run one charge period for a subscription. Throws `ChargeError` (one of 6
|
|
111
|
+
* codes) on facilitator-side failure. Internally `scheme.charge` already
|
|
112
|
+
* updates the store on success (and on `planChangeTriggered`); the client is
|
|
113
|
+
* a pass-through.
|
|
114
|
+
*/
|
|
115
|
+
async charge(subId) {
|
|
116
|
+
return this.scheme.charge(subId);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Seller-initiated cancel (e.g. ToS violation, fraud, business reason).
|
|
120
|
+
*
|
|
121
|
+
* The SDK does NOT hold the Seller's merchant private key; the Seller must
|
|
122
|
+
* construct + sign a `CancelAuth` with `by=1 (MERCHANT)` outside and pass
|
|
123
|
+
* it in. SDK runs verifyCancel (sanity check on the auth) then settleCancel
|
|
124
|
+
* (facilitator + store mark canceled).
|
|
125
|
+
*
|
|
126
|
+
* Throws on either verify or settle failure.
|
|
127
|
+
*/
|
|
128
|
+
async cancelBySeller(subId, auth, _reason) {
|
|
129
|
+
const v = await this.scheme.verifyCancel(auth, subId);
|
|
130
|
+
if (!v.ok) {
|
|
131
|
+
throw new Error(`cancelBySeller.verify failed: ${v.error}`);
|
|
132
|
+
}
|
|
133
|
+
const r = await this.scheme.settleCancel(auth, subId);
|
|
134
|
+
if (!r.success) {
|
|
135
|
+
throw new Error(`cancelBySeller.settle failed: ${r.error}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Re-sync a subscription from chain and repair the store. Use when:
|
|
140
|
+
* - `charge` threw `SubscriptionNotActive` (buyer may have cancelled
|
|
141
|
+
* directly via the facilitator or contract)
|
|
142
|
+
* - `charge` threw `ConfirmationTimeout` (network-level failure; chain
|
|
143
|
+
* may or may not have written)
|
|
144
|
+
* - periodic reconciliation
|
|
145
|
+
*
|
|
146
|
+
* If the synced sub is in `"changed"` state, the downstream `changedToSubId`
|
|
147
|
+
* is also fetched and persisted, so the Seller's `dueIndex` can switch over
|
|
148
|
+
* to the new sub without manual intervention.
|
|
149
|
+
*/
|
|
150
|
+
async syncFromChain(subId) {
|
|
151
|
+
const latest = await this.scheme.getSubscription(subId);
|
|
152
|
+
if (!latest) return null;
|
|
153
|
+
await this.store.put(latest);
|
|
154
|
+
if (latest.state === "changed" && latest.changedToSubId) {
|
|
155
|
+
const newSub = await this.scheme.getSubscription(latest.changedToSubId);
|
|
156
|
+
if (newSub) await this.store.put(newSub);
|
|
157
|
+
}
|
|
158
|
+
return latest;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Direct store read. Cheap; does NOT touch the chain. Use this for hot-path
|
|
162
|
+
* lookups (e.g. resolving subId to plan/tier for business logic). For chain
|
|
163
|
+
* state of record, use `syncFromChain`.
|
|
164
|
+
*/
|
|
165
|
+
async getSubscription(subId) {
|
|
166
|
+
return this.store.get(subId);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// src/subscription/codec/verify-terms.ts
|
|
171
|
+
function addrEq(a, b) {
|
|
172
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
173
|
+
}
|
|
174
|
+
function hexEq(a, b) {
|
|
175
|
+
return !!a && a.toLowerCase() === b.toLowerCase();
|
|
176
|
+
}
|
|
177
|
+
function verifyTermsBindRequirements(terms, requirements) {
|
|
178
|
+
const extra = requirements.extra ?? {};
|
|
179
|
+
if (!extra.plan || extra.amountPerPeriod === void 0 || extra.facilitator === void 0) {
|
|
180
|
+
return ErrorCode.TermsBindingInvalid;
|
|
181
|
+
}
|
|
182
|
+
if (!addrEq(terms.merchant, requirements.payTo)) return ErrorCode.MerchantMismatch;
|
|
183
|
+
if (!addrEq(terms.token, requirements.asset)) return ErrorCode.TermsBindingInvalid;
|
|
184
|
+
if (!addrEq(terms.facilitator, extra.facilitator)) return ErrorCode.TermsBindingInvalid;
|
|
185
|
+
if (terms.amountPerPeriod !== extra.amountPerPeriod) return ErrorCode.TermsBindingInvalid;
|
|
186
|
+
if (terms.periodSec !== extra.periodSec) return ErrorCode.TermsBindingInvalid;
|
|
187
|
+
if (terms.maxPeriods !== extra.maxPeriods) return ErrorCode.TermsBindingInvalid;
|
|
188
|
+
if (terms.periodMode !== (extra.periodMode ?? 0)) return ErrorCode.TermsBindingInvalid;
|
|
189
|
+
if (terms.planTier !== extra.plan.tier) return ErrorCode.TermsBindingInvalid;
|
|
190
|
+
if (extra.startAt !== void 0 && terms.startAt !== extra.startAt) {
|
|
191
|
+
return ErrorCode.TermsBindingInvalid;
|
|
192
|
+
}
|
|
193
|
+
const expectedInitPeriods = extra.initialCharge?.periodCount ?? 0;
|
|
194
|
+
const expectedInitAmount = extra.initialCharge?.totalAmount ?? "0";
|
|
195
|
+
if (terms.initialChargePeriods !== expectedInitPeriods) return ErrorCode.TermsBindingInvalid;
|
|
196
|
+
if (terms.initialChargeAmount !== expectedInitAmount) return ErrorCode.TermsBindingInvalid;
|
|
197
|
+
if (extra.changeFrom) {
|
|
198
|
+
if (!hexEq(terms.changeFromSubId, extra.changeFrom.fromSubId)) {
|
|
199
|
+
return ErrorCode.TermsBindingInvalid;
|
|
200
|
+
}
|
|
201
|
+
const expectedEff = extra.changeFrom.effectiveAt === "immediate" ? 1 : extra.changeFrom.effectiveAt === "period_end" ? 2 : 0;
|
|
202
|
+
if (terms.changeEffectiveAt !== expectedEff) return ErrorCode.TermsBindingInvalid;
|
|
203
|
+
} else {
|
|
204
|
+
if (terms.changeFromSubId !== ZERO_BYTES32) return ErrorCode.TermsBindingInvalid;
|
|
205
|
+
if (terms.changeEffectiveAt !== 0) return ErrorCode.TermsBindingInvalid;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/subscription/codec/period-math.ts
|
|
211
|
+
var PERIOD_MODE_CALENDAR_MONTH = 1;
|
|
212
|
+
function computeElapsedPeriods(periodMode, startAt, billingAnchorAt, periodSec, nowSec) {
|
|
213
|
+
if (nowSec < startAt) return 0;
|
|
214
|
+
if (periodMode === PERIOD_MODE_CALENDAR_MONTH) {
|
|
215
|
+
const anchor = billingAnchorAt > 0 ? billingAnchorAt : startAt;
|
|
216
|
+
const startOffset = elapsedCalendarMonths(anchor, startAt);
|
|
217
|
+
return elapsedCalendarMonths(anchor, nowSec) - startOffset + 1;
|
|
218
|
+
}
|
|
219
|
+
if (periodSec <= 0) return 0;
|
|
220
|
+
return Math.floor((nowSec - startAt) / periodSec) + 1;
|
|
221
|
+
}
|
|
222
|
+
function elapsedCalendarMonths(anchorSec, tsSec) {
|
|
223
|
+
if (tsSec <= anchorSec) return 0;
|
|
224
|
+
const anchor = new Date(anchorSec * 1e3);
|
|
225
|
+
const ts = new Date(tsSec * 1e3);
|
|
226
|
+
let diff = (ts.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (ts.getUTCMonth() - anchor.getUTCMonth());
|
|
227
|
+
if (diff < 0) return 0;
|
|
228
|
+
if (addCalendarMonths(anchorSec, diff) > tsSec) diff--;
|
|
229
|
+
return Math.max(diff, 0);
|
|
230
|
+
}
|
|
231
|
+
function addCalendarMonths(anchorSec, n) {
|
|
232
|
+
const anchor = new Date(anchorSec * 1e3);
|
|
233
|
+
const targetYear = anchor.getUTCFullYear() + Math.floor((anchor.getUTCMonth() + n) / 12);
|
|
234
|
+
const targetMonth = ((anchor.getUTCMonth() + n) % 12 + 12) % 12;
|
|
235
|
+
const daysInTargetMonth = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate();
|
|
236
|
+
const day = Math.min(anchor.getUTCDate(), daysInTargetMonth);
|
|
237
|
+
const ts = Date.UTC(
|
|
238
|
+
targetYear,
|
|
239
|
+
targetMonth,
|
|
240
|
+
day,
|
|
241
|
+
anchor.getUTCHours(),
|
|
242
|
+
anchor.getUTCMinutes(),
|
|
243
|
+
anchor.getUTCSeconds(),
|
|
244
|
+
anchor.getUTCMilliseconds()
|
|
245
|
+
);
|
|
246
|
+
return Math.floor(ts / 1e3);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/subscription/codec/access-proof.ts
|
|
250
|
+
import { encodePacked, keccak256 } from "viem";
|
|
251
|
+
function buildAccessProofMessage(input) {
|
|
252
|
+
return keccak256(
|
|
253
|
+
encodePacked(
|
|
254
|
+
["bytes32", "address", "uint256"],
|
|
255
|
+
[input.subId, input.payer, BigInt(input.timestamp)]
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
function encodeAccessProof(proof) {
|
|
260
|
+
return base64EncodeUtf8(JSON.stringify(proof));
|
|
261
|
+
}
|
|
262
|
+
function decodeAccessProof(headerValue) {
|
|
263
|
+
const json = base64DecodeUtf8(headerValue);
|
|
264
|
+
const parsed = JSON.parse(json);
|
|
265
|
+
if (!parsed || parsed.kind !== "subscription-id") {
|
|
266
|
+
throw new Error(`decodeAccessProof: expected kind="subscription-id", got "${parsed?.kind}"`);
|
|
267
|
+
}
|
|
268
|
+
return parsed;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/subscription/facilitator-client.ts
|
|
272
|
+
function supportsSubscription(client) {
|
|
273
|
+
const c = client;
|
|
274
|
+
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";
|
|
275
|
+
}
|
|
276
|
+
export {
|
|
277
|
+
CANCEL_AUTH_TYPES,
|
|
278
|
+
ChargeError,
|
|
279
|
+
ChargeErrorCode,
|
|
280
|
+
ErrorCode,
|
|
281
|
+
InMemoryStore,
|
|
282
|
+
PENDING_CHANGE_CANCEL_AUTH_TYPES,
|
|
283
|
+
PERMIT2_TYPES,
|
|
284
|
+
SUBSCRIPTION_TERMS_TYPES,
|
|
285
|
+
SubscriptionClient,
|
|
286
|
+
ZERO_BYTES32,
|
|
287
|
+
addCalendarMonths,
|
|
288
|
+
asSubscriptionPaymentInner,
|
|
289
|
+
base64DecodeUtf8,
|
|
290
|
+
base64EncodeUtf8,
|
|
291
|
+
buildAccessProofMessage,
|
|
292
|
+
buildCancelAuthTypedData,
|
|
293
|
+
buildPendingChangeCancelAuthTypedData,
|
|
294
|
+
buildPermit2TypedData,
|
|
295
|
+
buildSubscriptionTermsTypedData,
|
|
296
|
+
computeElapsedPeriods,
|
|
297
|
+
computePermitSingleStructHash,
|
|
298
|
+
decodeAccessProof,
|
|
299
|
+
decodePaymentPayload,
|
|
300
|
+
elapsedCalendarMonths,
|
|
301
|
+
encodeAccessProof,
|
|
302
|
+
encodePaymentPayload,
|
|
303
|
+
hasSubscriptionCapability,
|
|
304
|
+
parseChainIdFromNetwork,
|
|
305
|
+
parsePaymentRequired,
|
|
306
|
+
supportsSubscription,
|
|
307
|
+
verifyTermsBindRequirements
|
|
308
|
+
};
|
|
309
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/subscription/types.ts","../../../src/subscription/errors.ts","../../../src/subscription/store.ts","../../../src/subscription/client.ts","../../../src/subscription/codec/verify-terms.ts","../../../src/subscription/codec/period-math.ts","../../../src/subscription/codec/access-proof.ts","../../../src/subscription/facilitator-client.ts"],"sourcesContent":["import type { PaymentPayload, PaymentRequirements } from \"../types/payments\";\n\n/**\n * Subscription state: `active`, `canceled`, `completed`, `changed`.\n */\n/**\n * Local mirror of facilitator's SubscriptionState enum. Numeric mapping:\n * 0 pending / 1 active / 2 completed / 3 canceled / 4 changed / 99 failed\n */\nexport type SubscriptionState =\n | \"pending\"\n | \"active\"\n | \"completed\"\n | \"canceled\"\n | \"changed\"\n | \"failed\";\n\n/**\n * Set on an ACTIVE sub when a downgrade has been scheduled but not yet\n * activated. `state` lets the seller observe the terminal disposition\n * (PENDING / ACTIVATED / CANCELED / EXPIRED) via GET /pending.\n */\nexport interface PendingPlanChange {\n subId: string;\n newSubId: string;\n effectiveFromPeriod: number;\n /** 0 pending / 1 activated / 2 canceled / 3 expired. */\n state: number;\n}\n\n/**\n * Seller-side projection of a subscription. All fields come from the\n * facilitator GET /subscriptions/detail endpoint; the store never holds\n * data the facilitator can't refresh.\n *\n * Snapshot fields (`isActive` / `serviceEnded` / `currentPeriod` /\n * `elapsedPeriods` / `nextChargeableAt`) are valid as of the last sync\n * only; they drift with wall-clock time.\n */\nexport interface Subscription {\n subId: string;\n payer: string;\n merchant: string;\n token: string;\n amountPerPeriod: string;\n /** 0 fixed_seconds / 1 calendar_month. */\n periodMode: number;\n periodSec: number;\n /** Calendar-month billing anchor (Unix s). Undefined or 0 in fixed_seconds mode. */\n billingAnchorAt?: number;\n maxPeriods: number;\n startAt: number;\n state: SubscriptionState;\n lastChargedPeriod: number;\n totalPulled: string;\n planId: string;\n planTier: number;\n changedToSubId?: string;\n pendingPlanChange?: PendingPlanChange;\n /** Derived snapshot — true iff state==ACTIVE && now < endAt. */\n isActive?: boolean;\n /** Derived snapshot — true iff state==ACTIVE && !isActive (expired, not yet finalized). */\n serviceEnded?: boolean;\n /** Derived snapshot — current period number, clamped to maxPeriods. */\n currentPeriod?: number;\n /** Derived snapshot — real elapsed period count, NOT clamped (use this for expiry checks). */\n elapsedPeriods?: number;\n /** Derived snapshot — next chargeable boundary (Unix s); null when all periods are charged. */\n nextChargeableAt?: number;\n}\n\nexport interface AccessProof {\n kind: \"subscription-id\";\n subId: string;\n payer: string;\n timestamp: number;\n signature: string;\n}\n\n/**\n * `CancelAuth.initiator` enum — only payer / merchant.\n */\nexport type CancelInitiator = \"payer\" | \"merchant\";\n\n/**\n * EIP-712 `CancelAuth` payload (subscription contract domain):\n * `CancelAuth(uint8 action, bytes32 subId, uint8 initiator, bytes32 nonce, uint64 deadline)`\n *\n * `action` is locked to `0 = cancel_subscription`. `cancel_pending_change`\n * uses the standalone `cancel-pending-change` endpoint with its own\n * `PendingChangeCancelAuth` signature.\n */\nexport interface CancelAuth {\n action: 0;\n subId: string;\n initiator: 0 | 1;\n nonce: string;\n deadline: number;\n signature: string;\n}\n\n/**\n * EIP-712 `PendingChangeCancelAuth` payload (subscription contract domain).\n * Payer-only.\n *\n * TypeHash:\n * keccak256(\"PendingChangeCancelAuth(bytes32 subId,bytes32 newSubId,bytes32 nonce,uint64 deadline)\")\n *\n * `newSubId` MUST equal the currently-PENDING `pendingPlanChange.newSubId`\n * (the to-be-cancelled downgrade target); facilitator rejects with\n * `pending_cancel_target_mismatch` otherwise.\n */\nexport interface PendingChangeCancelAuth {\n subId: string;\n newSubId: string;\n nonce: string;\n deadline: number;\n signature: string;\n}\n\nexport interface PlanInitialCharge {\n periodCount: number;\n totalAmount: string;\n}\n\nexport interface PlanCatalogEntry {\n id: string;\n tier: number;\n amountPerPeriod: string;\n /** 0 fixed_seconds (default) / 1 calendar_month. */\n periodMode?: 0 | 1;\n periodSec: number;\n maxPeriods: number;\n /**\n * ERC-20 token address. Optional — if omitted, the EVM scheme fills from\n * `getDefaultAsset(network)` (same per-network map exact / upto /\n * aggr_deferred consume).\n */\n asset?: string;\n payTo: string;\n initialCharge?: PlanInitialCharge;\n name?: string;\n}\n\nexport type PlanCatalog = Record<string, PlanCatalogEntry>;\n\nexport interface AccessRouteRequirements {\n /**\n * PlanIds that satisfy this route. Derived from the route's `accepts`\n * payment options (`accepts[].extra.plan.id`). A subscription is allowed\n * access iff its `planId` appears in this list.\n *\n * `undefined` means \"no plan restriction\" — any active subscription on\n * the route passes (use sparingly).\n */\n acceptedPlanIds?: string[];\n\n /**\n * Full `PaymentRequirements` for every plan the route accepts — the same\n * list the seller declared as `RouteConfig.accepts`, resolved to wire\n * format. Each entry carries the plan metadata in `extra.plan`\n * (`{ id, tier, name }`) plus `extra.amountPerPeriod`, `extra.periodSec`,\n * `extra.periodMode`, `extra.maxPeriods`, etc. — everything an\n * `onBeforeAccess` hook needs to decide policy against catalog details\n * (upgrade offers, tier ceilings, per-plan feature flags) without joining\n * an external catalog table.\n */\n accepts?: PaymentRequirements[];\n}\n\n/**\n * Context passed to `OnBeforeAccessHook`. Carries the full stored\n * `Subscription` (so the seller can inspect any field — payer, planId,\n * lastChargedPeriod, changedToSubId, etc. — for arbitrary policy) plus\n * the incoming HTTP request shape and route metadata.\n */\nexport interface OnBeforeAccessContext {\n subscription: Subscription;\n request: {\n path: string;\n method: string;\n headers: Record<string, string>;\n };\n route: AccessRouteRequirements;\n}\n\n/**\n * Result of an `OnBeforeAccessHook`:\n * - `{ ok: true }` → allow the request through\n * - `{ ok: false }` → deny; `error` shows up in the 402 body, `retryAfter`\n * (seconds) becomes a `Retry-After` header hint, `upgradeOffers` lets\n * the seller point the buyer at an alternate plan\n *\n * Denial use cases: rate-limiting, quota exhaustion, bans / blacklists,\n * per-plan feature gating beyond the simple `acceptedPlanIds` allowlist.\n */\nexport type OnBeforeAccessResult =\n | { ok: true }\n | {\n ok: false;\n error?: string;\n retryAfter?: number;\n upgradeOffers?: PaymentRequirements[];\n };\n\n/**\n * Route-level hook fired AFTER `verifyAccess` succeeded (signature +\n * payer + plan-allowlist + period math) but BEFORE the handler runs.\n * The seller uses it to implement custom access policy — e.g. a ban list\n * keyed by subId or payer, per-plan feature flags, or dynamic quota.\n */\nexport type OnBeforeAccessHook = (ctx: OnBeforeAccessContext) => Promise<OnBeforeAccessResult>;\n\nexport type VerifyResultOk = { ok: true };\nexport type VerifyResultFail = { ok: false; error: string };\nexport type VerifyResult = VerifyResultOk | VerifyResultFail;\n\nexport interface VerifyChangeOk {\n ok: true;\n oldSubId: string;\n direction: \"upgrade\" | \"downgrade\";\n}\nexport type VerifyChangeResult = VerifyChangeOk | VerifyResultFail;\n\nexport interface VerifyAccessOk {\n ok: true;\n subscription: Subscription;\n}\nexport type VerifyAccessResult = VerifyAccessOk | VerifyResultFail;\n\n/**\n * Result of `verifyOwnership` — used by the change-route sniff path to\n * confirm the AccessProof signer owns the named subscription, without\n * imposing the plan-allowlist / period-math gating that `verifyAccess`\n * applies for resource consumption.\n */\nexport interface VerifyOwnershipOk {\n ok: true;\n subId: string;\n payer: string;\n subscription: Subscription;\n}\nexport type VerifyOwnershipResult = VerifyOwnershipOk | VerifyResultFail;\n\nexport type SettleResultFail = {\n success: false;\n error: string;\n /**\n * Set when the chain operation may still complete asynchronously: the\n * facilitator accepted the write call but returned `state=pending`, and the\n * SDK's client-side polling (5×1s) timed out before settlement. Seller\n * should remember `subId` and call `syncFromChain(subId)` later.\n */\n subId?: string;\n pending?: boolean;\n};\n\nexport interface SettleSubscribeOk {\n success: true;\n subId: string;\n subscription: Subscription;\n headers: Record<string, string>;\n}\nexport type SettleSubscribeResult = SettleSubscribeOk | SettleResultFail;\n\nexport interface SettleChangeOk {\n success: true;\n oldSubId: string;\n newSubId: string;\n operationType: \"upgrade\" | \"downgrade\";\n scheduledFromPeriod?: number;\n headers: Record<string, string>;\n}\nexport type SettleChangeResult = SettleChangeOk | SettleResultFail;\n\nexport interface SettleCancelOk {\n success: true;\n subId: string;\n headers: Record<string, string>;\n}\nexport type SettleCancelResult = SettleCancelOk | SettleResultFail;\n\n/**\n * Result of `settleCancelPendingChange` — cancels a scheduled downgrade\n * (removes `pendingPlanChange` on the old sub while the sub itself stays\n * ACTIVE). No refund, no state transition on the sub itself.\n */\nexport interface SettleCancelPendingChangeOk {\n success: true;\n subId: string;\n headers: Record<string, string>;\n}\nexport type SettleCancelPendingChangeResult = SettleCancelPendingChangeOk | SettleResultFail;\n\nexport interface ChargeResult {\n success: true;\n period: number;\n amount: string;\n txHash?: string;\n planChangeTriggered?: boolean;\n newSubId?: string;\n}\n\nexport interface SubscriptionCapability {\n readonly settlementMode: \"pre\";\n\n verifySubscribe(\n payload: PaymentPayload,\n requirements: PaymentRequirements,\n ): Promise<VerifyResult>;\n\n settleSubscribe(\n payload: PaymentPayload,\n requirements: PaymentRequirements,\n ): Promise<SettleSubscribeResult>;\n\n /**\n * Inject `extra.changeFrom = { fromSubId, fromPlanId, fromPlanTier,\n * direction, effectiveAt }` into each accept of a change route's 402\n * accepts. Direction / effectiveAt are derived per-accept by comparing\n * `accept.extra.plan.tier` against the stored `oldSub.planTier`. Same-tier\n * accepts are dropped (a change to the same tier is illegal —\n * `tier_same`).\n *\n * Returns `null` when the seller's local store has no record of\n * `currentSubId` — middleware then 404s the GET so buyers see the misuse.\n */\n enrichAcceptsForChange(\n accepts: PaymentRequirements[],\n currentSubId: string,\n ): Promise<PaymentRequirements[] | null>;\n\n verifyChange(\n payload: PaymentPayload,\n requirements: PaymentRequirements,\n ): Promise<VerifyChangeResult>;\n\n settleChange(\n payload: PaymentPayload,\n requirements: PaymentRequirements,\n ): Promise<SettleChangeResult>;\n\n verifyCancel(auth: CancelAuth, subId: string): Promise<VerifyResult>;\n\n settleCancel(auth: CancelAuth, subId: string): Promise<SettleCancelResult>;\n\n /**\n * Verify a `PendingChangeCancelAuth` before facilitator submission.\n * Requires `auth.subId == body.subId` and `auth.newSubId` equal to the\n * currently PENDING `pendingPlanChange.newSubId` — the SDK checks the\n * former; facilitator enforces the latter as\n * `pending_cancel_target_mismatch`.\n */\n verifyCancelPendingChange(auth: PendingChangeCancelAuth, subId: string): Promise<VerifyResult>;\n\n /**\n * Cancel a scheduled downgrade (`pendingPlanChange`) — the current sub\n * stays ACTIVE, only the pending row is retired. Facilitator returns the\n * new state; SDK re-pulls GET /detail to refresh the store entry.\n */\n settleCancelPendingChange(\n auth: PendingChangeCancelAuth,\n subId: string,\n ): Promise<SettleCancelPendingChangeResult>;\n\n verifyAccess(proof: AccessProof, route: AccessRouteRequirements): Promise<VerifyAccessResult>;\n\n /**\n * Lightweight ownership check for the change-route sniff path. Verifies\n * the AccessProof signature, looks up the sub in the store, and confirms\n * `sub.payer == proof.payer`. Deliberately does NOT enforce plan\n * allowlist or period math — the caller is identifying themselves to\n * receive change offers, not consuming a protected resource.\n */\n verifyOwnership(proof: AccessProof): Promise<VerifyOwnershipResult>;\n\n charge(subId: string): Promise<ChargeResult>;\n\n getSubscription(subId: string): Promise<Subscription | null>;\n}\n\nexport function hasSubscriptionCapability(scheme: unknown): scheme is SubscriptionCapability {\n return (\n typeof scheme === \"object\" &&\n scheme !== null &&\n \"verifyAccess\" in scheme &&\n \"settlementMode\" in scheme &&\n (scheme as { settlementMode: unknown }).settlementMode === \"pre\"\n );\n}\n","/**\n * Canonical facilitator error reasons. The SDK uses these literal strings\n * verbatim in verify / settle / charge return values so seller code can\n * switch on a single set of codes regardless of whether the rejection\n * originated in the SDK pre-check or in the facilitator response.\n */\nexport const ErrorCode = {\n // subscribe / change\n TermsBindingInvalid: \"terms_binding_invalid\",\n AllowanceInsufficient: \"allowance_insufficient\",\n AllowanceExpired: \"allowance_expired\",\n // charge\n PeriodNotDue: \"period_not_due\",\n InsufficientBalance: \"insufficient_balance\",\n // charge / cancel / access\n SubscriptionNotActive: \"subscription_not_active\",\n /**\n * SDK-local code. Surfaced by `verifyAccess` when local period math\n * yields `currentCalculatePeriod === 0` — subscription exists but\n * `nowSec < startAt`, i.e. has not yet entered its first chargeable\n * period.\n */\n SubscriptionNotYetActive: \"subscription_not_yet_active\",\n UnauthorizedCaller: \"unauthorized_caller\",\n // cancel\n CancelSignatureInvalid: \"cancel_signature_invalid\",\n CancelNonceUsed: \"cancel_nonce_used\",\n // change\n TierSame: \"tier_same\",\n ChangeEffectiveAtMismatch: \"change_effective_at_mismatch\",\n MerchantMismatch: \"merchant_mismatch\",\n PayerMismatch: \"payer_mismatch\",\n PendingChangeExists: \"pending_change_exists\",\n SubNotActiveForChange: \"sub_not_active_for_change\",\n // cancel-pending-change\n NoPendingChange: \"no_pending_change\",\n // all writes\n ConfirmationTimeout: \"confirmation_timeout\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\n/**\n * Charge-flow subset of ErrorCode. Seller scheduler switches on these 6\n * codes.\n */\nexport const ChargeErrorCode = {\n PeriodNotDue: ErrorCode.PeriodNotDue,\n SubscriptionNotActive: ErrorCode.SubscriptionNotActive,\n InsufficientBalance: ErrorCode.InsufficientBalance,\n AllowanceExpired: ErrorCode.AllowanceExpired,\n UnauthorizedCaller: ErrorCode.UnauthorizedCaller,\n ConfirmationTimeout: ErrorCode.ConfirmationTimeout,\n} as const;\n\nexport type ChargeErrorCode = (typeof ChargeErrorCode)[keyof typeof ChargeErrorCode];\n\nexport class ChargeError extends Error {\n public readonly code: ChargeErrorCode;\n public readonly subId: string;\n public readonly txHash?: string;\n\n constructor(code: ChargeErrorCode, subId: string, txHash?: string) {\n super(`charge failed: ${code} (sub=${subId})`);\n this.name = \"ChargeError\";\n this.code = code;\n this.subId = subId;\n this.txHash = txHash;\n }\n}\n","import type { Subscription } from \"./types\";\n\n/**\n * Persistence abstraction for subscription state.\n *\n * Intentionally minimal — every state transition is performed by `put`-ing\n * the full Subscription object, and every lookup is by `subId`. Address /\n * payer-indexed queries are deliberately NOT part of this contract: the\n * seller's own data model should own that (e.g. by mapping wallet →\n * subId(s) in its user table), keeping the SDK store free of any secondary\n * index requirement.\n */\nexport interface SubscriptionStore {\n get(subId: string): Promise<Subscription | null>;\n put(sub: Subscription): Promise<void>;\n delete(subId: string): Promise<void>;\n}\n\n/**\n * In-memory reference implementation. Suitable for development / unit tests /\n * single-process demos only — multi-process deployments must replace this\n * with a shared persistent backend.\n */\nexport class InMemoryStore implements SubscriptionStore {\n private readonly data = new Map<string, Subscription>();\n\n async get(subId: string): Promise<Subscription | null> {\n const sub = this.data.get(subId);\n return sub ? { ...sub } : null;\n }\n\n async put(sub: Subscription): Promise<void> {\n this.data.set(sub.subId, { ...sub });\n }\n\n async delete(subId: string): Promise<void> {\n this.data.delete(subId);\n }\n\n /**\n * Return all subscriptions, ordered by `startAt` ascending. Not part of\n * the SubscriptionStore interface — admin/debug helper, not used by the\n * scheme. Production backends should expose paginated equivalents.\n */\n async list(): Promise<Subscription[]> {\n return Array.from(this.data.values())\n .map(s => ({ ...s }))\n .sort((a, b) => a.startAt - b.startAt);\n }\n}\n","import type { CancelAuth, ChargeResult, Subscription, SubscriptionCapability } from \"./types\";\nimport type { SubscriptionStore } from \"./store\";\n\nexport interface SubscriptionClientConfig {\n /**\n * The scheme instance to delegate facilitator-touching operations to.\n * Typically the same `PermitSubscriptionScheme` instance that is registered\n * to `x402ResourceServer`, so HTTP main-line state and out-of-band charge\n * state share one source of truth.\n */\n scheme: SubscriptionCapability;\n /**\n * The store the scheme writes to. Required here because `getSubscription`\n * reads from the store (fast path) while `syncFromChain` writes to it after\n * re-fetching from chain.\n */\n store: SubscriptionStore;\n}\n\n/**\n * Out-of-band primitives for Seller scheduler / business code. SDK ships the\n * per-call atoms (charge / cancelBySeller / syncFromChain / etc.) but NOT a\n * scheduler — cron, due index, retry policy, distributed locks are all\n * Seller infrastructure.\n *\n * All methods are thin wrappers:\n * - `charge` delegates to scheme.charge (throws ChargeError on fail)\n * - `cancelBySeller` delegates to scheme.settleCancel with a Seller-built\n * CancelAuth\n * - `syncFromChain` re-pulls Subscription via scheme.getSubscription and\n * repairs the store; connects with the `state===\"changed\"`\n * chain to also sync the downstream new sub\n * - `getSubscription` direct store read (does NOT touch the chain)\n */\nexport class SubscriptionClient {\n protected readonly scheme: SubscriptionCapability;\n protected readonly store: SubscriptionStore;\n\n constructor(config: SubscriptionClientConfig) {\n this.scheme = config.scheme;\n this.store = config.store;\n }\n\n /**\n * Run one charge period for a subscription. Throws `ChargeError` (one of 6\n * codes) on facilitator-side failure. Internally `scheme.charge` already\n * updates the store on success (and on `planChangeTriggered`); the client is\n * a pass-through.\n */\n async charge(subId: string): Promise<ChargeResult> {\n return this.scheme.charge(subId);\n }\n\n /**\n * Seller-initiated cancel (e.g. ToS violation, fraud, business reason).\n *\n * The SDK does NOT hold the Seller's merchant private key; the Seller must\n * construct + sign a `CancelAuth` with `by=1 (MERCHANT)` outside and pass\n * it in. SDK runs verifyCancel (sanity check on the auth) then settleCancel\n * (facilitator + store mark canceled).\n *\n * Throws on either verify or settle failure.\n */\n async cancelBySeller(subId: string, auth: CancelAuth, _reason?: string): Promise<void> {\n const v = await this.scheme.verifyCancel(auth, subId);\n if (!v.ok) {\n throw new Error(`cancelBySeller.verify failed: ${v.error}`);\n }\n const r = await this.scheme.settleCancel(auth, subId);\n if (!r.success) {\n throw new Error(`cancelBySeller.settle failed: ${r.error}`);\n }\n }\n\n /**\n * Re-sync a subscription from chain and repair the store. Use when:\n * - `charge` threw `SubscriptionNotActive` (buyer may have cancelled\n * directly via the facilitator or contract)\n * - `charge` threw `ConfirmationTimeout` (network-level failure; chain\n * may or may not have written)\n * - periodic reconciliation\n *\n * If the synced sub is in `\"changed\"` state, the downstream `changedToSubId`\n * is also fetched and persisted, so the Seller's `dueIndex` can switch over\n * to the new sub without manual intervention.\n */\n async syncFromChain(subId: string): Promise<Subscription | null> {\n const latest = await this.scheme.getSubscription(subId);\n if (!latest) return null;\n await this.store.put(latest);\n\n if (latest.state === \"changed\" && latest.changedToSubId) {\n const newSub = await this.scheme.getSubscription(latest.changedToSubId);\n if (newSub) await this.store.put(newSub);\n }\n return latest;\n }\n\n /**\n * Direct store read. Cheap; does NOT touch the chain. Use this for hot-path\n * lookups (e.g. resolving subId to plan/tier for business logic). For chain\n * state of record, use `syncFromChain`.\n */\n async getSubscription(subId: string): Promise<Subscription | null> {\n return this.store.get(subId);\n }\n}\n","import type { Hex } from \"viem\";\n\nimport type { PaymentRequirements } from \"../../types/payments\";\nimport { ErrorCode } from \"../errors\";\nimport type { SubscriptionTerms } from \"./payload\";\nimport { ZERO_BYTES32, type SubscriptionRequirementsExtra } from \"./typed-data\";\n\nfunction addrEq(a: string, b: string): boolean {\n return a.toLowerCase() === b.toLowerCase();\n}\n\nfunction hexEq(a: Hex | undefined, b: Hex): boolean {\n return !!a && a.toLowerCase() === b.toLowerCase();\n}\n\n/**\n * Verify that buyer-signed `terms` bind to the server-advertised\n * PaymentRequirements. Without this field-by-field check a buyer could sign\n * a basic-tier accepted but submit terms claiming enterprise-tier access at\n * basic price. The contract verifies the signature but cannot see what the\n * seller offered in 402 — only the SDK can enforce this binding.\n *\n * All mismatches collapse to `terms_binding_invalid` so the caller's switch\n * statement stays compact.\n *\n * Returns `null` on match, an `ErrorCode` on first mismatch.\n */\nexport function verifyTermsBindRequirements(\n terms: SubscriptionTerms,\n requirements: PaymentRequirements,\n): ErrorCode | null {\n const extra = (requirements.extra ?? {}) as Partial<SubscriptionRequirementsExtra>;\n if (!extra.plan || extra.amountPerPeriod === undefined || extra.facilitator === undefined) {\n return ErrorCode.TermsBindingInvalid;\n }\n\n if (!addrEq(terms.merchant, requirements.payTo)) return ErrorCode.MerchantMismatch;\n if (!addrEq(terms.token, requirements.asset)) return ErrorCode.TermsBindingInvalid;\n if (!addrEq(terms.facilitator, extra.facilitator)) return ErrorCode.TermsBindingInvalid;\n\n if (terms.amountPerPeriod !== extra.amountPerPeriod) return ErrorCode.TermsBindingInvalid;\n if (terms.periodSec !== extra.periodSec) return ErrorCode.TermsBindingInvalid;\n if (terms.maxPeriods !== extra.maxPeriods) return ErrorCode.TermsBindingInvalid;\n if (terms.periodMode !== (extra.periodMode ?? 0)) return ErrorCode.TermsBindingInvalid;\n if (terms.planTier !== extra.plan.tier) return ErrorCode.TermsBindingInvalid;\n\n // Server-pinned startAt must be honored; if server left it unset (==undefined),\n // accept whatever the buyer signed (contract resolves 0 → block.timestamp).\n if (extra.startAt !== undefined && terms.startAt !== extra.startAt) {\n return ErrorCode.TermsBindingInvalid;\n }\n\n const expectedInitPeriods = extra.initialCharge?.periodCount ?? 0;\n const expectedInitAmount = extra.initialCharge?.totalAmount ?? \"0\";\n if (terms.initialChargePeriods !== expectedInitPeriods) return ErrorCode.TermsBindingInvalid;\n if (terms.initialChargeAmount !== expectedInitAmount) return ErrorCode.TermsBindingInvalid;\n\n // planId itself isn't compared here — the other field-level checks\n // (planTier / amountPerPeriod / merchant / …) already pin down the\n // economic effect of the subscription.\n\n // changeFrom binding — direction encoded in changeEffectiveAt:\n // undefined → create (changeFromSubId==0 / changeEffectiveAt==0)\n // immediate → upgrade (changeEffectiveAt==1)\n // period_end → downgrade (changeEffectiveAt==2)\n if (extra.changeFrom) {\n if (!hexEq(terms.changeFromSubId, extra.changeFrom.fromSubId)) {\n return ErrorCode.TermsBindingInvalid;\n }\n const expectedEff =\n extra.changeFrom.effectiveAt === \"immediate\"\n ? 1\n : extra.changeFrom.effectiveAt === \"period_end\"\n ? 2\n : 0;\n if (terms.changeEffectiveAt !== expectedEff) return ErrorCode.TermsBindingInvalid;\n } else {\n if (terms.changeFromSubId !== ZERO_BYTES32) return ErrorCode.TermsBindingInvalid;\n if (terms.changeEffectiveAt !== 0) return ErrorCode.TermsBindingInvalid;\n }\n\n return null;\n}\n","/**\n * Local mirror of the facilitator's elapsed-period math. Used by\n * `verifyAccess` to decide whether `lastChargedPeriod` is current without\n * hitting the facilitator on every request — only when the local\n * computation says we're behind does the SDK fall through to a GET /detail\n * refresh.\n *\n * MUST stay bit-for-bit equivalent to the backend implementation. Returns\n * `0` when the subscription has not yet started — caller treats this as a\n * special \"pre-start\" state.\n */\n\nconst PERIOD_MODE_CALENDAR_MONTH = 1;\n\n/**\n * Compute the real elapsed period count at `nowSec`, NOT clamped to\n * `maxPeriods`. Period 1 is the first chargeable period; `0` means\n * pre-start (subscription not yet effective).\n */\nexport function computeElapsedPeriods(\n periodMode: number,\n startAt: number,\n billingAnchorAt: number,\n periodSec: number,\n nowSec: number,\n): number {\n if (nowSec < startAt) return 0;\n if (periodMode === PERIOD_MODE_CALENDAR_MONTH) {\n const anchor = billingAnchorAt > 0 ? billingAnchorAt : startAt;\n const startOffset = elapsedCalendarMonths(anchor, startAt);\n return elapsedCalendarMonths(anchor, nowSec) - startOffset + 1;\n }\n if (periodSec <= 0) return 0;\n return Math.floor((nowSec - startAt) / periodSec) + 1;\n}\n\n/**\n * Number of calendar-month boundaries crossed between `anchorSec` (exclusive)\n * and `tsSec` (inclusive). Each boundary is `addCalendarMonths(anchor, n)`\n * for n=1,2,…, with month-end truncation (1/31 + 1m → 2/28/29).\n */\nexport function elapsedCalendarMonths(anchorSec: number, tsSec: number): number {\n if (tsSec <= anchorSec) return 0;\n const anchor = new Date(anchorSec * 1000);\n const ts = new Date(tsSec * 1000);\n let diff =\n (ts.getUTCFullYear() - anchor.getUTCFullYear()) * 12 +\n (ts.getUTCMonth() - anchor.getUTCMonth());\n if (diff < 0) return 0;\n if (addCalendarMonths(anchorSec, diff) > tsSec) diff--;\n return Math.max(diff, 0);\n}\n\n/**\n * Add `n` calendar months to `anchorSec`, keeping the anchor's day-of-month\n * but truncating to month-end when overflowing (1/31 + 1m → 2/28 or 2/29).\n * Returned timestamp preserves the anchor's hour / minute / second / ms in UTC.\n */\nexport function addCalendarMonths(anchorSec: number, n: number): number {\n const anchor = new Date(anchorSec * 1000);\n const targetYear = anchor.getUTCFullYear() + Math.floor((anchor.getUTCMonth() + n) / 12);\n const targetMonth = (((anchor.getUTCMonth() + n) % 12) + 12) % 12;\n // Days in target month (UTC) — day 0 of month+1 is the last day of month.\n const daysInTargetMonth = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate();\n const day = Math.min(anchor.getUTCDate(), daysInTargetMonth);\n const ts = Date.UTC(\n targetYear,\n targetMonth,\n day,\n anchor.getUTCHours(),\n anchor.getUTCMinutes(),\n anchor.getUTCSeconds(),\n anchor.getUTCMilliseconds(),\n );\n return Math.floor(ts / 1000);\n}\n","import { encodePacked, keccak256, type Hex } from \"viem\";\n\nimport type { AccessProof } from \"../types\";\nimport { base64DecodeUtf8, base64EncodeUtf8 } from \"./base64\";\n\nexport interface AccessProofMessageInput {\n subId: Hex; // bytes32\n payer: Hex; // address\n timestamp: number; // uint64, unix seconds\n}\n\n/**\n * Returns the inner 32-byte hash that wallets sign via EIP-191 personal_sign.\n *\n * Layout:\n * inner = keccak256(abi.encodePacked(bytes32 subId, address payer, uint256 timestamp))\n *\n * `timestamp` is encoded as `uint256` (32 bytes). Switching to anything\n * narrower yields a different digest and silently produces\n * `signature_invalid` on every access call.\n *\n * IMPORTANT: callers must NOT prepend the EIP-191 envelope themselves —\n * `wallet.signMessage(buildAccessProofMessage(...))` will add the\n * `\\x19Ethereum Signed Message:\\n32` prefix automatically. The recover step on\n * the server must mirror this (verify EIP-191 signature, not raw keccak).\n */\nexport function buildAccessProofMessage(input: AccessProofMessageInput): Hex {\n return keccak256(\n encodePacked(\n [\"bytes32\", \"address\", \"uint256\"],\n [input.subId, input.payer, BigInt(input.timestamp)],\n ),\n );\n}\n\nexport function encodeAccessProof(proof: AccessProof): string {\n return base64EncodeUtf8(JSON.stringify(proof));\n}\n\nexport function decodeAccessProof(headerValue: string): AccessProof {\n const json = base64DecodeUtf8(headerValue);\n const parsed = JSON.parse(json) as AccessProof;\n if (!parsed || parsed.kind !== \"subscription-id\") {\n throw new Error(`decodeAccessProof: expected kind=\"subscription-id\", got \"${parsed?.kind}\"`);\n }\n return parsed;\n}\n","import type { FacilitatorClient } from \"../http/httpFacilitatorClient\";\nimport type { PaymentPayload, PaymentRequirements } from \"../types/payments\";\nimport type { CancelAuth, PendingChangeCancelAuth, Subscription } from \"./types\";\n\n/**\n * Standard OKX facilitator response envelope. `code === 0` means success\n * (NUMBER, not string).\n */\nexport interface FacilitatorEnvelope<T> {\n code: number;\n msg?: string | null;\n data?: T;\n}\n\n/** POST /api/v6/pay/x402/subscriptions response data. */\nexport interface FacilitatorSubscribeData {\n subId: string;\n txHash?: string;\n state: number;\n}\n\n/** POST /api/v6/pay/x402/subscriptions/change response data. */\nexport interface FacilitatorChangeData {\n newSubId: string;\n txHash?: string;\n state: number;\n}\n\n/** POST /api/v6/pay/x402/subscriptions/{id}/cancel response data. */\nexport interface FacilitatorCancelData {\n subId: string;\n txHash?: string;\n state: number;\n}\n\n/** POST /api/v6/pay/x402/subscriptions/{id}/cancel-pending-change response data. */\nexport interface FacilitatorCancelPendingData {\n subId: string;\n txHash?: string;\n state: number;\n}\n\n/** POST /api/v6/pay/x402/subscriptions/{id}/charge response data. */\nexport interface FacilitatorChargeData {\n subId: string;\n period: number;\n txHash?: string;\n /** SubscriptionChargeState — 0 pending / 1 success / 2 failed. */\n state: number;\n planChangeTriggered?: boolean;\n newSubId?: string | null;\n}\n\n/** POST /api/v6/pay/x402/subscriptions/{id}/finalize-expired response data. */\nexport interface FacilitatorFinalizeExpiredData {\n subId: string;\n txHash?: string;\n state: number;\n}\n\n/** One row of the charges feed (GET /api/v6/pay/x402/subscriptions/charges). */\nexport interface FacilitatorChargeRow {\n subId: string;\n period: number;\n /** 1 initial / 2 periodic / 3 downgrade_first_period / 4 finalize_expired_marker. */\n chargeType: number;\n amount: string;\n /** 0 pending / 1 success / 2 failed. */\n state: number;\n txHash?: string;\n planChangeTriggered?: boolean;\n newSubId?: string | null;\n}\n\n/** GET /api/v6/pay/x402/subscriptions/charges response data. */\nexport interface FacilitatorGetChargesData {\n charges: FacilitatorChargeRow[];\n}\n\n/** GET /api/v6/pay/x402/subscriptions/pending response data (most recent row). */\nexport interface FacilitatorPendingChangeRow {\n subId: string;\n newSubId: string;\n effectiveFromPeriod: number;\n /** 0 pending / 1 activated / 2 canceled / 3 expired. */\n state: number;\n}\n\n/** GET /api/v6/pay/x402/subscriptions/{id} response data. */\nexport interface FacilitatorGetSubscriptionData {\n subId: string;\n state: number;\n payer: string;\n merchant: string;\n token: string;\n amountPerPeriod: string;\n periodSec: number;\n /** 0 fixed_seconds / 1 calendar_month. */\n periodMode: number;\n maxPeriods: number;\n startAt: number;\n /** Calendar-month billing anchor (Unix s); 0/undefined in fixed_seconds mode. */\n billingAnchorAt?: number;\n /** Seller-side business identifier (NOT on-chain); facilitator echoes from its DB. */\n planId?: string;\n /** Plan tier from on-chain terms.planTier. */\n planTier?: number;\n lastChargedPeriod: number;\n totalPulled: string;\n changedToSubId?: string | null;\n isActive?: boolean;\n serviceEnded?: boolean;\n /** Mode-aware current period, clamped to maxPeriods (boundary = next period). */\n currentPeriod?: number;\n /**\n * Real elapsed period number, NOT clamped — `elapsedPeriods > maxPeriods`\n * means the service window already ended. SDK polls until\n * `lastChargedPeriod >= elapsedPeriods` to confirm a write settled.\n */\n elapsedPeriods?: number;\n nextChargeableAt?: number;\n pendingPlanChange?: {\n subId: string;\n newSubId: string;\n effectiveFromPeriod: number;\n state: number;\n } | null;\n}\n\n/**\n * Subscription write-flow request body shared by subscribe / change. The\n * facilitator parses these field-by-field — DO NOT wrap in an x402 envelope\n * (no `paymentPayload` / `paymentRequirements` wrapping).\n */\nexport interface SubscriptionWriteRequest {\n chainIndex: number;\n terms: PaymentPayload[\"payload\"] extends { terms: infer T } ? T : unknown;\n permit: PaymentPayload[\"payload\"] extends { permit: infer P } ? P : unknown;\n termsSig: string;\n permitSig: string;\n syncSettle?: boolean;\n}\n\n/** Subscription-aware facilitator client. Extends the base FacilitatorClient. */\nexport interface SubscriptionFacilitatorClient extends FacilitatorClient {\n /** POST /api/v6/pay/x402/subscriptions */\n subscribe(\n payload: PaymentPayload,\n requirements: PaymentRequirements,\n syncSettle?: boolean,\n ): Promise<FacilitatorEnvelope<FacilitatorSubscribeData>>;\n\n /** POST /api/v6/pay/x402/subscriptions/change */\n changeSubscription(\n payload: PaymentPayload,\n requirements: PaymentRequirements,\n oldSubId: string,\n syncSettle?: boolean,\n ): Promise<FacilitatorEnvelope<FacilitatorChangeData>>;\n\n /** POST /api/v6/pay/x402/subscriptions/{subId}/cancel */\n cancelSubscription(\n subId: string,\n cancelAuth: CancelAuth,\n syncSettle?: boolean,\n ): Promise<FacilitatorEnvelope<FacilitatorCancelData>>;\n\n /** POST /api/v6/pay/x402/subscriptions/{subId}/cancel-pending-change */\n cancelPendingChange(\n subId: string,\n cancelAuth: PendingChangeCancelAuth,\n syncSettle?: boolean,\n ): Promise<FacilitatorEnvelope<FacilitatorCancelPendingData>>;\n\n /** POST /api/v6/pay/x402/subscriptions/{subId}/charge */\n chargeSubscription(\n subId: string,\n syncSettle?: boolean,\n ): Promise<FacilitatorEnvelope<FacilitatorChargeData>>;\n\n /** POST /api/v6/pay/x402/subscriptions/finalize-expired — cleans up an ACTIVE sub whose service window has ended. */\n finalizeExpired(\n subId: string,\n syncSettle?: boolean,\n ): Promise<FacilitatorEnvelope<FacilitatorFinalizeExpiredData>>;\n\n /** GET /api/v6/pay/x402/subscriptions/charges — paginated charge feed. */\n getCharges(\n subId: string,\n limit?: number,\n offset?: number,\n ): Promise<FacilitatorEnvelope<FacilitatorGetChargesData>>;\n\n /** GET /api/v6/pay/x402/subscriptions/pending — most recent pendingPlanChange row (any state). */\n getPendingChange(subId: string): Promise<FacilitatorEnvelope<FacilitatorPendingChangeRow | null>>;\n\n /** GET /api/v6/pay/x402/subscriptions/{subId} */\n getSubscription(subId: string): Promise<FacilitatorEnvelope<FacilitatorGetSubscriptionData>>;\n}\n\n/**\n * Type guard: does this FacilitatorClient implement the subscription\n * endpoints?\n */\nexport function supportsSubscription(\n client: FacilitatorClient,\n): client is SubscriptionFacilitatorClient {\n const c = client as Partial<SubscriptionFacilitatorClient>;\n return (\n typeof c.subscribe === \"function\" &&\n typeof c.changeSubscription === \"function\" &&\n typeof c.cancelSubscription === \"function\" &&\n typeof c.cancelPendingChange === \"function\" &&\n typeof c.chargeSubscription === \"function\" &&\n typeof c.finalizeExpired === \"function\" &&\n typeof c.getCharges === \"function\" &&\n typeof c.getPendingChange === \"function\" &&\n typeof c.getSubscription === \"function\"\n );\n}\n\n/** Re-export `Subscription` so consumers don't have to import from `./types`. */\nexport type { Subscription };\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ai as AssetAmount, aj as FacilitatorContext, F as FacilitatorExtension, f as FacilitatorResponseError, ak as Money, al as MoneyParser, N as Network, P as PaymentPayload, am as PaymentPayloadContext, an as PaymentPayloadResult, c as PaymentRequired, ao as PaymentRequiredContext, a as PaymentRequirements, ah as Price, ap as ResourceInfo, aq as ResourceServerExtension, a9 as SchemeNetworkClient, b as SchemeNetworkFacilitator, ar as SchemeNetworkServer, as as SettleError, at as SettleRequest, S as SettleResponse, ae as SettleResultContext, i as SettleStatusResponse, h as SupportedResponse, au as VerifyError, av as VerifyRequest, V as VerifyResponse, g as getFacilitatorResponseError } from '../mechanisms-LhI9qkRo.mjs';
|
package/dist/esm/{x402HTTPResourceServer-DBeutKxq.d.mts → x402HTTPResourceServer-56Tq3Jup.d.mts}
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ah as Price, N as Network, S as SettleResponse, D as OnBeforeAccessHook, P as PaymentPayload, a as PaymentRequirements, u as Subscription, c as PaymentRequired, ag as x402ResourceServer, af as SettlementOverrides, v as SubscriptionCapability } from './mechanisms-LhI9qkRo.mjs';
|
|
2
|
+
import { _ as ___subscription } from './index-DKbqlTu_.mjs';
|
|
2
3
|
|
|
3
4
|
declare const SETTLEMENT_OVERRIDES_HEADER = "settlement-overrides";
|
|
4
5
|
/**
|
|
@@ -12,6 +13,12 @@ interface HTTPAdapter {
|
|
|
12
13
|
getUrl(): string;
|
|
13
14
|
getAcceptHeader(): string;
|
|
14
15
|
getUserAgent(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Return the full request headers as a plain lowercase-keyed record.
|
|
18
|
+
* Optional; adapters that don't implement it cause hooks like
|
|
19
|
+
* `onBeforeAccess` to receive an empty headers map.
|
|
20
|
+
*/
|
|
21
|
+
getHeaders?(): Record<string, string>;
|
|
15
22
|
/**
|
|
16
23
|
* Get query parameters from the request URL
|
|
17
24
|
*
|
|
@@ -128,6 +135,22 @@ interface RouteConfig {
|
|
|
128
135
|
*/
|
|
129
136
|
settlementFailedResponseBody?: SettlementFailedResponseBody;
|
|
130
137
|
extensions?: Record<string, unknown>;
|
|
138
|
+
/**
|
|
139
|
+
* When set, the route is a special-operation endpoint for the subscription
|
|
140
|
+
* scheme:
|
|
141
|
+
* - "change": two-stage upgrade/downgrade flow
|
|
142
|
+
* - "cancel": cancel-subscription flow
|
|
143
|
+
* Ignored by non-subscription schemes.
|
|
144
|
+
*/
|
|
145
|
+
operation?: "change" | "cancel" | "cancel-pending-change";
|
|
146
|
+
/**
|
|
147
|
+
* Route-level hook fired AFTER `verifyAccess` succeeded (signature +
|
|
148
|
+
* payer + plan allowlist + period math) but BEFORE the handler runs.
|
|
149
|
+
* Seller uses it for custom access policy: rate limiting, quota, feature
|
|
150
|
+
* flags, bans / blacklists (return `{ok:false, error:"banned"}`). Full
|
|
151
|
+
* `Subscription` object is on the context so any field is inspectable.
|
|
152
|
+
*/
|
|
153
|
+
onBeforeAccess?: OnBeforeAccessHook;
|
|
131
154
|
}
|
|
132
155
|
/**
|
|
133
156
|
* Routes configuration - maps path patterns to route configs
|
|
@@ -200,6 +223,22 @@ type HTTPProcessResult = {
|
|
|
200
223
|
} | {
|
|
201
224
|
type: "payment-error";
|
|
202
225
|
response: HTTPResponseInstructions;
|
|
226
|
+
} | {
|
|
227
|
+
type: "payment-presettle";
|
|
228
|
+
paymentPayload: PaymentPayload;
|
|
229
|
+
paymentRequirements: PaymentRequirements;
|
|
230
|
+
/** Settle action to run AFTER verify; bound to scheme + operation. */
|
|
231
|
+
settle: () => Promise<{
|
|
232
|
+
success: boolean;
|
|
233
|
+
headers?: Record<string, string>;
|
|
234
|
+
data?: any;
|
|
235
|
+
error?: string;
|
|
236
|
+
}>;
|
|
237
|
+
operation: "subscribe" | "change" | "cancel" | "cancel-pending-change";
|
|
238
|
+
} | {
|
|
239
|
+
type: "access-verified";
|
|
240
|
+
subscription: Subscription;
|
|
241
|
+
headers?: Record<string, string>;
|
|
203
242
|
};
|
|
204
243
|
/**
|
|
205
244
|
* Result of processSettlement
|
|
@@ -264,6 +303,7 @@ declare class x402HTTPResourceServer {
|
|
|
264
303
|
private paywallProvider?;
|
|
265
304
|
private protectedRequestHooks;
|
|
266
305
|
private timeoutRecoveryHook?;
|
|
306
|
+
private beforeAccessHooks;
|
|
267
307
|
private pollDeadlineMs;
|
|
268
308
|
/**
|
|
269
309
|
* Creates a new x402HTTPResourceServer instance.
|
|
@@ -316,6 +356,19 @@ declare class x402HTTPResourceServer {
|
|
|
316
356
|
* @returns The x402HTTPResourceServer instance for chaining
|
|
317
357
|
*/
|
|
318
358
|
onProtectedRequest(hook: ProtectedRequestHook): this;
|
|
359
|
+
/**
|
|
360
|
+
* Register a seller-global `onBeforeAccess` hook fired on every access-
|
|
361
|
+
* verified subscription request, AFTER `verifyAccess` (signature + payer
|
|
362
|
+
* + plan allowlist + period math) but BEFORE the handler runs. Seller
|
|
363
|
+
* uses it for cross-cutting access policy (quota / ban list / feature
|
|
364
|
+
* gating). Hooks are executed in order of registration; the first one
|
|
365
|
+
* to return `{ ok: false }` denies (→ 402). Route-level
|
|
366
|
+
* `RouteConfig.onBeforeAccess` runs AFTER all global hooks.
|
|
367
|
+
*
|
|
368
|
+
* @param hook - The hook function
|
|
369
|
+
* @returns The x402HTTPResourceServer instance for chaining
|
|
370
|
+
*/
|
|
371
|
+
onBeforeAccess(hook: OnBeforeAccessHook): this;
|
|
319
372
|
/**
|
|
320
373
|
* Register a hook to call when the facilitator returns status="timeout".
|
|
321
374
|
* The hook should verify the tx on-chain and return { confirmed: boolean }.
|
|
@@ -360,6 +413,60 @@ declare class x402HTTPResourceServer {
|
|
|
360
413
|
* @returns True if the route requires payment, false otherwise
|
|
361
414
|
*/
|
|
362
415
|
requiresPayment(context: HTTPRequestContext): boolean;
|
|
416
|
+
/**
|
|
417
|
+
* Lazy loader for the subscription submodule. The `import()` cache makes
|
|
418
|
+
* this effectively free after the first hit; isolating it in one place
|
|
419
|
+
* keeps dispatch helpers free of dynamic-import boilerplate and lets
|
|
420
|
+
* bundlers tree-shake the entire subscription path when no caller touches
|
|
421
|
+
* it.
|
|
422
|
+
*/
|
|
423
|
+
protected loadSubscriptionModule(): Promise<typeof ___subscription>;
|
|
424
|
+
/**
|
|
425
|
+
* Single chokepoint for "is this (network, scheme) backed by a
|
|
426
|
+
* SubscriptionCapability-implementing scheme?". Returns the narrowed
|
|
427
|
+
* capability (so callers get full typing on `verifyAccess` / `verifySubscribe`
|
|
428
|
+
* / etc.) or null if not registered or not a subscription scheme.
|
|
429
|
+
*/
|
|
430
|
+
protected resolveSubscriptionScheme(network: Network, schemeName: string): Promise<SubscriptionCapability | null>;
|
|
431
|
+
/**
|
|
432
|
+
* period dispatch helper — Access flow.
|
|
433
|
+
*
|
|
434
|
+
* Returns an `access-verified` (or `payment-error`) HTTPProcessResult when
|
|
435
|
+
* the request carries `APP-Access` AND a subscription-capable scheme is
|
|
436
|
+
* registered for one of the route's accepted (scheme, network) pairs.
|
|
437
|
+
* Returns `null` to indicate the dispatcher should fall through to classic
|
|
438
|
+
* pay-per-request handling.
|
|
439
|
+
*/
|
|
440
|
+
protected tryDispatchAccessFlow(adapter: HTTPAdapter, routeConfig: RouteConfig, paymentOptions: PaymentOption[], paymentRequired: PaymentRequired): Promise<HTTPProcessResult | null>;
|
|
441
|
+
/**
|
|
442
|
+
* period dispatch helper — Subscribe presettle flow.
|
|
443
|
+
*
|
|
444
|
+
* When the buyer presents a PaymentPayload whose `accepted.scheme` is a
|
|
445
|
+
* subscription scheme with `settlementMode === "pre"`, this runs verify +
|
|
446
|
+
* (settle on demand) and returns `payment-presettle`. The middleware is
|
|
447
|
+
* expected to call `result.settle()` AFTER decision-time but BEFORE
|
|
448
|
+
* `next()` so handler only runs when the chain creation succeeded.
|
|
449
|
+
*
|
|
450
|
+
* Returns `null` to fall through to classic post-settle path-verified flow.
|
|
451
|
+
*/
|
|
452
|
+
protected tryDispatchSubscriptionPresettle(paymentPayload: PaymentPayload, serverAccepts: PaymentRequirements[], operation: "subscribe" | "change"): Promise<HTTPProcessResult | null>;
|
|
453
|
+
/**
|
|
454
|
+
* period dispatch helper — Cancel flow.
|
|
455
|
+
*
|
|
456
|
+
* Reads JSON body { auth: CancelAuth, subId: string }, runs verifyCancel
|
|
457
|
+
* then wraps settleCancel as a payment-presettle (settle-before-handler so
|
|
458
|
+
* the cancelation is on-chain before the seller's response).
|
|
459
|
+
*/
|
|
460
|
+
protected tryDispatchCancelFlow(adapter: HTTPAdapter, routeConfig: RouteConfig, paymentOptions: PaymentOption[]): Promise<HTTPProcessResult | null>;
|
|
461
|
+
/**
|
|
462
|
+
* period dispatch helper — Cancel-Pending-Change flow.
|
|
463
|
+
*
|
|
464
|
+
* Reads JSON body `{ auth: PendingChangeCancelAuth, subId: string }`. The
|
|
465
|
+
* auth must carry `newSubId` (matches the currently PENDING downgrade
|
|
466
|
+
* target). Runs verifyCancelPendingChange then wraps
|
|
467
|
+
* settleCancelPendingChange as a payment-presettle.
|
|
468
|
+
*/
|
|
469
|
+
protected tryDispatchCancelPendingChangeFlow(adapter: HTTPAdapter, routeConfig: RouteConfig, paymentOptions: PaymentOption[]): Promise<HTTPProcessResult | null>;
|
|
363
470
|
/**
|
|
364
471
|
* Build HTTPResponseInstructions for settlement failure.
|
|
365
472
|
* Uses settlementFailedResponseBody hook if configured, otherwise defaults to empty body.
|
|
@@ -399,6 +506,12 @@ declare class x402HTTPResourceServer {
|
|
|
399
506
|
* @returns Decoded payment payload or null
|
|
400
507
|
*/
|
|
401
508
|
private extractPayment;
|
|
509
|
+
/**
|
|
510
|
+
* Extract `APP-Access` header (subscription access-flow). Returns the raw
|
|
511
|
+
* base64 string so callers can pass it through to `decodeAccessProof` in
|
|
512
|
+
* the subscription codec.
|
|
513
|
+
*/
|
|
514
|
+
private extractAccessProofHeader;
|
|
402
515
|
/**
|
|
403
516
|
* Check if request is from a web browser
|
|
404
517
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okxweb3/app-x402-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"main": "./dist/cjs/index.js",
|
|
5
5
|
"module": "./dist/esm/index.js",
|
|
6
6
|
"types": "./dist/cjs/index.d.ts",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"typescript": "^5.7.3"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"viem": "^2.39.3",
|
|
34
35
|
"zod": "^3.24.2"
|
|
35
36
|
},
|
|
36
37
|
"exports": {
|
|
@@ -113,6 +114,16 @@
|
|
|
113
114
|
"types": "./dist/cjs/schemas/index.d.ts",
|
|
114
115
|
"default": "./dist/cjs/schemas/index.js"
|
|
115
116
|
}
|
|
117
|
+
},
|
|
118
|
+
"./subscription": {
|
|
119
|
+
"import": {
|
|
120
|
+
"types": "./dist/esm/subscription/index.d.mts",
|
|
121
|
+
"default": "./dist/esm/subscription/index.mjs"
|
|
122
|
+
},
|
|
123
|
+
"require": {
|
|
124
|
+
"types": "./dist/cjs/subscription/index.d.ts",
|
|
125
|
+
"default": "./dist/cjs/subscription/index.js"
|
|
126
|
+
}
|
|
116
127
|
}
|
|
117
128
|
},
|
|
118
129
|
"files": [
|