s402 0.1.8 → 0.2.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,45 @@ 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.2.0] - 2026-03-01
9
+
10
+ ### Added
11
+
12
+ - **Receipt HTTP helpers** — `s402/receipts` sub-path export with `formatReceiptHeader()`, `parseReceiptHeader()`, `S402_RECEIPT_HEADER`. Chain-agnostic receipt wire format (`v2:base64(sig):callNumber:timestampMs:base64(hash)`) for v0.2 signed usage receipts.
13
+ - **S7 chain-agnostic boundary invariant** — formal safety invariant enforced by `test/boundary.test.ts`. Greps `src/` for chain-specific patterns (Sui address regex, Solana base58, Ethereum imports) and fails the build if any are found.
14
+ - **v0.2 prepaid type extensions** — `providerPubkey` and `disputeWindowMs` fields on `s402PrepaidExtra` for signed receipt mode.
15
+ - **Body transport** — `application/s402+json` content type for large payloads that don't fit in HTTP headers.
16
+ - **Formal safety invariants** (S1-S7) documented in AGENTS.md.
17
+
18
+ ### Fixed
19
+
20
+ - **Chain-agnostic payTo/protocolFeeAddress validation** — removed Sui-specific address regex (`/^0x[0-9a-fA-F]{64}$/`) from `http.ts`. Replaced with chain-agnostic checks (non-empty string, no control characters). Chain-specific validation belongs in `@sweefi/sui`.
21
+ - **x402 compat validation parity** — `normalizeRequirements()` now runs `validateRequirementsShape()` on x402 conversion output, ensuring identical validation regardless of input format.
22
+ - **Prepaid pairing invariant enforcement** — `providerPubkey` and `disputeWindowMs` must both be present (v0.2) or both absent (v0.1). Was documented in JSDoc but not enforced at wire decode.
23
+ - **Receipt BigInt coercion** — `parseReceiptHeader()` rejects empty strings and whitespace-only strings that JavaScript's `BigInt()` would silently coerce to `0n`.
24
+ - **Removed Sui default for `asset`** — `s402RouteConfig.asset` is now required (was optional with `'0x2::sui::SUI'` default). Chain-specific defaults don't belong in the protocol layer.
25
+
26
+ ### Changed
27
+
28
+ - **BREAKING**: `s402RouteConfig.asset` is now required (was optional).
29
+ - JSDoc on `s402PaymentRequirements` updated to chain-agnostic wording (network, asset, amount fields).
30
+ - 258 tests across 11 suites (was 207 at v0.1.0).
31
+
32
+ ## [0.1.8] - 2026-02-27
33
+
34
+ ### Added
35
+
36
+ - Body transport (`application/s402+json`) for large payloads
37
+ - v0.2 prepaid type extensions (`providerPubkey`, `disputeWindowMs`)
38
+ - `FUNDING.yml` and cross-linked SweeFi in README
39
+
40
+ ## [0.1.7] - 2026-02-25
41
+
42
+ ### Added
43
+
44
+ - Formal safety invariants (Lamport-style proofs)
45
+ - `isValidU64Amount()` magnitude checks
46
+
8
47
  ## [0.1.6] - 2026-02-19
9
48
 
10
49
  ### Fixed
@@ -77,6 +116,9 @@ _Version bump for npm publish after license change._
77
116
  - Property-based fuzz testing via fast-check
78
117
  - 207 tests, zero runtime dependencies
79
118
 
119
+ [0.2.0]: https://github.com/s402-protocol/core/compare/v0.1.8...v0.2.0
120
+ [0.1.8]: https://github.com/s402-protocol/core/compare/v0.1.7...v0.1.8
121
+ [0.1.7]: https://github.com/s402-protocol/core/compare/v0.1.6...v0.1.7
80
122
  [0.1.6]: https://github.com/s402-protocol/core/compare/v0.1.5...v0.1.6
81
123
  [0.1.5]: https://github.com/s402-protocol/core/compare/v0.1.4...v0.1.5
82
124
  [0.1.4]: https://github.com/s402-protocol/core/compare/v0.1.3...v0.1.4
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  **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.
7
7
 
