s402 0.2.3 → 0.3.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,35 @@ 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.3.0] - 2026-04-11
9
+
10
+ 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.
11
+
12
+ ### Added
13
+
14
+ - **`verifySettlement` — client-side causal-binding check (S8 Facilitator Accountability).** New optional method on `s402ClientScheme`. For all client-signed schemes (`exact`, `stream`, `escrow`, `unlock` TX1), this is a **local, offline comparison**: derive the expected transaction digest from the signed BCS bytes and compare to `SettleResponse.txDigest`. No RPC call required. Closes the causal-binding hole where a malicious facilitator could substitute an unrelated-but-real transaction digest — that digest would correspond to different signed bytes the client never produced, and the check would reject it. Interface-only in this release; concrete implementations land in `@sweefi/sui` per ADR-002. See `typescript/src/scheme.ts` and `INVARIANTS.md` § S8 for the full contract and copy-paste implementation template.
15
+ - **`s402SettlementVerification` type** — return shape for `verifySettlement`: `{ verified, expectedDigest, actualDigest, reason? }`.
16
+ - **`DIGEST_MISMATCH` error code** — `retryable: false`, with a `suggestedAction` warning callers NOT to retry on mismatch. Retrying is dangerous: the signed bytes may have already landed on-chain under the *expected* digest (the facilitator may simply be lying about what it broadcast), and a fresh retry would double-pay. The correct failure mode is to mark the payment as non-settled and stop trusting the facilitator. See `typescript/src/errors.ts`.
17
+ - **S8. Facilitator Accountability** — first-class safety invariant alongside S1–S7. Full statement, formal proof for the `exact` scheme on Sui (by blake2b-256 collision resistance), per-scheme scope table, and a copy-paste implementation template for downstream Sui adapters now live in `INVARIANTS.md` § S8. The Allium behavioral spec is at `spec/allium/s8-facilitator-accountability.allium`.
18
+ - **ADR-001 — Protocol Boundaries.** Documents four decisions from the scale-fragility council: (1) facilitator trust boundary sealed by client-side digest verification, (2) receipt cardinality is a non-guarantee at the protocol layer, (3) scheme cap at five with burden-of-proof for any new scheme, (4) extension hygiene rules. See `docs/adr/001-protocol-boundaries.md`.
19
+ - **ADR-002 — s402 is a pure protocol repo.** Decides that this repo contains NO chain-specific code at any path — not in `typescript/src/`, not in a sibling package, not anywhere. All Sui-specific implementation moves to SweeFi. Corollary: the S7 chain-agnostic boundary is now enforced repo-wide, not just inside `src/`. See `docs/adr/002-s402-is-pure-protocol.md`.
20
+
21
+ ### Removed
22
+
23
+ - **`mcp-server/` directory deleted.** The Sui-specific MCP server that previously shipped in this repo has been relocated to `@sweefi/mcp` (canonical implementation in the SweeFi repo) per ADR-002. **This does not affect the npm `s402` package** — `mcp-server` was a separate consumer of this package, not part of it. Users who were installing from the repo directly should migrate to `npx @sweefi/mcp`; a forthcoming `npm deprecate s402-mcp` will redirect the legacy standalone package to the new name.
24
+
25
+ ### Changed
26
+
27
+ - **S7 scope strengthened to repo-level.** `INVARIANTS.md` § S7 scope note now reads: "chain-specific code lives in downstream implementation repos (e.g. `@sweefi/sui` for the Sui implementation) which consume this package from npm and add chain validation on top. Per ADR-002, the s402 repo itself contains NO chain-specific imports at the repo level — the protocol-pure boundary is enforced repo-wide, not just inside `src/`."
28
+ - **`INVARIANTS.md` Sui references rewritten as downstream pointers.** Prior revisions of the S8 proof block referenced a reference implementation at `mcp-server/src/sui-exact.ts`. That path no longer exists; the proof block now points at `sweefi/packages/sui/src/s402/exact/client.ts` as the canonical Sui adapter per ADR-002.
29
+ - **Demo API distribution surfaces** (`demo-api/public/index.html` served at `demo.s402-protocol.org`, and `demo-api/src/server.ts`) now reference `@sweefi/mcp` with the correct `SUI_PRIVATE_KEY` / `SUI_NETWORK` environment variables, matching SweeFi's documented `mcpServers` config shape. Outside the npm package scope but noted here for consumers browsing the monorepo.
30
+
31
+ ### Compatibility
32
+
33
+ - **TypeScript type compatibility**: `verifySettlement` is optional on `s402ClientScheme`, and `DIGEST_MISMATCH` is a purely additive enum member. Existing adapter implementations compile unchanged against `^0.3.0`.
34
+ - **Wire format**: unchanged from v0.2.3. The 132 conformance test vectors in `test/conformance/vectors/` still pass byte-for-byte against v0.3.0.
35
+ - **Minor-bump rationale**: the 0.2.3 → 0.3.0 jump reflects the semantic significance of adding a new safety invariant (S8) and the repo-level architectural decisions (ADR-001/002), not a breaking wire-format change. Under semver 0.x, minor bumps are treated as breaking by `^0.x.y` ranges — consumers should expect to opt-in explicitly.
36
+
8
37
  ## [0.2.1] - 2026-03-02
