s402 0.1.7 → 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/CHANGELOG.md +42 -0
- package/README.md +8 -1
- package/dist/compat.mjs +8 -2
- package/dist/http.d.mts +26 -1
- package/dist/http.mjs +77 -5
- package/dist/index.d.mts +11 -8
- package/dist/index.mjs +4 -3
- package/dist/receipts.d.mts +61 -0
- package/dist/receipts.mjs +62 -0
- package/dist/types.d.mts +8 -3
- package/package.json +18 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,45 @@ 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.2.0] - 2026-03-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Receipt HTTP helpers** — `s402/receipts` sub-path export with `formatReceiptHeader()`, `parseReceiptHeader()`, `S402_RECEIPT_HEADER`. Chain-agnostic receipt wire format (`v2:base64(sig):callNumber:timestampMs:base64(hash)`) for v0.2 signed usage receipts.
|
|
13
|
+
- **S7 chain-agnostic boundary invariant** — formal safety invariant enforced by `test/boundary.test.ts`. Greps `src/` for chain-specific patterns (Sui address regex, Solana base58, Ethereum imports) and fails the build if any are found.
|
|
14
|
+
- **v0.2 prepaid type extensions** — `providerPubkey` and `disputeWindowMs` fields on `s402PrepaidExtra` for signed receipt mode.
|
|
15
|
+
- **Body transport** — `application/s402+json` content type for large payloads that don't fit in HTTP headers.
|
|
16
|
+
- **Formal safety invariants** (S1-S7) documented in AGENTS.md.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Chain-agnostic payTo/protocolFeeAddress validation** — removed Sui-specific address regex (`/^0x[0-9a-fA-F]{64}$/`) from `http.ts`. Replaced with chain-agnostic checks (non-empty string, no control characters). Chain-specific validation belongs in `@sweefi/sui`.
|
|
21
|
+
- **x402 compat validation parity** — `normalizeRequirements()` now runs `validateRequirementsShape()` on x402 conversion output, ensuring identical validation regardless of input format.
|
|
22
|
+
- **Prepaid pairing invariant enforcement** — `providerPubkey` and `disputeWindowMs` must both be present (v0.2) or both absent (v0.1). Was documented in JSDoc but not enforced at wire decode.
|
|
23
|
+
- **Receipt BigInt coercion** — `parseReceiptHeader()` rejects empty strings and whitespace-only strings that JavaScript's `BigInt()` would silently coerce to `0n`.
|
|
24
|
+
- **Removed Sui default for `asset`** — `s402RouteConfig.asset` is now required (was optional with `'0x2::sui::SUI'` default). Chain-specific defaults don't belong in the protocol layer.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **BREAKING**: `s402RouteConfig.asset` is now required (was optional).
|
|
29
|
+
- JSDoc on `s402PaymentRequirements` updated to chain-agnostic wording (network, asset, amount fields).
|
|
30
|
+
- 258 tests across 11 suites (was 207 at v0.1.0).
|
|
31
|
+
|
|
32
|
+
## [0.1.8] - 2026-02-27
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Body transport (`application/s402+json`) for large payloads
|
|
37
|
+
- v0.2 prepaid type extensions (`providerPubkey`, `disputeWindowMs`)
|
|
38
|
+
- `FUNDING.yml` and cross-linked SweeFi in README
|
|
39
|
+
|
|
40
|
+
## [0.1.7] - 2026-02-25
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- Formal safety invariants (Lamport-style proofs)
|
|
45
|
+
- `isValidU64Amount()` magnitude checks
|
|
46
|
+
|
|
8
47
|
## [0.1.6] - 2026-02-19
|
|
9
48
|
|
|
10
49
|
### Fixed
|
|
@@ -77,6 +116,9 @@ _Version bump for npm publish after license change._
|
|
|
77
116
|
- Property-based fuzz testing via fast-check
|
|
78
117
|
- 207 tests, zero runtime dependencies
|
|
79
118
|
|
|
119
|
+
[0.2.0]: https://github.com/s402-protocol/core/compare/v0.1.8...v0.2.0
|
|
120
|
+
[0.1.8]: https://github.com/s402-protocol/core/compare/v0.1.7...v0.1.8
|
|
121
|
+
[0.1.7]: https://github.com/s402-protocol/core/compare/v0.1.6...v0.1.7
|
|
80
122
|
[0.1.6]: https://github.com/s402-protocol/core/compare/v0.1.5...v0.1.6
|
|
81
123
|
[0.1.5]: https://github.com/s402-protocol/core/compare/v0.1.4...v0.1.5
|
|
82
124
|
[0.1.4]: https://github.com/s402-protocol/core/compare/v0.1.3...v0.1.4
|
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
**Sui-native HTTP 402 protocol.** Atomic settlement via Sui's Programmable Transaction Blocks (PTBs). Includes an optional compat layer (`s402/compat`) for normalizing x402 input.
|
|
7
7
|
|
|
8
|
+
s402 is the Sui-native implementation of HTTP 402 (Payment Required) — an open protocol that lets AI agents pay for API calls in a single HTTP request with no per-call on-chain transaction. Unlike Coinbase's x402 on Base (EVM L2), s402 uses Sui's Programmable Transaction Blocks to reduce 1,000 payments to just 2 on-chain transactions via the Prepaid scheme, cutting per-call effective gas from $0.007 to $0.000014 and making micropayments economically viable for AI agents for the first time.
|
|
9
|
+
|
|
8
10
|
```bash
|
|
9
11
|
npm install s402
|
|
10
12
|
pnpm add s402
|
|
@@ -25,7 +27,7 @@ HTTP 402 ("Payment Required") has been reserved since 1999 — waiting for a pay
|
|
|
25
27
|
| **Settlement** | Two-step: verify then settle (temporal gap) | Atomic: verify + settle in one PTB |
|
|
26
28
|
| **Finality** | 12+ second blocks (EVM L1) | ~400ms (Sui) |
|
|
27
29
|
| **Payment models** | Exact (one-shot) only | Five schemes: Exact, Prepaid, Escrow, Unlock, Stream |
|
|
28
|
-
| **Micro-payments** |
|
|
30
|
+
| **Micro-payments** | ~$1.60 gas per 1K calls on Base (broken) | $0.014 gas per 1K calls (prepaid) |
|
|
29
31
|
| **Coin handling** | approve + transferFrom | Native `coinWithBalance` + `splitCoins` |
|
|
30
32
|
| **Agent auth** | None | AP2 mandate delegation |
|
|
31
33
|
| **Direct mode** | No | Yes (no facilitator needed) |
|
|
@@ -323,6 +325,11 @@ const requirements: s402PaymentRequirements = {
|
|
|
323
325
|
|
|
324
326
|
5. **Errors tell you what to do.** Every error code includes `retryable` (can the client try again?) and `suggestedAction` (what should it do?). Agents can self-recover.
|
|
325
327
|
|
|
328
|
+
## Related
|
|
329
|
+
|
|
330
|
+
- **[SweeFi](https://github.com/sweeinc/sweefi)** — Open-source payment SDK built on s402. 10 packages including PTB builders, MCP tools, CLI, and UI components.
|
|
331
|
+
- **[Sui Gas Station](https://github.com/Danny-Devs/sui-gas-station)** — Sponsored transaction infrastructure for Sui.
|
|
332
|
+
|
|
326
333
|
## License
|
|
327
334
|
|
|
328
335
|
Apache-2.0 — see [LICENSE](./LICENSE) for details.
|
package/dist/compat.mjs
CHANGED
|
@@ -133,10 +133,16 @@ function normalizeRequirements(obj) {
|
|
|
133
133
|
validateRequirementsShape(obj);
|
|
134
134
|
return pickRequirementsFields(obj);
|
|
135
135
|
}
|
|
136
|
-
if (isX402Envelope(obj))
|
|
136
|
+
if (isX402Envelope(obj)) {
|
|
137
|
+
const record = fromX402Envelope(obj);
|
|
138
|
+
validateRequirementsShape(record);
|
|
139
|
+
return pickRequirementsFields(record);
|
|
140
|
+
}
|
|
137
141
|
if (isX402(obj)) {
|
|
138
142
|
validateX402Shape(obj);
|
|
139
|
-
|
|
143
|
+
const record = fromX402Requirements(obj);
|
|
144
|
+
validateRequirementsShape(record);
|
|
145
|
+
return pickRequirementsFields(record);
|
|
140
146
|
}
|
|
141
147
|
throw new s402Error("INVALID_PAYLOAD", "Unrecognized payment requirements format: missing s402Version or x402Version");
|
|
142
148
|
}
|
package/dist/http.d.mts
CHANGED
|
@@ -74,6 +74,31 @@ declare function validatePrepaidShape(value: unknown): void;
|
|
|
74
74
|
declare function validateSubObjects(record: Record<string, unknown>): void;
|
|
75
75
|
/** Validate that decoded payment requirements have the required shape. */
|
|
76
76
|
declare function validateRequirementsShape(obj: unknown): void;
|
|
77
|
+
/** Content type for s402 JSON body transport */
|
|
78
|
+
declare const S402_CONTENT_TYPE: "application/s402+json";
|
|
79
|
+
/** Encode payment requirements as JSON string (for response body) */
|
|
80
|
+
declare function encodeRequirementsBody(requirements: s402PaymentRequirements): string;
|
|
81
|
+
/** Decode payment requirements from JSON string (from response body) */
|
|
82
|
+
declare function decodeRequirementsBody(body: string): s402PaymentRequirements;
|
|
83
|
+
/** Encode payment payload as JSON string (for request body) */
|
|
84
|
+
declare function encodePayloadBody(payload: s402PaymentPayload): string;
|
|
85
|
+
/** Decode payment payload from JSON string (from request body) */
|
|
86
|
+
declare function decodePayloadBody(body: string): s402PaymentPayload;
|
|
87
|
+
/** Encode settlement response as JSON string (for response body) */
|
|
88
|
+
declare function encodeSettleBody(response: s402SettleResponse): string;
|
|
89
|
+
/** Decode settlement response from JSON string (from response body) */
|
|
90
|
+
declare function decodeSettleBody(body: string): s402SettleResponse;
|
|
91
|
+
/**
|
|
92
|
+
* Detect transport mode from an incoming request.
|
|
93
|
+
*
|
|
94
|
+
* Checks Content-Type for body transport, then falls back to header detection.
|
|
95
|
+
* Returns 'body' if Content-Type is application/s402+json.
|
|
96
|
+
* Returns 'header' if x-payment header is present.
|
|
97
|
+
* Returns 'unknown' otherwise.
|
|
98
|
+
*/
|
|
99
|
+
declare function detectTransport(request: {
|
|
100
|
+
headers: Headers;
|
|
101
|
+
}): 'header' | 'body' | 'unknown';
|
|
77
102
|
/**
|
|
78
103
|
* Detect whether a 402 response uses s402 or x402 protocol.
|
|
79
104
|
*
|
|
@@ -88,4 +113,4 @@ declare function detectProtocol(headers: Headers): 's402' | 'x402' | 'unknown';
|
|
|
88
113
|
*/
|
|
89
114
|
declare function extractRequirementsFromResponse(response: Response): s402PaymentRequirements | null;
|
|
90
115
|
//#endregion
|
|
91
|
-
export { decodePaymentPayload, decodePaymentRequired, decodeSettleResponse, detectProtocol, encodePaymentPayload, encodePaymentRequired, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, pickPayloadFields, pickRequirementsFields, pickSettleResponseFields, validateEscrowShape, validateMandateShape, validatePrepaidShape, validateRequirementsShape, validateStreamShape, validateSubObjects, validateUnlockShape };
|
|
116
|
+
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 };
|
package/dist/http.mjs
CHANGED
|
@@ -83,7 +83,9 @@ const S402_SUB_OBJECT_KEYS = {
|
|
|
83
83
|
"ratePerCall",
|
|
84
84
|
"maxCalls",
|
|
85
85
|
"minDeposit",
|
|
86
|
-
"withdrawalDelayMs"
|
|
86
|
+
"withdrawalDelayMs",
|
|
87
|
+
"providerPubkey",
|
|
88
|
+
"disputeWindowMs"
|
|
87
89
|
])
|
|
88
90
|
};
|
|
89
91
|
/** Strip unknown keys from a sub-object, returning a clean copy. */
|
|
@@ -316,8 +318,16 @@ function validatePrepaidShape(value) {
|
|
|
316
318
|
assertString(obj, "minDeposit", "prepaid");
|
|
317
319
|
if (typeof obj.minDeposit === "string" && !isValidAmount(obj.minDeposit)) throw new s402Error("INVALID_PAYLOAD", `prepaid.minDeposit must be a non-negative integer string, got "${obj.minDeposit}"`);
|
|
318
320
|
assertString(obj, "withdrawalDelayMs", "prepaid");
|
|
319
|
-
if (typeof obj.withdrawalDelayMs === "string"
|
|
321
|
+
if (typeof obj.withdrawalDelayMs === "string") {
|
|
322
|
+
if (!isValidAmount(obj.withdrawalDelayMs)) throw new s402Error("INVALID_PAYLOAD", `prepaid.withdrawalDelayMs must be a non-negative integer string (milliseconds), got "${obj.withdrawalDelayMs}"`);
|
|
323
|
+
const delayMs = BigInt(obj.withdrawalDelayMs);
|
|
324
|
+
if (delayMs < 60000n || delayMs > 604800000n) throw new s402Error("INVALID_PAYLOAD", `prepaid.withdrawalDelayMs must be between 60000 (1 min) and 604800000 (7 days), got "${obj.withdrawalDelayMs}"`);
|
|
325
|
+
}
|
|
320
326
|
assertOptionalString(obj, "maxCalls", "prepaid");
|
|
327
|
+
assertOptionalString(obj, "providerPubkey", "prepaid");
|
|
328
|
+
assertOptionalString(obj, "disputeWindowMs", "prepaid");
|
|
329
|
+
const hasPubkey = typeof obj.providerPubkey === "string";
|
|
330
|
+
if (hasPubkey !== (typeof obj.disputeWindowMs === "string")) throw new s402Error("INVALID_PAYLOAD", `prepaid: providerPubkey and disputeWindowMs must both be present (v0.2) or both absent (v0.1), got ${hasPubkey ? "providerPubkey only" : "disputeWindowMs only"}`);
|
|
321
331
|
}
|
|
322
332
|
/**
|
|
323
333
|
* Validate all optional sub-objects on a requirements record.
|
|
@@ -343,10 +353,11 @@ function validateRequirementsShape(obj) {
|
|
|
343
353
|
if (typeof record.amount !== "string") missing.push("amount (string)");
|
|
344
354
|
else if (!isValidU64Amount(record.amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${record.amount}": must be a non-negative integer string within u64 range`);
|
|
345
355
|
if (typeof record.payTo !== "string") missing.push("payTo (string)");
|
|
346
|
-
else if (
|
|
356
|
+
else if (record.payTo.length === 0) throw new s402Error("INVALID_PAYLOAD", "payTo must be a non-empty string");
|
|
347
357
|
if (missing.length > 0) throw new s402Error("INVALID_PAYLOAD", `Malformed payment requirements: missing ${missing.join(", ")}`);
|
|
348
358
|
if (/[\x00-\x1f\x7f]/.test(record.network)) throw new s402Error("INVALID_PAYLOAD", "network contains control characters");
|
|
349
359
|
if (/[\x00-\x1f\x7f]/.test(record.asset)) throw new s402Error("INVALID_PAYLOAD", "asset contains control characters");
|
|
360
|
+
if (/[\x00-\x1f\x7f]/.test(record.payTo)) throw new s402Error("INVALID_PAYLOAD", "payTo contains control characters");
|
|
350
361
|
if (Array.isArray(record.accepts) && record.accepts.length === 0) throw new s402Error("INVALID_PAYLOAD", "accepts array must contain at least one scheme");
|
|
351
362
|
const accepts = record.accepts;
|
|
352
363
|
for (const scheme of accepts) if (typeof scheme !== "string") throw new s402Error("INVALID_PAYLOAD", `Invalid entry in accepts array: expected string, got ${typeof scheme}`);
|
|
@@ -357,7 +368,8 @@ function validateRequirementsShape(obj) {
|
|
|
357
368
|
if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt) || record.expiresAt <= 0) throw new s402Error("INVALID_PAYLOAD", `expiresAt must be a positive finite number (Unix timestamp ms), got ${record.expiresAt}`);
|
|
358
369
|
}
|
|
359
370
|
if (record.protocolFeeAddress !== void 0) {
|
|
360
|
-
if (typeof record.protocolFeeAddress !== "string" ||
|
|
371
|
+
if (typeof record.protocolFeeAddress !== "string" || record.protocolFeeAddress.length === 0) throw new s402Error("INVALID_PAYLOAD", `protocolFeeAddress must be a non-empty string, got ${JSON.stringify(record.protocolFeeAddress)}`);
|
|
372
|
+
if (/[\x00-\x1f\x7f]/.test(record.protocolFeeAddress)) throw new s402Error("INVALID_PAYLOAD", "protocolFeeAddress contains control characters");
|
|
361
373
|
}
|
|
362
374
|
if (record.facilitatorUrl !== void 0) {
|
|
363
375
|
if (typeof record.facilitatorUrl !== "string") throw new s402Error("INVALID_PAYLOAD", `facilitatorUrl must be a string, got ${typeof record.facilitatorUrl}`);
|
|
@@ -386,6 +398,66 @@ function validateSettleShape(obj) {
|
|
|
386
398
|
if (obj == null || typeof obj !== "object") throw new s402Error("INVALID_PAYLOAD", "Settle response is not an object");
|
|
387
399
|
if (typeof obj.success !== "boolean") throw new s402Error("INVALID_PAYLOAD", "Malformed settle response: missing or invalid \"success\" (boolean)");
|
|
388
400
|
}
|
|
401
|
+
/** Content type for s402 JSON body transport */
|
|
402
|
+
const S402_CONTENT_TYPE = "application/s402+json";
|
|
403
|
+
/** Encode payment requirements as JSON string (for response body) */
|
|
404
|
+
function encodeRequirementsBody(requirements) {
|
|
405
|
+
return JSON.stringify(requirements);
|
|
406
|
+
}
|
|
407
|
+
/** Decode payment requirements from JSON string (from response body) */
|
|
408
|
+
function decodeRequirementsBody(body) {
|
|
409
|
+
let parsed;
|
|
410
|
+
try {
|
|
411
|
+
parsed = JSON.parse(body);
|
|
412
|
+
} catch (e) {
|
|
413
|
+
throw new s402Error("INVALID_PAYLOAD", `Failed to parse s402 requirements body: ${e instanceof Error ? e.message : "invalid JSON"}`);
|
|
414
|
+
}
|
|
415
|
+
validateRequirementsShape(parsed);
|
|
416
|
+
return pickRequirementsFields(parsed);
|
|
417
|
+
}
|
|
418
|
+
/** Encode payment payload as JSON string (for request body) */
|
|
419
|
+
function encodePayloadBody(payload) {
|
|
420
|
+
return JSON.stringify(payload);
|
|
421
|
+
}
|
|
422
|
+
/** Decode payment payload from JSON string (from request body) */
|
|
423
|
+
function decodePayloadBody(body) {
|
|
424
|
+
let parsed;
|
|
425
|
+
try {
|
|
426
|
+
parsed = JSON.parse(body);
|
|
427
|
+
} catch (e) {
|
|
428
|
+
throw new s402Error("INVALID_PAYLOAD", `Failed to parse s402 payload body: ${e instanceof Error ? e.message : "invalid JSON"}`);
|
|
429
|
+
}
|
|
430
|
+
validatePayloadShape(parsed);
|
|
431
|
+
return pickPayloadFields(parsed);
|
|
432
|
+
}
|
|
433
|
+
/** Encode settlement response as JSON string (for response body) */
|
|
434
|
+
function encodeSettleBody(response) {
|
|
435
|
+
return JSON.stringify(response);
|
|
436
|
+
}
|
|
437
|
+
/** Decode settlement response from JSON string (from response body) */
|
|
438
|
+
function decodeSettleBody(body) {
|
|
439
|
+
let parsed;
|
|
440
|
+
try {
|
|
441
|
+
parsed = JSON.parse(body);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
throw new s402Error("INVALID_PAYLOAD", `Failed to parse s402 settle body: ${e instanceof Error ? e.message : "invalid JSON"}`);
|
|
444
|
+
}
|
|
445
|
+
validateSettleShape(parsed);
|
|
446
|
+
return pickSettleResponseFields(parsed);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Detect transport mode from an incoming request.
|
|
450
|
+
*
|
|
451
|
+
* Checks Content-Type for body transport, then falls back to header detection.
|
|
452
|
+
* Returns 'body' if Content-Type is application/s402+json.
|
|
453
|
+
* Returns 'header' if x-payment header is present.
|
|
454
|
+
* Returns 'unknown' otherwise.
|
|
455
|
+
*/
|
|
456
|
+
function detectTransport(request) {
|
|
457
|
+
if (request.headers.get("content-type")?.includes(S402_CONTENT_TYPE)) return "body";
|
|
458
|
+
if (request.headers.get(S402_HEADERS.PAYMENT)) return "header";
|
|
459
|
+
return "unknown";
|
|
460
|
+
}
|
|
389
461
|
/**
|
|
390
462
|
* Detect whether a 402 response uses s402 or x402 protocol.
|
|
391
463
|
*
|
|
@@ -421,4 +493,4 @@ function extractRequirementsFromResponse(response) {
|
|
|
421
493
|
}
|
|
422
494
|
|
|
423
495
|
//#endregion
|
|
424
|
-
export { decodePaymentPayload, decodePaymentRequired, decodeSettleResponse, detectProtocol, encodePaymentPayload, encodePaymentRequired, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, pickPayloadFields, pickRequirementsFields, pickSettleResponseFields, validateEscrowShape, validateMandateShape, validatePrepaidShape, validateRequirementsShape, validateStreamShape, validateSubObjects, validateUnlockShape };
|
|
496
|
+
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 };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +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, s402StreamExtra, s402StreamPayload, s402UnlockExtra, s402UnlockPayload, s402VerifyResponse } from "./types.mjs";
|
|
3
|
-
import { decodePaymentPayload, decodePaymentRequired, decodeSettleResponse, detectProtocol, encodePaymentPayload, encodePaymentRequired, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, validateRequirementsShape } from "./http.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 { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader, s402Receipt, s402ReceiptSigner, s402ReceiptVerifier } from "./receipts.mjs";
|
|
4
5
|
|
|
5
6
|
//#region src/scheme.d.ts
|
|
6
7
|
/** Implemented by each scheme on the client side */
|
|
@@ -49,14 +50,14 @@ interface s402DirectScheme {
|
|
|
49
50
|
interface s402RouteConfig {
|
|
50
51
|
/** Which payment scheme(s) to accept. Always includes "exact" for x402 compat. */
|
|
51
52
|
schemes: s402Scheme[];
|
|
52
|
-
/** Amount in base units, same as wire format (e.g., "1000000"
|
|
53
|
+
/** Amount in base units, same as wire format (e.g., "1000000") */
|
|
53
54
|
price: string;
|
|
54
|
-
/**
|
|
55
|
+
/** Network identifier (e.g., "sui:mainnet", "solana:mainnet-beta") */
|
|
55
56
|
network: string;
|
|
56
|
-
/** Recipient address */
|
|
57
|
+
/** Recipient address (chain-specific format, validated by chain adapter) */
|
|
57
58
|
payTo: string;
|
|
58
|
-
/**
|
|
59
|
-
asset
|
|
59
|
+
/** Asset/coin type identifier (chain-specific, e.g., Sui Move type or Solana mint address) */
|
|
60
|
+
asset: string;
|
|
60
61
|
/** Facilitator URL (optional for direct settlement) */
|
|
61
62
|
facilitatorUrl?: string;
|
|
62
63
|
/** Settlement mode preference */
|
|
@@ -89,7 +90,9 @@ interface s402RouteConfig {
|
|
|
89
90
|
ratePerCall: string;
|
|
90
91
|
maxCalls?: string;
|
|
91
92
|
minDeposit: string;
|
|
92
|
-
withdrawalDelayMs: string;
|
|
93
|
+
withdrawalDelayMs: string; /** Provider Ed25519 pubkey (hex). Enables v0.2 signed receipt mode. @since v0.2 */
|
|
94
|
+
providerPubkey?: string; /** Dispute window in ms. Required when providerPubkey is set. @since v0.2 */
|
|
95
|
+
disputeWindowMs?: string;
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
98
|
//#endregion
|
|
@@ -192,4 +195,4 @@ declare class s402ResourceServer {
|
|
|
192
195
|
process(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
|
|
193
196
|
}
|
|
194
197
|
//#endregion
|
|
195
|
-
export { S402_HEADERS, S402_VERSION, createS402Error, decodePaymentPayload, decodePaymentRequired, decodeSettleResponse, detectProtocol, encodePaymentPayload, encodePaymentRequired, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, 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 s402RegistryQuery, s402ResourceServer, type s402RouteConfig, type s402Scheme, type s402ServerScheme, type s402ServiceEntry, type s402SettleResponse, type s402SettlementMode, type s402StreamExtra, type s402StreamPayload, type s402UnlockExtra, type s402UnlockPayload, type s402VerifyResponse, validateRequirementsShape };
|
|
198
|
+
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 s402StreamExtra, type s402StreamPayload, type s402UnlockExtra, type s402UnlockPayload, type s402VerifyResponse, validateRequirementsShape };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { S402_HEADERS, S402_VERSION } from "./types.mjs";
|
|
2
2
|
import { createS402Error, s402Error, s402ErrorCode } from "./errors.mjs";
|
|
3
|
-
import { decodePaymentPayload, decodePaymentRequired, decodeSettleResponse, detectProtocol, encodePaymentPayload, encodePaymentRequired, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, validateRequirementsShape } from "./http.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 { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader } from "./receipts.mjs";
|
|
4
5
|
|
|
5
6
|
//#region src/client.ts
|
|
6
7
|
var s402Client = class {
|
|
@@ -81,7 +82,7 @@ var s402ResourceServer = class {
|
|
|
81
82
|
s402Version: S402_VERSION,
|
|
82
83
|
accepts: [...new Set([...config.schemes, "exact"])],
|
|
83
84
|
network: config.network,
|
|
84
|
-
asset: config.asset
|
|
85
|
+
asset: config.asset,
|
|
85
86
|
amount: config.price,
|
|
86
87
|
payTo: config.payTo,
|
|
87
88
|
facilitatorUrl: config.facilitatorUrl,
|
|
@@ -317,4 +318,4 @@ var s402Facilitator = class {
|
|
|
317
318
|
};
|
|
318
319
|
|
|
319
320
|
//#endregion
|
|
320
|
-
export { S402_HEADERS, S402_VERSION, createS402Error, decodePaymentPayload, decodePaymentRequired, decodeSettleResponse, detectProtocol, encodePaymentPayload, encodePaymentRequired, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, s402Client, s402Error, s402ErrorCode, s402Facilitator, s402ResourceServer, validateRequirementsShape };
|
|
321
|
+
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, s402Error, s402ErrorCode, s402Facilitator, s402ResourceServer, validateRequirementsShape };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/receipts.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* s402 Receipt HTTP Helpers — chain-agnostic receipt header format/parse.
|
|
4
|
+
*
|
|
5
|
+
* Providers sign each API response with Ed25519. The signature, call number,
|
|
6
|
+
* timestamp, and response hash are transported as a single HTTP header:
|
|
7
|
+
*
|
|
8
|
+
* X-S402-Receipt: v2:base64(signature):callNumber:timestampMs:base64(responseHash)
|
|
9
|
+
*
|
|
10
|
+
* This module handles the header wire format ONLY. It does not:
|
|
11
|
+
* - Construct BCS messages (chain-specific — see @sweefi/sui receipts.ts)
|
|
12
|
+
* - Generate Ed25519 keys (implementations provide their own)
|
|
13
|
+
* - Accumulate receipts (client-side concern — see @sweefi/sui ReceiptAccumulator)
|
|
14
|
+
*
|
|
15
|
+
* Zero runtime dependencies. Uses built-in btoa/atob.
|
|
16
|
+
*
|
|
17
|
+
* @see docs/schemes/prepaid.md — v0.2 spec
|
|
18
|
+
*/
|
|
19
|
+
/** Receipt data extracted from an HTTP header. */
|
|
20
|
+
interface s402Receipt {
|
|
21
|
+
/** Header version — always 'v2' for signed receipts. */
|
|
22
|
+
version: 'v2';
|
|
23
|
+
/** 64-byte Ed25519 signature over the BCS-encoded receipt message. */
|
|
24
|
+
signature: Uint8Array;
|
|
25
|
+
/** Sequential call number (1-indexed). */
|
|
26
|
+
callNumber: bigint;
|
|
27
|
+
/** Unix timestamp in milliseconds when the response was generated. */
|
|
28
|
+
timestampMs: bigint;
|
|
29
|
+
/** Hash of the response body (typically SHA-256, 32 bytes). */
|
|
30
|
+
responseHash: Uint8Array;
|
|
31
|
+
}
|
|
32
|
+
/** Signing function — implementations inject their own Ed25519 signer. */
|
|
33
|
+
type s402ReceiptSigner = (message: Uint8Array) => Promise<Uint8Array>;
|
|
34
|
+
/** Verification function — implementations inject their own Ed25519 verifier. */
|
|
35
|
+
type s402ReceiptVerifier = (message: Uint8Array, signature: Uint8Array) => Promise<boolean>;
|
|
36
|
+
/** HTTP header name for s402 signed usage receipts. */
|
|
37
|
+
declare const S402_RECEIPT_HEADER = "X-S402-Receipt";
|
|
38
|
+
/**
|
|
39
|
+
* Encode receipt fields into the `X-S402-Receipt` HTTP header value.
|
|
40
|
+
*
|
|
41
|
+
* Format: `v2:base64(signature):callNumber:timestampMs:base64(responseHash)`
|
|
42
|
+
*
|
|
43
|
+
* @param receipt - Receipt fields to encode
|
|
44
|
+
* @returns Header string ready for HTTP response
|
|
45
|
+
*/
|
|
46
|
+
declare function formatReceiptHeader(receipt: {
|
|
47
|
+
signature: Uint8Array;
|
|
48
|
+
callNumber: bigint;
|
|
49
|
+
timestampMs: bigint;
|
|
50
|
+
responseHash: Uint8Array;
|
|
51
|
+
}): string;
|
|
52
|
+
/**
|
|
53
|
+
* Decode an `X-S402-Receipt` header value back into typed fields.
|
|
54
|
+
*
|
|
55
|
+
* @param header - Raw header value string
|
|
56
|
+
* @returns Parsed receipt with typed fields
|
|
57
|
+
* @throws On empty string, wrong number of parts, or unknown version
|
|
58
|
+
*/
|
|
59
|
+
declare function parseReceiptHeader(header: string): s402Receipt;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader, s402Receipt, s402ReceiptSigner, s402ReceiptVerifier };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/receipts.ts
|
|
2
|
+
/** HTTP header name for s402 signed usage receipts. */
|
|
3
|
+
const S402_RECEIPT_HEADER = "X-S402-Receipt";
|
|
4
|
+
const HEADER_VERSION = "v2";
|
|
5
|
+
/** Encode a Uint8Array to base64 using built-in btoa. */
|
|
6
|
+
function uint8ToBase64(bytes) {
|
|
7
|
+
let binary = "";
|
|
8
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
9
|
+
return btoa(binary);
|
|
10
|
+
}
|
|
11
|
+
/** Decode a base64 string to Uint8Array using built-in atob. */
|
|
12
|
+
function base64ToUint8(b64) {
|
|
13
|
+
const binary = atob(b64);
|
|
14
|
+
const bytes = new Uint8Array(binary.length);
|
|
15
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
16
|
+
return bytes;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Encode receipt fields into the `X-S402-Receipt` HTTP header value.
|
|
20
|
+
*
|
|
21
|
+
* Format: `v2:base64(signature):callNumber:timestampMs:base64(responseHash)`
|
|
22
|
+
*
|
|
23
|
+
* @param receipt - Receipt fields to encode
|
|
24
|
+
* @returns Header string ready for HTTP response
|
|
25
|
+
*/
|
|
26
|
+
function formatReceiptHeader(receipt) {
|
|
27
|
+
return [
|
|
28
|
+
HEADER_VERSION,
|
|
29
|
+
uint8ToBase64(receipt.signature),
|
|
30
|
+
receipt.callNumber.toString(),
|
|
31
|
+
receipt.timestampMs.toString(),
|
|
32
|
+
uint8ToBase64(receipt.responseHash)
|
|
33
|
+
].join(":");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decode an `X-S402-Receipt` header value back into typed fields.
|
|
37
|
+
*
|
|
38
|
+
* @param header - Raw header value string
|
|
39
|
+
* @returns Parsed receipt with typed fields
|
|
40
|
+
* @throws On empty string, wrong number of parts, or unknown version
|
|
41
|
+
*/
|
|
42
|
+
function parseReceiptHeader(header) {
|
|
43
|
+
if (!header) throw new Error("Empty receipt header");
|
|
44
|
+
const parts = header.split(":");
|
|
45
|
+
if (parts.length !== 5) throw new Error(`Malformed receipt header: expected 5 colon-separated parts, got ${parts.length}`);
|
|
46
|
+
const [version, sigB64, callNumberStr, timestampMsStr, hashB64] = parts;
|
|
47
|
+
if (version !== HEADER_VERSION) throw new Error(`Unknown receipt header version: "${version}" (expected "${HEADER_VERSION}")`);
|
|
48
|
+
const callNumber = BigInt(callNumberStr);
|
|
49
|
+
const timestampMs = BigInt(timestampMsStr);
|
|
50
|
+
if (callNumber <= 0n) throw new Error(`Invalid receipt callNumber: must be positive, got ${callNumber}`);
|
|
51
|
+
if (timestampMs <= 0n) throw new Error(`Invalid receipt timestampMs: must be positive, got ${timestampMs}`);
|
|
52
|
+
return {
|
|
53
|
+
version: "v2",
|
|
54
|
+
signature: base64ToUint8(sigB64),
|
|
55
|
+
callNumber,
|
|
56
|
+
timestampMs,
|
|
57
|
+
responseHash: base64ToUint8(hashB64)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader };
|
package/dist/types.d.mts
CHANGED
|
@@ -16,11 +16,11 @@ interface s402PaymentRequirements {
|
|
|
16
16
|
s402Version: typeof S402_VERSION;
|
|
17
17
|
/** Which payment schemes the server accepts. Always includes "exact" for x402 compat. */
|
|
18
18
|
accepts: s402Scheme[];
|
|
19
|
-
/**
|
|
19
|
+
/** Network identifier (e.g., "sui:mainnet", "solana:mainnet-beta", "eip155:8453") */
|
|
20
20
|
network: string;
|
|
21
|
-
/**
|
|
21
|
+
/** Asset/coin type identifier (chain-specific format, opaque to s402 core) */
|
|
22
22
|
asset: string;
|
|
23
|
-
/** Amount in base units
|
|
23
|
+
/** Amount in base units as a non-negative integer string */
|
|
24
24
|
amount: string;
|
|
25
25
|
/** Recipient address */
|
|
26
26
|
payTo: string;
|
|
@@ -115,6 +115,11 @@ interface s402UnlockExtra {
|
|
|
115
115
|
* Move cannot verify calls actually happened. Agent's protection: rate cap,
|
|
116
116
|
* max_calls, deposit ceiling, small deposits + short refill cycles, reputation.
|
|
117
117
|
* v0.2 adds signed usage receipts for cryptographic fraud proofs. See ADR-007.
|
|
118
|
+
*
|
|
119
|
+
* PAIRING INVARIANT: `providerPubkey` and `disputeWindowMs` are a pair.
|
|
120
|
+
* Both must be present (v0.2 signed receipt mode) or both absent (v0.1 default).
|
|
121
|
+
* Setting `providerPubkey` without `disputeWindowMs` (or vice versa) is invalid
|
|
122
|
+
* and will be rejected by the on-chain contract at deposit time.
|
|
118
123
|
*/
|
|
119
124
|
interface s402PrepaidExtra {
|
|
120
125
|
/** Maximum base units per API call (rate cap) */
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s402",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "s402 —
|
|
5
|
+
"description": "s402 — Chain-agnostic HTTP 402 wire format. Types, HTTP encoding, and scheme registry for five payment schemes. Wire-compatible with x402. Zero runtime dependencies.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "SweeInc <daniel@sweeinc.com> (https://s402-protocol.org)",
|
|
8
8
|
"repository": {
|
|
@@ -76,24 +76,30 @@
|
|
|
76
76
|
"default": "./dist/errors.mjs"
|
|
77
77
|
},
|
|
78
78
|
"default": "./dist/errors.mjs"
|
|
79
|
+
},
|
|
80
|
+
"./receipts": {
|
|
81
|
+
"import": {
|
|
82
|
+
"types": "./dist/receipts.d.mts",
|
|
83
|
+
"default": "./dist/receipts.mjs"
|
|
84
|
+
},
|
|
85
|
+
"default": "./dist/receipts.mjs"
|
|
79
86
|
}
|
|
80
87
|
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
90
|
+
"fast-check": "^4.5.3",
|
|
91
|
+
"tsdown": "^0.20.3",
|
|
92
|
+
"typescript": "^5.7.0",
|
|
93
|
+
"vitepress": "^1.6.4",
|
|
94
|
+
"vitest": "^3.0.5"
|
|
95
|
+
},
|
|
81
96
|
"scripts": {
|
|
82
97
|
"build": "tsdown",
|
|
83
98
|
"typecheck": "tsc --noEmit",
|
|
84
99
|
"test": "vitest run",
|
|
85
100
|
"test:watch": "vitest",
|
|
86
|
-
"prepublishOnly": "npm run build && npm run typecheck && npm run test",
|
|
87
101
|
"docs:dev": "vitepress dev docs",
|
|
88
102
|
"docs:build": "vitepress build docs",
|
|
89
103
|
"docs:preview": "vitepress preview docs"
|
|
90
|
-
},
|
|
91
|
-
"devDependencies": {
|
|
92
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
93
|
-
"fast-check": "^4.5.3",
|
|
94
|
-
"tsdown": "^0.20.3",
|
|
95
|
-
"typescript": "^5.7.0",
|
|
96
|
-
"vitepress": "^1.6.4",
|
|
97
|
-
"vitest": "^3.0.5"
|
|
98
104
|
}
|
|
99
|
-
}
|
|
105
|
+
}
|