s402 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.mjs CHANGED
@@ -156,6 +156,7 @@ function pickRequirementsFields(obj) {
156
156
  * ```
157
157
  */
158
158
  function decodePaymentRequired(header) {
159
+ if (typeof header !== "string") throw new s402Error("INVALID_PAYLOAD", `payment-required header must be a string, got ${typeof header}`);
159
160
  if (header.length > MAX_HEADER_BYTES) throw new s402Error("INVALID_PAYLOAD", `payment-required header exceeds maximum size (${header.length} > ${MAX_HEADER_BYTES})`);
160
161
  let parsed;
161
162
  try {
@@ -228,6 +229,7 @@ function pickPayloadFields(obj) {
228
229
  * ```
229
230
  */
230
231
  function decodePaymentPayload(header) {
232
+ if (typeof header !== "string") throw new s402Error("INVALID_PAYLOAD", `x-payment header must be a string, got ${typeof header}`);
231
233
  if (header.length > MAX_HEADER_BYTES) throw new s402Error("INVALID_PAYLOAD", `x-payment header exceeds maximum size (${header.length} > ${MAX_HEADER_BYTES})`);
232
234
  let parsed;
233
235
  try {
@@ -261,6 +263,7 @@ function pickSettleResponseFields(obj) {
261
263
  }
262
264
  /** Decode settlement response from the `payment-response` header */
263
265
  function decodeSettleResponse(header) {
266
+ if (typeof header !== "string") throw new s402Error("INVALID_PAYLOAD", `payment-response header must be a string, got ${typeof header}`);
264
267
  if (header.length > MAX_HEADER_BYTES) throw new s402Error("INVALID_PAYLOAD", `payment-response header exceeds maximum size (${header.length} > ${MAX_HEADER_BYTES})`);
265
268
  let parsed;
266
269
  try {
@@ -495,6 +498,7 @@ function encodeRequirementsBody(requirements) {
495
498
  }
496
499
  /** Decode payment requirements from JSON string (from response body) */
497
500
  function decodeRequirementsBody(body) {
501
+ if (typeof body !== "string") throw new s402Error("INVALID_PAYLOAD", `s402 requirements body must be a string, got ${typeof body}`);
498
502
  let parsed;
499
503
  try {
500
504
  parsed = JSON.parse(body);
@@ -510,6 +514,7 @@ function encodePayloadBody(payload) {
510
514
  }
511
515
  /** Decode payment payload from JSON string (from request body) */
512
516
  function decodePayloadBody(body) {
517
+ if (typeof body !== "string") throw new s402Error("INVALID_PAYLOAD", `s402 payload body must be a string, got ${typeof body}`);
513
518
  let parsed;
514
519
  try {
515
520
  parsed = JSON.parse(body);
@@ -525,6 +530,7 @@ function encodeSettleBody(response) {
525
530
  }
526
531
  /** Decode settlement response from JSON string (from response body) */
527
532
  function decodeSettleBody(body) {
533
+ if (typeof body !== "string") throw new s402Error("INVALID_PAYLOAD", `s402 settle body must be a string, got ${typeof body}`);
528
534
  let parsed;
529
535
  try {
530
536
  parsed = JSON.parse(body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s402",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
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",
@@ -90,22 +90,21 @@
90
90
  "default": "./dist/receipts.mjs"
91
91
  }
92
92
  },
93
+ "devDependencies": {
94
+ "@vitest/coverage-v8": "^3.2.4",
95
+ "fast-check": "^4.5.3",
96
+ "tsdown": "^0.20.3",
97
+ "typescript": "^5.7.0",
98
+ "vitepress": "^1.6.4",
99
+ "vitest": "^3.0.5"
100
+ },
93
101
  "scripts": {
94
102
  "build": "tsdown",
95
103
  "typecheck": "tsc --noEmit",
96
104
  "test": "vitest run",
97
105
  "test:watch": "vitest",
98
- "prepublishOnly": "npm run build && npm run typecheck && npm run test",
99
106
  "docs:dev": "vitepress dev docs",
100
107
  "docs:build": "vitepress build docs",
101
108
  "docs:preview": "vitepress preview docs"
102
- },
103
- "devDependencies": {
104
- "@vitest/coverage-v8": "^3.2.4",
105
- "fast-check": "^4.5.3",
106
- "tsdown": "^0.20.3",
107
- "typescript": "^5.7.0",
108
- "vitepress": "^1.6.4",
109
- "vitest": "^3.0.5"
110
109
  }
111
- }
110
+ }
@@ -1,172 +0,0 @@
1
- # s402 Conformance Test Vectors
2
-
3
- Machine-readable test vectors that define s402-compliant behavior. Use these to verify any s402 implementation — TypeScript, Go, Python, Rust, etc.
4
-
5
- ## Getting the Vectors
6
-
7
- The vectors ship with the `s402` npm package:
8
-
9
- ```bash
10
- npm pack s402
11
- tar xzf s402-*.tgz
12
- ls package/test/conformance/vectors/
13
- ```
14
-
15
- Or clone the repo directly:
16
-
17
- ```bash
18
- git clone https://github.com/s402-protocol/core.git
19
- ls core/test/conformance/vectors/
20
- ```
21
-
22
- ## Vector Format
23
-
24
- Each JSON file contains an array of test cases:
25
-
26
- ```json
27
- [
28
- {
29
- "description": "Human-readable test name",
30
- "input": { ... },
31
- "expected": { ... },
32
- "shouldReject": false
33
- }
34
- ]
35
- ```
36
-
37
- ### Accept vectors (`shouldReject: false`)
38
-
39
- The implementation MUST produce `expected` when given `input`.
40
-
41
- ### Reject vectors (`shouldReject: true`)
42
-
43
- The implementation MUST reject the input with an error. The `expectedErrorCode` field indicates which error:
44
-
45
- ```json
46
- {
47
- "description": "Rejects negative amount",
48
- "input": { "header": "base64-encoded-malformed-json" },
49
- "shouldReject": true,
50
- "expectedErrorCode": "INVALID_PAYLOAD"
51
- }
52
- ```
53
-
54
- ## Vector Files
55
-
56
- | File | Tests | Description |
57
- |------|-------|-------------|
58
- | `requirements-encode.json` | Encode | `s402PaymentRequirements` object → base64 header string |
59
- | `requirements-decode.json` | Decode | base64 header string → `s402PaymentRequirements` object |
60
- | `payload-encode.json` | Encode | `s402PaymentPayload` object → base64 header string |
61
- | `payload-decode.json` | Decode | base64 header string → `s402PaymentPayload` object |
62
- | `settle-encode.json` | Encode | `s402SettleResponse` object → base64 header string |
63
- | `settle-decode.json` | Decode | base64 header string → `s402SettleResponse` object |
64
- | `body-transport.json` | Both | Requirements/payload/settle via JSON body (no base64) |
65
- | `compat-normalize.json` | Normalize | x402 V1/V2 input → normalized s402 output |
66
- | `receipt-format.json` | Format | Receipt fields → `X-S402-Receipt` header string |
67
- | `receipt-parse.json` | Parse | `X-S402-Receipt` header string → parsed receipt fields |
68
- | `validation-reject.json` | Reject | Malformed inputs that MUST be rejected |
69
- | `roundtrip.json` | Roundtrip | encode → decode → re-encode = identical output |
70
-
71
- ## Encoding Scheme
72
-
73
- s402 uses **Unicode-safe base64** for HTTP header transport:
74
-
75
- 1. JSON-serialize the object: `JSON.stringify(obj)`
76
- 2. UTF-8 encode the string: `TextEncoder.encode(json)`
77
- 3. Base64 encode the bytes: `btoa(bytes.map(b => String.fromCharCode(b)).join(''))`
78
-
79
- For ASCII-only content (the common case), this produces the same output as plain `btoa(json)`.
80
-
81
- Body transport uses raw JSON (no base64).
82
-
83
- ## Receipt Header Format
84
-
85
- The `X-S402-Receipt` header uses colon-separated fields:
86
-
87
- ```
88
- v2:base64(signature):callNumber:timestampMs:base64(responseHash)
89
- ```
90
-
91
- - `v2` — version prefix (always "v2" for signed receipts)
92
- - `signature` — 64-byte Ed25519 signature, base64-encoded
93
- - `callNumber` — sequential call number (1-indexed, bigint string)
94
- - `timestampMs` — Unix timestamp in milliseconds (bigint string)
95
- - `responseHash` — response body hash (typically SHA-256, 32 bytes), base64-encoded
96
-
97
- In the vector JSON files, `signature` and `responseHash` are represented as arrays of byte values (0-255) since JSON has no binary type.
98
-
99
- ## Implementing in Another Language
100
-
101
- 1. Load the JSON vector files
102
- 2. For each vector where `shouldReject` is `false`:
103
- - Feed `input` to your encode/decode function
104
- - Compare the result to `expected`
105
- 3. For each vector where `shouldReject` is `true`:
106
- - Feed `input` to your decode function
107
- - Verify it throws/returns an error with the matching error code
108
- 4. For roundtrip vectors:
109
- - Verify `expected.identical` is `true`
110
- - Verify `expected.firstEncode === expected.reEncode`
111
-
112
- ### Key stripping on decode
113
-
114
- All three decode functions (`decodePaymentRequired`, `decodePaymentPayload`, `decodeSettleResponse`) MUST strip unknown top-level keys. Only known fields should survive the decode. This is a defense-in-depth measure at the HTTP trust boundary.
115
-
116
- **Requirements known top-level keys:** `s402Version`, `accepts`, `network`, `asset`, `amount`, `payTo`, `facilitatorUrl`, `mandate`, `protocolFeeBps`, `protocolFeeAddress`, `receiptRequired`, `settlementMode`, `expiresAt`, `stream`, `escrow`, `unlock`, `prepaid`, `extensions`.
117
-
118
- **Requirements sub-object known keys** (also stripped of unknowns):
119
- - `mandate`: `required`, `minPerTx`, `coinType`
120
- - `stream`: `ratePerSecond`, `budgetCap`, `minDeposit`, `streamSetupUrl`
121
- - `escrow`: `seller`, `arbiter`, `deadlineMs`
122
- - `unlock`: `encryptionId`, `walrusBlobId`, `encryptionPackageId`
123
- - `prepaid`: `ratePerCall`, `maxCalls`, `minDeposit`, `withdrawalDelayMs`, `providerPubkey`, `disputeWindowMs`
124
-
125
- **Payload known top-level keys:** `s402Version`, `scheme`, `payload`.
126
-
127
- **Payload inner keys** (per scheme):
128
- - `exact`, `stream`, `escrow`: `transaction`, `signature`
129
- - `unlock`: `transaction`, `signature`, `encryptionId`
130
- - `prepaid`: `transaction`, `signature`, `ratePerCall`, `maxCalls`
131
-
132
- **Settle response known keys:** `success`, `txDigest`, `receiptId`, `finalityMs`, `streamId`, `escrowId`, `balanceId`, `error`, `errorCode`.
133
-
134
- ### JSON key ordering
135
-
136
- s402 vectors use JavaScript's `JSON.stringify()` key ordering (insertion order). Cross-language implementations MUST serialize keys in the same order as the vector files to produce identical base64 output. If your language's JSON serializer uses alphabetical ordering by default, you'll need ordered serialization (e.g., Go's `json.Marshal` on a struct with ordered fields, Python's `json.dumps` with `sort_keys=False`).
137
-
138
- Note: **decode also reorders keys**. The decode functions iterate their allowlist in the order listed above, not the input order. If the input JSON has keys in a different order, the decoded output follows the allowlist order. This affects roundtrip tests — the re-encoded output uses the allowlist order, which may differ from the original input order.
139
-
140
- ### Header size limits
141
-
142
- Implementations SHOULD enforce a maximum header size of **64 KiB** (`MAX_HEADER_BYTES = 65536`). Headers exceeding this limit indicate abuse or misconfiguration and SHOULD be rejected before base64 decoding.
143
-
144
- ### Rejection vector dispatch
145
-
146
- Rejection vectors are dispatched in this precedence order:
147
-
148
- 1. If `expectedErrorCode` is `"RECEIPT_PARSE_ERROR"` → test `parseReceiptHeader`. Note: receipts throw a plain `Error`, not an `s402Error`, since receipts are a separate subsystem.
149
- 2. If `input.decodeAs` is `"payload"` → test `decodePaymentPayload`
150
- 3. If `input.decodeAs` is `"compat"` → test `normalizeRequirements` with the raw JSON (not base64)
151
- 4. Otherwise → test `decodePaymentRequired` (the default)
152
-
153
- ### Error codes
154
-
155
- Rejection vectors use `expectedErrorCode` values from the s402 error code enum:
156
-
157
- - `INVALID_PAYLOAD` — malformed or invalid input
158
- - `RECEIPT_PARSE_ERROR` — malformed receipt header (vector convention for receipt-specific parsing errors)
159
-
160
- ## Regenerating Vectors
161
-
162
- If you modify the s402 encoding logic, regenerate vectors:
163
-
164
- ```bash
165
- npx tsx test/conformance/generate-vectors.ts
166
- ```
167
-
168
- Then run the conformance tests to verify:
169
-
170
- ```bash
171
- pnpm run test
172
- ```