8
- s402 is the Sui-native implementation of HTTP 402 (Payment Required) — an open protocol that lets AI agents pay for API calls in a single HTTP request with no per-call on-chain transaction. Unlike Coinbase's x402 on Ethereum, s402 uses Sui's Programmable Transaction Blocks to reduce 1,000 payments to just 2 on-chain transactions via the Prepaid scheme, cutting per-call effective gas from $0.007 to $0.000014 and making micropayments economically viable for AI agents for the first time.
8
+ s402 is the Sui-native implementation of HTTP 402 (Payment Required) — an open protocol that lets AI agents pay for API calls in a single HTTP request with no per-call on-chain transaction. Unlike Coinbase's x402 on Base (EVM L2), s402 uses Sui's Programmable Transaction Blocks to reduce 1,000 payments to just 2 on-chain transactions via the Prepaid scheme, cutting per-call effective gas from $0.007 to $0.000014 and making micropayments economically viable for AI agents for the first time.
9
9
 
10
10
  ```bash
11
11
  npm install s402
@@ -27,7 +27,7 @@ HTTP 402 ("Payment Required") has been reserved since 1999 — waiting for a pay
27
27
  | **Settlement** | Two-step: verify then settle (temporal gap) | Atomic: verify + settle in one PTB |
28
28
  | **Finality** | 12+ second blocks (EVM L1) | ~400ms (Sui) |
29
29
  | **Payment models** | Exact (one-shot) only | Five schemes: Exact, Prepaid, Escrow, Unlock, Stream |
30
- | **Micro-payments** | $7.00 gas per 1K calls (broken) | $0.014 gas per 1K calls (prepaid) |
30
+ | **Micro-payments** | ~$1.60 gas per 1K calls on Base (broken) | $0.014 gas per 1K calls (prepaid) |
31
31
  | **Coin handling** | approve + transferFrom | Native `coinWithBalance` + `splitCoins` |
32
32
  | **Agent auth** | None | AP2 mandate delegation |
33
33
  | **Direct mode** | No | Yes (no facilitator needed) |
@@ -325,6 +325,11 @@ const requirements: s402PaymentRequirements = {
325
325
 
326
326
  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.
327
327
 
328
+ ## Related
329
+
330
+ - **[SweeFi](https://github.com/sweeinc/sweefi)** — Open-source payment SDK built on s402. 10 packages including PTB builders, MCP tools, CLI, and UI components.
331
+ - **[Sui Gas Station](https://github.com/Danny-Devs/sui-gas-station)** — Sponsored transaction infrastructure for Sui.
332
+
328
333
  ## License
329
334
 
330
335
  Apache-2.0 — see [LICENSE](./LICENSE) for details.
package/dist/compat.mjs CHANGED
@@ -133,10 +133,16 @@ function normalizeRequirements(obj) {
133
133
  validateRequirementsShape(obj);
134
134
  return pickRequirementsFields(obj);
135
135
  }
136
- if (isX402Envelope(obj)) return fromX402Envelope(obj);
136
+ if (isX402Envelope(obj)) {
137
+ const record = fromX402Envelope(obj);
138
+ validateRequirementsShape(record);
139
+ return pickRequirementsFields(record);
140
+ }
137
141
  if (isX402(obj)) {
138
142
  validateX402Shape(obj);
139
- return fromX402Requirements(obj);
143
+ const record = fromX402Requirements(obj);
144
+ validateRequirementsShape(record);
145
+ return pickRequirementsFields(record);
140
146
  }
141
147
  throw new s402Error("INVALID_PAYLOAD", "Unrecognized payment requirements format: missing s402Version or x402Version");
142
148
  }
package/dist/http.mjs CHANGED
@@ -318,10 +318,16 @@ function validatePrepaidShape(value) {
318
318
  assertString(obj, "minDeposit", "prepaid");
319
319
  if (typeof obj.minDeposit === "string" && !isValidAmount(obj.minDeposit)) throw new s402Error("INVALID_PAYLOAD", `prepaid.minDeposit must be a non-negative integer string, got "${obj.minDeposit}"`);
320
320
  assertString(obj, "withdrawalDelayMs", "prepaid");
321
- if (typeof obj.withdrawalDelayMs === "string" && !isValidAmount(obj.withdrawalDelayMs)) throw new s402Error("INVALID_PAYLOAD", `prepaid.withdrawalDelayMs must be a non-negative integer string (milliseconds), got "${obj.withdrawalDelayMs}"`);
321
+ if (typeof obj.withdrawalDelayMs === "string") {
322
+ if (!isValidAmount(obj.withdrawalDelayMs)) throw new s402Error("INVALID_PAYLOAD", `prepaid.withdrawalDelayMs must be a non-negative integer string (milliseconds), got "${obj.withdrawalDelayMs}"`);
323
+ const delayMs = BigInt(obj.withdrawalDelayMs);
324
+ if (delayMs < 60000n || delayMs > 604800000n) throw new s402Error("INVALID_PAYLOAD", `prepaid.withdrawalDelayMs must be between 60000 (1 min) and 604800000 (7 days), got "${obj.withdrawalDelayMs}"`);
325
+ }
322
326
  assertOptionalString(obj, "maxCalls", "prepaid");
323
327
  assertOptionalString(obj, "providerPubkey", "prepaid");
324
328
  assertOptionalString(obj, "disputeWindowMs", "prepaid");
329
+ const hasPubkey = typeof obj.providerPubkey === "string";
330
+ if (hasPubkey !== (typeof obj.disputeWindowMs === "string")) throw new s402Error("INVALID_PAYLOAD", `prepaid: providerPubkey and disputeWindowMs must both be present (v0.2) or both absent (v0.1), got ${hasPubkey ? "providerPubkey only" : "disputeWindowMs only"}`);
325
331
  }
326
332
  /**
327
333
  * Validate all optional sub-objects on a requirements record.
@@ -347,10 +353,11 @@ function validateRequirementsShape(obj) {
347
353
  if (typeof record.amount !== "string") missing.push("amount (string)");
348
354
  else if (!isValidU64Amount(record.amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${record.amount}": must be a non-negative integer string within u64 range`);
