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 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 };
@@ -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 };