s402 0.3.0 → 0.4.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 CHANGED
@@ -5,6 +5,20 @@ 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.4.0] - 2026-04-11
9
+
10
+ ### Changed
11
+ - **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.
12
+ - Updated JSDoc: `@since 0.4.0 — required (was optional in 0.3.0)`
13
+ - `mockExactClientScheme()` in `test-utils.ts` now includes `verifySettlement()` returning `{ verified: false }` with reason `'mock scheme'`
14
+
15
+ ### Added
16
+ - **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`.
17
+
18
+ ### Compatibility
19
+ - **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.
20
+ - Wire format: unchanged from v0.3.0.
21
+
8
22
  ## [0.3.0] - 2026-04-11
9
23
 
10
24
  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 +172,7 @@ _Version bump for npm publish after license change._
158
172
  - Property-based fuzz testing via fast-check
159
173
  - 207 tests, zero runtime dependencies
160
174
 
175
+ [0.4.0]: https://github.com/s402-protocol/core/compare/v0.3.0...v0.4.0
161
176
  [0.3.0]: https://github.com/s402-protocol/core/compare/v0.2.3...v0.3.0
162
177
  [0.2.1]: https://github.com/s402-protocol/core/compare/v0.2.0...v0.2.1
163
178
  [0.2.0]: https://github.com/s402-protocol/core/compare/v0.1.8...v0.2.0
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, s402StreamExtra, s402StreamPayload, s402UnlockExtra, s402UnlockPayload, 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-D7qqwo3Q.mjs";
4
+ import { a as s402ServerScheme, i as s402RouteConfig, n as s402DirectScheme, o as s402SettlementVerification, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-tVj4sOr-.mjs";
5
5
  import { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader, s402Receipt, s402ReceiptSigner, s402ReceiptVerifier } from "./receipts.mjs";
6
6
 
7
7
  //#region src/client.d.ts
@@ -32,21 +32,22 @@ interface s402ClientScheme {
32
32
  * signed payload the client actually sent.
33
33
  *
34
34
  * For schemes where the client signs the full transaction before sending
35
- * (exact, stream, escrow, unlock-TX1), this is a **local, offline check**:
36
- * derive the expected tx digest from the signed bytes and compare it to the
37
- * digest the facilitator returned. No RPC call required. This closes the
38
- * causal-binding hole identified in the April 2026 scale-fragility review
39
- * (see `knowledge/scale-fragility-council-v03.md`): a malicious facilitator
40
- * cannot substitute an unrelated-but-real tx digest, because that other
41
- * digest would correspond to different signed bytes the client never
35
+ * (exact, stream, escrow, prepaid, unlock-TX1), this is a **local, offline
36
+ * check**: derive the expected tx digest from the signed bytes and compare it
37
+ * to the digest the facilitator returned. No RPC call required. This closes
38
+ * the causal-binding hole identified in the April 2026 S8 review: a malicious
39
+ * facilitator cannot substitute an unrelated-but-real tx digest, because that
40
+ * other digest would correspond to different signed bytes the client never
42
41
  * produced.
43
42
  *
44
- * Optional for backward-compatibility. Schemes that cannot verify locally
43
+ * Every scheme MUST implement this method. Schemes that cannot verify locally
45
44
  * (e.g. unlock-TX2, which is facilitator-constructed) should return
46
45
  * `{ verified: false, reason: 'scheme does not support local verification' }`
47
46
  * and rely on other attestation mechanisms.
47
+ *
48
+ * @since 0.4.0 — required (was optional in 0.3.0)
48
49
  */
49
- verifySettlement?(payload: s402PaymentPayload, settleResponse: s402SettleResponse): s402SettlementVerification;
50
+ verifySettlement(payload: s402PaymentPayload, settleResponse: s402SettleResponse): s402SettlementVerification;
50
51
  }
51
52
  /** Implemented by each scheme on the server side */
52
53
  interface s402ServerScheme {
@@ -1,5 +1,5 @@
1
1
  import { s402PaymentPayload, s402PaymentRequirements, s402SettleResponse, s402VerifyResponse } from "./types.mjs";
2
- import { a as s402ServerScheme, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-D7qqwo3Q.mjs";
2
+ import { a as s402ServerScheme, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-tVj4sOr-.mjs";
3
3
 
4
4
  //#region src/test-utils.d.ts
5
5
  /**
@@ -29,6 +29,14 @@ function mockExactClientScheme() {
29
29
  signature: "mock-signature"
30
30
  }
31
31
  };
32
+ },
33
+ verifySettlement() {
34
+ return {
35
+ verified: false,
36
+ expectedDigest: "",
37
+ actualDigest: null,
38
+ reason: "mock scheme"
39
+ };
32
40
  }
33
41
  };
34
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s402",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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",
@@ -0,0 +1,180 @@
1
+ [
2
+ {
3
+ "description": "Matching digest — verified is true, digests equal",
4
+ "payload": {
5
+ "s402Version": "1",
6
+ "scheme": "exact",
7
+ "payload": {
8
+ "transaction": "dHgtYnl0ZXMtaGVyZQ==",
9
+ "signature": "c2lnLWJ5dGVzLWhlcmU="
10
+ }
11
+ },
12
+ "settleResponse": {
13
+ "success": true,
14
+ "txDigest": "KNOWN_DIGEST_ABC123"
15
+ },
16
+ "expectedShape": {
17
+ "verified": "boolean",
18
+ "expectedDigest": "string",
19
+ "actualDigest": "string|null"
20
+ },
21
+ "invariants": [
22
+ "IF verified === true THEN expectedDigest === actualDigest",
23
+ "actualDigest MUST equal settleResponse.txDigest"
24
+ ],
25
+ "notes": "The implementation derives expectedDigest from payload.payload.transaction using a chain-specific algorithm. If the facilitator returned the correct digest, verified MUST be true."
26
+ },
27
+ {
28
+ "description": "Mismatched digest — verified is false with DIGEST_MISMATCH reason",
29
+ "payload": {
30
+ "s402Version": "1",
31
+ "scheme": "exact",
32
+ "payload": {
33
+ "transaction": "dHgtYnl0ZXMtaGVyZQ==",
34
+ "signature": "c2lnLWJ5dGVzLWhlcmU="
35
+ }
36
+ },
37
+ "settleResponse": {
38
+ "success": true,
39
+ "txDigest": "WRONG_DIGEST_XYZ789"
40
+ },
41
+ "expectedShape": {
42
+ "verified": "boolean",
43
+ "expectedDigest": "string",
44
+ "actualDigest": "string|null"
45
+ },
46
+ "invariants": [
47
+ "verified MUST be false",
48
+ "expectedDigest MUST NOT equal actualDigest",
49
+ "actualDigest MUST equal settleResponse.txDigest",
50
+ "reason SHOULD contain 'DIGEST_MISMATCH' or indicate the mismatch"
51
+ ],
52
+ "notes": "A malicious facilitator returns a real but unrelated digest. The client detects the mismatch locally without any RPC call."
53
+ },
54
+ {
55
+ "description": "Settle failed — no txDigest available",
56
+ "payload": {
57
+ "s402Version": "1",
58
+ "scheme": "exact",
59
+ "payload": {
60
+ "transaction": "dHgtYnl0ZXMtaGVyZQ==",
61
+ "signature": "c2lnLWJ5dGVzLWhlcmU="
62
+ }
63
+ },
64
+ "settleResponse": {
65
+ "success": false,
66
+ "error": "Insufficient gas"
67
+ },
68
+ "expectedShape": {
69
+ "verified": "boolean",
70
+ "expectedDigest": "string",
71
+ "actualDigest": "string|null"
72
+ },
73
+ "invariants": [
74
+ "verified MUST be false",
75
+ "actualDigest MUST be null (no digest was returned)",
76
+ "reason SHOULD indicate no digest was available"
77
+ ],
78
+ "notes": "When settlement fails, there is no digest to verify. The implementation should handle this gracefully rather than throwing."
79
+ },
80
+ {
81
+ "description": "Settle succeeded but txDigest is undefined",
82
+ "payload": {
83
+ "s402Version": "1",
84
+ "scheme": "exact",
85
+ "payload": {
86
+ "transaction": "dHgtYnl0ZXMtaGVyZQ==",
87
+ "signature": "c2lnLWJ5dGVzLWhlcmU="
88
+ }
89
+ },
90
+ "settleResponse": {
91
+ "success": true
92
+ },
93
+ "expectedShape": {
94
+ "verified": "boolean",
95
+ "expectedDigest": "string",
96
+ "actualDigest": "string|null"
97
+ },
98
+ "invariants": [
99
+ "verified MUST be false",
100
+ "actualDigest MUST be null",
101
+ "reason SHOULD indicate missing digest"
102
+ ],
103
+ "notes": "Edge case: facilitator reports success but omits the digest. This should be treated as unverifiable — the client cannot confirm settlement."
104
+ },
105
+ {
106
+ "description": "Invalid base64 in payload transaction bytes",
107
+ "payload": {
108
+ "s402Version": "1",
109
+ "scheme": "exact",
110
+ "payload": {
111
+ "transaction": "!!!NOT-BASE64!!!",
112
+ "signature": "c2lnLWJ5dGVzLWhlcmU="
113
+ }
114
+ },
115
+ "settleResponse": {
116
+ "success": true,
117
+ "txDigest": "SOME_DIGEST"
118
+ },
119
+ "expectedShape": {
120
+ "verified": "boolean",
121
+ "expectedDigest": "string",
122
+ "actualDigest": "string|null"
123
+ },
124
+ "invariants": [
125
+ "verified MUST be false",
126
+ "MUST NOT throw — return a verification result with reason"
127
+ ],
128
+ "notes": "Malformed input must fail gracefully. Implementations MUST catch decode errors and return { verified: false } rather than propagating exceptions."
129
+ },
130
+ {
131
+ "description": "Stream scheme — same verification contract applies",
132
+ "payload": {
133
+ "s402Version": "1",
134
+ "scheme": "stream",
135
+ "payload": {
136
+ "transaction": "c3RyZWFtLXR4LWJ5dGVz",
137
+ "signature": "c3RyZWFtLXNpZw=="
138
+ }
139
+ },
140
+ "settleResponse": {
141
+ "success": true,
142
+ "txDigest": "STREAM_DIGEST_123"
143
+ },
144
+ "expectedShape": {
145
+ "verified": "boolean",
146
+ "expectedDigest": "string",
147
+ "actualDigest": "string|null"
148
+ },
149
+ "invariants": [
150
+ "IF verified === true THEN expectedDigest === actualDigest",
151
+ "actualDigest MUST equal settleResponse.txDigest"
152
+ ],
153
+ "notes": "The S8 invariant applies to ALL client-signed schemes, not just exact. Stream, escrow, prepaid, and unlock-TX1 all sign full transactions before sending to the facilitator."
154
+ },
155
+ {
156
+ "description": "Scheme that cannot verify locally (e.g. unlock-TX2)",
157
+ "payload": {
158
+ "s402Version": "1",
159
+ "scheme": "unlock",
160
+ "payload": {
161
+ "transaction": "",
162
+ "signature": ""
163
+ }
164
+ },
165
+ "settleResponse": {
166
+ "success": true,
167
+ "txDigest": "FACILITATOR_BUILT_TX"
168
+ },
169
+ "expectedShape": {
170
+ "verified": "boolean",
171
+ "expectedDigest": "string",
172
+ "actualDigest": "string|null"
173
+ },
174
+ "invariants": [
175
+ "verified MUST be false",
176
+ "reason MUST be present and explain why verification is not possible"
177
+ ],
178
+ "notes": "For schemes where the facilitator constructs the transaction (e.g. unlock phase 2), the client has no signed bytes to derive a digest from. The implementation MUST return { verified: false } with a reason, not throw."
179
+ }
180
+ ]