9
38
 
10
39
  ### Added
@@ -129,6 +158,7 @@ _Version bump for npm publish after license change._
129
158
  - Property-based fuzz testing via fast-check
130
159
  - 207 tests, zero runtime dependencies
131
160
 
161
+ [0.3.0]: https://github.com/s402-protocol/core/compare/v0.2.3...v0.3.0
132
162
  [0.2.1]: https://github.com/s402-protocol/core/compare/v0.2.0...v0.2.1
133
163
  [0.2.0]: https://github.com/s402-protocol/core/compare/v0.1.8...v0.2.0
134
164
  [0.1.8]: https://github.com/s402-protocol/core/compare/v0.1.7...v0.1.8
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  [![CI](https://github.com/s402-protocol/core/actions/workflows/ci.yml/badge.svg)](https://github.com/s402-protocol/core/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/s402.svg)](https://www.npmjs.com/package/s402)
5
5
 
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.
6
+ **Chain-agnostic HTTP 402 protocol.** Five payment schemes for AI agent commerce. Wire-compatible with x402. Zero runtime dependencies. 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 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.
8
+ s402 is a chain-agnostic HTTP 402 wire formattypes, HTTP encoding, scheme registry, and error handling for five payment schemes. The protocol layer contains no chain-specific logic (see [S7 invariant](./AGENTS.md)). The reference implementation on Sui uses 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
package/dist/compat.mjs CHANGED
@@ -15,6 +15,7 @@ function fromX402Requirements(x402) {
15
15
  if (x402.facilitatorUrl !== void 0) try {
16
16
  const url = new URL(x402.facilitatorUrl);
17
17
  if (url.protocol !== "https:" && url.protocol !== "http:") throw new s402Error("INVALID_PAYLOAD", `facilitatorUrl must use https:// or http://, got "${url.protocol}"`);
18
+ if (url.username || url.password) throw new s402Error("INVALID_PAYLOAD", "facilitatorUrl must not contain embedded credentials (user:password@)");
18
19
  } catch (e) {
19
20
  if (e instanceof s402Error) throw e;
20
21
  throw new s402Error("INVALID_PAYLOAD", "facilitatorUrl is not a valid URL");
package/dist/errors.d.mts CHANGED
@@ -23,6 +23,7 @@ declare const s402ErrorCode: {
23
23
  readonly REQUIREMENTS_EXPIRED: "REQUIREMENTS_EXPIRED";
24
24
  readonly VERIFICATION_FAILED: "VERIFICATION_FAILED";
25
25
  readonly SETTLEMENT_FAILED: "SETTLEMENT_FAILED";
26
+ readonly DIGEST_MISMATCH: "DIGEST_MISMATCH";
26
27
  };
27
28
  type s402ErrorCodeType = (typeof s402ErrorCode)[keyof typeof s402ErrorCode];
28
29
  interface s402ErrorInfo {
@@ -34,7 +35,7 @@ interface s402ErrorInfo {
34
35
  /**
35
36
  * Create a typed s402 error with recovery hints.
36
37
  *
37
- * @param code - One of the 15 s402 error codes (e.g. 'SETTLEMENT_FAILED')
38
+ * @param code - One of the 16 s402 error codes (e.g. 'SETTLEMENT_FAILED')
38
39
  * @param message - Optional human-readable message (defaults to the code)
39
40
  * @returns Error info object with code, message, retryable flag, and suggestedAction
40
41
  *
package/dist/errors.mjs CHANGED
@@ -22,7 +22,8 @@ const s402ErrorCode = {
22
22
  SIGNATURE_INVALID: "SIGNATURE_INVALID",
23
23
  REQUIREMENTS_EXPIRED: "REQUIREMENTS_EXPIRED",
24
24
  VERIFICATION_FAILED: "VERIFICATION_FAILED",
25
- SETTLEMENT_FAILED: "SETTLEMENT_FAILED"
25
+ SETTLEMENT_FAILED: "SETTLEMENT_FAILED",
26
+ DIGEST_MISMATCH: "DIGEST_MISMATCH"
26
27
  };
27
28
  /** Error recovery hints for each error code */
28
29
  const ERROR_HINTS = {
@@ -68,7 +69,7 @@ const ERROR_HINTS = {
68
69
  },
69
70
  NETWORK_MISMATCH: {
70
71
  retryable: false,
71
- suggestedAction: "Ensure client and server are on the same Sui network"
72
+ suggestedAction: "Ensure client and server are on the same network"
72
73
  },
73
74
  SIGNATURE_INVALID: {
74
75
  retryable: false,
@@ -85,12 +86,16 @@ const ERROR_HINTS = {
85
86
  SETTLEMENT_FAILED: {
86
87
  retryable: true,
87
88
  suggestedAction: "Transient RPC failure during settlement — retry in a few seconds"
89
+ },
90
+ DIGEST_MISMATCH: {
91
+ retryable: false,
92
+ suggestedAction: "Facilitator returned a transaction digest that does not match the signed payload. Do NOT retry — treat payment as non-settled and investigate the facilitator."
88
93
  }
89
94
  };
90
95
  /**
91
96
  * Create a typed s402 error with recovery hints.
92
97
  *
93
- * @param code - One of the 15 s402 error codes (e.g. 'SETTLEMENT_FAILED')
98
+ * @param code - One of the 16 s402 error codes (e.g. 'SETTLEMENT_FAILED')
94
99
  * @param message - Optional human-readable message (defaults to the code)
95
100
  * @returns Error info object with code, message, retryable flag, and suggestedAction
96
101
  *
package/dist/http.mjs CHANGED
@@ -109,8 +109,8 @@ const S402_SUB_OBJECT_KEYS = {
109
109
  ]),
110
110
  unlock: new Set([
111
111
  "encryptionId",
112
- "walrusBlobId",
113
- "encryptionPackageId"
112
+ "encryptedContentId",
113
+ "encryptionServiceId"
114
114
  ]),
115
115
  prepaid: new Set([
116
116
  "ratePerCall",
@@ -373,8 +373,8 @@ function validateUnlockShape(value) {
373
373
  assertPlainObject(value, "unlock");
374
374
  const obj = value;
375
375
  assertString(obj, "encryptionId", "unlock");
376
- assertString(obj, "walrusBlobId", "unlock");
377
- assertString(obj, "encryptionPackageId", "unlock");
376
+ assertString(obj, "encryptedContentId", "unlock");
377
+ assertString(obj, "encryptionServiceId", "unlock");
378
378
  }
379
379
  /**
380
380
  * Validate prepaid sub-object.
@@ -420,7 +420,7 @@ function validateRequirementsShape(obj) {
420
420
  if (typeof record.network !== "string") missing.push("network (string)");
421
421
  if (typeof record.asset !== "string") missing.push("asset (string)");
422
422
  if (typeof record.amount !== "string") missing.push("amount (string)");
423
- else if (!isValidU64Amount(record.amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${record.amount}": must be a non-negative integer string within u64 range`);
423
+ else if (!isValidAmount(record.amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${record.amount}": must be a non-negative integer string`);
424
424
  if (typeof record.payTo !== "string") missing.push("payTo (string)");
425
425
  else if (record.payTo.length === 0) throw new s402Error("INVALID_PAYLOAD", "payTo must be a non-empty string");
426
426
  if (missing.length > 0) throw new s402Error("INVALID_PAYLOAD", `Malformed payment requirements: missing ${missing.join(", ")}`);
@@ -446,6 +446,7 @@ function validateRequirementsShape(obj) {
446
446
  try {
447
447
  const url = new URL(record.facilitatorUrl);
448
448
  if (url.protocol !== "https:" && url.protocol !== "http:") throw new s402Error("INVALID_PAYLOAD", `facilitatorUrl must use https:// or http://, got "${url.protocol}"`);
449
+ if (url.username || url.password) throw new s402Error("INVALID_PAYLOAD", "facilitatorUrl must not contain embedded credentials (user:password@)");
449
450
  } catch (e) {
450
451
  if (e instanceof s402Error) throw e;
451
452
  throw new s402Error("INVALID_PAYLOAD", "facilitatorUrl is not a valid URL");
package/dist/index.d.mts CHANGED
@@ -1,101 +1,9 @@
1
1
  import { createS402Error, s402Error, s402ErrorCode, s402ErrorCodeType, s402ErrorInfo } from "./errors.mjs";
2
2
  import { S402_HEADERS, S402_VERSION, s402Discovery, s402EscrowExtra, s402EscrowPayload, s402ExactPayload, s402Mandate, s402MandateRequirements, s402PaymentPayload, s402PaymentPayloadBase, s402PaymentRequirements, s402PaymentSession, s402PrepaidExtra, s402PrepaidPayload, s402RegistryQuery, s402Scheme, s402ServiceEntry, s402SettleResponse, s402SettlementMode, s402StreamExtra, s402StreamPayload, s402UnlockExtra, s402UnlockPayload, s402VerifyResponse } from "./types.mjs";
3
3
  import { S402_CONTENT_TYPE, decodePayloadBody, decodePaymentPayload, decodePaymentRequired, decodeRequirementsBody, decodeSettleBody, decodeSettleResponse, detectProtocol, detectTransport, encodePayloadBody, encodePaymentPayload, encodePaymentRequired, encodeRequirementsBody, encodeSettleBody, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, isValidU64Amount, validateRequirementsShape } from "./http.mjs";
4
+ import { a as s402ServerScheme, i as s402RouteConfig, n as s402DirectScheme, o as s402SettlementVerification, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-D7qqwo3Q.mjs";
4
5
  import { S402_RECEIPT_HEADER, formatReceiptHeader, parseReceiptHeader, s402Receipt, s402ReceiptSigner, s402ReceiptVerifier } from "./receipts.mjs";
5
6
 
6
- //#region src/scheme.d.ts
7
- /** Implemented by each scheme on the client side */
8
- interface s402ClientScheme {
9
- /** Which scheme this implements */
10
- readonly scheme: s402Scheme;
11
- /** Create a signed payment payload from server requirements */
12
- createPayment(requirements: s402PaymentRequirements): Promise<s402PaymentPayload>;
13
- }
14
- /** Implemented by each scheme on the server side */
15
- interface s402ServerScheme {
16
- readonly scheme: s402Scheme;
17
- /** Build payment requirements from route config */
18
- buildRequirements(config: s402RouteConfig): s402PaymentRequirements;
19
- }
20
- /**
21
- * Implemented by each scheme in the facilitator.
22
- *
23
- * Critical: each scheme has its OWN verify logic.
24
- * - Exact: signature recovery + dry-run simulation + balance check
25
- * - Stream: stream creation PTB validation + deposit check
26
- * - Escrow: escrow creation PTB validation + arbiter/deadline check
27
- * - Unlock: escrow validation (key release is separate PTB)
28
- */
29
- interface s402FacilitatorScheme {
30
- readonly scheme: s402Scheme;
31
- /** Verify a payment payload without broadcasting */
32
- verify(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402VerifyResponse>;
33
- /** Verify and broadcast the transaction */
34
- settle(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
35
- }
36
- /**
37
- * For self-sovereign agents that hold their own keys.
38
- * Builds, signs, and broadcasts in one step — no facilitator needed.
39
- *
40
- * MUST call waitForTransaction() before returning success.
41
- * Without finality confirmation, server could grant access for a
42
- * transaction that gets reverted.
43
- */
44
- interface s402DirectScheme {
45
- readonly scheme: s402Scheme;
46
- /** Build, sign, broadcast, and wait for finality */
47
- settleDirectly(requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
48
- }
49
- /** Per-route payment configuration for the server middleware */
50
- interface s402RouteConfig {
51
- /** Which payment scheme(s) to accept. Always includes "exact" for x402 compat. */
52
- schemes: s402Scheme[];
53
- /** Amount in base units, same as wire format (e.g., "1000000") */
54
- price: string;
55
- /** Network identifier (e.g., "sui:mainnet", "solana:mainnet-beta") */
56
- network: string;
57
- /** Recipient address (chain-specific format, validated by chain adapter) */
58
- payTo: string;
59
- /** Asset/coin type identifier (chain-specific, e.g., Sui Move type or Solana mint address) */
60
- asset: string;
61
- /** Facilitator URL (optional for direct settlement) */
62
- facilitatorUrl?: string;
63
- /** Settlement mode preference */
64
- settlementMode?: s402SettlementMode;
65
- /** AP2 mandate requirements */
66
- mandate?: {
67
- required: boolean;
68
- minPerTx?: string;
69
- };
70
- /** Protocol fee in basis points */
71
- protocolFeeBps?: number;
72
- /** Require on-chain receipt */
73
- receiptRequired?: boolean;
74
- stream?: {
75
- ratePerSecond: string;
76
- budgetCap: string;
77
- minDeposit: string;
78
- };
79
- escrow?: {
80
- seller: string;
81
- arbiter?: string;
82
- deadlineMs: string;
83
- };
84
- unlock?: {
85
- encryptionId: string;
86
- walrusBlobId: string;
87
- encryptionPackageId: string;
88
- };
89
- prepaid?: {
90
- ratePerCall: string;
91
- maxCalls?: string;
92
- minDeposit: 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;
96
- };
97
- }
98
- //#endregion
99
7
  //#region src/client.d.ts
100
8
  declare class s402Client {
101
9
  private schemes;
@@ -277,4 +185,4 @@ declare class s402ResourceServer {
277
185
  process(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
278
186
  }
279
187
  //#endregion
280
- 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 };
188
+ 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 s402SettlementVerification, type s402StreamExtra, type s402StreamPayload, type s402UnlockExtra, type s402UnlockPayload, type s402VerifyResponse, validateRequirementsShape };
@@ -0,0 +1,136 @@
1
+ import { s402PaymentPayload, s402PaymentRequirements, s402Scheme, s402SettleResponse, s402SettlementMode, s402VerifyResponse } from "./types.mjs";
2
+
3
+ //#region src/scheme.d.ts
4
+ /**
5
+ * Result of client-side settlement verification.
6
+ *
7
+ * See `s402ClientScheme.verifySettlement` — this is the outcome of the
8
+ * client independently checking that the facilitator's returned digest
9
+ * is causally bound to the signed payload the client actually sent.
10
+ */
11
+ interface s402SettlementVerification {
12
+ /** True iff the facilitator's digest matches the one derived from the signed payload bytes. */
13
+ verified: boolean;
14
+ /** The digest the client computed locally from its own signed bytes. */
15
+ expectedDigest: string;
16
+ /** The digest the facilitator returned (copied from SettleResponse). */
17
+ actualDigest: string | null;
18
+ /**
19
+ * Human-readable reason when `verified` is false. Present only on mismatch,
20
+ * unknown-digest, or when the scheme cannot verify the settlement locally.
21
+ */
22
+ reason?: string;
23
+ }
24
+ /** Implemented by each scheme on the client side */
25
+ interface s402ClientScheme {
26
+ /** Which scheme this implements */
27
+ readonly scheme: s402Scheme;
28
+ /** Create a signed payment payload from server requirements */
29
+ createPayment(requirements: s402PaymentRequirements): Promise<s402PaymentPayload>;
30
+ /**
31
+ * Verify that the facilitator's `SettleResponse` is causally bound to the
32
+ * signed payload the client actually sent.
33
+ *
34
+ * For schemes where the client signs the full transaction before sending
35
+ * (exact, stream, escrow, unlock-TX1), this is a **local, offline check**:
36
+ * derive the expected tx digest from the signed bytes and compare it to the
37
+ * digest the facilitator returned. No RPC call required. This closes the
38
+ * causal-binding hole identified in the April 2026 scale-fragility review
39
+ * (see `knowledge/scale-fragility-council-v03.md`): a malicious facilitator
40
+ * cannot substitute an unrelated-but-real tx digest, because that other
41
+ * digest would correspond to different signed bytes the client never
42
+ * produced.
43
+ *
44
+ * Optional for backward-compatibility. Schemes that cannot verify locally
45
+ * (e.g. unlock-TX2, which is facilitator-constructed) should return
46
+ * `{ verified: false, reason: 'scheme does not support local verification' }`
47
+ * and rely on other attestation mechanisms.
48
+ */
49
+ verifySettlement?(payload: s402PaymentPayload, settleResponse: s402SettleResponse): s402SettlementVerification;
50
+ }
51
+ /** Implemented by each scheme on the server side */
52
+ interface s402ServerScheme {
53
+ readonly scheme: s402Scheme;
54
+ /** Build payment requirements from route config */
55
+ buildRequirements(config: s402RouteConfig): s402PaymentRequirements;
56
+ }
57
+ /**
58
+ * Implemented by each scheme in the facilitator.
59
+ *
60
+ * Critical: each scheme has its OWN verify logic.
61
+ * - Exact: signature recovery + dry-run simulation + balance check
62
+ * - Stream: stream creation PTB validation + deposit check
63
+ * - Escrow: escrow creation PTB validation + arbiter/deadline check
64
+ * - Unlock: escrow validation (key release is separate PTB)
65
+ */
66
+ interface s402FacilitatorScheme {
67
+ readonly scheme: s402Scheme;
68
+ /** Verify a payment payload without broadcasting */
69
+ verify(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402VerifyResponse>;
70
+ /** Verify and broadcast the transaction */
71
+ settle(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
72
+ }
73
+ /**
74
+ * For self-sovereign agents that hold their own keys.
75
+ * Builds, signs, and broadcasts in one step — no facilitator needed.
76
+ *
77
+ * MUST call waitForTransaction() before returning success.
78
+ * Without finality confirmation, server could grant access for a
79
+ * transaction that gets reverted.
80
+ */
81
+ interface s402DirectScheme {
82
+ readonly scheme: s402Scheme;
83
+ /** Build, sign, broadcast, and wait for finality */
84
+ settleDirectly(requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
85
+ }
86
+ /** Per-route payment configuration for the server middleware */
87
+ interface s402RouteConfig {
88
+ /** Which payment scheme(s) to accept. Always includes "exact" for x402 compat. */
89
+ schemes: s402Scheme[];
90
+ /** Amount in base units, same as wire format (e.g., "1000000") */
91
+ price: string;
92
+ /** Network identifier (e.g., "sui:mainnet", "solana:mainnet-beta") */
93
+ network: string;
94
+ /** Recipient address (chain-specific format, validated by chain adapter) */
95
+ payTo: string;
96
+ /** Asset/coin type identifier (chain-specific, e.g., Sui Move type or Solana mint address) */
97
+ asset: string;
98
+ /** Facilitator URL (optional for direct settlement) */
99
+ facilitatorUrl?: string;
100
+ /** Settlement mode preference */
101
+ settlementMode?: s402SettlementMode;
102
+ /** AP2 mandate requirements */
103
+ mandate?: {
104
+ required: boolean;
105
+ minPerTx?: string;
106
+ };
107
+ /** Protocol fee in basis points */
108
+ protocolFeeBps?: number;
109
+ /** Require on-chain receipt */
110
+ receiptRequired?: boolean;
111
+ stream?: {
112
+ ratePerSecond: string;
113
+ budgetCap: string;
114
+ minDeposit: string;
115
+ };
116
+ escrow?: {
117
+ seller: string;
118
+ arbiter?: string;
119
+ deadlineMs: string;
120
+ };
121
+ unlock?: {
122
+ encryptionId: string;
123
+ encryptedContentId: string;
124
+ encryptionServiceId: string;
125
+ };
126
+ prepaid?: {
127
+ ratePerCall: string;
128
+ maxCalls?: string;
129
+ minDeposit: string;
130
+ withdrawalDelayMs: string; /** Provider Ed25519 pubkey (hex). Enables v0.2 signed receipt mode. @since v0.2 */
131
+ providerPubkey?: string; /** Dispute window in ms. Required when providerPubkey is set. @since v0.2 */
132
+ disputeWindowMs?: string;
133
+ };
134
+ }
135
+ //#endregion
136
+ export { s402ServerScheme as a, s402RouteConfig as i, s402DirectScheme as n, s402SettlementVerification as o, s402FacilitatorScheme as r, s402ClientScheme as t };
@@ -0,0 +1,74 @@
1
+ import { s402PaymentPayload, s402PaymentRequirements, s402SettleResponse, s402VerifyResponse } from "./types.mjs";
2
+ import { a as s402ServerScheme, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-D7qqwo3Q.mjs";
3
+
4
+ //#region src/test-utils.d.ts
5
+ /**
6
+ * Create a mock client scheme for the `exact` payment type.
7
+ *
8
+ * Produces payloads with deterministic transaction/signature strings
9
+ * derived from the requirements (amount + payTo). Useful for testing
10
+ * the client→server→facilitator flow without real keys.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const client = new s402Client();
15
+ * client.register('sui:testnet', mockExactClientScheme());
16
+ *
17
+ * const payload = await client.createPayment(requirements);
18
+ * // payload.payload.transaction === 'mock-pay-1000000-to-0xabc...'
19
+ * ```
20
+ */
21
+ declare function mockExactClientScheme(): s402ClientScheme;
22
+ /**
23
+ * Create a mock facilitator scheme for the `exact` payment type.
24
+ *
25
+ * Verifies that the payload's transaction string matches the expected
26
+ * format from `mockExactClientScheme()`. Settle always succeeds with
27
+ * a deterministic digest.
28
+ *
29
+ * Pair this with `mockExactClientScheme()` for end-to-end testing.
30
+ *
31
+ * @param options.txDigest - Custom transaction digest (default: 'mock-tx-digest')
32
+ * @param options.finalityMs - Custom finality time (default: 400)
33
+ * @param options.verifyFn - Override the verify function for custom behavior
34
+ * @param options.settleFn - Override the settle function for custom behavior
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const facilitator = new s402Facilitator();
39
+ * facilitator.register('sui:testnet', mockExactFacilitatorScheme());
40
+ *
41
+ * const result = await facilitator.process(payload, requirements);
42
+ * // result.success === true
43
+ * // result.txDigest === 'mock-tx-digest'
44
+ * ```
45
+ */
46
+ declare function mockExactFacilitatorScheme(options?: {
47
+ txDigest?: string;
48
+ finalityMs?: number;
49
+ verifyFn?: (payload: s402PaymentPayload, requirements: s402PaymentRequirements) => Promise<s402VerifyResponse>;
50
+ settleFn?: (payload: s402PaymentPayload, requirements: s402PaymentRequirements) => Promise<s402SettleResponse>;
51
+ }): s402FacilitatorScheme;
52
+ /**
53
+ * Create a mock server scheme for the `exact` payment type.
54
+ *
55
+ * Builds payment requirements from a route config. Useful for testing
56
+ * server middleware without chain-specific adapters.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const server = new s402ResourceServer();
61
+ * server.register('sui:testnet', mockExactServerScheme());
62
+ *
63
+ * const requirements = server.buildRequirements({
64
+ * schemes: ['exact'],
65
+ * price: '1000000',
66
+ * network: 'sui:testnet',
67
+ * payTo: '0xabc...',
68
+ * asset: 'SUI',
69
+ * });
70
+ * ```
71
+ */
72
+ declare function mockExactServerScheme(): s402ServerScheme;
73
+ //#endregion
74
+ export { mockExactClientScheme, mockExactFacilitatorScheme, mockExactServerScheme };
@@ -0,0 +1,132 @@
1
+ import { S402_VERSION } from "./types.mjs";
2
+
3
+ //#region src/test-utils.ts
4
+ /**
5
+ * Create a mock client scheme for the `exact` payment type.
6
+ *
7
+ * Produces payloads with deterministic transaction/signature strings
8
+ * derived from the requirements (amount + payTo). Useful for testing
9
+ * the client→server→facilitator flow without real keys.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const client = new s402Client();
14
+ * client.register('sui:testnet', mockExactClientScheme());
15
+ *
16
+ * const payload = await client.createPayment(requirements);
17
+ * // payload.payload.transaction === 'mock-pay-1000000-to-0xabc...'
18
+ * ```
19
+ */
20
+ function mockExactClientScheme() {
21
+ return {
22
+ scheme: "exact",
23
+ async createPayment(requirements) {
24
+ return {
25
+ s402Version: S402_VERSION,
26
+ scheme: "exact",
27
+ payload: {
28
+ transaction: `mock-pay-${requirements.amount}-to-${requirements.payTo}`,
29
+ signature: "mock-signature"
30
+ }
31
+ };
32
+ }
33
+ };
34
+ }
35
+ /**
36
+ * Create a mock facilitator scheme for the `exact` payment type.
37
+ *
38
+ * Verifies that the payload's transaction string matches the expected
39
+ * format from `mockExactClientScheme()`. Settle always succeeds with
40
+ * a deterministic digest.
41
+ *
42
+ * Pair this with `mockExactClientScheme()` for end-to-end testing.
43
+ *
44
+ * @param options.txDigest - Custom transaction digest (default: 'mock-tx-digest')
45
+ * @param options.finalityMs - Custom finality time (default: 400)
46
+ * @param options.verifyFn - Override the verify function for custom behavior
47
+ * @param options.settleFn - Override the settle function for custom behavior
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const facilitator = new s402Facilitator();
52
+ * facilitator.register('sui:testnet', mockExactFacilitatorScheme());
53
+ *
54
+ * const result = await facilitator.process(payload, requirements);
55
+ * // result.success === true
56
+ * // result.txDigest === 'mock-tx-digest'
57
+ * ```
58
+ */
59
+ function mockExactFacilitatorScheme(options) {
60
+ const txDigest = options?.txDigest ?? "mock-tx-digest";
61
+ const finalityMs = options?.finalityMs ?? 400;
62
+ return {
63
+ scheme: "exact",
64
+ async verify(payload, requirements) {
65
+ if (options?.verifyFn) return options.verifyFn(payload, requirements);
66
+ if (payload.scheme !== "exact") return {
67
+ valid: false,
68
+ invalidReason: `Expected exact scheme, got ${payload.scheme}`
69
+ };
70
+ const exact = payload;
71
+ const expectedTx = `mock-pay-${requirements.amount}-to-${requirements.payTo}`;
72
+ if (exact.payload.transaction !== expectedTx) return {
73
+ valid: false,
74
+ invalidReason: "Transaction does not match expected mock format"
75
+ };
76
+ return {
77
+ valid: true,
78
+ payerAddress: "0xmock-payer"
79
+ };
80
+ },
81
+ async settle(payload, requirements) {
82
+ if (options?.settleFn) return options.settleFn(payload, requirements);
83
+ return {
84
+ success: true,
85
+ txDigest,
86
+ finalityMs
87
+ };
88
+ }
89
+ };
90
+ }
91
+ /**
92
+ * Create a mock server scheme for the `exact` payment type.
93
+ *
94
+ * Builds payment requirements from a route config. Useful for testing
95
+ * server middleware without chain-specific adapters.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const server = new s402ResourceServer();
100
+ * server.register('sui:testnet', mockExactServerScheme());
101
+ *
102
+ * const requirements = server.buildRequirements({
103
+ * schemes: ['exact'],
104
+ * price: '1000000',
105
+ * network: 'sui:testnet',
106
+ * payTo: '0xabc...',
107
+ * asset: 'SUI',
108
+ * });
109
+ * ```
110
+ */
111
+ function mockExactServerScheme() {
112
+ return {
113
+ scheme: "exact",
114
+ buildRequirements(config) {
115
+ return {
116
+ s402Version: S402_VERSION,
117
+ accepts: [...new Set([...config.schemes, "exact"])],
118
+ network: config.network,
119
+ asset: config.asset,
120
+ amount: config.price,
121
+ payTo: config.payTo,
122
+ facilitatorUrl: config.facilitatorUrl,
123
+ protocolFeeBps: config.protocolFeeBps,
124
+ receiptRequired: config.receiptRequired,
125
+ settlementMode: config.settlementMode
126
+ };
127
+ }
128
+ };
129
+ }
130
+
131
+ //#endregion
132
+ export { mockExactClientScheme, mockExactFacilitatorScheme, mockExactServerScheme };
package/dist/types.d.mts CHANGED
@@ -24,7 +24,15 @@ interface s402PaymentRequirements {
24
24
  amount: string;
25
25
  /** Recipient address */
26
26
  payTo: string;
27
- /** Facilitator URL (optional for direct settlement) */
27
+ /**
28
+ * Facilitator URL (optional for direct settlement).
29
+ *
30
+ * Validated at the wire layer for protocol (`https:` or `http:` only) and
31
+ * embedded credentials (rejected). Consumers that fetch this URL MUST apply
32
+ * their own hostname/IP restrictions to prevent SSRF (block RFC 1918 private
33
+ * addresses, link-local 169.254.x.x, loopback, cloud metadata endpoints).
34
+ * DNS-based SSRF cannot be caught at URL parse time.
35
+ */
28
36
  facilitatorUrl?: string;
29
37
  /** AP2 mandate requirements (if agent spending authorization is needed) */
30
38
  mandate?: s402MandateRequirements;
@@ -99,10 +107,10 @@ interface s402EscrowExtra {
99
107
  interface s402UnlockExtra {
100
108
  /** Encryption ID for key servers */
101
109
  encryptionId: string;
102
- /** Walrus blob ID containing the encrypted content */
103
- walrusBlobId: string;
104
- /** Encryption package ID on Sui */
105
- encryptionPackageId: string;
110
+ /** Content identifier for the encrypted blob (e.g., Walrus blob ID, IPFS CID) */
111
+ encryptedContentId: string;
112
+ /** Identifier for the encryption service or module (e.g., Sui package ID, EVM contract address) */
113
+ encryptionServiceId: string;
106
114
  }
107
115
  /**
108
116
  * Prepaid-specific requirements.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s402",
3
- "version": "0.2.3",
3
+ "version": "0.3.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",
@@ -88,6 +88,13 @@
88
88
  "default": "./dist/receipts.mjs"
89
89
  },
90
90
  "default": "./dist/receipts.mjs"
91
+ },
92
+ "./test-utils": {
93
+ "import": {
94
+ "types": "./dist/test-utils.d.mts",
95
+ "default": "./dist/test-utils.mjs"
96
+ },
97
+ "default": "./dist/test-utils.mjs"
91
98
  }
92
99
  },
93
100
  "devDependencies": {
@@ -62,7 +62,7 @@
62
62
  {
63
63
  "description": "Decode unlock scheme",
64
64
  "input": {
65
- "header": "eyJzNDAyVmVyc2lvbiI6IjEiLCJhY2NlcHRzIjpbInVubG9jayJdLCJuZXR3b3JrIjoic3VpOm1haW5uZXQiLCJhc3NldCI6IjB4Mjo6c3VpOjpTVUkiLCJhbW91bnQiOiIxMDAwMDAwIiwicGF5VG8iOiIweGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTAiLCJ1bmxvY2siOnsiZW5jcnlwdGlvbklkIjoiZW5jLWFiYy0xMjMiLCJ3YWxydXNCbG9iSWQiOiJibG9iLXh5ei03ODkiLCJlbmNyeXB0aW9uUGFja2FnZUlkIjoiMHhwa2cxMjM0NTY3ODkwIn19"
65
+ "header": "eyJzNDAyVmVyc2lvbiI6IjEiLCJhY2NlcHRzIjpbInVubG9jayJdLCJuZXR3b3JrIjoic3VpOm1haW5uZXQiLCJhc3NldCI6IjB4Mjo6c3VpOjpTVUkiLCJhbW91bnQiOiIxMDAwMDAwIiwicGF5VG8iOiIweGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTAiLCJ1bmxvY2siOnsiZW5jcnlwdGlvbklkIjoiZW5jLWFiYy0xMjMiLCJlbmNyeXB0ZWRDb250ZW50SWQiOiJibG9iLXh5ei03ODkiLCJlbmNyeXB0aW9uU2VydmljZUlkIjoiMHhwa2cxMjM0NTY3ODkwIn19"
66
66
  },
67
67
  "expected": {
68
68
  "s402Version": "1",
@@ -75,8 +75,8 @@
75
75
  "payTo": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
76
76
  "unlock": {
77
77
  "encryptionId": "enc-abc-123",
78
- "walrusBlobId": "blob-xyz-789",
79
- "encryptionPackageId": "0xpkg1234567890"
78
+ "encryptedContentId": "blob-xyz-789",
79
+ "encryptionServiceId": "0xpkg1234567890"
80
80
  }
81
81
  },
82
82
  "shouldReject": false
@@ -72,12 +72,12 @@
72
72
  "payTo": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
73
73
  "unlock": {
74
74
  "encryptionId": "enc-abc-123",
75
- "walrusBlobId": "blob-xyz-789",
76
- "encryptionPackageId": "0xpkg1234567890"
75
+ "encryptedContentId": "blob-xyz-789",
76
+ "encryptionServiceId": "0xpkg1234567890"
77
77
  }
78
78
  },
79
79
  "expected": {
80
- "header": "eyJzNDAyVmVyc2lvbiI6IjEiLCJhY2NlcHRzIjpbInVubG9jayJdLCJuZXR3b3JrIjoic3VpOm1haW5uZXQiLCJhc3NldCI6IjB4Mjo6c3VpOjpTVUkiLCJhbW91bnQiOiIxMDAwMDAwIiwicGF5VG8iOiIweGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTAiLCJ1bmxvY2siOnsiZW5jcnlwdGlvbklkIjoiZW5jLWFiYy0xMjMiLCJ3YWxydXNCbG9iSWQiOiJibG9iLXh5ei03ODkiLCJlbmNyeXB0aW9uUGFja2FnZUlkIjoiMHhwa2cxMjM0NTY3ODkwIn19"
80
+ "header": "eyJzNDAyVmVyc2lvbiI6IjEiLCJhY2NlcHRzIjpbInVubG9jayJdLCJuZXR3b3JrIjoic3VpOm1haW5uZXQiLCJhc3NldCI6IjB4Mjo6c3VpOjpTVUkiLCJhbW91bnQiOiIxMDAwMDAwIiwicGF5VG8iOiIweGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTAiLCJ1bmxvY2siOnsiZW5jcnlwdGlvbklkIjoiZW5jLWFiYy0xMjMiLCJlbmNyeXB0ZWRDb250ZW50SWQiOiJibG9iLXh5ei03ODkiLCJlbmNyeXB0aW9uU2VydmljZUlkIjoiMHhwa2cxMjM0NTY3ODkwIn19"
81
81
  },
82
82
  "shouldReject": false
83
83
  },
@@ -175,14 +175,6 @@
175
175
  "shouldReject": true,
176
176
  "expectedErrorCode": "INVALID_PAYLOAD"
177
177
  },
178
- {
179
- "description": "Rejects amount exceeding u64 max",
180
- "input": {
181
- "header": "eyJzNDAyVmVyc2lvbiI6IjEiLCJhY2NlcHRzIjpbImV4YWN0Il0sIm5ldHdvcmsiOiJzdWk6bWFpbm5ldCIsImFzc2V0IjoiU1VJIiwiYW1vdW50IjoiMTg0NDY3NDQwNzM3MDk1NTE2MTYiLCJwYXlUbyI6IjB4YWJjIn0="
182
- },
183
- "shouldReject": true,
184
- "expectedErrorCode": "INVALID_PAYLOAD"
185
- },
186
178
  {
187
179
  "description": "Rejects invalid base64 header",
188
180
  "input": {