349
355
  if (typeof record.payTo !== "string") missing.push("payTo (string)");
350
- else if (!/^0x[0-9a-fA-F]{64}$/.test(record.payTo)) throw new s402Error("INVALID_PAYLOAD", `payTo must be a 32-byte Sui address (0x + 64 hex chars), got "${record.payTo.substring(0, 20)}..."`);
356
+ else if (record.payTo.length === 0) throw new s402Error("INVALID_PAYLOAD", "payTo must be a non-empty string");
351
357
  if (missing.length > 0) throw new s402Error("INVALID_PAYLOAD", `Malformed payment requirements: missing ${missing.join(", ")}`);
352
358
  if (/[\x00-\x1f\x7f]/.test(record.network)) throw new s402Error("INVALID_PAYLOAD", "network contains control characters");
353
359
  if (/[\x00-\x1f\x7f]/.test(record.asset)) throw new s402Error("INVALID_PAYLOAD", "asset contains control characters");
360
+ if (/[\x00-\x1f\x7f]/.test(record.payTo)) throw new s402Error("INVALID_PAYLOAD", "payTo contains control characters");
354
361
  if (Array.isArray(record.accepts) && record.accepts.length === 0) throw new s402Error("INVALID_PAYLOAD", "accepts array must contain at least one scheme");
355
362
  const accepts = record.accepts;
356
363
  for (const scheme of accepts) if (typeof scheme !== "string") throw new s402Error("INVALID_PAYLOAD", `Invalid entry in accepts array: expected string, got ${typeof scheme}`);
@@ -361,7 +368,8 @@ function validateRequirementsShape(obj) {
361
368
  if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt) || record.expiresAt <= 0) throw new s402Error("INVALID_PAYLOAD", `expiresAt must be a positive finite number (Unix timestamp ms), got ${record.expiresAt}`);
362
369
  }
