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-
|
|
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
|
|
36
|
-
* derive the expected tx digest from the signed bytes and compare it
|
|
37
|
-
* digest the facilitator returned. No RPC call required. This closes
|
|
38
|
-
* causal-binding hole identified in the April 2026
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
-
*
|
|
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
|
|
50
|
+
verifySettlement(payload: s402PaymentPayload, settleResponse: s402SettleResponse): s402SettlementVerification;
|
|
50
51
|
}
|
|
51
52
|
/** Implemented by each scheme on the server side */
|
|
52
53
|
interface s402ServerScheme {
|
package/dist/test-utils.d.mts
CHANGED
|
@@ -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-
|
|
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
|
/**
|
package/dist/test-utils.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s402",
|
|
3
|
-
"version": "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
|
+
]
|