s402 0.5.0 → 0.6.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 +32 -0
- package/README.md +10 -0
- package/dist/compat-mpp.d.mts +160 -0
- package/dist/compat-mpp.mjs +363 -0
- package/dist/errors.d.mts +3 -1
- package/dist/errors.mjs +12 -2
- package/dist/http.d.mts +24 -7
- package/dist/http.mjs +30 -8
- package/dist/index.d.mts +301 -6
- package/dist/index.mjs +595 -86
- package/dist/test-utils.d.mts +1 -1
- package/dist/types.d.mts +2 -1
- package/dist/types.mjs +2 -1
- package/package.json +8 -1
- /package/dist/{scheme-m-uk4zyH.d.mts → scheme-CKinOhyx.d.mts} +0 -0
package/dist/http.d.mts
CHANGED
|
@@ -14,10 +14,10 @@ import { s402PaymentPayload, s402PaymentRequirements, s402SettleResponse } from
|
|
|
14
14
|
* const header = encodePaymentRequired({
|
|
15
15
|
* s402Version: '1',
|
|
16
16
|
* accepts: ['exact'],
|
|
17
|
-
* network: '
|
|
18
|
-
* asset: '
|
|
17
|
+
* network: 'your-chain:mainnet',
|
|
18
|
+
* asset: 'NATIVE_TOKEN',
|
|
19
19
|
* amount: '1000000',
|
|
20
|
-
* payTo: '
|
|
20
|
+
* payTo: 'YOUR_ADDRESS',
|
|
21
21
|
* });
|
|
22
22
|
* response.headers.set('payment-required', header);
|
|
23
23
|
* ```
|
|
@@ -144,8 +144,23 @@ declare function validatePrepaidShape(value: unknown): void;
|
|
|
144
144
|
declare function validateSubObjects(record: Record<string, unknown>): void;
|
|
145
145
|
/** Validate that decoded payment requirements have the required shape. */
|
|
146
146
|
declare function validateRequirementsShape(obj: unknown): void;
|
|
147
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Content type for s402 JSON body transport.
|
|
149
|
+
*
|
|
150
|
+
* Covers generic s402 request/response bodies (payload, requirements, legacy
|
|
151
|
+
* settle). The settlement envelope (ADR-007) has its own more specific media
|
|
152
|
+
* type — see `S402_ENVELOPE_CONTENT_TYPE` in `./envelope`.
|
|
153
|
+
*/
|
|
148
154
|
declare const S402_CONTENT_TYPE: "application/s402+json";
|
|
155
|
+
/**
|
|
156
|
+
* Maximum body size (1 MB). Defense-in-depth against oversized JSON payloads.
|
|
157
|
+
* Bigger than MAX_HEADER_BYTES (64KB) because body transport is designed for
|
|
158
|
+
* large PTBs; still bounded so a malicious client can't exhaust memory via
|
|
159
|
+
* JSON.parse. Hosts should also enforce their own limits upstream (Express
|
|
160
|
+
* `limit`, Nginx `client_max_body_size`), but a wire format library should
|
|
161
|
+
* not rely on runtime enforcement.
|
|
162
|
+
*/
|
|
163
|
+
declare const MAX_BODY_BYTES: number;
|
|
149
164
|
/** Encode payment requirements as JSON string (for response body) */
|
|
150
165
|
declare function encodeRequirementsBody(requirements: s402PaymentRequirements): string;
|
|
151
166
|
/** Decode payment requirements from JSON string (from response body) */
|
|
@@ -161,8 +176,10 @@ declare function decodeSettleBody(body: string): s402SettleResponse;
|
|
|
161
176
|
/**
|
|
162
177
|
* Detect transport mode from an incoming request.
|
|
163
178
|
*
|
|
164
|
-
* Checks Content-Type for body transport
|
|
165
|
-
*
|
|
179
|
+
* Checks Content-Type for body transport (generic `application/s402+json`
|
|
180
|
+
* OR the envelope-specific `application/vnd.s402.envelope+json`), then falls
|
|
181
|
+
* back to header detection.
|
|
182
|
+
* Returns 'body' if either s402 body media type is present.
|
|
166
183
|
* Returns 'header' if x-payment header is present.
|
|
167
184
|
* Returns 'unknown' otherwise.
|
|
168
185
|
*/
|
|
@@ -199,4 +216,4 @@ declare function detectProtocol(headers: Headers): 's402' | 'x402' | 'unknown';
|
|
|
199
216
|
*/
|
|
200
217
|
declare function extractRequirementsFromResponse(response: Response): s402PaymentRequirements | null;
|
|
201
218
|
//#endregion
|
|
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 };
|
|
219
|
+
export { MAX_BODY_BYTES, 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
|
@@ -26,10 +26,10 @@ function fromBase64(b64) {
|
|
|
26
26
|
* const header = encodePaymentRequired({
|
|
27
27
|
* s402Version: '1',
|
|
28
28
|
* accepts: ['exact'],
|
|
29
|
-
* network: '
|
|
30
|
-
* asset: '
|
|
29
|
+
* network: 'your-chain:mainnet',
|
|
30
|
+
* asset: 'NATIVE_TOKEN',
|
|
31
31
|
* amount: '1000000',
|
|
32
|
-
* payTo: '
|
|
32
|
+
* payTo: 'YOUR_ADDRESS',
|
|
33
33
|
* });
|
|
34
34
|
* response.headers.set('payment-required', header);
|
|
35
35
|
* ```
|
|
@@ -556,8 +556,23 @@ function validateSettleShape(obj) {
|
|
|
556
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}`);
|
|
557
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}`);
|
|
558
558
|
}
|
|
559
|
-
/**
|
|
559
|
+
/**
|
|
560
|
+
* Content type for s402 JSON body transport.
|
|
561
|
+
*
|
|
562
|
+
* Covers generic s402 request/response bodies (payload, requirements, legacy
|
|
563
|
+
* settle). The settlement envelope (ADR-007) has its own more specific media
|
|
564
|
+
* type — see `S402_ENVELOPE_CONTENT_TYPE` in `./envelope`.
|
|
565
|
+
*/
|
|
560
566
|
const S402_CONTENT_TYPE = "application/s402+json";
|
|
567
|
+
/**
|
|
568
|
+
* Maximum body size (1 MB). Defense-in-depth against oversized JSON payloads.
|
|
569
|
+
* Bigger than MAX_HEADER_BYTES (64KB) because body transport is designed for
|
|
570
|
+
* large PTBs; still bounded so a malicious client can't exhaust memory via
|
|
571
|
+
* JSON.parse. Hosts should also enforce their own limits upstream (Express
|
|
572
|
+
* `limit`, Nginx `client_max_body_size`), but a wire format library should
|
|
573
|
+
* not rely on runtime enforcement.
|
|
574
|
+
*/
|
|
575
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
561
576
|
/** Encode payment requirements as JSON string (for response body) */
|
|
562
577
|
function encodeRequirementsBody(requirements) {
|
|
563
578
|
return JSON.stringify(requirements);
|
|
@@ -565,6 +580,7 @@ function encodeRequirementsBody(requirements) {
|
|
|
565
580
|
/** Decode payment requirements from JSON string (from response body) */
|
|
566
581
|
function decodeRequirementsBody(body) {
|
|
567
582
|
if (typeof body !== "string") throw new s402Error("INVALID_PAYLOAD", `s402 requirements body must be a string, got ${typeof body}`);
|
|
583
|
+
if (body.length > MAX_BODY_BYTES) throw new s402Error("INVALID_PAYLOAD", `s402 requirements body exceeds maximum size (${body.length} > ${MAX_BODY_BYTES})`);
|
|
568
584
|
let parsed;
|
|
569
585
|
try {
|
|
570
586
|
parsed = JSON.parse(body);
|
|
@@ -581,6 +597,7 @@ function encodePayloadBody(payload) {
|
|
|
581
597
|
/** Decode payment payload from JSON string (from request body) */
|
|
582
598
|
function decodePayloadBody(body) {
|
|
583
599
|
if (typeof body !== "string") throw new s402Error("INVALID_PAYLOAD", `s402 payload body must be a string, got ${typeof body}`);
|
|
600
|
+
if (body.length > MAX_BODY_BYTES) throw new s402Error("INVALID_PAYLOAD", `s402 payload body exceeds maximum size (${body.length} > ${MAX_BODY_BYTES})`);
|
|
584
601
|
let parsed;
|
|
585
602
|
try {
|
|
586
603
|
parsed = JSON.parse(body);
|
|
@@ -597,6 +614,7 @@ function encodeSettleBody(response) {
|
|
|
597
614
|
/** Decode settlement response from JSON string (from response body) */
|
|
598
615
|
function decodeSettleBody(body) {
|
|
599
616
|
if (typeof body !== "string") throw new s402Error("INVALID_PAYLOAD", `s402 settle body must be a string, got ${typeof body}`);
|
|
617
|
+
if (body.length > MAX_BODY_BYTES) throw new s402Error("INVALID_PAYLOAD", `s402 settle body exceeds maximum size (${body.length} > ${MAX_BODY_BYTES})`);
|
|
600
618
|
let parsed;
|
|
601
619
|
try {
|
|
602
620
|
parsed = JSON.parse(body);
|
|
@@ -609,13 +627,17 @@ function decodeSettleBody(body) {
|
|
|
609
627
|
/**
|
|
610
628
|
* Detect transport mode from an incoming request.
|
|
611
629
|
*
|
|
612
|
-
* Checks Content-Type for body transport
|
|
613
|
-
*
|
|
630
|
+
* Checks Content-Type for body transport (generic `application/s402+json`
|
|
631
|
+
* OR the envelope-specific `application/vnd.s402.envelope+json`), then falls
|
|
632
|
+
* back to header detection.
|
|
633
|
+
* Returns 'body' if either s402 body media type is present.
|
|
614
634
|
* Returns 'header' if x-payment header is present.
|
|
615
635
|
* Returns 'unknown' otherwise.
|
|
616
636
|
*/
|
|
617
637
|
function detectTransport(request) {
|
|
618
|
-
|
|
638
|
+
const contentType = request.headers.get("content-type");
|
|
639
|
+
if (contentType?.includes(S402_CONTENT_TYPE)) return "body";
|
|
640
|
+
if (contentType?.includes("application/vnd.s402.envelope+json")) return "body";
|
|
619
641
|
if (request.headers.get(S402_HEADERS.PAYMENT)) return "header";
|
|
620
642
|
return "unknown";
|
|
621
643
|
}
|
|
@@ -670,4 +692,4 @@ function extractRequirementsFromResponse(response) {
|
|
|
670
692
|
}
|
|
671
693
|
|
|
672
694
|
//#endregion
|
|
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 };
|
|
695
|
+
export { MAX_BODY_BYTES, 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
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
|
-
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-
|
|
3
|
+
import { MAX_BODY_BYTES, 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-CKinOhyx.mjs";
|
|
5
5
|
import { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader, s402Receipt, s402ReceiptSigner, s402ReceiptVerifier } from "./receipts.mjs";
|
|
6
6
|
|
|
7
7
|
//#region src/client.d.ts
|
|
@@ -199,12 +199,50 @@ interface s402ProcessOptions {
|
|
|
199
199
|
* @default false
|
|
200
200
|
*/
|
|
201
201
|
skipVerify?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Caller-supplied idempotency key (e.g., from an `Idempotency-Key` HTTP
|
|
204
|
+
* header). When present, this string becomes the dedup cache key instead of
|
|
205
|
+
* the auto-computed payload fingerprint.
|
|
206
|
+
*
|
|
207
|
+
* Semantics (Stripe-compatible):
|
|
208
|
+
* - Same key + same payload → returns the cached original result (retry dedup)
|
|
209
|
+
* - Same key while first call is in flight → awaits the in-flight promise (concurrent dedup)
|
|
210
|
+
* - Key collisions across distinct payloads are the caller's responsibility
|
|
211
|
+
*
|
|
212
|
+
* Omit this field to fall back to payload-based dedup (JSON fingerprint).
|
|
213
|
+
*/
|
|
214
|
+
idempotencyKey?: string;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Constructor options for `s402Facilitator`.
|
|
218
|
+
*/
|
|
219
|
+
interface s402FacilitatorOptions {
|
|
220
|
+
/**
|
|
221
|
+
* How long a completed result stays in the retry-dedup cache, in milliseconds.
|
|
222
|
+
* A retry arriving within this window returns the cached original result
|
|
223
|
+
* instead of re-executing. Tune based on your client's retry budget.
|
|
224
|
+
*
|
|
225
|
+
* @default 300_000 (5 minutes)
|
|
226
|
+
*/
|
|
227
|
+
dedupTtlMs?: number;
|
|
228
|
+
/**
|
|
229
|
+
* Maximum entries retained in the retry-dedup cache. When the cache exceeds
|
|
230
|
+
* this size, the oldest entry (by insertion order) is evicted. Bound memory
|
|
231
|
+
* under high request volume; higher values retain more retry history.
|
|
232
|
+
*
|
|
233
|
+
* @default 10_000
|
|
234
|
+
*/
|
|
235
|
+
dedupMaxEntries?: number;
|
|
202
236
|
}
|
|
203
237
|
declare class s402Facilitator {
|
|
204
238
|
private schemes;
|
|
205
239
|
private inFlight;
|
|
240
|
+
private completed;
|
|
241
|
+
private dedupTtlMs;
|
|
242
|
+
private dedupMaxEntries;
|
|
206
243
|
private extensionRegistry;
|
|
207
244
|
private extensionErrorHandler?;
|
|
245
|
+
constructor(options?: s402FacilitatorOptions);
|
|
208
246
|
/**
|
|
209
247
|
* Register a scheme-specific facilitator for a network.
|
|
210
248
|
*/
|
|
@@ -263,6 +301,9 @@ declare class s402Facilitator {
|
|
|
263
301
|
* ```
|
|
264
302
|
*/
|
|
265
303
|
process(payload: s402PaymentPayload, requirements: s402PaymentRequirements, options?: s402ProcessOptions): Promise<s402SettleResponse>;
|
|
304
|
+
private executeProcess;
|
|
305
|
+
private getCached;
|
|
306
|
+
private cacheResult;
|
|
266
307
|
/**
|
|
267
308
|
* Check if a scheme is supported for a network.
|
|
268
309
|
*/
|
|
@@ -303,9 +344,9 @@ declare class s402ResourceServer {
|
|
|
303
344
|
* const requirements = server.buildRequirements({
|
|
304
345
|
* schemes: ['exact'],
|
|
305
346
|
* price: '1000000',
|
|
306
|
-
* network: '
|
|
307
|
-
* payTo: '
|
|
308
|
-
* asset: '
|
|
347
|
+
* network: 'your-chain:mainnet',
|
|
348
|
+
* payTo: 'YOUR_ADDRESS',
|
|
349
|
+
* asset: 'NATIVE_TOKEN',
|
|
309
350
|
* });
|
|
310
351
|
* res.status(402).setHeader('payment-required', encodePaymentRequired(requirements));
|
|
311
352
|
* ```
|
|
@@ -342,4 +383,258 @@ declare class s402ResourceServer {
|
|
|
342
383
|
process(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
|
|
343
384
|
}
|
|
344
385
|
//#endregion
|
|
345
|
-
|
|
386
|
+
//#region src/envelope.d.ts
|
|
387
|
+
/** Content type for the settlement envelope wire format. */
|
|
388
|
+
declare const S402_ENVELOPE_CONTENT_TYPE: "application/vnd.s402.envelope+json";
|
|
389
|
+
/** SRI-compatible digest algorithm identifiers. See ADR-007. */
|
|
390
|
+
type s402DigestAlg = 'sha256' | 'sha384' | 'sha512' | 'blake3';
|
|
391
|
+
/** Signature algorithm identifiers. See ADR-007. */
|
|
392
|
+
type s402SigAlg = 'ed25519' | 'ed25519ph' | 'secp256k1' | 'ml-dsa-44';
|
|
393
|
+
/** Algorithm identifiers carried in every envelope for crypto-agility. */
|
|
394
|
+
interface s402Algs {
|
|
395
|
+
digest: s402DigestAlg;
|
|
396
|
+
sig: s402SigAlg;
|
|
397
|
+
}
|
|
398
|
+
interface s402EnvelopeBase {
|
|
399
|
+
/** Protocol version — matches `s402-Version` header. */
|
|
400
|
+
s402Version: string;
|
|
401
|
+
/** Scheme name. */
|
|
402
|
+
scheme: s402Scheme;
|
|
403
|
+
/** Content-hash of the scheme spec this response was computed against (SRI form). */
|
|
404
|
+
specDigest: string;
|
|
405
|
+
/** Cryptographic request→response binding (SRI form). See `computeTxBinding`. */
|
|
406
|
+
txBinding: string;
|
|
407
|
+
/** Network identifier (e.g. "sui:mainnet"). Prevents cross-network replay. */
|
|
408
|
+
network: string;
|
|
409
|
+
/** Algorithm identifiers — enables migration without wire-break. */
|
|
410
|
+
algs: s402Algs;
|
|
411
|
+
/** ISO-8601 UTC millisecond timestamp. Client rejects if skew > 5 min. */
|
|
412
|
+
timestamp: string;
|
|
413
|
+
/** Facilitator identities that contributed to this envelope. */
|
|
414
|
+
facilitatorIds?: string[];
|
|
415
|
+
}
|
|
416
|
+
interface s402EnvelopeSettled extends s402EnvelopeBase {
|
|
417
|
+
status: 'settled';
|
|
418
|
+
settled: {
|
|
419
|
+
/** Chain-specific settlement blob — opaque at protocol layer. */settlement: unknown;
|
|
420
|
+
settledAt: string; /** Scheme-specific attestation (e.g., unlock TX2). Inline per ADR-008 S11. */
|
|
421
|
+
attestation?: unknown;
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
interface s402EnvelopeVerified extends s402EnvelopeBase {
|
|
425
|
+
status: 'verified';
|
|
426
|
+
verified: Record<string, never>;
|
|
427
|
+
}
|
|
428
|
+
interface s402EnvelopeRejected extends s402EnvelopeBase {
|
|
429
|
+
status: 'rejected';
|
|
430
|
+
rejected: {
|
|
431
|
+
error: {
|
|
432
|
+
code: s402ErrorCodeType;
|
|
433
|
+
message: string;
|
|
434
|
+
};
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
interface s402EnvelopePending extends s402EnvelopeBase {
|
|
438
|
+
status: 'pending';
|
|
439
|
+
pending: {
|
|
440
|
+
retryAfter?: number;
|
|
441
|
+
reason: string;
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
type s402Envelope = s402EnvelopeSettled | s402EnvelopeVerified | s402EnvelopeRejected | s402EnvelopePending;
|
|
445
|
+
/**
|
|
446
|
+
* Compute the `txBinding` value for a request pair.
|
|
447
|
+
*
|
|
448
|
+
* ```
|
|
449
|
+
* txBinding = "sha256-" || base64url_no_pad(
|
|
450
|
+
* sha256(
|
|
451
|
+
* "s402-txbinding-v1\0"
|
|
452
|
+
* || JCS(requirements)
|
|
453
|
+
* || 0x1E
|
|
454
|
+
* || JCS(payload)
|
|
455
|
+
* )
|
|
456
|
+
* )
|
|
457
|
+
* ```
|
|
458
|
+
*
|
|
459
|
+
* Clients recompute this locally from their OWN `{requirements, payload}` and
|
|
460
|
+
* compare to `envelope.txBinding` using a constant-time primitive. See S14.
|
|
461
|
+
*/
|
|
462
|
+
declare function computeTxBinding(requirements: s402PaymentRequirements, payload: s402PaymentPayload, alg?: s402DigestAlg): Promise<string>;
|
|
463
|
+
interface BuildEnvelopeContext {
|
|
464
|
+
s402Version: string;
|
|
465
|
+
scheme: s402Scheme;
|
|
466
|
+
specDigest: string;
|
|
467
|
+
network: string;
|
|
468
|
+
requirements: s402PaymentRequirements;
|
|
469
|
+
payload: s402PaymentPayload;
|
|
470
|
+
algs?: s402Algs;
|
|
471
|
+
facilitatorIds?: string[];
|
|
472
|
+
timestamp?: string;
|
|
473
|
+
}
|
|
474
|
+
declare function buildSettledEnvelope(ctx: BuildEnvelopeContext, settled: s402EnvelopeSettled['settled']): Promise<s402EnvelopeSettled>;
|
|
475
|
+
declare function buildVerifiedEnvelope(ctx: BuildEnvelopeContext): Promise<s402EnvelopeVerified>;
|
|
476
|
+
declare function buildRejectedEnvelope(ctx: BuildEnvelopeContext, error: {
|
|
477
|
+
code: s402ErrorCodeType;
|
|
478
|
+
message: string;
|
|
479
|
+
}): Promise<s402EnvelopeRejected>;
|
|
480
|
+
declare function buildPendingEnvelope(ctx: BuildEnvelopeContext, pending: s402EnvelopePending['pending']): Promise<s402EnvelopePending>;
|
|
481
|
+
declare function encodeEnvelopeBody(envelope: s402Envelope): string;
|
|
482
|
+
declare function decodeEnvelopeBody(body: string): s402Envelope;
|
|
483
|
+
declare function validateEnvelopeShape(v: unknown): asserts v is s402Envelope;
|
|
484
|
+
interface VerifyEnvelopeOptions {
|
|
485
|
+
/** The original request the client sent. `txBinding` is recomputed from these. */
|
|
486
|
+
originalRequest: {
|
|
487
|
+
requirements: s402PaymentRequirements;
|
|
488
|
+
payload: s402PaymentPayload;
|
|
489
|
+
};
|
|
490
|
+
/** Scheme-digest the client pinned or discovered. */
|
|
491
|
+
expectedSpecDigest: string;
|
|
492
|
+
/** Digest algorithms the client accepts. Reject anything outside this set. */
|
|
493
|
+
acceptedDigestAlgs?: s402DigestAlg[];
|
|
494
|
+
/** Signature algorithms the client accepts. Reject anything outside this set. */
|
|
495
|
+
acceptedSigAlgs?: s402SigAlg[];
|
|
496
|
+
/** Max acceptable skew between envelope.timestamp and local clock, in ms. */
|
|
497
|
+
maxTimestampSkewMs?: number;
|
|
498
|
+
/** Override clock for testing. */
|
|
499
|
+
now?: () => number;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Perform the ADR-007 client-side MUST checks. Throws on any failure.
|
|
503
|
+
*
|
|
504
|
+
* Scheme-match, spec-digest, network, txBinding (constant-time), timestamp,
|
|
505
|
+
* and algorithm acceptance. Resource-binding and unlock-attestation checks
|
|
506
|
+
* live in higher layers (they need request-intent + scheme-specific context).
|
|
507
|
+
*/
|
|
508
|
+
declare function verifyEnvelope(envelope: s402Envelope, options: VerifyEnvelopeOptions): Promise<void>;
|
|
509
|
+
/**
|
|
510
|
+
* Constant-time string equality — S14 invariant.
|
|
511
|
+
*
|
|
512
|
+
* Compares every character regardless of mismatches, so the time-to-answer
|
|
513
|
+
* does not leak digest prefix information. Lengths are compared first (their
|
|
514
|
+
* difference is not secret — a mismatched length means the response is
|
|
515
|
+
* definitely not ours).
|
|
516
|
+
*/
|
|
517
|
+
declare function constantTimeStringEqual(a: string, b: string): boolean;
|
|
518
|
+
//#endregion
|
|
519
|
+
//#region src/canonicalization.d.ts
|
|
520
|
+
/**
|
|
521
|
+
* s402 canonicalization — RFC 8785 JSON Canonicalization Scheme (JCS).
|
|
522
|
+
*
|
|
523
|
+
* JCS produces a deterministic byte sequence from a JSON value. Two parsers
|
|
524
|
+
* that honor JCS produce identical bytes for semantically equal JSON, enabling
|
|
525
|
+
* content-hashing and cross-language interop.
|
|
526
|
+
*
|
|
527
|
+
* Full spec: see `spec/canonicalization.md` (project-local, normative).
|
|
528
|
+
* Summary applied here:
|
|
529
|
+
* - Object keys sorted by UTF-16 code unit order (RFC 8785 §3.2.3)
|
|
530
|
+
* - Numbers rendered per ECMA-404 shortest-roundtrip (RFC 8785 §3.2.2)
|
|
531
|
+
* - Strings escape only the minimum required (RFC 8785 §3.2.1)
|
|
532
|
+
* - Arrays preserve order
|
|
533
|
+
* - No whitespace
|
|
534
|
+
* - Reject non-finite numbers, BigInt, undefined, functions, symbols
|
|
535
|
+
*
|
|
536
|
+
* This module implements JCS *serialization* only. Strict parsing with
|
|
537
|
+
* duplicate-key rejection is deferred to a later PR — envelope txBinding
|
|
538
|
+
* verification never parses untrusted canonical JSON, so dup-key handling is
|
|
539
|
+
* not on this critical path. (Client recomputes from its own objects.)
|
|
540
|
+
*/
|
|
541
|
+
/**
|
|
542
|
+
* JCS value type — mirrors JSON's data model.
|
|
543
|
+
* `unknown` arrays/objects are fine; we'll type-check at serialize time.
|
|
544
|
+
*/
|
|
545
|
+
type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
546
|
+
[key: string]: JsonValue;
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* Serialize a JSON value to RFC 8785 canonical form.
|
|
550
|
+
*
|
|
551
|
+
* Returns a UTF-8 byte string (represented as `Uint8Array`). Callers hash the
|
|
552
|
+
* bytes directly; do NOT re-encode via `TextEncoder` (double-encoding risk).
|
|
553
|
+
*
|
|
554
|
+
* @throws {s402Error} INVALID_PAYLOAD on non-JSON-safe values (NaN, Infinity,
|
|
555
|
+
* bigint, undefined, functions, symbols, circular refs).
|
|
556
|
+
*/
|
|
557
|
+
declare function canonicalize(value: unknown): Uint8Array;
|
|
558
|
+
/**
|
|
559
|
+
* Convenience: canonicalize to a UTF-8 string.
|
|
560
|
+
*
|
|
561
|
+
* Use only when a downstream API demands a string; prefer the Uint8Array form
|
|
562
|
+
* for digest inputs to avoid an extra encode/decode round-trip.
|
|
563
|
+
*/
|
|
564
|
+
declare function canonicalizeToString(value: unknown): string;
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/accept-payment.d.ts
|
|
567
|
+
/**
|
|
568
|
+
* `Accept-Payment` header — content negotiation for HTTP 402 protocols.
|
|
569
|
+
*
|
|
570
|
+
* Modeled on RFC 7231 `Accept` / `Accept-Language` with q-value preference.
|
|
571
|
+
* A client advertises which payment schemes it can produce; the server picks
|
|
572
|
+
* the best scheme both sides support. This lets s402, x402, and MPP
|
|
573
|
+
* ({@link https://machinepayments.org}) coexist on the same endpoint.
|
|
574
|
+
*
|
|
575
|
+
* Grammar (informal):
|
|
576
|
+
* ```
|
|
577
|
+
* Accept-Payment = 1#( scheme [ OWS ";" OWS "q=" qvalue ] )
|
|
578
|
+
* scheme = token e.g. "s402/prepaid", "tempo/charge"
|
|
579
|
+
* qvalue = 0.0 - 1.0 default 1.0, 3-decimal precision
|
|
580
|
+
* ```
|
|
581
|
+
*
|
|
582
|
+
* Entries with `q=0` are explicit rejections — they are retained in parsed
|
|
583
|
+
* output (callers may need to know the client named them) but {@link selectBestScheme}
|
|
584
|
+
* will never pick them.
|
|
585
|
+
*
|
|
586
|
+
* @packageDocumentation
|
|
587
|
+
*/
|
|
588
|
+
interface AcceptPaymentEntry {
|
|
589
|
+
/** Scheme token, normalized to lowercase (e.g. "s402/prepaid"). */
|
|
590
|
+
readonly scheme: string;
|
|
591
|
+
/** Quality value 0.0–1.0. Default 1.0 when omitted. */
|
|
592
|
+
readonly q: number;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Parse an `Accept-Payment` header into sorted preference entries.
|
|
596
|
+
*
|
|
597
|
+
* - Entries are returned in descending q-value order; ties preserve input order
|
|
598
|
+
* (stable sort).
|
|
599
|
+
* - Malformed entries are dropped silently (robustness principle).
|
|
600
|
+
* - Scheme tokens are lowercased. Whitespace around `;` and `,` is tolerated.
|
|
601
|
+
* - Duplicate schemes: the highest-q occurrence wins.
|
|
602
|
+
*
|
|
603
|
+
* @example
|
|
604
|
+
* parseAcceptPayment('s402/prepaid, s402/exact;q=0.8, tempo/charge;q=0.5');
|
|
605
|
+
* // [
|
|
606
|
+
* // { scheme: 's402/prepaid', q: 1 },
|
|
607
|
+
* // { scheme: 's402/exact', q: 0.8 },
|
|
608
|
+
* // { scheme: 'tempo/charge', q: 0.5 },
|
|
609
|
+
* // ]
|
|
610
|
+
*/
|
|
611
|
+
declare function parseAcceptPayment(header: string | null | undefined): AcceptPaymentEntry[];
|
|
612
|
+
/**
|
|
613
|
+
* Format a list of entries back into an `Accept-Payment` header string.
|
|
614
|
+
*
|
|
615
|
+
* Entries with `q=1` omit the parameter (it's the default). Other q-values
|
|
616
|
+
* are emitted with up to 3 decimals, trailing zeros trimmed.
|
|
617
|
+
*
|
|
618
|
+
* @example
|
|
619
|
+
* formatAcceptPayment([
|
|
620
|
+
* { scheme: 's402/prepaid', q: 1 },
|
|
621
|
+
* { scheme: 'tempo/charge', q: 0.5 },
|
|
622
|
+
* ]);
|
|
623
|
+
* // "s402/prepaid, tempo/charge;q=0.5"
|
|
624
|
+
*/
|
|
625
|
+
declare function formatAcceptPayment(entries: readonly AcceptPaymentEntry[]): string;
|
|
626
|
+
/**
|
|
627
|
+
* Select the best scheme both sides agree on.
|
|
628
|
+
*
|
|
629
|
+
* - Walks `preferred` in order (parseAcceptPayment returns them sorted by q).
|
|
630
|
+
* - Returns the first scheme that appears in `supported`.
|
|
631
|
+
* - Entries with `q=0` are explicit rejections and are skipped.
|
|
632
|
+
* - Scheme comparison is case-insensitive; the returned string matches the
|
|
633
|
+
* casing from `supported`.
|
|
634
|
+
* - If `preferred` is empty (no header), falls back to the first entry in
|
|
635
|
+
* `supported` (server's default).
|
|
636
|
+
* - Returns `null` if no overlap exists.
|
|
637
|
+
*/
|
|
638
|
+
declare function selectBestScheme(preferred: readonly AcceptPaymentEntry[], supported: readonly string[]): string | null;
|
|
639
|
+
//#endregion
|
|
640
|
+
export { type AcceptPaymentEntry, type BuildEnvelopeContext, type JsonValue, MAX_BODY_BYTES, S402_CONTENT_TYPE, S402_ENVELOPE_CONTENT_TYPE, S402_HEADERS, S402_RECEIPT_HEADER, S402_VERSION, type VerifyEnvelopeOptions, buildPendingEnvelope, buildRejectedEnvelope, buildSettledEnvelope, buildVerifiedEnvelope, canonicalize, canonicalizeToString, computeTxBinding, constantTimeStringEqual, createS402Error, decodeEnvelopeBody, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodeEnvelopeBody, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, formatAcceptPayment, formatReceiptHeader, getExtensionData, isValidAmount, isValidU64Amount, parseAcceptPayment, parseReceiptHeader, runExtensionHooks, type s402Algs, s402Client, type s402ClientExtension, type s402ClientScheme, type s402DigestAlg, type s402DirectScheme, type s402Discovery, type s402Envelope, type s402EnvelopeBase, type s402EnvelopePending, type s402EnvelopeRejected, type s402EnvelopeSettled, type s402EnvelopeVerified, 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 s402SigAlg, type s402StreamExtra, type s402StreamPayload, type s402UnlockExtra, type s402UnlockPayload, type s402UptoExtra, type s402UptoPayload, type s402VerifyResponse, selectBestScheme, setExtensionData, validateEnvelopeShape, validateRequirementsShape, verifyEnvelope };
|