s402 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -45,11 +45,11 @@ s402 <-- You are here. Protocol spec. Zero runtime deps.
45
45
  |-- Compat Optional x402 migration aid
46
46
  |-- Errors Typed error codes with recovery hints
47
47
  |
48
- @sweepay/sui <-- Sui-specific implementations (coming soon)
49
- @sweepay/sdk <-- High-level DX (coming soon)
48
+ @sweefi/sui <-- Sui-specific implementations (coming soon)
49
+ @sweefi/sdk <-- High-level DX (coming soon)
50
50
  ```
51
51
 
52
- `s402` is **chain-agnostic protocol plumbing**. It defines _what_ gets sent over HTTP. The Sui-specific _how_ will live in `@sweepay/sui` (coming soon).
52
+ `s402` is **chain-agnostic protocol plumbing**. It defines _what_ gets sent over HTTP. The Sui-specific _how_ will live in `@sweefi/sui` (coming soon).
53
53
 
54
54
  ## Payment Schemes
55
55
 
@@ -212,7 +212,7 @@ import { s402Client } from 's402';
212
212
 
213
213
  const client = new s402Client();
214
214
 
215
- // Register scheme implementations (from @sweepay/sui or your own)
215
+ // Register scheme implementations (from @sweefi/sui or your own)
216
216
  client.register('sui:mainnet', exactScheme);
217
217
  client.register('sui:mainnet', streamScheme);
218
218
 
@@ -258,7 +258,7 @@ import type {
258
258
  } from 's402';
259
259
  ```
260
260
 
261
- The reference Sui implementation of all five schemes will be available in `@sweepay/sui` (coming soon).
261
+ The reference Sui implementation of all five schemes will be available in `@sweefi/sui` (coming soon).
262
262
 
263
263
  ## Wire Format
264
264
 
