s402 0.1.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 +21 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +41 -0
- package/dist/compat.d.mts +114 -0
- package/dist/compat.mjs +152 -0
- package/dist/errors.d.mts +48 -0
- package/dist/errors.mjs +123 -0
- package/dist/http.d.mts +67 -0
- package/dist/http.mjs +274 -0
- package/dist/index.d.mts +192 -0
- package/dist/index.mjs +209 -0
- package/dist/types.d.mts +252 -0
- package/dist/types.mjs +17 -0
- package/package.json +99 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-02-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Five payment scheme types: exact, prepaid, escrow, unlock, stream
|
|
13
|
+
- HTTP header encoding/decoding (base64 JSON wire format)
|
|
14
|
+
- Client, server, and facilitator scheme registries
|
|
15
|
+
- Optional x402 compat layer (`s402/compat`) — normalizes V1 and V2 formats
|
|
16
|
+
- Typed error codes with `retryable` flag and `suggestedAction` for agent self-recovery
|
|
17
|
+
- Sub-path exports: `s402/types`, `s402/http`, `s402/compat`, `s402/errors`
|
|
18
|
+
- Property-based fuzz testing via fast-check
|
|
19
|
+
- 207 tests, zero runtime dependencies
|
|
20
|
+
|
|
21
|
+
[0.1.0]: https://github.com/s402-protocol/core/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pixel Drift Co
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# s402
|
|
2
|
+
|
|
3
|
+
**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.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install s402
|
|
7
|
+
pnpm add s402
|
|
8
|
+
bun add s402
|
|
9
|
+
deno add npm:s402
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
> **ESM-only.** This package ships ES modules only (`"type": "module"`). Requires Node.js >= 18. CommonJS `require()` is not supported.
|
|
13
|
+
|
|
14
|
+
## Why s402?
|
|
15
|
+
|
|
16
|
+
HTTP 402 ("Payment Required") has been reserved since 1999 — waiting for a payment protocol that actually works. Coinbase's x402 proved the concept on EVM. s402 takes it further by leveraging what makes Sui different.
|
|
17
|
+
|
|
18
|
+
### s402 vs x402
|
|
19
|
+
|
|
20
|
+
| | x402 (Coinbase) | s402 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| **Settlement** | Two-step: verify then settle (temporal gap) | Atomic: verify + settle in one PTB |
|
|
23
|
+
| **Finality** | 12+ second blocks (EVM L1) | ~400ms (Sui) |
|
|
24
|
+
| **Payment models** | Exact (one-shot) only | Five schemes: Exact, Prepaid, Escrow, Unlock, Stream |
|
|
25
|
+
| **Micro-payments** | $7.00 gas per 1K calls (broken) | $0.014 gas per 1K calls (prepaid) |
|
|
26
|
+
| **Coin handling** | approve + transferFrom | Native `coinWithBalance` + `splitCoins` |
|
|
27
|
+
| **Agent auth** | None | AP2 mandate delegation |
|
|
28
|
+
| **Direct mode** | No | Yes (no facilitator needed) |
|
|
29
|
+
| **Receipts** | Off-chain | On-chain NFT proofs |
|
|
30
|
+
| **Compatibility** | n/a | Optional x402 compat layer (`s402/compat`) |
|
|
31
|
+
|
|
32
|
+
**s402 is Sui-native by design.** These advantages come from Sui's object model, PTBs, and sub-second finality. They can't be replicated on EVM — and they don't need to be. x402 already handles EVM well. s402 handles Sui better.
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
s402 <-- You are here. Protocol spec. Zero runtime deps.
|
|
38
|
+
|
|
|
39
|
+
|-- Types Payment requirements, payloads, responses
|
|
40
|
+
|-- Schemes Client/Server/Facilitator interfaces per scheme
|
|
41
|
+
|-- HTTP Encode/decode for HTTP headers (base64 JSON)
|
|
42
|
+
|-- Compat Optional x402 migration aid
|
|
43
|
+
|-- Errors Typed error codes with recovery hints
|
|
44
|
+
|
|
|
45
|
+
@sweepay/sui <-- Sui-specific implementations (coming soon)
|
|
46
|
+
@sweepay/sdk <-- High-level DX (coming soon)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`s402` is **chain-agnostic protocol plumbing**. It defines _what_ gets sent over HTTP. The Sui-specific _how_ will live in `@sweepay/sui` (coming soon).
|
|
50
|
+
|
|
51
|
+
## Payment Schemes
|
|
52
|
+
|
|
53
|
+
### Exact (v0.1)
|
|
54
|
+
|
|
55
|
+
One-shot payment. Client builds a signed transfer PTB, facilitator verifies + broadcasts atomically.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Client Server Facilitator
|
|
59
|
+
|--- GET /api/data ------->| |
|
|
60
|
+
|<-- 402 + requirements ---| |
|
|
61
|
+
| | |
|
|
62
|
+
| (build PTB, sign) | |
|
|
63
|
+
|--- GET + x-payment ----->|--- verify + settle ---->|
|
|
64
|
+
| |<--- { success, tx } ----|
|
|
65
|
+
|<-- 200 + data -----------| |
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This is the x402-compatible baseline. An x402 client can talk to an s402 server using this scheme with zero modifications.
|
|
69
|
+
|
|
70
|
+
### Prepaid (v0.1)
|
|
71
|
+
|
|
72
|
+
Deposit-based access. Agent deposits funds into an on-chain Balance shared object targeted at a specific provider. API calls happen off-chain. Provider batch-claims accumulated usage. Move module enforces rate caps — no trust required.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Phase 1 (deposit — one on-chain TX):
|
|
76
|
+
Agent deposits 10 SUI → Balance shared object created
|
|
77
|
+
Gas: ~$0.007
|
|
78
|
+
|
|
79
|
+
Phase 2 (usage — off-chain, zero gas):
|
|
80
|
+
Agent makes 1,000 API calls
|
|
81
|
+
Server tracks usage, no on-chain TX per call
|
|
82
|
+
|
|
83
|
+
Phase 3 (claim — one on-chain TX):
|
|
84
|
+
Provider claims accumulated $1.00 from Balance
|
|
85
|
+
Gas: ~$0.007
|
|
86
|
+
─────────────────────────────────────────────────────
|
|
87
|
+
Total gas: $0.014 for 1,000 calls
|
|
88
|
+
Per-call effective gas: $0.000014
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is the agent-native payment pattern. Without prepaid, per-call settlement costs $7.00 in gas for $1.00 of API usage (economically impossible). With prepaid, it costs $0.014 (economically trivial).
|
|
92
|
+
|
|
93
|
+
Use cases: AI agent API budgets, high-frequency API access, compute metering.
|
|
94
|
+
|
|
95
|
+
### Escrow (v0.1)
|
|
96
|
+
|
|
97
|
+
Time-locked vault with arbiter dispute resolution. Full state machine: `ACTIVE -> DISPUTED -> RELEASED / REFUNDED`.
|
|
98
|
+
|
|
99
|
+
- Buyer deposits funds, locked until release or deadline
|
|
100
|
+
- Buyer confirms delivery -> funds release to seller (receipt minted)
|
|
101
|
+
- Deadline passes -> permissionless refund (anyone can trigger)
|
|
102
|
+
- Either party disputes -> arbiter resolves
|
|
103
|
+
|
|
104
|
+
Use cases: digital goods delivery, freelance payments, trustless commerce.
|
|
105
|
+
|
|
106
|
+
### Unlock
|
|
107
|
+
|
|
108
|
+
Pay-to-decrypt encrypted content. Escrow + encrypted content delivery. The buyer pays into escrow; on release, the `EscrowReceipt` unlocks encrypted content stored on [Walrus](https://docs.walrus.site). Currently powered by [Sui SEAL](https://docs.sui.io/concepts/cryptography/seal).
|
|
109
|
+
|
|
110
|
+
This scheme depends on encryption key server infrastructure and is under active development.
|
|
111
|
+
|
|
112
|
+
### Stream
|
|
113
|
+
|
|
114
|
+
Per-second micropayments via on-chain `StreamingMeter`. Client deposits funds into a shared object; recipient claims accrued tokens over time.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
Phase 1 (402 exchange):
|
|
118
|
+
Client builds stream creation PTB --> facilitator broadcasts
|
|
119
|
+
Result: StreamingMeter shared object on-chain
|
|
120
|
+
|
|
121
|
+
Phase 2 (ongoing access):
|
|
122
|
+
Client includes x-stream-id header --> server checks on-chain balance
|
|
123
|
+
Server grants access as long as stream has funds
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Use cases: AI inference sessions, video streaming, real-time data feeds.
|
|
127
|
+
|
|
128
|
+
## Quick Start
|
|
129
|
+
|
|
130
|
+
### Types only (most common)
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import type {
|
|
134
|
+
s402PaymentRequirements,
|
|
135
|
+
s402PaymentPayload,
|
|
136
|
+
s402SettleResponse,
|
|
137
|
+
} from 's402';
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### HTTP header encoding
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import {
|
|
144
|
+
encodePaymentRequired,
|
|
145
|
+
decodePaymentRequired,
|
|
146
|
+
encodePaymentPayload,
|
|
147
|
+
decodePaymentPayload,
|
|
148
|
+
detectProtocol,
|
|
149
|
+
} from 's402';
|
|
150
|
+
|
|
151
|
+
// Server: build 402 response
|
|
152
|
+
const requirements: s402PaymentRequirements = {
|
|
153
|
+
s402Version: '1',
|
|
154
|
+
accepts: ['exact', 'stream'],
|
|
155
|
+
network: 'sui:mainnet',
|
|
156
|
+
asset: '0x2::sui::SUI',
|
|
157
|
+
amount: '1000000', // 0.001 SUI in MIST
|
|
158
|
+
payTo: '0xrecipient...',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
response.status = 402;
|
|
162
|
+
response.headers.set('payment-required', encodePaymentRequired(requirements));
|
|
163
|
+
|
|
164
|
+
// Client: read 402 response
|
|
165
|
+
const header = response.headers.get('payment-required')!;
|
|
166
|
+
const reqs = decodePaymentRequired(header);
|
|
167
|
+
console.log(reqs.accepts); // ['exact', 'stream']
|
|
168
|
+
console.log(reqs.amount); // '1000000'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### x402 compat (opt-in)
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import {
|
|
175
|
+
normalizeRequirements,
|
|
176
|
+
isS402,
|
|
177
|
+
isX402,
|
|
178
|
+
toX402Requirements,
|
|
179
|
+
fromX402Requirements,
|
|
180
|
+
} from 's402/compat';
|
|
181
|
+
|
|
182
|
+
// Normalize x402 JSON (V1 or V2) to s402 format
|
|
183
|
+
const requirements = normalizeRequirements(rawJsonObject);
|
|
184
|
+
|
|
185
|
+
// Convert s402 -> x402 V1 for legacy clients
|
|
186
|
+
const x402Reqs = toX402Requirements(requirements);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Error handling
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { s402Error, s402ErrorCode } from 's402';
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await facilitator.settle(payload, requirements);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (e instanceof s402Error) {
|
|
198
|
+
console.log(e.code); // 'INSUFFICIENT_BALANCE'
|
|
199
|
+
console.log(e.retryable); // false
|
|
200
|
+
console.log(e.suggestedAction); // 'Top up wallet balance...'
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Scheme registry (client)
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { s402Client } from 's402';
|
|
209
|
+
|
|
210
|
+
const client = new s402Client();
|
|
211
|
+
|
|
212
|
+
// Register scheme implementations (from @sweepay/sui or your own)
|
|
213
|
+
client.register('sui:mainnet', exactScheme);
|
|
214
|
+
client.register('sui:mainnet', streamScheme);
|
|
215
|
+
|
|
216
|
+
// Auto-selects best scheme from server's accepts array
|
|
217
|
+
const payload = await client.createPayment(requirements);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Scheme registry (facilitator)
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { s402Facilitator } from 's402';
|
|
224
|
+
|
|
225
|
+
const facilitator = new s402Facilitator();
|
|
226
|
+
facilitator.register('sui:mainnet', exactFacilitatorScheme);
|
|
227
|
+
|
|
228
|
+
// Atomic verify + settle
|
|
229
|
+
const result = await facilitator.process(payload, requirements);
|
|
230
|
+
if (result.success) {
|
|
231
|
+
console.log(result.txDigest); // Sui transaction digest
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Sub-path Exports
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { ... } from 's402'; // Everything
|
|
239
|
+
import type { ... } from 's402/types'; // Types + constants only
|
|
240
|
+
import { ... } from 's402/http'; // HTTP encode/decode
|
|
241
|
+
import { ... } from 's402/compat'; // x402 interop
|
|
242
|
+
import { ... } from 's402/errors'; // Error types
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Implementing a Scheme
|
|
246
|
+
|
|
247
|
+
s402 is designed as a plugin system. Each payment scheme implements three interfaces:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import type {
|
|
251
|
+
s402ClientScheme, // Client: build payment payload
|
|
252
|
+
s402ServerScheme, // Server: build payment requirements
|
|
253
|
+
s402FacilitatorScheme, // Facilitator: verify + settle
|
|
254
|
+
s402DirectScheme, // Optional: settle without facilitator
|
|
255
|
+
} from 's402';
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The reference Sui implementation of all five schemes will be available in `@sweepay/sui` (coming soon).
|
|
259
|
+
|
|
260
|
+
## Wire Format
|
|
261
|
+
|
|
262
|
+
s402 uses the same HTTP headers as x402 V1:
|
|
263
|
+
|
|
264
|
+
| Header | Direction | Content |
|
|
265
|
+
|--------|-----------|---------|
|
|
266
|
+
| `payment-required` | Server -> Client | Base64-encoded `s402PaymentRequirements` JSON |
|
|
267
|
+
| `x-payment` | Client -> Server | Base64-encoded `s402PaymentPayload` JSON |
|
|
268
|
+
| `payment-response` | Server -> Client | Base64-encoded `s402SettleResponse` JSON |
|
|
269
|
+
|
|
270
|
+
> **Note:** x402 V2 renamed the client payment header to `payment-signature`. s402 uses `x-payment` (matching x402 V1). All header names are lowercase per HTTP/2 (RFC 9113 §8.2.1). x402 V2 servers accept both headers, so s402 clients work with both versions. If your server needs to accept x402 V2 clients, also check `payment-signature`.
|
|
271
|
+
|
|
272
|
+
The presence of `s402Version` in the decoded JSON distinguishes s402 from x402. Clients and servers can auto-detect the protocol using `detectProtocol()`.
|
|
273
|
+
|
|
274
|
+
## Discovery
|
|
275
|
+
|
|
276
|
+
Servers can advertise s402 support at `/.well-known/s402.json`:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"s402Version": "1",
|
|
281
|
+
"schemes": ["exact", "stream", "escrow", "unlock", "prepaid"],
|
|
282
|
+
"networks": ["sui:mainnet"],
|
|
283
|
+
"assets": ["0x2::sui::SUI", "0xdba...::usdc::USDC"],
|
|
284
|
+
"directSettlement": true,
|
|
285
|
+
"mandateSupport": true,
|
|
286
|
+
"protocolFeeBps": 50
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Security
|
|
291
|
+
|
|
292
|
+
**HTTPS is required.** s402 payment data (requirements, payloads, settlement responses) travels in HTTP headers as base64-encoded JSON. Without TLS, this data is visible to any network observer. All production deployments MUST use HTTPS.
|
|
293
|
+
|
|
294
|
+
**Requirements expiration.** Servers SHOULD set `expiresAt` on payment requirements to prevent replay of stale 402 responses. The facilitator rejects expired requirements before processing.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
const requirements: s402PaymentRequirements = {
|
|
298
|
+
s402Version: '1',
|
|
299
|
+
accepts: ['exact'],
|
|
300
|
+
network: 'sui:mainnet',
|
|
301
|
+
asset: '0x2::sui::SUI',
|
|
302
|
+
amount: '1000000',
|
|
303
|
+
payTo: '0xrecipient...',
|
|
304
|
+
expiresAt: Date.now() + 5 * 60 * 1000, // 5-minute window
|
|
305
|
+
};
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Design Principles
|
|
309
|
+
|
|
310
|
+
1. **Protocol-agnostic core, Sui-native reference.** `s402` defines chain-agnostic protocol types and HTTP encoding. The reference implementation (`@sweepay/sui`, coming soon) will exploit Sui's unique properties — PTBs, object model, sub-second finality. Other chains can implement s402 schemes using their own primitives.
|
|
311
|
+
|
|
312
|
+
2. **Optional x402 compat.** The `s402/compat` subpath provides a migration aid for codebases with x402-formatted JSON. It normalizes x402 V1 (`maxAmountRequired`) and V2 (`amount`) to s402 format. This is opt-in — the core protocol has no x402 dependency.
|
|
313
|
+
|
|
314
|
+
3. **Scheme-specific verification.** Each scheme has its own verify logic. Exact verify (signature recovery + dry-run) is fundamentally different from stream verify (deposit check + rate validation). The facilitator dispatches — it doesn't share logic.
|
|
315
|
+
|
|
316
|
+
4. **Zero runtime dependencies.** `s402` is pure TypeScript protocol definitions. No Sui SDK, no crypto libraries, no HTTP framework. Chain-specific code belongs in adapters.
|
|
317
|
+
|
|
318
|
+
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.
|
|
319
|
+
|
|
320
|
+
## License
|
|
321
|
+
|
|
322
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
**Please do not open public issues for security vulnerabilities.**
|
|
6
|
+
|
|
7
|
+
If you discover a security issue in s402, please report it privately:
|
|
8
|
+
|
|
9
|
+
- **Email:** dannydevs@proton.me
|
|
10
|
+
- **Subject line:** `[s402 security] <brief description>`
|
|
11
|
+
|
|
12
|
+
You will receive an acknowledgment within 48 hours. We aim to provide a fix or mitigation plan within 7 days of confirmation.
|
|
13
|
+
|
|
14
|
+
## Scope
|
|
15
|
+
|
|
16
|
+
This policy covers the `s402` npm package — the protocol types, HTTP encoding/decoding, scheme registry, and compat layer.
|
|
17
|
+
|
|
18
|
+
Security issues in downstream packages (`@sweepay/sui`, `@sweepay/sdk`, etc.) should be reported to the same email.
|
|
19
|
+
|
|
20
|
+
## What qualifies
|
|
21
|
+
|
|
22
|
+
- Wire format parsing vulnerabilities (header injection, base64 decode exploits)
|
|
23
|
+
- Type confusion that could lead to incorrect payment verification
|
|
24
|
+
- Compat layer normalization bugs that silently alter payment amounts
|
|
25
|
+
- Anything that could cause funds loss, incorrect settlement, or unauthorized access
|
|
26
|
+
|
|
27
|
+
## What does not qualify
|
|
28
|
+
|
|
29
|
+
- Bugs in example code or documentation
|
|
30
|
+
- Denial-of-service via malformed input (we validate and throw typed errors)
|
|
31
|
+
- Issues in development dependencies (vitest, tsdown, etc.)
|
|
32
|
+
|
|
33
|
+
## Disclosure
|
|
34
|
+
|
|
35
|
+
We follow coordinated disclosure. Once a fix is released, we will:
|
|
36
|
+
|
|
37
|
+
1. Credit the reporter (unless anonymity is requested)
|
|
38
|
+
2. Publish a security advisory on GitHub
|
|
39
|
+
3. Release a patched version on npm
|
|
40
|
+
|
|
41
|
+
Thank you for helping keep s402 secure.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { s402ExactPayload, s402PaymentPayload, s402PaymentRequirements } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/compat.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* x402 PaymentRequirements shape — supports both V1 and V2 wire formats.
|
|
6
|
+
*
|
|
7
|
+
* V1 wire format uses `maxAmountRequired`; V2 uses `amount`.
|
|
8
|
+
* Both versions require `maxTimeoutSeconds`.
|
|
9
|
+
* V1 includes resource metadata (`resource`, `description`, `mimeType`) inline;
|
|
10
|
+
* V2 hoists these to the `PaymentRequired` envelope.
|
|
11
|
+
*/
|
|
12
|
+
interface x402PaymentRequirements {
|
|
13
|
+
x402Version: number;
|
|
14
|
+
scheme: string;
|
|
15
|
+
network: string;
|
|
16
|
+
asset: string;
|
|
17
|
+
/** V2 amount field (base units). */
|
|
18
|
+
amount?: string;
|
|
19
|
+
/** V1 amount field (renamed to `amount` in V2). */
|
|
20
|
+
maxAmountRequired?: string;
|
|
21
|
+
payTo: string;
|
|
22
|
+
/** Required in x402. Seconds the facilitator will wait before rejecting. */
|
|
23
|
+
maxTimeoutSeconds?: number;
|
|
24
|
+
/** V1-only: resource URL. V2 moves this to the PaymentRequired envelope. */
|
|
25
|
+
resource?: string;
|
|
26
|
+
/** V1-only: human-readable description. */
|
|
27
|
+
description?: string;
|
|
28
|
+
facilitatorUrl?: string;
|
|
29
|
+
extensions?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* x402 V2 PaymentRequired envelope — wraps an array of requirements.
|
|
33
|
+
* In V2, `x402Version` lives on this envelope, not on individual requirements.
|
|
34
|
+
* Resource metadata and extensions are also at the envelope level.
|
|
35
|
+
*/
|
|
36
|
+
interface x402PaymentRequiredEnvelope {
|
|
37
|
+
x402Version: number;
|
|
38
|
+
accepts: x402PaymentRequirements[];
|
|
39
|
+
resource?: {
|
|
40
|
+
url?: string;
|
|
41
|
+
mimeType?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
};
|
|
44
|
+
extensions?: Record<string, unknown>;
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Minimal x402 PaymentPayload shape */
|
|
48
|
+
interface x402PaymentPayload {
|
|
49
|
+
x402Version: number;
|
|
50
|
+
scheme: string;
|
|
51
|
+
payload: {
|
|
52
|
+
transaction: string;
|
|
53
|
+
signature: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convert inbound x402 requirements to s402 format.
|
|
58
|
+
* Handles both V1 (`maxAmountRequired`) and V2 (`amount`) wire formats.
|
|
59
|
+
* Maps x402's single scheme to s402's accepts array.
|
|
60
|
+
*/
|
|
61
|
+
declare function fromX402Requirements(x402: x402PaymentRequirements): s402PaymentRequirements;
|
|
62
|
+
/**
|
|
63
|
+
* Convert inbound x402 payment payload to s402 format.
|
|
64
|
+
* Validates that required fields are present and correctly typed.
|
|
65
|
+
*/
|
|
66
|
+
declare function fromX402Payload(x402: x402PaymentPayload): s402ExactPayload;
|
|
67
|
+
/**
|
|
68
|
+
* Convert outbound s402 requirements to x402 V1 wire format.
|
|
69
|
+
* Strips s402-only fields (mandate, stream, escrow, unlock extensions).
|
|
70
|
+
* Only works for "exact" scheme — other schemes have no x402 equivalent.
|
|
71
|
+
*
|
|
72
|
+
* Includes both `maxAmountRequired` (V1) and `amount` (V2) for maximum interop.
|
|
73
|
+
* Includes `maxTimeoutSeconds` (required in x402, defaults to 60s).
|
|
74
|
+
* V1 metadata fields (`resource`, `description`) default to empty strings.
|
|
75
|
+
*/
|
|
76
|
+
declare function toX402Requirements(s402: s402PaymentRequirements, overrides?: {
|
|
77
|
+
maxTimeoutSeconds?: number;
|
|
78
|
+
resource?: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
}): x402PaymentRequirements;
|
|
81
|
+
/**
|
|
82
|
+
* Convert outbound s402 payload to x402 format.
|
|
83
|
+
* Only works for exact scheme payloads.
|
|
84
|
+
*/
|
|
85
|
+
declare function toX402Payload(s402: s402PaymentPayload): x402PaymentPayload | null;
|
|
86
|
+
/**
|
|
87
|
+
* Check if a decoded JSON object is s402 format.
|
|
88
|
+
*/
|
|
89
|
+
declare function isS402(obj: Record<string, unknown>): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Check if a decoded JSON object is x402 format (V1 flat or V2 envelope).
|
|
92
|
+
*/
|
|
93
|
+
declare function isX402(obj: Record<string, unknown>): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Check if a decoded JSON object is an x402 V2 envelope (has `accepts` array).
|
|
96
|
+
* V2 envelopes wrap requirements in an `accepts` array instead of flat fields.
|
|
97
|
+
*/
|
|
98
|
+
declare function isX402Envelope(obj: Record<string, unknown>): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Convert an x402 V2 envelope to s402 format.
|
|
101
|
+
* Picks the first requirement from the `accepts` array.
|
|
102
|
+
* Copies `x402Version` from the envelope onto the requirement for downstream processing.
|
|
103
|
+
*/
|
|
104
|
+
declare function fromX402Envelope(envelope: x402PaymentRequiredEnvelope): s402PaymentRequirements;
|
|
105
|
+
/**
|
|
106
|
+
* Auto-detect and normalize: if x402, convert to s402. If already s402, validate and pass through.
|
|
107
|
+
* Handles x402 V1 (flat), x402 V2 (envelope with accepts array), and s402 formats.
|
|
108
|
+
* Validates required fields to catch malformed/malicious payloads at the trust boundary.
|
|
109
|
+
*
|
|
110
|
+
* Returns a clean object with only known s402 fields — unknown top-level keys are stripped.
|
|
111
|
+
*/
|
|
112
|
+
declare function normalizeRequirements(obj: Record<string, unknown>): s402PaymentRequirements;
|
|
113
|
+
//#endregion
|
|
114
|
+
export { fromX402Envelope, fromX402Payload, fromX402Requirements, isS402, isX402, isX402Envelope, normalizeRequirements, toX402Payload, toX402Requirements, x402PaymentPayload, x402PaymentRequiredEnvelope, x402PaymentRequirements };
|
package/dist/compat.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { S402_VERSION } from "./types.mjs";
|
|
2
|
+
import { s402Error } from "./errors.mjs";
|
|
3
|
+
import { isValidAmount, pickRequirementsFields, validateRequirementsShape } from "./http.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/compat.ts
|
|
6
|
+
/**
|
|
7
|
+
* Convert inbound x402 requirements to s402 format.
|
|
8
|
+
* Handles both V1 (`maxAmountRequired`) and V2 (`amount`) wire formats.
|
|
9
|
+
* Maps x402's single scheme to s402's accepts array.
|
|
10
|
+
*/
|
|
11
|
+
function fromX402Requirements(x402) {
|
|
12
|
+
const amount = x402.amount ?? x402.maxAmountRequired;
|
|
13
|
+
if (!amount) throw new s402Error("INVALID_PAYLOAD", "x402 requirements missing both \"amount\" (V2) and \"maxAmountRequired\" (V1)");
|
|
14
|
+
if (!isValidAmount(amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${amount}": must be a non-negative integer string`);
|
|
15
|
+
return {
|
|
16
|
+
s402Version: S402_VERSION,
|
|
17
|
+
accepts: ["exact"],
|
|
18
|
+
network: x402.network,
|
|
19
|
+
asset: x402.asset,
|
|
20
|
+
amount,
|
|
21
|
+
payTo: x402.payTo,
|
|
22
|
+
facilitatorUrl: x402.facilitatorUrl,
|
|
23
|
+
extensions: x402.extensions
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Convert inbound x402 payment payload to s402 format.
|
|
28
|
+
* Validates that required fields are present and correctly typed.
|
|
29
|
+
*/
|
|
30
|
+
function fromX402Payload(x402) {
|
|
31
|
+
if (x402.payload == null || typeof x402.payload !== "object") throw new s402Error("INVALID_PAYLOAD", "x402 payload missing or not an object");
|
|
32
|
+
if (typeof x402.payload.transaction !== "string") throw new s402Error("INVALID_PAYLOAD", `x402 payload.transaction must be a string, got ${typeof x402.payload.transaction}`);
|
|
33
|
+
if (typeof x402.payload.signature !== "string") throw new s402Error("INVALID_PAYLOAD", `x402 payload.signature must be a string, got ${typeof x402.payload.signature}`);
|
|
34
|
+
return {
|
|
35
|
+
s402Version: S402_VERSION,
|
|
36
|
+
scheme: "exact",
|
|
37
|
+
payload: {
|
|
38
|
+
transaction: x402.payload.transaction,
|
|
39
|
+
signature: x402.payload.signature
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert outbound s402 requirements to x402 V1 wire format.
|
|
45
|
+
* Strips s402-only fields (mandate, stream, escrow, unlock extensions).
|
|
46
|
+
* Only works for "exact" scheme — other schemes have no x402 equivalent.
|
|
47
|
+
*
|
|
48
|
+
* Includes both `maxAmountRequired` (V1) and `amount` (V2) for maximum interop.
|
|
49
|
+
* Includes `maxTimeoutSeconds` (required in x402, defaults to 60s).
|
|
50
|
+
* V1 metadata fields (`resource`, `description`) default to empty strings.
|
|
51
|
+
*/
|
|
52
|
+
function toX402Requirements(s402, overrides) {
|
|
53
|
+
return {
|
|
54
|
+
x402Version: 1,
|
|
55
|
+
scheme: "exact",
|
|
56
|
+
network: s402.network,
|
|
57
|
+
asset: s402.asset,
|
|
58
|
+
amount: s402.amount,
|
|
59
|
+
maxAmountRequired: s402.amount,
|
|
60
|
+
payTo: s402.payTo,
|
|
61
|
+
facilitatorUrl: s402.facilitatorUrl,
|
|
62
|
+
maxTimeoutSeconds: overrides?.maxTimeoutSeconds ?? 60,
|
|
63
|
+
resource: overrides?.resource ?? "",
|
|
64
|
+
description: overrides?.description ?? "",
|
|
65
|
+
extensions: s402.extensions
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Convert outbound s402 payload to x402 format.
|
|
70
|
+
* Only works for exact scheme payloads.
|
|
71
|
+
*/
|
|
72
|
+
function toX402Payload(s402) {
|
|
73
|
+
if (s402.scheme !== "exact") return null;
|
|
74
|
+
const exact = s402;
|
|
75
|
+
return {
|
|
76
|
+
x402Version: 1,
|
|
77
|
+
scheme: "exact",
|
|
78
|
+
payload: {
|
|
79
|
+
transaction: exact.payload.transaction,
|
|
80
|
+
signature: exact.payload.signature
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if a decoded JSON object is s402 format.
|
|
86
|
+
*/
|
|
87
|
+
function isS402(obj) {
|
|
88
|
+
return "s402Version" in obj;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if a decoded JSON object is x402 format (V1 flat or V2 envelope).
|
|
92
|
+
*/
|
|
93
|
+
function isX402(obj) {
|
|
94
|
+
return "x402Version" in obj && !("s402Version" in obj);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if a decoded JSON object is an x402 V2 envelope (has `accepts` array).
|
|
98
|
+
* V2 envelopes wrap requirements in an `accepts` array instead of flat fields.
|
|
99
|
+
*/
|
|
100
|
+
function isX402Envelope(obj) {
|
|
101
|
+
return "x402Version" in obj && Array.isArray(obj.accepts) && !("s402Version" in obj);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Convert an x402 V2 envelope to s402 format.
|
|
105
|
+
* Picks the first requirement from the `accepts` array.
|
|
106
|
+
* Copies `x402Version` from the envelope onto the requirement for downstream processing.
|
|
107
|
+
*/
|
|
108
|
+
function fromX402Envelope(envelope) {
|
|
109
|
+
if (!envelope.accepts || envelope.accepts.length === 0) throw new s402Error("INVALID_PAYLOAD", "x402 V2 envelope has empty accepts array");
|
|
110
|
+
const req = {
|
|
111
|
+
...envelope.accepts[0],
|
|
112
|
+
x402Version: envelope.x402Version
|
|
113
|
+
};
|
|
114
|
+
validateX402Shape(req);
|
|
115
|
+
return fromX402Requirements(req);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Auto-detect and normalize: if x402, convert to s402. If already s402, validate and pass through.
|
|
119
|
+
* Handles x402 V1 (flat), x402 V2 (envelope with accepts array), and s402 formats.
|
|
120
|
+
* Validates required fields to catch malformed/malicious payloads at the trust boundary.
|
|
121
|
+
*
|
|
122
|
+
* Returns a clean object with only known s402 fields — unknown top-level keys are stripped.
|
|
123
|
+
*/
|
|
124
|
+
function normalizeRequirements(obj) {
|
|
125
|
+
if (isS402(obj)) {
|
|
126
|
+
validateRequirementsShape(obj);
|
|
127
|
+
return pickRequirementsFields(obj);
|
|
128
|
+
}
|
|
129
|
+
if (isX402Envelope(obj)) return fromX402Envelope(obj);
|
|
130
|
+
if (isX402(obj)) {
|
|
131
|
+
validateX402Shape(obj);
|
|
132
|
+
return fromX402Requirements(obj);
|
|
133
|
+
}
|
|
134
|
+
throw new s402Error("INVALID_PAYLOAD", "Unrecognized payment requirements format: missing s402Version or x402Version");
|
|
135
|
+
}
|
|
136
|
+
/** Validate that an x402 object has required fields (supports V1 and V2). */
|
|
137
|
+
function validateX402Shape(obj) {
|
|
138
|
+
const missing = [];
|
|
139
|
+
if (typeof obj.scheme !== "string") missing.push("scheme (string)");
|
|
140
|
+
if (typeof obj.network !== "string") missing.push("network (string)");
|
|
141
|
+
if (typeof obj.asset !== "string") missing.push("asset (string)");
|
|
142
|
+
if (typeof obj.payTo !== "string") missing.push("payTo (string)");
|
|
143
|
+
if (typeof obj.amount !== "string" && typeof obj.maxAmountRequired !== "string") missing.push("amount or maxAmountRequired (string)");
|
|
144
|
+
else {
|
|
145
|
+
const amt = typeof obj.amount === "string" ? obj.amount : obj.maxAmountRequired;
|
|
146
|
+
if (!isValidAmount(amt)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${amt}": must be a non-negative integer string`);
|
|
147
|
+
}
|
|
148
|
+
if (missing.length > 0) throw new s402Error("INVALID_PAYLOAD", `Malformed x402 requirements: missing or invalid fields: ${missing.join(", ")}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
export { fromX402Envelope, fromX402Payload, fromX402Requirements, isS402, isX402, isX402Envelope, normalizeRequirements, toX402Payload, toX402Requirements };
|