363
370
  if (record.protocolFeeAddress !== void 0) {
364
- if (typeof record.protocolFeeAddress !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(record.protocolFeeAddress)) throw new s402Error("INVALID_PAYLOAD", `protocolFeeAddress must be a 32-byte Sui address (0x + 64 hex chars), got "${String(record.protocolFeeAddress).substring(0, 20)}..."`);
371
+ if (typeof record.protocolFeeAddress !== "string" || record.protocolFeeAddress.length === 0) throw new s402Error("INVALID_PAYLOAD", `protocolFeeAddress must be a non-empty string, got ${JSON.stringify(record.protocolFeeAddress)}`);
372
+ if (/[\x00-\x1f\x7f]/.test(record.protocolFeeAddress)) throw new s402Error("INVALID_PAYLOAD", "protocolFeeAddress contains control characters");
365
373
  }
366
374
  if (record.facilitatorUrl !== void 0) {
367
375
  if (typeof record.facilitatorUrl !== "string") throw new s402Error("INVALID_PAYLOAD", `facilitatorUrl must be a string, got ${typeof record.facilitatorUrl}`);
package/dist/index.d.mts CHANGED
@@ -1,6 +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 { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader, s402Receipt, s402ReceiptSigner, s402ReceiptVerifier } from "./receipts.mjs";
4
5
 
5
6
  //#region src/scheme.d.ts
6
7
  /** Implemented by each scheme on the client side */
@@ -49,14 +50,14 @@ interface s402DirectScheme {
49
50
  interface s402RouteConfig {
50
51
  /** Which payment scheme(s) to accept. Always includes "exact" for x402 compat. */
51
52
  schemes: s402Scheme[];
52
- /** Amount in base units, same as wire format (e.g., "1000000" for 0.001 SUI in MIST) */
53
+ /** Amount in base units, same as wire format (e.g., "1000000") */
53
54
  price: string;
54
- /** Sui network */
55
+ /** Network identifier (e.g., "sui:mainnet", "solana:mainnet-beta") */
55
56
  network: string;
56
- /** Recipient address */
57
+ /** Recipient address (chain-specific format, validated by chain adapter) */
57
58
  payTo: string;
58
- /** Move coin type */
59
- asset?: string;
59
+ /** Asset/coin type identifier (chain-specific, e.g., Sui Move type or Solana mint address) */
60
+ asset: string;
60
61
  /** Facilitator URL (optional for direct settlement) */
61
62
  facilitatorUrl?: string;
62
63
  /** Settlement mode preference */
@@ -89,7 +90,9 @@ interface s402RouteConfig {
89
90
  ratePerCall: string;
90
91
  maxCalls?: string;
91
92
  minDeposit: string;
92
- withdrawalDelayMs: string;
93
+ withdrawalDelayMs: string; /** Provider Ed25519 pubkey (hex). Enables v0.2 signed receipt mode. @since v0.2 */
94
+ providerPubkey?: string; /** Dispute window in ms. Required when providerPubkey is set. @since v0.2 */
95
+ disputeWindowMs?: string;
93
96
  };
94
97
  }
95
98
  //#endregion
@@ -192,4 +195,4 @@ declare class s402ResourceServer {
192
195
  process(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
193
196
  }
194
197
  //#endregion
195
- export { S402_CONTENT_TYPE, S402_HEADERS, S402_VERSION, createS402Error, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, s402Client, type s402ClientScheme, type s402DirectScheme, type s402Discovery, s402Error, s402ErrorCode, type s402ErrorCodeType, type s402ErrorInfo, type s402EscrowExtra, type s402EscrowPayload, type s402ExactPayload, s402Facilitator, type s402FacilitatorScheme, type s402Mandate, type s402MandateRequirements, type s402PaymentPayload, type s402PaymentPayloadBase, type s402PaymentRequirements, type s402PaymentSession, type s402PrepaidExtra, type s402PrepaidPayload, type s402RegistryQuery, s402ResourceServer, type s402RouteConfig, type s402Scheme, type s402ServerScheme, type s402ServiceEntry, type s402SettleResponse, type s402SettlementMode, type s402StreamExtra, type s402StreamPayload, type s402UnlockExtra, type s402UnlockPayload, type s402VerifyResponse, validateRequirementsShape };
198
+ export { S402_CONTENT_TYPE, S402_HEADERS, S402_RECEIPT_HEADER, S402_VERSION, createS402Error, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, formatReceiptHeader, isValidAmount, isValidU64Amount, parseReceiptHeader, s402Client, type s402ClientScheme, type s402DirectScheme, type s402Discovery, s402Error, s402ErrorCode, type s402ErrorCodeType, type s402ErrorInfo, type s402EscrowExtra, type s402EscrowPayload, type s402ExactPayload, s402Facilitator, type s402FacilitatorScheme, type s402Mandate, type s402MandateRequirements, type s402PaymentPayload, type s402PaymentPayloadBase, type s402PaymentRequirements, type s402PaymentSession, type s402PrepaidExtra, type s402PrepaidPayload, type s402Receipt, type s402ReceiptSigner, type s402ReceiptVerifier, type s402RegistryQuery, s402ResourceServer, type s402RouteConfig, type s402Scheme, type s402ServerScheme, type s402ServiceEntry, type s402SettleResponse, type s402SettlementMode, type s402StreamExtra, type s402StreamPayload, type s402UnlockExtra, type s402UnlockPayload, type s402VerifyResponse, validateRequirementsShape };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { S402_HEADERS, S402_VERSION } from "./types.mjs";
2
2
  import { createS402Error, s402Error, s402ErrorCode } from "./errors.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 { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader } from "./receipts.mjs";
4
5
 
5
6
  //#region src/client.ts
6
7
  var s402Client = class {
@@ -81,7 +82,7 @@ var s402ResourceServer = class {
81
82
  s402Version: S402_VERSION,
82
83
  accepts: [...new Set([...config.schemes, "exact"])],
83
84
  network: config.network,
84
- asset: config.asset ?? "0x2::sui::SUI",
85
+ asset: config.asset,
85
86
  amount: config.price,
86
87
  payTo: config.payTo,
87
88
  facilitatorUrl: config.facilitatorUrl,
@@ -317,4 +318,4 @@ var s402Facilitator = class {
317
318
  };
318
319
 
319
320
  //#endregion
320
- export { S402_CONTENT_TYPE, S402_HEADERS, S402_VERSION, createS402Error, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, s402Client, s402Error, s402ErrorCode, s402Facilitator, s402ResourceServer, validateRequirementsShape };
321
+ export { S402_CONTENT_TYPE, S402_HEADERS, S402_RECEIPT_HEADER, S402_VERSION, createS402Error, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, formatReceiptHeader, isValidAmount, isValidU64Amount, parseReceiptHeader, s402Client, s402Error, s402ErrorCode, s402Facilitator, s402ResourceServer, validateRequirementsShape };
@@ -0,0 +1,61 @@
1
+ //#region src/receipts.d.ts
2
+ /**
3
+ * s402 Receipt HTTP Helpers — chain-agnostic receipt header format/parse.
4
+ *
5
+ * Providers sign each API response with Ed25519. The signature, call number,
6
+ * timestamp, and response hash are transported as a single HTTP header:
7
+ *
8
+ * X-S402-Receipt: v2:base64(signature):callNumber:timestampMs:base64(responseHash)
9
+ *
10
+ * This module handles the header wire format ONLY. It does not:
11
+ * - Construct BCS messages (chain-specific — see @sweefi/sui receipts.ts)
12
+ * - Generate Ed25519 keys (implementations provide their own)
13
+ * - Accumulate receipts (client-side concern — see @sweefi/sui ReceiptAccumulator)
14
+ *
15
+ * Zero runtime dependencies. Uses built-in btoa/atob.
16
+ *
17
+ * @see docs/schemes/prepaid.md — v0.2 spec
18
+ */
19
+ /** Receipt data extracted from an HTTP header. */
20
+ interface s402Receipt {
21
+ /** Header version — always 'v2' for signed receipts. */
22
+ version: 'v2';
23
+ /** 64-byte Ed25519 signature over the BCS-encoded receipt message. */
24
+ signature: Uint8Array;
25
+ /** Sequential call number (1-indexed). */
26
+ callNumber: bigint;
27
+ /** Unix timestamp in milliseconds when the response was generated. */
28
+ timestampMs: bigint;
29
+ /** Hash of the response body (typically SHA-256, 32 bytes). */
30
+ responseHash: Uint8Array;
31
+ }
32
+ /** Signing function — implementations inject their own Ed25519 signer. */
33
+ type s402ReceiptSigner = (message: Uint8Array) => Promise<Uint8Array>;
34
+ /** Verification function — implementations inject their own Ed25519 verifier. */
35
+ type s402ReceiptVerifier = (message: Uint8Array, signature: Uint8Array) => Promise<boolean>;
36
+ /** HTTP header name for s402 signed usage receipts. */
37
+ declare const S402_RECEIPT_HEADER = "X-S402-Receipt";
38
+ /**
39
+ * Encode receipt fields into the `X-S402-Receipt` HTTP header value.
40
+ *
41
+ * Format: `v2:base64(signature):callNumber:timestampMs:base64(responseHash)`
42
+ *
43
+ * @param receipt - Receipt fields to encode
44
+ * @returns Header string ready for HTTP response
45
+ */
46
+ declare function formatReceiptHeader(receipt: {
47
+ signature: Uint8Array;
48
+ callNumber: bigint;
49
+ timestampMs: bigint;
50
+ responseHash: Uint8Array;
51
+ }): string;
52
+ /**
53
+ * Decode an `X-S402-Receipt` header value back into typed fields.
54
+ *
55
+ * @param header - Raw header value string
56
+ * @returns Parsed receipt with typed fields
57
+ * @throws On empty string, wrong number of parts, or unknown version
58
+ */
59
+ declare function parseReceiptHeader(header: string): s402Receipt;
60
+ //#endregion
61
+ export { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader, s402Receipt, s402ReceiptSigner, s402ReceiptVerifier };
@@ -0,0 +1,62 @@
1
+ //#region src/receipts.ts
2
+ /** HTTP header name for s402 signed usage receipts. */
3
+ const S402_RECEIPT_HEADER = "X-S402-Receipt";
4
+ const HEADER_VERSION = "v2";
5
+ /** Encode a Uint8Array to base64 using built-in btoa. */
6
+ function uint8ToBase64(bytes) {
7
+ let binary = "";
8
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
9
+ return btoa(binary);
10
+ }
11
+ /** Decode a base64 string to Uint8Array using built-in atob. */
12
+ function base64ToUint8(b64) {
13
+ const binary = atob(b64);
14
+ const bytes = new Uint8Array(binary.length);
15
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
16
+ return bytes;
17
+ }
18
+ /**
19
+ * Encode receipt fields into the `X-S402-Receipt` HTTP header value.
20
+ *
21
+ * Format: `v2:base64(signature):callNumber:timestampMs:base64(responseHash)`
22
+ *
23
+ * @param receipt - Receipt fields to encode
24
+ * @returns Header string ready for HTTP response
25
+ */
26
+ function formatReceiptHeader(receipt) {
27
+ return [
28
+ HEADER_VERSION,
29
+ uint8ToBase64(receipt.signature),
30
+ receipt.callNumber.toString(),
31
+ receipt.timestampMs.toString(),
32
+ uint8ToBase64(receipt.responseHash)
33
+ ].join(":");
34
+ }
35
+ /**
36
+ * Decode an `X-S402-Receipt` header value back into typed fields.
37
+ *
38
+ * @param header - Raw header value string
39
+ * @returns Parsed receipt with typed fields
40
+ * @throws On empty string, wrong number of parts, or unknown version
41
+ */
42
+ function parseReceiptHeader(header) {
43
+ if (!header) throw new Error("Empty receipt header");
44
+ const parts = header.split(":");
45
+ if (parts.length !== 5) throw new Error(`Malformed receipt header: expected 5 colon-separated parts, got ${parts.length}`);
46
+ const [version, sigB64, callNumberStr, timestampMsStr, hashB64] = parts;
47
+ if (version !== HEADER_VERSION) throw new Error(`Unknown receipt header version: "${version}" (expected "${HEADER_VERSION}")`);
48
+ const callNumber = BigInt(callNumberStr);
49
+ const timestampMs = BigInt(timestampMsStr);
50
+ if (callNumber <= 0n) throw new Error(`Invalid receipt callNumber: must be positive, got ${callNumber}`);
51
+ if (timestampMs <= 0n) throw new Error(`Invalid receipt timestampMs: must be positive, got ${timestampMs}`);
52
+ return {
53
+ version: "v2",
54
+ signature: base64ToUint8(sigB64),
55
+ callNumber,
56
+ timestampMs,
57
+ responseHash: base64ToUint8(hashB64)
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ export { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader };
package/dist/types.d.mts CHANGED
@@ -16,11 +16,11 @@ interface s402PaymentRequirements {
16
16
  s402Version: typeof S402_VERSION;
17
17
  /** Which payment schemes the server accepts. Always includes "exact" for x402 compat. */
18
18
  accepts: s402Scheme[];
19
- /** Sui network (e.g., "sui:testnet", "sui:mainnet") */
19
+ /** Network identifier (e.g., "sui:mainnet", "solana:mainnet-beta", "eip155:8453") */
20
20
  network: string;
21
- /** Move coin type (e.g., "0x2::sui::SUI") */
21
+ /** Asset/coin type identifier (chain-specific format, opaque to s402 core) */
22
22
  asset: string;
23
- /** Amount in base units (MIST for SUI, 6-decimal for USDC) */
23
+ /** Amount in base units as a non-negative integer string */
24
24
  amount: string;
25
25
  /** Recipient address */
26
26
  payTo: string;
@@ -115,6 +115,11 @@ interface s402UnlockExtra {
115
115
  * Move cannot verify calls actually happened. Agent's protection: rate cap,
116
116
  * max_calls, deposit ceiling, small deposits + short refill cycles, reputation.
117
117
  * v0.2 adds signed usage receipts for cryptographic fraud proofs. See ADR-007.
118
+ *
119
+ * PAIRING INVARIANT: `providerPubkey` and `disputeWindowMs` are a pair.
120
+ * Both must be present (v0.2 signed receipt mode) or both absent (v0.1 default).
121
+ * Setting `providerPubkey` without `disputeWindowMs` (or vice versa) is invalid
122
+ * and will be rejected by the on-chain contract at deposit time.
118
123
  */
119
124
  interface s402PrepaidExtra {
120
125
  /** Maximum base units per API call (rate cap) */
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "s402",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "description": "s402 — Sui-native HTTP 402 wire format. Types, HTTP encoding, and scheme registry for five payment schemes. Wire-compatible with x402. Zero runtime dependencies.",
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",
7
7
  "author": "SweeInc <daniel@sweeinc.com> (https://s402-protocol.org)",
8
8
  "repository": {
@@ -76,24 +76,30 @@
76
76
  "default": "./dist/errors.mjs"
77
77
  },
78
78
  "default": "./dist/errors.mjs"
79
+ },
80
+ "./receipts": {
81
+ "import": {
82
+ "types": "./dist/receipts.d.mts",
83
+ "default": "./dist/receipts.mjs"
84
+ },
85
+ "default": "./dist/receipts.mjs"
79
86
  }
80
87
  },
88
+ "devDependencies": {
89
+ "@vitest/coverage-v8": "^3.2.4",
90
+ "fast-check": "^4.5.3",
91
+ "tsdown": "^0.20.3",
92
+ "typescript": "^5.7.0",
93
+ "vitepress": "^1.6.4",
94
+ "vitest": "^3.0.5"
95
+ },
81
96
  "scripts": {
82
97
  "build": "tsdown",
83
98
  "typecheck": "tsc --noEmit",
84
99
  "test": "vitest run",
85
100
  "test:watch": "vitest",
86
- "prepublishOnly": "npm run build && npm run typecheck && npm run test",
87
101
  "docs:dev": "vitepress dev docs",
88
102
  "docs:build": "vitepress build docs",
89
103
  "docs:preview": "vitepress preview docs"
90
- },
91
- "devDependencies": {
92
- "@vitest/coverage-v8": "^3.2.4",
93
- "fast-check": "^4.5.3",
94
- "tsdown": "^0.20.3",
95
- "typescript": "^5.7.0",
96
- "vitepress": "^1.6.4",
97
- "vitest": "^3.0.5"
98
104
  }
99
- }
105
+ }