@@ -310,7 +310,7 @@ const requirements: s402PaymentRequirements = {
310
310
 
311
311
  ## Design Principles
312
312
 
313
- 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.
313
+ 1. **Protocol-agnostic core, Sui-native reference.** `s402` defines chain-agnostic protocol types and HTTP encoding. The reference implementation (`@sweefi/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.
314
314
 
315
315
  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.
316
316
 
package/SECURITY.md CHANGED
@@ -15,7 +15,7 @@ You will receive an acknowledgment within 48 hours. We aim to provide a fix or m
15
15
 
16
16
  This policy covers the `s402` npm package — the protocol types, HTTP encoding/decoding, scheme registry, and compat layer.
17
17
 
18
- Security issues in downstream packages (`@sweepay/sui`, `@sweepay/sdk`, etc.) should be reported to the same email.
18
+ Security issues in downstream packages (`@sweefi/sui`, `@sweefi/sdk`, etc.) should be reported to the same email.
19
19
 
20
20
  ## What qualifies
21
21
 
package/dist/compat.mjs CHANGED
@@ -12,6 +12,13 @@ function fromX402Requirements(x402) {
12
12
  const amount = x402.amount ?? x402.maxAmountRequired;
13
13
  if (!amount) throw new s402Error("INVALID_PAYLOAD", "x402 requirements missing both \"amount\" (V2) and \"maxAmountRequired\" (V1)");
14
14
  if (!isValidAmount(amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${amount}": must be a non-negative integer string`);
15
+ if (x402.facilitatorUrl !== void 0) try {
16
+ const url = new URL(x402.facilitatorUrl);
17
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new s402Error("INVALID_PAYLOAD", `facilitatorUrl must use https:// or http://, got "${url.protocol}"`);
18
+ } catch (e) {
19
+ if (e instanceof s402Error) throw e;
20
+ throw new s402Error("INVALID_PAYLOAD", "facilitatorUrl is not a valid URL");
21
+ }
15
22
  return {
16
23
  s402Version: S402_VERSION,
17
24
  accepts: ["exact"],
package/dist/errors.d.mts CHANGED
@@ -22,6 +22,7 @@ declare const s402ErrorCode: {
22
22
  readonly SIGNATURE_INVALID: "SIGNATURE_INVALID";
23
23
  readonly REQUIREMENTS_EXPIRED: "REQUIREMENTS_EXPIRED";
24
24
  readonly VERIFICATION_FAILED: "VERIFICATION_FAILED";
25
+ readonly SETTLEMENT_FAILED: "SETTLEMENT_FAILED";
25
26
  };
26
27
  type s402ErrorCodeType = (typeof s402ErrorCode)[keyof typeof s402ErrorCode];
27
28
  interface s402ErrorInfo {
package/dist/errors.mjs CHANGED
@@ -21,7 +21,8 @@ const s402ErrorCode = {
21
21
  NETWORK_MISMATCH: "NETWORK_MISMATCH",
22
22
  SIGNATURE_INVALID: "SIGNATURE_INVALID",
23
23
  REQUIREMENTS_EXPIRED: "REQUIREMENTS_EXPIRED",
24
- VERIFICATION_FAILED: "VERIFICATION_FAILED"
24
+ VERIFICATION_FAILED: "VERIFICATION_FAILED",
25
+ SETTLEMENT_FAILED: "SETTLEMENT_FAILED"
25
26
  };
26
27
  /** Error recovery hints for each error code */
27
28
  const ERROR_HINTS = {
@@ -80,6 +81,10 @@ const ERROR_HINTS = {
80
81
  VERIFICATION_FAILED: {
81
82
  retryable: false,
82
83
  suggestedAction: "Check payment amount and transaction structure"
84
+ },
85
+ SETTLEMENT_FAILED: {
86
+ retryable: true,
87
+ suggestedAction: "Transient RPC failure during settlement — retry in a few seconds"
83
88
  }
84
89
  };
85
90
  /**
package/dist/http.mjs CHANGED
@@ -341,9 +341,9 @@ function validateRequirementsShape(obj) {
341
341
  if (typeof record.network !== "string") missing.push("network (string)");
342
342
  if (typeof record.asset !== "string") missing.push("asset (string)");
343
343
  if (typeof record.amount !== "string") missing.push("amount (string)");
344
- else if (!isValidAmount(record.amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${record.amount}": must be a non-negative integer string`);
344
+ else if (!isValidU64Amount(record.amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${record.amount}": must be a non-negative integer string within u64 range`);
345
345
  if (typeof record.payTo !== "string") missing.push("payTo (string)");
346
- else if (!record.payTo.startsWith("0x")) throw new s402Error("INVALID_PAYLOAD", `payTo must be a hex address starting with "0x", got "${record.payTo.substring(0, 20)}..."`);
346
+ 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)}..."`);
347
347
  if (missing.length > 0) throw new s402Error("INVALID_PAYLOAD", `Malformed payment requirements: missing ${missing.join(", ")}`);
348
348
  if (Array.isArray(record.accepts) && record.accepts.length === 0) throw new s402Error("INVALID_PAYLOAD", "accepts array must contain at least one scheme");
349
349
  const accepts = record.accepts;
@@ -352,7 +352,7 @@ function validateRequirementsShape(obj) {
352
352
  if (typeof record.protocolFeeBps !== "number" || !Number.isFinite(record.protocolFeeBps) || !Number.isInteger(record.protocolFeeBps) || record.protocolFeeBps < 0 || record.protocolFeeBps > 1e4) throw new s402Error("INVALID_PAYLOAD", `protocolFeeBps must be an integer between 0 and 10000, got ${record.protocolFeeBps}`);
353
353
  }
354
354
  if (record.expiresAt !== void 0) {
355
- if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)) throw new s402Error("INVALID_PAYLOAD", `expiresAt must be a finite number (Unix timestamp ms), got ${typeof record.expiresAt}`);
355
+ 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}`);
356
356
  }
357
357
  validateSubObjects(record);
358
358
  }
package/dist/index.d.mts CHANGED
@@ -122,6 +122,7 @@ declare class s402Client {
122
122
  //#region src/facilitator.d.ts
123
123
  declare class s402Facilitator {
124
124
  private schemes;
125
+ private inFlight;
125
126
  /**
126
127
  * Register a scheme-specific facilitator for a network.
127
128
  */
package/dist/index.mjs CHANGED
@@ -128,6 +128,7 @@ var s402ResourceServer = class {
128
128
  //#region src/facilitator.ts
129
129
  var s402Facilitator = class {
130
130
  schemes = /* @__PURE__ */ new Map();
131
+ inFlight = /* @__PURE__ */ new Set();
131
132
  /**
132
133
  * Register a scheme-specific facilitator for a network.
133
134
  */
@@ -157,7 +158,18 @@ var s402Facilitator = class {
157
158
  invalidReason: `Scheme "${payload.scheme}" is not accepted by these requirements. Accepted: [${requirements.accepts.join(", ")}]`
158
159
  };
159
160
  }
160
- return this.resolveScheme(payload.scheme, requirements.network).verify(payload, requirements);
161
+ try {
162
+ return this.resolveScheme(payload.scheme, requirements.network).verify(payload, requirements);
163
+ } catch (e) {
164
+ if (e instanceof s402Error) return {
165
+ valid: false,
166
+ invalidReason: e.message
167
+ };
168
+ return {
169
+ valid: false,
170
+ invalidReason: "Unexpected error resolving scheme"
171
+ };
172
+ }
161
173
  }
162
174
  /**
163
175
  * Settle a payment by dispatching to the correct scheme.
@@ -183,7 +195,20 @@ var s402Facilitator = class {
183
195
  errorCode: "SCHEME_NOT_SUPPORTED"
184
196
  };
185
197
  }
186
- return this.resolveScheme(payload.scheme, requirements.network).settle(payload, requirements);
198
+ try {
199
+ return this.resolveScheme(payload.scheme, requirements.network).settle(payload, requirements);
200
+ } catch (e) {
201
+ if (e instanceof s402Error) return {
202
+ success: false,
203
+ error: e.message,
204
+ errorCode: e.code
205
+ };
206
+ return {
207
+ success: false,
208
+ error: "Unexpected error resolving scheme",
209
+ errorCode: "SCHEME_NOT_SUPPORTED"
210
+ };
211
+ }
187
212
  }
188
213
  /**
189
214
  * Expiration-guarded verify + settle in one call.
@@ -213,26 +238,60 @@ var s402Facilitator = class {
213
238
  errorCode: "SCHEME_NOT_SUPPORTED"
214
239
  };
215
240
  }
216
- const scheme = this.resolveScheme(payload.scheme, requirements.network);
217
- const verifyResult = await scheme.verify(payload, requirements);
218
- if (!verifyResult.valid) return {
219
- success: false,
220
- error: verifyResult.invalidReason ?? "Payment verification failed",
221
- errorCode: "VERIFICATION_FAILED"
222
- };
223
- if (typeof requirements.expiresAt === "number" && Date.now() > requirements.expiresAt) return {
224
- success: false,
225
- error: `Payment requirements expired during verification at ${new Date(requirements.expiresAt).toISOString()}`,
226
- errorCode: "REQUIREMENTS_EXPIRED"
227
- };
241
+ let scheme;
228
242
  try {
229
- return await scheme.settle(payload, requirements);
243
+ scheme = this.resolveScheme(payload.scheme, requirements.network);
230
244
  } catch (e) {
245
+ if (e instanceof s402Error) return {
246
+ success: false,
247
+ error: e.message,
248
+ errorCode: e.code
249
+ };
231
250
  return {
232
251
  success: false,
233
- error: e instanceof Error ? e.message : "Settlement failed with an unexpected error",
252
+ error: "Failed to resolve payment scheme",
253
+ errorCode: "SCHEME_NOT_SUPPORTED"
254
+ };
255
+ }
256
+ const dedupeKey = JSON.stringify(payload);
257
+ if (this.inFlight.has(dedupeKey)) return {
258
+ success: false,
259
+ error: "Duplicate payment request already in flight",
260
+ errorCode: "INVALID_PAYLOAD"
261
+ };
262
+ this.inFlight.add(dedupeKey);
263
+ try {
264
+ let verifyResult;
265
+ try {
266
+ verifyResult = await Promise.race([scheme.verify(payload, requirements), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Verification timed out after 5s")), 5e3))]);
267
+ } catch (e) {
268
+ return {
269
+ success: false,
270
+ error: e instanceof Error ? e.message : "Verification threw an unexpected error",
271
+ errorCode: "VERIFICATION_FAILED"
272
+ };
273
+ }
274
+ if (!verifyResult.valid) return {
275
+ success: false,
276
+ error: verifyResult.invalidReason ?? "Payment verification failed",
234
277
  errorCode: "VERIFICATION_FAILED"
235
278
  };
279
+ if (typeof requirements.expiresAt === "number" && Date.now() > requirements.expiresAt) return {
280
+ success: false,
281
+ error: `Payment requirements expired during verification at ${new Date(requirements.expiresAt).toISOString()}`,
282
+ errorCode: "REQUIREMENTS_EXPIRED"
283
+ };
284
+ try {
285
+ return await Promise.race([scheme.settle(payload, requirements), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Settlement timed out after 15s")), 15e3))]);
286
+ } catch (e) {
287
+ return {
288
+ success: false,
289
+ error: e instanceof Error ? e.message : "Settlement failed with an unexpected error",
290
+ errorCode: "SETTLEMENT_FAILED"
291
+ };
292
+ }
293
+ } finally {
294
+ this.inFlight.delete(dedupeKey);
236
295
  }
237
296
  }
238
297
  /**
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "s402",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
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.",
6
6
  "license": "Apache-2.0",
7
- "author": "Swee Group LLC <daniel@sweeinc.com> (https://s402-protocol.org)",
7
+ "author": "SweeInc <daniel@sweeinc.com> (https://s402-protocol.org)",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/s402-protocol/core.git"