s402 0.3.0 → 0.5.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/CHANGELOG.md +46 -0
- package/dist/compat.d.mts +4 -4
- package/dist/compat.mjs +9 -7
- package/dist/errors.d.mts +1 -0
- package/dist/errors.mjs +6 -1
- package/dist/http.d.mts +7 -3
- package/dist/http.mjs +88 -23
- package/dist/index.d.mts +161 -4
- package/dist/index.mjs +218 -23
- package/dist/{scheme-D7qqwo3Q.d.mts → scheme-m-uk4zyH.d.mts} +26 -17
- package/dist/test-utils.d.mts +1 -1
- package/dist/test-utils.mjs +8 -0
- package/dist/types.d.mts +141 -48
- package/package.json +9 -2
- package/test/conformance/vectors/body-transport.json +42 -0
- package/test/conformance/vectors/compat-normalize.json +2 -1
- package/test/conformance/vectors/payload-decode.json +33 -0
- package/test/conformance/vectors/payload-encode.json +33 -0
- package/test/conformance/vectors/requirements-decode.json +44 -0
- package/test/conformance/vectors/requirements-encode.json +44 -0
- package/test/conformance/vectors/roundtrip.json +52 -0
- package/test/conformance/vectors/settle-decode.json +14 -0
- package/test/conformance/vectors/settle-encode.json +28 -0
- package/test/conformance/vectors/settlement-verification.json +180 -0
- package/test/conformance/vectors/validation-reject.json +69 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.0] - 2026-04-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`upto` scheme V2 features (DAN-284).** Two new fields close x402's upto overcharge vulnerability:
|
|
13
|
+
- `estimatedAmount` on `s402UptoExtra` — server's advisory cost estimate so clients can set tight ceilings
|
|
14
|
+
- `settlementCeiling` on `s402UptoPayload` — client-chosen, on-chain-enforced cap. Move contract rejects `actualAmount > settlementCeiling`. Must satisfy `1 <= settlementCeiling <= maxAmount`. See ADR-003 §Decision 3 and §Decision 8.
|
|
15
|
+
- **Extension system (DAN-285, ADR-004).** Typed, lifecycle-aware plugin architecture:
|
|
16
|
+
- Three actor-specific interfaces: `s402ClientExtension`, `s402ServerExtension`, `s402FacilitatorExtension`
|
|
17
|
+
- Four facilitator hooks in pipeline order: `beforeVerify` → `afterVerify` → `beforeSettle` → `afterSettle`
|
|
18
|
+
- `s402ExtensionRegistry` with dependency ordering via Kahn's topological sort
|
|
19
|
+
- Critical vs advisory error handling: `critical: true` extensions throw, advisory extensions log and continue
|
|
20
|
+
- `getExtensionData<T>()` / `setExtensionData()` type-safe helpers
|
|
21
|
+
- `./extensions` sub-path export added to package.json
|
|
22
|
+
- **`skipVerify` option on `process()`.** New `s402ProcessOptions` interface with `skipVerify?: boolean`. Eliminates the verify() dry-run RPC round-trip (~200-400ms) for chains where failed transactions cost zero gas (Sui PTBs). All pre-flight checks (expiration, scheme-mismatch, dedup) still run.
|
|
23
|
+
- **`EXTENSION_FAILED` error code** — `retryable: false`, for critical extension pipeline failures.
|
|
24
|
+
- **154 conformance test vectors** (was ~130). New vectors for: upto requirements with estimatedAmount, upto payloads with settlementCeiling, settle responses with actualAmount/depositId, V2 rejection vectors, upto roundtrips, mandate.minPerTx validation.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Settle response type validation (M1).** `validateSettleShape` now rejects non-string `actualAmount` and `depositId` — a malicious facilitator could previously inject numeric types that passed through to consumer code.
|
|
29
|
+
- **Prepaid payload amount validation (M2).** `ratePerCall` and `maxCalls` in payload now validated with `isValidAmount()`, matching the requirements-side validation. Previously only type-checked as strings.
|
|
30
|
+
- **Mandate minPerTx amount validation (L1).** `mandate.minPerTx` now validated with `isValidAmount()` for consistency with other amount fields.
|
|
31
|
+
- **afterSettle error observability.** Catch block now forwards to `extensionErrorHandler` instead of silently swallowing critical extension errors (the settlement result is still never changed — tx is already on-chain).
|
|
32
|
+
- **Stale comment in validatePayloadShape.** Updated to document upto's scheme-specific inner keys alongside prepaid and unlock.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **831 tests across 17 files** (was 798). New coverage: standalone verify/settle guard tests, V2 validation edge cases, settle response type checks, prepaid amount validation, extension system integration.
|
|
37
|
+
- Conformance README updated with `estimatedAmount` in upto sub-object keys and `settlementCeiling` in payload inner keys.
|
|
38
|
+
|
|
39
|
+
## [0.4.0] - 2026-04-11
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- **BREAKING: `verifySettlement` is now required on `s402ClientScheme` (DAN-280).** The `?` was removed — every scheme implementation MUST provide `verifySettlement()`. Schemes that cannot verify locally (e.g. unlock-TX2) should return `{ verified: false, reason: '...' }`. All 5 SweeFi adapters already implement this method; only custom third-party implementations that relied on the optional marker will need updating.
|
|
43
|
+
- Updated JSDoc: `@since 0.4.0 — required (was optional in 0.3.0)`
|
|
44
|
+
- `mockExactClientScheme()` in `test-utils.ts` now includes `verifySettlement()` returning `{ verified: false }` with reason `'mock scheme'`
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- **S8 conformance test vectors (DAN-282).** `spec/vectors/settlement-verification.json` — 7 chain-agnostic test vectors covering the `verifySettlement` interface contract: matching digest, mismatched digest (malicious facilitator), settle failed, missing txDigest, invalid base64, stream scheme, and non-verifiable scheme. Each vector includes `expectedShape`, `invariants`, and implementation `notes`.
|
|
48
|
+
|
|
49
|
+
### Compatibility
|
|
50
|
+
- **BREAKING for 0.3.x consumers**: implementations that omitted `verifySettlement` will now fail type-checking. Add a stub returning `{ verified: false, expectedDigest: '', actualDigest: null, reason: 'not implemented' }` to restore compilation.
|
|
51
|
+
- Wire format: unchanged from v0.3.0.
|
|
52
|
+
|
|
8
53
|
## [0.3.0] - 2026-04-11
|
|
9
54
|
|
|
10
55
|
This release closes the facilitator causal-binding hole identified in the April 2026 scale-fragility review, and establishes s402 as a pure chain-agnostic protocol repo (no Sui code anywhere). Chain-specific implementations now live in downstream adapter repos — the canonical Sui reference is `@sweefi/sui` in the SweeFi monorepo.
|
|
@@ -158,6 +203,7 @@ _Version bump for npm publish after license change._
|
|
|
158
203
|
- Property-based fuzz testing via fast-check
|
|
159
204
|
- 207 tests, zero runtime dependencies
|
|
160
205
|
|
|
206
|
+
[0.4.0]: https://github.com/s402-protocol/core/compare/v0.3.0...v0.4.0
|
|
161
207
|
[0.3.0]: https://github.com/s402-protocol/core/compare/v0.2.3...v0.3.0
|
|
162
208
|
[0.2.1]: https://github.com/s402-protocol/core/compare/v0.2.0...v0.2.1
|
|
163
209
|
[0.2.0]: https://github.com/s402-protocol/core/compare/v0.1.8...v0.2.0
|
package/dist/compat.d.mts
CHANGED
|
@@ -58,7 +58,7 @@ interface x402PaymentPayload {
|
|
|
58
58
|
* Handles both V1 (`maxAmountRequired`) and V2 (`amount`) wire formats.
|
|
59
59
|
* Maps x402's single scheme to s402's accepts array.
|
|
60
60
|
*/
|
|
61
|
-
declare function fromX402Requirements(x402: x402PaymentRequirements): s402PaymentRequirements;
|
|
61
|
+
declare function fromX402Requirements(x402: x402PaymentRequirements, now?: number): s402PaymentRequirements;
|
|
62
62
|
/**
|
|
63
63
|
* Convert inbound x402 payment payload to s402 format.
|
|
64
64
|
* Validates that required fields are present and correctly typed.
|
|
@@ -66,7 +66,7 @@ declare function fromX402Requirements(x402: x402PaymentRequirements): s402Paymen
|
|
|
66
66
|
declare function fromX402Payload(x402: x402PaymentPayload): s402ExactPayload;
|
|
67
67
|
/**
|
|
68
68
|
* Convert outbound s402 requirements to x402 V1 wire format.
|
|
69
|
-
* Strips s402-only fields (mandate, stream, escrow, unlock extensions).
|
|
69
|
+
* Strips s402-only fields (mandate, upto, prepaid, stream, escrow, unlock extensions).
|
|
70
70
|
* Only works for "exact" scheme — other schemes have no x402 equivalent.
|
|
71
71
|
*
|
|
72
72
|
* Includes both `maxAmountRequired` (V1) and `amount` (V2) for maximum interop.
|
|
@@ -101,7 +101,7 @@ declare function isX402Envelope(obj: Record<string, unknown>): boolean;
|
|
|
101
101
|
* Picks the first requirement from the `accepts` array.
|
|
102
102
|
* Copies `x402Version` from the envelope onto the requirement for downstream processing.
|
|
103
103
|
*/
|
|
104
|
-
declare function fromX402Envelope(envelope: x402PaymentRequiredEnvelope): s402PaymentRequirements;
|
|
104
|
+
declare function fromX402Envelope(envelope: x402PaymentRequiredEnvelope, now?: number): s402PaymentRequirements;
|
|
105
105
|
/**
|
|
106
106
|
* Auto-detect and normalize: if x402, convert to s402. If already s402, validate and pass through.
|
|
107
107
|
* Handles x402 V1 (flat), x402 V2 (envelope with accepts array), and s402 formats.
|
|
@@ -123,6 +123,6 @@ declare function fromX402Envelope(envelope: x402PaymentRequiredEnvelope): s402Pa
|
|
|
123
123
|
* // Always returns s402PaymentRequirements regardless of input format
|
|
124
124
|
* ```
|
|
125
125
|
*/
|
|
126
|
-
declare function normalizeRequirements(obj: Record<string, unknown
|
|
126
|
+
declare function normalizeRequirements(obj: Record<string, unknown>, now?: number): s402PaymentRequirements;
|
|
127
127
|
//#endregion
|
|
128
128
|
export { fromX402Envelope, fromX402Payload, fromX402Requirements, isS402, isX402, isX402Envelope, normalizeRequirements, toX402Payload, toX402Requirements, x402PaymentPayload, x402PaymentRequiredEnvelope, x402PaymentRequirements };
|
package/dist/compat.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { isValidAmount, pickRequirementsFields, validateRequirementsShape } from
|
|
|
8
8
|
* Handles both V1 (`maxAmountRequired`) and V2 (`amount`) wire formats.
|
|
9
9
|
* Maps x402's single scheme to s402's accepts array.
|
|
10
10
|
*/
|
|
11
|
-
function fromX402Requirements(x402) {
|
|
11
|
+
function fromX402Requirements(x402, now) {
|
|
12
12
|
const amount = x402.amount ?? x402.maxAmountRequired;
|
|
13
13
|
if (!amount) throw new s402Error("INVALID_PAYLOAD", "x402 requirements missing both \"amount\" (V2) and \"maxAmountRequired\" (V1)");
|
|
14
14
|
if (!isValidAmount(amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${amount}": must be a non-negative integer string`);
|
|
@@ -20,6 +20,7 @@ function fromX402Requirements(x402) {
|
|
|
20
20
|
if (e instanceof s402Error) throw e;
|
|
21
21
|
throw new s402Error("INVALID_PAYLOAD", "facilitatorUrl is not a valid URL");
|
|
22
22
|
}
|
|
23
|
+
const expiresAt = x402.maxTimeoutSeconds != null && x402.maxTimeoutSeconds > 0 ? (now ?? Date.now()) + x402.maxTimeoutSeconds * 1e3 : void 0;
|
|
23
24
|
return {
|
|
24
25
|
s402Version: S402_VERSION,
|
|
25
26
|
accepts: ["exact"],
|
|
@@ -28,6 +29,7 @@ function fromX402Requirements(x402) {
|
|
|
28
29
|
amount,
|
|
29
30
|
payTo: x402.payTo,
|
|
30
31
|
facilitatorUrl: x402.facilitatorUrl,
|
|
32
|
+
expiresAt,
|
|
31
33
|
extensions: x402.extensions
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -50,7 +52,7 @@ function fromX402Payload(x402) {
|
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* Convert outbound s402 requirements to x402 V1 wire format.
|
|
53
|
-
* Strips s402-only fields (mandate, stream, escrow, unlock extensions).
|
|
55
|
+
* Strips s402-only fields (mandate, upto, prepaid, stream, escrow, unlock extensions).
|
|
54
56
|
* Only works for "exact" scheme — other schemes have no x402 equivalent.
|
|
55
57
|
*
|
|
56
58
|
* Includes both `maxAmountRequired` (V1) and `amount` (V2) for maximum interop.
|
|
@@ -113,14 +115,14 @@ function isX402Envelope(obj) {
|
|
|
113
115
|
* Picks the first requirement from the `accepts` array.
|
|
114
116
|
* Copies `x402Version` from the envelope onto the requirement for downstream processing.
|
|
115
117
|
*/
|
|
116
|
-
function fromX402Envelope(envelope) {
|
|
118
|
+
function fromX402Envelope(envelope, now) {
|
|
117
119
|
if (!envelope.accepts || envelope.accepts.length === 0) throw new s402Error("INVALID_PAYLOAD", "x402 V2 envelope has empty accepts array");
|
|
118
120
|
const req = {
|
|
119
121
|
...envelope.accepts[0],
|
|
120
122
|
x402Version: envelope.x402Version
|
|
121
123
|
};
|
|
122
124
|
validateX402Shape(req);
|
|
123
|
-
return fromX402Requirements(req);
|
|
125
|
+
return fromX402Requirements(req, now);
|
|
124
126
|
}
|
|
125
127
|
/**
|
|
126
128
|
* Auto-detect and normalize: if x402, convert to s402. If already s402, validate and pass through.
|
|
@@ -143,20 +145,20 @@ function fromX402Envelope(envelope) {
|
|
|
143
145
|
* // Always returns s402PaymentRequirements regardless of input format
|
|
144
146
|
* ```
|
|
145
147
|
*/
|
|
146
|
-
function normalizeRequirements(obj) {
|
|
148
|
+
function normalizeRequirements(obj, now) {
|
|
147
149
|
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) throw new s402Error("INVALID_PAYLOAD", `Payment requirements must be a plain object, got ${obj === null ? "null" : Array.isArray(obj) ? "array" : typeof obj}`);
|
|
148
150
|
if (isS402(obj)) {
|
|
149
151
|
validateRequirementsShape(obj);
|
|
150
152
|
return pickRequirementsFields(obj);
|
|
151
153
|
}
|
|
152
154
|
if (isX402Envelope(obj)) {
|
|
153
|
-
const record = fromX402Envelope(obj);
|
|
155
|
+
const record = fromX402Envelope(obj, now);
|
|
154
156
|
validateRequirementsShape(record);
|
|
155
157
|
return pickRequirementsFields(record);
|
|
156
158
|
}
|
|
157
159
|
if (isX402(obj)) {
|
|
158
160
|
validateX402Shape(obj);
|
|
159
|
-
const record = fromX402Requirements(obj);
|
|
161
|
+
const record = fromX402Requirements(obj, now);
|
|
160
162
|
validateRequirementsShape(record);
|
|
161
163
|
return pickRequirementsFields(record);
|
|
162
164
|
}
|
package/dist/errors.d.mts
CHANGED
|
@@ -24,6 +24,7 @@ declare const s402ErrorCode: {
|
|
|
24
24
|
readonly VERIFICATION_FAILED: "VERIFICATION_FAILED";
|
|
25
25
|
readonly SETTLEMENT_FAILED: "SETTLEMENT_FAILED";
|
|
26
26
|
readonly DIGEST_MISMATCH: "DIGEST_MISMATCH";
|
|
27
|
+
readonly EXTENSION_FAILED: "EXTENSION_FAILED";
|
|
27
28
|
};
|
|
28
29
|
type s402ErrorCodeType = (typeof s402ErrorCode)[keyof typeof s402ErrorCode];
|
|
29
30
|
interface s402ErrorInfo {
|
package/dist/errors.mjs
CHANGED
|
@@ -23,7 +23,8 @@ const s402ErrorCode = {
|
|
|
23
23
|
REQUIREMENTS_EXPIRED: "REQUIREMENTS_EXPIRED",
|
|
24
24
|
VERIFICATION_FAILED: "VERIFICATION_FAILED",
|
|
25
25
|
SETTLEMENT_FAILED: "SETTLEMENT_FAILED",
|
|
26
|
-
DIGEST_MISMATCH: "DIGEST_MISMATCH"
|
|
26
|
+
DIGEST_MISMATCH: "DIGEST_MISMATCH",
|
|
27
|
+
EXTENSION_FAILED: "EXTENSION_FAILED"
|
|
27
28
|
};
|
|
28
29
|
/** Error recovery hints for each error code */
|
|
29
30
|
const ERROR_HINTS = {
|
|
@@ -90,6 +91,10 @@ const ERROR_HINTS = {
|
|
|
90
91
|
DIGEST_MISMATCH: {
|
|
91
92
|
retryable: false,
|
|
92
93
|
suggestedAction: "Facilitator returned a transaction digest that does not match the signed payload. Do NOT retry — treat payment as non-settled and investigate the facilitator."
|
|
94
|
+
},
|
|
95
|
+
EXTENSION_FAILED: {
|
|
96
|
+
retryable: false,
|
|
97
|
+
suggestedAction: "A critical extension blocked the payment flow. Contact the extension provider or disable the extension."
|
|
93
98
|
}
|
|
94
99
|
};
|
|
95
100
|
/**
|
package/dist/http.d.mts
CHANGED
|
@@ -117,9 +117,13 @@ declare function isValidU64Amount(s: string): boolean;
|
|
|
117
117
|
*/
|
|
118
118
|
declare function validateMandateShape(value: unknown): void;
|
|
119
119
|
/**
|
|
120
|
-
* Validate
|
|
121
|
-
* Checks required fields are present and correctly typed.
|
|
120
|
+
* Validate upto sub-object (usage-based variable settlement).
|
|
122
121
|
*/
|
|
122
|
+
declare function validateUptoShape(value: unknown): void;
|
|
123
|
+
/**
|
|
124
|
+
* Validate settlementOverrides sub-object.
|
|
125
|
+
*/
|
|
126
|
+
declare function validateSettlementOverridesShape(value: unknown): void;
|
|
123
127
|
declare function validateStreamShape(value: unknown): void;
|
|
124
128
|
/**
|
|
125
129
|
* Validate escrow sub-object.
|
|
@@ -195,4 +199,4 @@ declare function detectProtocol(headers: Headers): 's402' | 'x402' | 'unknown';
|
|
|
195
199
|
*/
|
|
196
200
|
declare function extractRequirementsFromResponse(response: Response): s402PaymentRequirements | null;
|
|
197
201
|
//#endregion
|
|
198
|
-
export { S402_CONTENT_TYPE, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, pickPayloadFields, pickRequirementsFields, pickSettleResponseFields, validateEscrowShape, validateMandateShape, validatePrepaidShape, validateRequirementsShape, validateStreamShape, validateSubObjects, validateUnlockShape };
|
|
202
|
+
export { S402_CONTENT_TYPE, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, pickPayloadFields, pickRequirementsFields, pickSettleResponseFields, validateEscrowShape, validateMandateShape, validatePrepaidShape, validateRequirementsShape, validateSettlementOverridesShape, validateStreamShape, validateSubObjects, validateUnlockShape, validateUptoShape };
|
package/dist/http.mjs
CHANGED
|
@@ -83,10 +83,12 @@ const S402_REQUIREMENTS_KEYS = new Set([
|
|
|
83
83
|
"receiptRequired",
|
|
84
84
|
"settlementMode",
|
|
85
85
|
"expiresAt",
|
|
86
|
+
"upto",
|
|
87
|
+
"settlementOverrides",
|
|
88
|
+
"prepaid",
|
|
86
89
|
"stream",
|
|
87
90
|
"escrow",
|
|
88
91
|
"unlock",
|
|
89
|
-
"prepaid",
|
|
90
92
|
"extensions"
|
|
91
93
|
]);
|
|
92
94
|
/** Known keys for each sub-object type — used to strip extra keys at the trust boundary. */
|
|
@@ -96,6 +98,21 @@ const S402_SUB_OBJECT_KEYS = {
|
|
|
96
98
|
"minPerTx",
|
|
97
99
|
"coinType"
|
|
98
100
|
]),
|
|
101
|
+
upto: new Set([
|
|
102
|
+
"maxAmount",
|
|
103
|
+
"settlementDeadlineMs",
|
|
104
|
+
"usageReportUrl",
|
|
105
|
+
"estimatedAmount"
|
|
106
|
+
]),
|
|
107
|
+
settlementOverrides: new Set(["actualAmount"]),
|
|
108
|
+
prepaid: new Set([
|
|
109
|
+
"ratePerCall",
|
|
110
|
+
"maxCalls",
|
|
111
|
+
"minDeposit",
|
|
112
|
+
"withdrawalDelayMs",
|
|
113
|
+
"providerPubkey",
|
|
114
|
+
"disputeWindowMs"
|
|
115
|
+
]),
|
|
99
116
|
stream: new Set([
|
|
100
117
|
"ratePerSecond",
|
|
101
118
|
"budgetCap",
|
|
@@ -111,14 +128,6 @@ const S402_SUB_OBJECT_KEYS = {
|
|
|
111
128
|
"encryptionId",
|
|
112
129
|
"encryptedContentId",
|
|
113
130
|
"encryptionServiceId"
|
|
114
|
-
]),
|
|
115
|
-
prepaid: new Set([
|
|
116
|
-
"ratePerCall",
|
|
117
|
-
"maxCalls",
|
|
118
|
-
"minDeposit",
|
|
119
|
-
"withdrawalDelayMs",
|
|
120
|
-
"providerPubkey",
|
|
121
|
-
"disputeWindowMs"
|
|
122
131
|
])
|
|
123
132
|
};
|
|
124
133
|
/** Strip unknown keys from a sub-object, returning a clean copy. */
|
|
@@ -178,22 +187,29 @@ const S402_PAYLOAD_TOP_KEYS = new Set([
|
|
|
178
187
|
]);
|
|
179
188
|
/**
|
|
180
189
|
* Known inner payload keys per scheme. All schemes share transaction + signature;
|
|
181
|
-
*
|
|
190
|
+
* upto adds maxAmount + settlementCeiling, prepaid adds ratePerCall + maxCalls,
|
|
191
|
+
* unlock adds encryptionId.
|
|
182
192
|
*/
|
|
183
193
|
const S402_PAYLOAD_INNER_KEYS = {
|
|
184
194
|
exact: new Set(["transaction", "signature"]),
|
|
185
|
-
|
|
186
|
-
escrow: new Set(["transaction", "signature"]),
|
|
187
|
-
unlock: new Set([
|
|
195
|
+
upto: new Set([
|
|
188
196
|
"transaction",
|
|
189
197
|
"signature",
|
|
190
|
-
"
|
|
198
|
+
"maxAmount",
|
|
199
|
+
"settlementCeiling"
|
|
191
200
|
]),
|
|
192
201
|
prepaid: new Set([
|
|
193
202
|
"transaction",
|
|
194
203
|
"signature",
|
|
195
204
|
"ratePerCall",
|
|
196
205
|
"maxCalls"
|
|
206
|
+
]),
|
|
207
|
+
stream: new Set(["transaction", "signature"]),
|
|
208
|
+
escrow: new Set(["transaction", "signature"]),
|
|
209
|
+
unlock: new Set([
|
|
210
|
+
"transaction",
|
|
211
|
+
"signature",
|
|
212
|
+
"encryptionId"
|
|
197
213
|
])
|
|
198
214
|
};
|
|
199
215
|
/** Return a clean payload object with only known s402 payload fields. */
|
|
@@ -249,9 +265,11 @@ const S402_SETTLE_RESPONSE_KEYS = new Set([
|
|
|
249
265
|
"txDigest",
|
|
250
266
|
"receiptId",
|
|
251
267
|
"finalityMs",
|
|
268
|
+
"actualAmount",
|
|
269
|
+
"depositId",
|
|
270
|
+
"balanceId",
|
|
252
271
|
"streamId",
|
|
253
272
|
"escrowId",
|
|
254
|
-
"balanceId",
|
|
255
273
|
"error",
|
|
256
274
|
"errorCode"
|
|
257
275
|
]);
|
|
@@ -277,10 +295,11 @@ function decodeSettleResponse(header) {
|
|
|
277
295
|
/** Valid s402 payment scheme values */
|
|
278
296
|
const VALID_SCHEMES = new Set([
|
|
279
297
|
"exact",
|
|
298
|
+
"upto",
|
|
299
|
+
"prepaid",
|
|
280
300
|
"stream",
|
|
281
301
|
"escrow",
|
|
282
|
-
"unlock"
|
|
283
|
-
"prepaid"
|
|
302
|
+
"unlock"
|
|
284
303
|
]);
|
|
285
304
|
/**
|
|
286
305
|
* Check that a string represents a canonical non-negative integer.
|
|
@@ -338,12 +357,37 @@ function validateMandateShape(value) {
|
|
|
338
357
|
const obj = value;
|
|
339
358
|
if (typeof obj.required !== "boolean") throw new s402Error("INVALID_PAYLOAD", `mandate.required must be a boolean, got ${typeof obj.required}`);
|
|
340
359
|
assertOptionalString(obj, "minPerTx", "mandate");
|
|
360
|
+
if (typeof obj.minPerTx === "string" && !isValidAmount(obj.minPerTx)) throw new s402Error("INVALID_PAYLOAD", `mandate.minPerTx must be a non-negative integer string, got "${obj.minPerTx}"`);
|
|
341
361
|
assertOptionalString(obj, "coinType", "mandate");
|
|
342
362
|
}
|
|
343
363
|
/**
|
|
344
|
-
* Validate
|
|
345
|
-
|
|
364
|
+
* Validate upto sub-object (usage-based variable settlement).
|
|
365
|
+
*/
|
|
366
|
+
function validateUptoShape(value) {
|
|
367
|
+
assertPlainObject(value, "upto");
|
|
368
|
+
const obj = value;
|
|
369
|
+
assertString(obj, "maxAmount", "upto");
|
|
370
|
+
if (typeof obj.maxAmount === "string" && !isValidAmount(obj.maxAmount)) throw new s402Error("INVALID_PAYLOAD", `upto.maxAmount must be a non-negative integer string, got "${obj.maxAmount}"`);
|
|
371
|
+
assertString(obj, "settlementDeadlineMs", "upto");
|
|
372
|
+
if (typeof obj.settlementDeadlineMs === "string" && !isValidAmount(obj.settlementDeadlineMs)) throw new s402Error("INVALID_PAYLOAD", `upto.settlementDeadlineMs must be a non-negative integer string (Unix timestamp ms), got "${obj.settlementDeadlineMs}"`);
|
|
373
|
+
assertOptionalString(obj, "usageReportUrl", "upto");
|
|
374
|
+
assertOptionalString(obj, "estimatedAmount", "upto");
|
|
375
|
+
if (typeof obj.estimatedAmount === "string") {
|
|
376
|
+
if (!isValidAmount(obj.estimatedAmount)) throw new s402Error("INVALID_PAYLOAD", `upto.estimatedAmount must be a non-negative integer string, got "${obj.estimatedAmount}"`);
|
|
377
|
+
if (typeof obj.maxAmount === "string" && isValidAmount(obj.maxAmount)) {
|
|
378
|
+
if (BigInt(obj.estimatedAmount) > BigInt(obj.maxAmount)) throw new s402Error("INVALID_PAYLOAD", `upto.estimatedAmount (${obj.estimatedAmount}) must be <= maxAmount (${obj.maxAmount})`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Validate settlementOverrides sub-object.
|
|
346
384
|
*/
|
|
385
|
+
function validateSettlementOverridesShape(value) {
|
|
386
|
+
assertPlainObject(value, "settlementOverrides");
|
|
387
|
+
const obj = value;
|
|
388
|
+
assertString(obj, "actualAmount", "settlementOverrides");
|
|
389
|
+
if (typeof obj.actualAmount === "string" && !isValidAmount(obj.actualAmount)) throw new s402Error("INVALID_PAYLOAD", `settlementOverrides.actualAmount must be a non-negative integer string, got "${obj.actualAmount}"`);
|
|
390
|
+
}
|
|
347
391
|
function validateStreamShape(value) {
|
|
348
392
|
assertPlainObject(value, "stream");
|
|
349
393
|
const obj = value;
|
|
@@ -404,10 +448,12 @@ function validatePrepaidShape(value) {
|
|
|
404
448
|
*/
|
|
405
449
|
function validateSubObjects(record) {
|
|
406
450
|
if (record.mandate !== void 0) validateMandateShape(record.mandate);
|
|
451
|
+
if (record.upto !== void 0) validateUptoShape(record.upto);
|
|
452
|
+
if (record.settlementOverrides !== void 0) validateSettlementOverridesShape(record.settlementOverrides);
|
|
453
|
+
if (record.prepaid !== void 0) validatePrepaidShape(record.prepaid);
|
|
407
454
|
if (record.stream !== void 0) validateStreamShape(record.stream);
|
|
408
455
|
if (record.escrow !== void 0) validateEscrowShape(record.escrow);
|
|
409
456
|
if (record.unlock !== void 0) validateUnlockShape(record.unlock);
|
|
410
|
-
if (record.prepaid !== void 0) validatePrepaidShape(record.prepaid);
|
|
411
457
|
}
|
|
412
458
|
/** Validate that decoded payment requirements have the required shape. */
|
|
413
459
|
function validateRequirementsShape(obj) {
|
|
@@ -474,8 +520,25 @@ function validatePayloadShape(obj) {
|
|
|
474
520
|
if (typeof inner.transaction !== "string") throw new s402Error("INVALID_PAYLOAD", `payload.transaction must be a string, got ${typeof inner.transaction}`);
|
|
475
521
|
if (typeof inner.signature !== "string") throw new s402Error("INVALID_PAYLOAD", `payload.signature must be a string, got ${typeof inner.signature}`);
|
|
476
522
|
if (record.scheme === "unlock" && typeof inner.encryptionId !== "string") throw new s402Error("INVALID_PAYLOAD", `unlock payload requires encryptionId (string), got ${typeof inner.encryptionId}`);
|
|
477
|
-
if (record.scheme === "
|
|
478
|
-
|
|
523
|
+
if (record.scheme === "upto") {
|
|
524
|
+
if (typeof inner.maxAmount !== "string") throw new s402Error("INVALID_PAYLOAD", `upto payload requires maxAmount (string), got ${typeof inner.maxAmount}`);
|
|
525
|
+
if (!isValidAmount(inner.maxAmount)) throw new s402Error("INVALID_PAYLOAD", `upto payload maxAmount must be a non-negative integer string, got "${inner.maxAmount}"`);
|
|
526
|
+
if (inner.settlementCeiling !== void 0) {
|
|
527
|
+
if (typeof inner.settlementCeiling !== "string") throw new s402Error("INVALID_PAYLOAD", `upto payload settlementCeiling must be a string if provided, got ${typeof inner.settlementCeiling}`);
|
|
528
|
+
if (!isValidAmount(inner.settlementCeiling)) throw new s402Error("INVALID_PAYLOAD", `upto payload settlementCeiling must be a non-negative integer string, got "${inner.settlementCeiling}"`);
|
|
529
|
+
const ceiling = BigInt(inner.settlementCeiling);
|
|
530
|
+
if (ceiling < 1n) throw new s402Error("INVALID_PAYLOAD", `upto payload settlementCeiling must be >= 1, got "${inner.settlementCeiling}"`);
|
|
531
|
+
if (ceiling > BigInt(inner.maxAmount)) throw new s402Error("INVALID_PAYLOAD", `upto payload settlementCeiling (${inner.settlementCeiling}) must be <= maxAmount (${inner.maxAmount})`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (record.scheme === "prepaid") {
|
|
535
|
+
if (typeof inner.ratePerCall !== "string") throw new s402Error("INVALID_PAYLOAD", `prepaid payload requires ratePerCall (string), got ${typeof inner.ratePerCall}`);
|
|
536
|
+
if (!isValidAmount(inner.ratePerCall)) throw new s402Error("INVALID_PAYLOAD", `prepaid payload ratePerCall must be a non-negative integer string, got "${inner.ratePerCall}"`);
|
|
537
|
+
if (inner.maxCalls !== void 0) {
|
|
538
|
+
if (typeof inner.maxCalls !== "string") throw new s402Error("INVALID_PAYLOAD", `prepaid payload maxCalls must be a string if provided, got ${typeof inner.maxCalls}`);
|
|
539
|
+
if (!isValidAmount(inner.maxCalls)) throw new s402Error("INVALID_PAYLOAD", `prepaid payload maxCalls must be a non-negative integer string, got "${inner.maxCalls}"`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
479
542
|
}
|
|
480
543
|
/** Validate that a decoded settle response has the required shape. */
|
|
481
544
|
function validateSettleShape(obj) {
|
|
@@ -488,6 +551,8 @@ function validateSettleShape(obj) {
|
|
|
488
551
|
if (record.streamId !== void 0 && typeof record.streamId !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: streamId must be a string, got ${typeof record.streamId}`);
|
|
489
552
|
if (record.escrowId !== void 0 && typeof record.escrowId !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: escrowId must be a string, got ${typeof record.escrowId}`);
|
|
490
553
|
if (record.balanceId !== void 0 && typeof record.balanceId !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: balanceId must be a string, got ${typeof record.balanceId}`);
|
|
554
|
+
if (record.actualAmount !== void 0 && typeof record.actualAmount !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: actualAmount must be a string, got ${typeof record.actualAmount}`);
|
|
555
|
+
if (record.depositId !== void 0 && typeof record.depositId !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: depositId must be a string, got ${typeof record.depositId}`);
|
|
491
556
|
if (record.error !== void 0 && typeof record.error !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: error must be a string, got ${typeof record.error}`);
|
|
492
557
|
if (record.errorCode !== void 0 && typeof record.errorCode !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: errorCode must be a string, got ${typeof record.errorCode}`);
|
|
493
558
|
}
|
|
@@ -605,4 +670,4 @@ function extractRequirementsFromResponse(response) {
|
|
|
605
670
|
}
|
|
606
671
|
|
|
607
672
|
//#endregion
|
|
608
|
-
export { S402_CONTENT_TYPE, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, pickPayloadFields, pickRequirementsFields, pickSettleResponseFields, validateEscrowShape, validateMandateShape, validatePrepaidShape, validateRequirementsShape, validateStreamShape, validateSubObjects, validateUnlockShape };
|
|
673
|
+
export { S402_CONTENT_TYPE, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, pickPayloadFields, pickRequirementsFields, pickSettleResponseFields, validateEscrowShape, validateMandateShape, validatePrepaidShape, validateRequirementsShape, validateSettlementOverridesShape, validateStreamShape, validateSubObjects, validateUnlockShape, validateUptoShape };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createS402Error, s402Error, s402ErrorCode, s402ErrorCodeType, s402ErrorInfo } from "./errors.mjs";
|
|
2
|
-
import { S402_HEADERS, S402_VERSION, s402Discovery, s402EscrowExtra, s402EscrowPayload, s402ExactPayload, s402Mandate, s402MandateRequirements, s402PaymentPayload, s402PaymentPayloadBase, s402PaymentRequirements, s402PaymentSession, s402PrepaidExtra, s402PrepaidPayload, s402RegistryQuery, s402Scheme, s402ServiceEntry, s402SettleResponse, s402SettlementMode, s402StreamExtra, s402StreamPayload, s402UnlockExtra, s402UnlockPayload, s402VerifyResponse } from "./types.mjs";
|
|
2
|
+
import { S402_HEADERS, S402_VERSION, s402Discovery, s402EscrowExtra, s402EscrowPayload, s402ExactPayload, s402Mandate, s402MandateRequirements, s402PaymentPayload, s402PaymentPayloadBase, s402PaymentRequirements, s402PaymentSession, s402PrepaidExtra, s402PrepaidPayload, s402RegistryQuery, s402Scheme, s402ServiceEntry, s402SettleResponse, s402SettlementMode, s402SettlementOverrides, s402StreamExtra, s402StreamPayload, s402UnlockExtra, s402UnlockPayload, s402UptoExtra, s402UptoPayload, s402VerifyResponse } from "./types.mjs";
|
|
3
3
|
import { S402_CONTENT_TYPE, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, validateRequirementsShape } from "./http.mjs";
|
|
4
|
-
import { a as s402ServerScheme, i as s402RouteConfig, n as s402DirectScheme, o as s402SettlementVerification, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-
|
|
4
|
+
import { a as s402ServerScheme, i as s402RouteConfig, n as s402DirectScheme, o as s402SettlementVerification, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-m-uk4zyH.mjs";
|
|
5
5
|
import { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader, s402Receipt, s402ReceiptSigner, s402ReceiptVerifier } from "./receipts.mjs";
|
|
6
6
|
|
|
7
7
|
//#region src/client.d.ts
|
|
@@ -58,14 +58,170 @@ declare class s402Client {
|
|
|
58
58
|
supports(network: string, scheme: s402Scheme): boolean;
|
|
59
59
|
}
|
|
60
60
|
//#endregion
|
|
61
|
+
//#region src/extensions.d.ts
|
|
62
|
+
/**
|
|
63
|
+
* Base extension interface. All three actor-specific interfaces extend this.
|
|
64
|
+
*
|
|
65
|
+
* Extensions use reverse-domain keys (per ADR-001 §4a) to avoid conflicts:
|
|
66
|
+
* e.g., "org.s402.discovery", "com.mycompany.ratelimit".
|
|
67
|
+
*/
|
|
68
|
+
interface s402Extension {
|
|
69
|
+
/** Reverse-domain key: e.g., "org.s402.discovery" */
|
|
70
|
+
readonly key: string;
|
|
71
|
+
/** Semver version (per ADR-001 §4b) */
|
|
72
|
+
readonly version: string;
|
|
73
|
+
/**
|
|
74
|
+
* If true, failure in this extension blocks the payment flow.
|
|
75
|
+
* If false, failure is logged but doesn't block (advisory).
|
|
76
|
+
*/
|
|
77
|
+
readonly critical: boolean;
|
|
78
|
+
/** Keys of extensions that must run before this one. */
|
|
79
|
+
readonly dependsOn?: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Client-side extension.
|
|
83
|
+
* Hooks fire during payment creation and settlement verification.
|
|
84
|
+
*/
|
|
85
|
+
interface s402ClientExtension extends s402Extension {
|
|
86
|
+
/** Enrich the payment payload before sending. */
|
|
87
|
+
enrichPayload?(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402PaymentPayload>;
|
|
88
|
+
/** Process extension data from the settle response. */
|
|
89
|
+
onSettlement?(response: s402SettleResponse, payload: s402PaymentPayload): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Server-side extension (resource server).
|
|
93
|
+
* Hooks fire during requirements building and settlement response.
|
|
94
|
+
*/
|
|
95
|
+
interface s402ServerExtension extends s402Extension {
|
|
96
|
+
/** Enrich payment requirements before sending the 402 response. */
|
|
97
|
+
enrichRequirements?(requirements: s402PaymentRequirements, config: s402RouteConfig): s402PaymentRequirements;
|
|
98
|
+
/** Enrich the settlement response before returning to client. */
|
|
99
|
+
enrichSettleResponse?(response: s402SettleResponse, payload: s402PaymentPayload): Promise<s402SettleResponse>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Facilitator-side extension.
|
|
103
|
+
* Hooks fire during the verify→settle pipeline.
|
|
104
|
+
*
|
|
105
|
+
* Four hooks covering every phase:
|
|
106
|
+
* beforeVerify → verify() → afterVerify → beforeSettle → settle() → afterSettle
|
|
107
|
+
*/
|
|
108
|
+
interface s402FacilitatorExtension extends s402Extension {
|
|
109
|
+
/** Called before verify(). Can reject by throwing. */
|
|
110
|
+
beforeVerify?(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<void>;
|
|
111
|
+
/** Called after successful verify(), before settle(). */
|
|
112
|
+
afterVerify?(payload: s402PaymentPayload, verifyResult: s402VerifyResponse): Promise<void>;
|
|
113
|
+
/** Called before settle(). Last chance to abort. */
|
|
114
|
+
beforeSettle?(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<void>;
|
|
115
|
+
/** Called after successful settle() only (not on settlement failure).
|
|
116
|
+
* Failures here never change the settle result — the tx is already on-chain. */
|
|
117
|
+
afterSettle?(payload: s402PaymentPayload, settleResult: s402SettleResponse): Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Type-safe extension data retrieval.
|
|
121
|
+
*
|
|
122
|
+
* The wire format is `Record<string, unknown>` for interop, but this helper
|
|
123
|
+
* provides typed access for TypeScript consumers.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* interface DiscoveryData { services: string[] }
|
|
128
|
+
* const data = getExtensionData<DiscoveryData>(requirements.extensions, 'org.s402.discovery');
|
|
129
|
+
* if (data) console.log(data.services);
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
declare function getExtensionData<T>(extensions: Record<string, unknown> | undefined, key: string): T | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Set extension data on an extensions record (creates if needed).
|
|
135
|
+
* Returns a new extensions object (does not mutate the input).
|
|
136
|
+
*/
|
|
137
|
+
declare function setExtensionData(extensions: Record<string, unknown> | undefined, key: string, data: unknown): Record<string, unknown>;
|
|
138
|
+
/**
|
|
139
|
+
* Registry for extensions with dependency-ordered execution.
|
|
140
|
+
*
|
|
141
|
+
* Extensions are stored by key and sorted topologically based on `dependsOn`.
|
|
142
|
+
* Within the same dependency level, registration order is preserved.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* const registry = new s402ExtensionRegistry<s402FacilitatorExtension>();
|
|
147
|
+
* registry.register(rateLimitExtension);
|
|
148
|
+
* registry.register(analyticsExtension);
|
|
149
|
+
* const sorted = registry.sorted(); // dependency-ordered list
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
declare class s402ExtensionRegistry<T extends s402Extension> {
|
|
153
|
+
private extensions;
|
|
154
|
+
private sortedCache;
|
|
155
|
+
/**
|
|
156
|
+
* Register an extension. Throws on duplicate key or dependency cycle.
|
|
157
|
+
*/
|
|
158
|
+
register(ext: T): void;
|
|
159
|
+
/** Get a registered extension by key. */
|
|
160
|
+
get(key: string): T | undefined;
|
|
161
|
+
/** Number of registered extensions. */
|
|
162
|
+
get size(): number;
|
|
163
|
+
/**
|
|
164
|
+
* Return extensions in topological (dependency) order.
|
|
165
|
+
* Cached until a new extension is registered.
|
|
166
|
+
*/
|
|
167
|
+
sorted(): T[];
|
|
168
|
+
}
|
|
169
|
+
/** Callback for advisory extension failures. */
|
|
170
|
+
type s402ExtensionErrorHandler = (ext: s402Extension, error: unknown) => void;
|
|
171
|
+
/**
|
|
172
|
+
* Run an async hook on all extensions in order.
|
|
173
|
+
* Critical extensions throw on failure; advisory extensions call the error handler.
|
|
174
|
+
*/
|
|
175
|
+
declare function runExtensionHooks<T extends s402Extension>(extensions: T[], hookName: string, runner: (ext: T) => Promise<void>, onError?: s402ExtensionErrorHandler): Promise<void>;
|
|
176
|
+
//#endregion
|
|
61
177
|
//#region src/facilitator.d.ts
|
|
178
|
+
/**
|
|
179
|
+
* Options for `s402Facilitator.process()`.
|
|
180
|
+
*
|
|
181
|
+
* Callers can tune the verify→settle pipeline per-request.
|
|
182
|
+
*/
|
|
183
|
+
interface s402ProcessOptions {
|
|
184
|
+
/**
|
|
185
|
+
* Skip the verify() dry-run and go straight to settle().
|
|
186
|
+
*
|
|
187
|
+
* On chains where failed transactions cost zero gas (e.g., Sui PTBs revert
|
|
188
|
+
* atomically with no gas charge), the dry-run is pure latency overhead — it
|
|
189
|
+
* adds a full RPC round-trip (~200-400ms) just to predict what settle() will
|
|
190
|
+
* discover anyway. Setting `skipVerify: true` eliminates that round-trip.
|
|
191
|
+
*
|
|
192
|
+
* When `true`, process() still performs: expiration checks, scheme-mismatch
|
|
193
|
+
* checks, deduplication, and settle timeout. Only the dry-run is skipped.
|
|
194
|
+
*
|
|
195
|
+
* **Use when:** your chain adapter knows failures are free (Sui, Aptos).
|
|
196
|
+
* **Don't use when:** failed settlements cost gas (EVM L1s) — the dry-run
|
|
197
|
+
* saves real money by catching bad txs before broadcast.
|
|
198
|
+
*
|
|
199
|
+
* @default false
|
|
200
|
+
*/
|
|
201
|
+
skipVerify?: boolean;
|
|
202
|
+
}
|
|
62
203
|
declare class s402Facilitator {
|
|
63
204
|
private schemes;
|
|
64
205
|
private inFlight;
|
|
206
|
+
private extensionRegistry;
|
|
207
|
+
private extensionErrorHandler?;
|
|
65
208
|
/**
|
|
66
209
|
* Register a scheme-specific facilitator for a network.
|
|
67
210
|
*/
|
|
68
211
|
register(network: string, scheme: s402FacilitatorScheme): this;
|
|
212
|
+
/**
|
|
213
|
+
* Register a facilitator extension. Extensions fire in dependency order
|
|
214
|
+
* at four points in the process() pipeline: beforeVerify, afterVerify,
|
|
215
|
+
* beforeSettle, afterSettle.
|
|
216
|
+
*
|
|
217
|
+
* @throws {s402Error} `EXTENSION_FAILED` on duplicate key or dependency cycle
|
|
218
|
+
*/
|
|
219
|
+
registerExtension(ext: s402FacilitatorExtension): this;
|
|
220
|
+
/**
|
|
221
|
+
* Set the handler for advisory (non-critical) extension failures.
|
|
222
|
+
* Critical extensions always throw; advisory extensions call this handler.
|
|
223
|
+
*/
|
|
224
|
+
onExtensionError(handler: s402ExtensionErrorHandler): this;
|
|
69
225
|
/**
|
|
70
226
|
* Verify a payment payload by dispatching to the correct scheme.
|
|
71
227
|
* Includes expiration guard and scheme-mismatch check.
|
|
@@ -88,6 +244,7 @@ declare class s402Facilitator {
|
|
|
88
244
|
*
|
|
89
245
|
* @param payload - Client's payment payload
|
|
90
246
|
* @param requirements - Server's payment requirements
|
|
247
|
+
* @param options - Optional process configuration (e.g., `{ skipVerify: true }` for zero-cost-failure chains)
|
|
91
248
|
* @returns Settlement result (check `result.success` and `result.errorCode`)
|
|
92
249
|
*
|
|
93
250
|
* @example
|
|
@@ -105,7 +262,7 @@ declare class s402Facilitator {
|
|
|
105
262
|
* }
|
|
106
263
|
* ```
|
|
107
264
|
*/
|
|
108
|
-
process(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
|
|
265
|
+
process(payload: s402PaymentPayload, requirements: s402PaymentRequirements, options?: s402ProcessOptions): Promise<s402SettleResponse>;
|
|
109
266
|
/**
|
|
110
267
|
* Check if a scheme is supported for a network.
|
|
111
268
|
*/
|
|
@@ -185,4 +342,4 @@ declare class s402ResourceServer {
|
|
|
185
342
|
process(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
|
|
186
343
|
}
|
|
187
344
|
//#endregion
|
|
188
|
-
export { S402_CONTENT_TYPE, S402_HEADERS, S402_RECEIPT_HEADER, S402_VERSION, createS402Error, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, formatReceiptHeader, isValidAmount, isValidU64Amount, parseReceiptHeader, s402Client, type s402ClientScheme, type s402DirectScheme, type s402Discovery, s402Error, s402ErrorCode, type s402ErrorCodeType, type s402ErrorInfo, type s402EscrowExtra, type s402EscrowPayload, type s402ExactPayload, s402Facilitator, type s402FacilitatorScheme, type s402Mandate, type s402MandateRequirements, type s402PaymentPayload, type s402PaymentPayloadBase, type s402PaymentRequirements, type s402PaymentSession, type s402PrepaidExtra, type s402PrepaidPayload, type s402Receipt, type s402ReceiptSigner, type s402ReceiptVerifier, type s402RegistryQuery, s402ResourceServer, type s402RouteConfig, type s402Scheme, type s402ServerScheme, type s402ServiceEntry, type s402SettleResponse, type s402SettlementMode, type s402SettlementVerification, type s402StreamExtra, type s402StreamPayload, type s402UnlockExtra, type s402UnlockPayload, type s402VerifyResponse, validateRequirementsShape };
|
|
345
|
+
export { S402_CONTENT_TYPE, S402_HEADERS, S402_RECEIPT_HEADER, S402_VERSION, createS402Error, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, formatReceiptHeader, getExtensionData, isValidAmount, isValidU64Amount, parseReceiptHeader, runExtensionHooks, s402Client, type s402ClientExtension, type s402ClientScheme, type s402DirectScheme, type s402Discovery, s402Error, s402ErrorCode, type s402ErrorCodeType, type s402ErrorInfo, type s402EscrowExtra, type s402EscrowPayload, type s402ExactPayload, type s402Extension, type s402ExtensionErrorHandler, s402ExtensionRegistry, s402Facilitator, type s402FacilitatorExtension, type s402FacilitatorScheme, type s402Mandate, type s402MandateRequirements, type s402PaymentPayload, type s402PaymentPayloadBase, type s402PaymentRequirements, type s402PaymentSession, type s402PrepaidExtra, type s402PrepaidPayload, type s402ProcessOptions, type s402Receipt, type s402ReceiptSigner, type s402ReceiptVerifier, type s402RegistryQuery, s402ResourceServer, type s402RouteConfig, type s402Scheme, type s402ServerExtension, type s402ServerScheme, type s402ServiceEntry, type s402SettleResponse, type s402SettlementMode, type s402SettlementOverrides, type s402SettlementVerification, type s402StreamExtra, type s402StreamPayload, type s402UnlockExtra, type s402UnlockPayload, type s402UptoExtra, type s402UptoPayload, type s402VerifyResponse, setExtensionData, validateRequirementsShape };
|