s402 0.4.0 → 0.5.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 +31 -0
- package/dist/compat.d.mts +4 -4
- package/dist/compat.mjs +9 -7
- package/dist/errors.d.mts +1 -0
- package/dist/errors.mjs +6 -1
- package/dist/http.d.mts +7 -3
- package/dist/http.mjs +88 -23
- package/dist/index.d.mts +161 -4
- package/dist/index.mjs +218 -23
- package/dist/{scheme-tVj4sOr-.d.mts → scheme-m-uk4zyH.d.mts} +16 -8
- package/dist/test-utils.d.mts +1 -1
- package/dist/types.d.mts +141 -48
- package/package.json +9 -2
- package/test/conformance/vectors/body-transport.json +42 -0
- package/test/conformance/vectors/compat-normalize.json +2 -1
- package/test/conformance/vectors/payload-decode.json +33 -0
- package/test/conformance/vectors/payload-encode.json +33 -0
- package/test/conformance/vectors/requirements-decode.json +44 -0
- package/test/conformance/vectors/requirements-encode.json +44 -0
- package/test/conformance/vectors/roundtrip.json +52 -0
- package/test/conformance/vectors/settle-decode.json +14 -0
- package/test/conformance/vectors/settle-encode.json +28 -0
- package/test/conformance/vectors/validation-reject.json +69 -0
package/dist/index.mjs
CHANGED
|
@@ -140,10 +140,11 @@ var s402ResourceServer = class {
|
|
|
140
140
|
protocolFeeBps: config.protocolFeeBps,
|
|
141
141
|
receiptRequired: config.receiptRequired,
|
|
142
142
|
settlementMode: config.settlementMode,
|
|
143
|
+
upto: config.upto,
|
|
144
|
+
prepaid: config.prepaid,
|
|
143
145
|
stream: config.stream,
|
|
144
146
|
escrow: config.escrow,
|
|
145
|
-
unlock: config.unlock
|
|
146
|
-
prepaid: config.prepaid
|
|
147
|
+
unlock: config.unlock
|
|
147
148
|
};
|
|
148
149
|
}
|
|
149
150
|
/**
|
|
@@ -186,11 +187,129 @@ var s402ResourceServer = class {
|
|
|
186
187
|
}
|
|
187
188
|
};
|
|
188
189
|
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/extensions.ts
|
|
192
|
+
/**
|
|
193
|
+
* Type-safe extension data retrieval.
|
|
194
|
+
*
|
|
195
|
+
* The wire format is `Record<string, unknown>` for interop, but this helper
|
|
196
|
+
* provides typed access for TypeScript consumers.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* interface DiscoveryData { services: string[] }
|
|
201
|
+
* const data = getExtensionData<DiscoveryData>(requirements.extensions, 'org.s402.discovery');
|
|
202
|
+
* if (data) console.log(data.services);
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
function getExtensionData(extensions, key) {
|
|
206
|
+
return extensions?.[key];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Set extension data on an extensions record (creates if needed).
|
|
210
|
+
* Returns a new extensions object (does not mutate the input).
|
|
211
|
+
*/
|
|
212
|
+
function setExtensionData(extensions, key, data) {
|
|
213
|
+
return {
|
|
214
|
+
...extensions,
|
|
215
|
+
[key]: data
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Registry for extensions with dependency-ordered execution.
|
|
220
|
+
*
|
|
221
|
+
* Extensions are stored by key and sorted topologically based on `dependsOn`.
|
|
222
|
+
* Within the same dependency level, registration order is preserved.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* const registry = new s402ExtensionRegistry<s402FacilitatorExtension>();
|
|
227
|
+
* registry.register(rateLimitExtension);
|
|
228
|
+
* registry.register(analyticsExtension);
|
|
229
|
+
* const sorted = registry.sorted(); // dependency-ordered list
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
var s402ExtensionRegistry = class {
|
|
233
|
+
extensions = /* @__PURE__ */ new Map();
|
|
234
|
+
sortedCache = null;
|
|
235
|
+
/**
|
|
236
|
+
* Register an extension. Throws on duplicate key or dependency cycle.
|
|
237
|
+
*/
|
|
238
|
+
register(ext) {
|
|
239
|
+
if (this.extensions.has(ext.key)) throw new s402Error("EXTENSION_FAILED", `Extension "${ext.key}" is already registered`);
|
|
240
|
+
this.extensions.set(ext.key, ext);
|
|
241
|
+
this.sortedCache = null;
|
|
242
|
+
}
|
|
243
|
+
/** Get a registered extension by key. */
|
|
244
|
+
get(key) {
|
|
245
|
+
return this.extensions.get(key);
|
|
246
|
+
}
|
|
247
|
+
/** Number of registered extensions. */
|
|
248
|
+
get size() {
|
|
249
|
+
return this.extensions.size;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Return extensions in topological (dependency) order.
|
|
253
|
+
* Cached until a new extension is registered.
|
|
254
|
+
*/
|
|
255
|
+
sorted() {
|
|
256
|
+
if (this.sortedCache) return this.sortedCache;
|
|
257
|
+
this.sortedCache = topologicalSort(this.extensions);
|
|
258
|
+
return this.sortedCache;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Topological sort of extensions based on `dependsOn` declarations.
|
|
263
|
+
* Uses Kahn's algorithm. Throws on cycles.
|
|
264
|
+
*/
|
|
265
|
+
function topologicalSort(extensions) {
|
|
266
|
+
if (extensions.size === 0) return [];
|
|
267
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
268
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
269
|
+
for (const [key] of extensions) {
|
|
270
|
+
inDegree.set(key, 0);
|
|
271
|
+
dependents.set(key, []);
|
|
272
|
+
}
|
|
273
|
+
for (const [key, ext] of extensions) if (ext.dependsOn) for (const dep of ext.dependsOn) {
|
|
274
|
+
if (!extensions.has(dep)) throw new s402Error("EXTENSION_FAILED", `Extension "${key}" depends on "${dep}" which is not registered`);
|
|
275
|
+
dependents.get(dep).push(key);
|
|
276
|
+
inDegree.set(key, (inDegree.get(key) ?? 0) + 1);
|
|
277
|
+
}
|
|
278
|
+
const queue = [];
|
|
279
|
+
for (const [key, degree] of inDegree) if (degree === 0) queue.push(key);
|
|
280
|
+
const sorted = [];
|
|
281
|
+
while (queue.length > 0) {
|
|
282
|
+
const key = queue.shift();
|
|
283
|
+
sorted.push(extensions.get(key));
|
|
284
|
+
for (const dependent of dependents.get(key)) {
|
|
285
|
+
const newDegree = inDegree.get(dependent) - 1;
|
|
286
|
+
inDegree.set(dependent, newDegree);
|
|
287
|
+
if (newDegree === 0) queue.push(dependent);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (sorted.length !== extensions.size) throw new s402Error("EXTENSION_FAILED", `Extension dependency cycle detected involving: ${[...extensions.keys()].filter((k) => !sorted.some((e) => e.key === k)).join(", ")}`);
|
|
291
|
+
return sorted;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Run an async hook on all extensions in order.
|
|
295
|
+
* Critical extensions throw on failure; advisory extensions call the error handler.
|
|
296
|
+
*/
|
|
297
|
+
async function runExtensionHooks(extensions, hookName, runner, onError) {
|
|
298
|
+
for (const ext of extensions) try {
|
|
299
|
+
await runner(ext);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
if (ext.critical) throw new s402Error("EXTENSION_FAILED", `Critical extension "${ext.key}" failed in ${hookName}: ${e instanceof Error ? e.message : String(e)}`);
|
|
302
|
+
onError?.(ext, e);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
189
306
|
//#endregion
|
|
190
307
|
//#region src/facilitator.ts
|
|
191
308
|
var s402Facilitator = class {
|
|
192
309
|
schemes = /* @__PURE__ */ new Map();
|
|
193
310
|
inFlight = /* @__PURE__ */ new Set();
|
|
311
|
+
extensionRegistry = new s402ExtensionRegistry();
|
|
312
|
+
extensionErrorHandler;
|
|
194
313
|
/**
|
|
195
314
|
* Register a scheme-specific facilitator for a network.
|
|
196
315
|
*/
|
|
@@ -200,6 +319,25 @@ var s402Facilitator = class {
|
|
|
200
319
|
return this;
|
|
201
320
|
}
|
|
202
321
|
/**
|
|
322
|
+
* Register a facilitator extension. Extensions fire in dependency order
|
|
323
|
+
* at four points in the process() pipeline: beforeVerify, afterVerify,
|
|
324
|
+
* beforeSettle, afterSettle.
|
|
325
|
+
*
|
|
326
|
+
* @throws {s402Error} `EXTENSION_FAILED` on duplicate key or dependency cycle
|
|
327
|
+
*/
|
|
328
|
+
registerExtension(ext) {
|
|
329
|
+
this.extensionRegistry.register(ext);
|
|
330
|
+
return this;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Set the handler for advisory (non-critical) extension failures.
|
|
334
|
+
* Critical extensions always throw; advisory extensions call this handler.
|
|
335
|
+
*/
|
|
336
|
+
onExtensionError(handler) {
|
|
337
|
+
this.extensionErrorHandler = handler;
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
203
341
|
* Verify a payment payload by dispatching to the correct scheme.
|
|
204
342
|
* Includes expiration guard and scheme-mismatch check.
|
|
205
343
|
*/
|
|
@@ -284,6 +422,7 @@ var s402Facilitator = class {
|
|
|
284
422
|
*
|
|
285
423
|
* @param payload - Client's payment payload
|
|
286
424
|
* @param requirements - Server's payment requirements
|
|
425
|
+
* @param options - Optional process configuration (e.g., `{ skipVerify: true }` for zero-cost-failure chains)
|
|
287
426
|
* @returns Settlement result (check `result.success` and `result.errorCode`)
|
|
288
427
|
*
|
|
289
428
|
* @example
|
|
@@ -301,7 +440,7 @@ var s402Facilitator = class {
|
|
|
301
440
|
* }
|
|
302
441
|
* ```
|
|
303
442
|
*/
|
|
304
|
-
async process(payload, requirements) {
|
|
443
|
+
async process(payload, requirements, options) {
|
|
305
444
|
if (requirements.expiresAt != null) {
|
|
306
445
|
if (typeof requirements.expiresAt !== "number" || !Number.isFinite(requirements.expiresAt)) return {
|
|
307
446
|
success: false,
|
|
@@ -343,33 +482,79 @@ var s402Facilitator = class {
|
|
|
343
482
|
errorCode: "INVALID_PAYLOAD"
|
|
344
483
|
};
|
|
345
484
|
this.inFlight.add(dedupeKey);
|
|
485
|
+
const extensions = this.extensionRegistry.size > 0 ? this.extensionRegistry.sorted() : null;
|
|
346
486
|
try {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
487
|
+
if (!options?.skipVerify) {
|
|
488
|
+
if (extensions) try {
|
|
489
|
+
await runExtensionHooks(extensions, "beforeVerify", (ext) => ext.beforeVerify ? ext.beforeVerify(payload, requirements) : Promise.resolve(), this.extensionErrorHandler);
|
|
490
|
+
} catch (e) {
|
|
491
|
+
if (e instanceof s402Error) return {
|
|
492
|
+
success: false,
|
|
493
|
+
error: e.message,
|
|
494
|
+
errorCode: e.code
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
error: "Extension beforeVerify failed",
|
|
499
|
+
errorCode: "EXTENSION_FAILED"
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
let verifyResult;
|
|
503
|
+
try {
|
|
504
|
+
let verifyTimer;
|
|
505
|
+
verifyResult = await Promise.race([scheme.verify(payload, requirements), new Promise((_, reject) => {
|
|
506
|
+
verifyTimer = setTimeout(() => reject(/* @__PURE__ */ new Error("Verification timed out after 5s")), 5e3);
|
|
507
|
+
})]).finally(() => clearTimeout(verifyTimer));
|
|
508
|
+
} catch (e) {
|
|
509
|
+
return {
|
|
510
|
+
success: false,
|
|
511
|
+
error: e instanceof Error ? e.message : "Verification threw an unexpected error",
|
|
512
|
+
errorCode: "VERIFICATION_FAILED"
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (!verifyResult.valid) return {
|
|
516
|
+
success: false,
|
|
517
|
+
error: verifyResult.invalidReason ?? "Payment verification failed",
|
|
518
|
+
errorCode: "VERIFICATION_FAILED"
|
|
519
|
+
};
|
|
520
|
+
if (extensions) try {
|
|
521
|
+
await runExtensionHooks(extensions, "afterVerify", (ext) => ext.afterVerify ? ext.afterVerify(payload, verifyResult) : Promise.resolve(), this.extensionErrorHandler);
|
|
522
|
+
} catch (e) {
|
|
523
|
+
if (e instanceof s402Error) return {
|
|
524
|
+
success: false,
|
|
525
|
+
error: e.message,
|
|
526
|
+
errorCode: e.code
|
|
527
|
+
};
|
|
528
|
+
return {
|
|
529
|
+
success: false,
|
|
530
|
+
error: "Extension afterVerify failed",
|
|
531
|
+
errorCode: "EXTENSION_FAILED"
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (typeof requirements.expiresAt === "number" && Date.now() > requirements.expiresAt) return {
|
|
535
|
+
success: false,
|
|
536
|
+
error: `Payment requirements expired during verification at ${new Date(requirements.expiresAt).toISOString()}`,
|
|
537
|
+
errorCode: "REQUIREMENTS_EXPIRED"
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (extensions) try {
|
|
541
|
+
await runExtensionHooks(extensions, "beforeSettle", (ext) => ext.beforeSettle ? ext.beforeSettle(payload, requirements) : Promise.resolve(), this.extensionErrorHandler);
|
|
353
542
|
} catch (e) {
|
|
543
|
+
if (e instanceof s402Error) return {
|
|
544
|
+
success: false,
|
|
545
|
+
error: e.message,
|
|
546
|
+
errorCode: e.code
|
|
547
|
+
};
|
|
354
548
|
return {
|
|
355
549
|
success: false,
|
|
356
|
-
error:
|
|
357
|
-
errorCode: "
|
|
550
|
+
error: "Extension beforeSettle failed",
|
|
551
|
+
errorCode: "EXTENSION_FAILED"
|
|
358
552
|
};
|
|
359
553
|
}
|
|
360
|
-
|
|
361
|
-
success: false,
|
|
362
|
-
error: verifyResult.invalidReason ?? "Payment verification failed",
|
|
363
|
-
errorCode: "VERIFICATION_FAILED"
|
|
364
|
-
};
|
|
365
|
-
if (typeof requirements.expiresAt === "number" && Date.now() > requirements.expiresAt) return {
|
|
366
|
-
success: false,
|
|
367
|
-
error: `Payment requirements expired during verification at ${new Date(requirements.expiresAt).toISOString()}`,
|
|
368
|
-
errorCode: "REQUIREMENTS_EXPIRED"
|
|
369
|
-
};
|
|
554
|
+
let settleResult;
|
|
370
555
|
try {
|
|
371
556
|
let settleTimer;
|
|
372
|
-
|
|
557
|
+
settleResult = await Promise.race([scheme.settle(payload, requirements), new Promise((_, reject) => {
|
|
373
558
|
settleTimer = setTimeout(() => reject(/* @__PURE__ */ new Error("Settlement timed out after 15s")), 15e3);
|
|
374
559
|
})]).finally(() => clearTimeout(settleTimer));
|
|
375
560
|
} catch (e) {
|
|
@@ -379,6 +564,16 @@ var s402Facilitator = class {
|
|
|
379
564
|
errorCode: "SETTLEMENT_FAILED"
|
|
380
565
|
};
|
|
381
566
|
}
|
|
567
|
+
if (extensions && settleResult.success) try {
|
|
568
|
+
await runExtensionHooks(extensions, "afterSettle", (ext) => ext.afterSettle ? ext.afterSettle(payload, settleResult) : Promise.resolve(), this.extensionErrorHandler);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
this.extensionErrorHandler?.({
|
|
571
|
+
key: "afterSettle",
|
|
572
|
+
version: "0",
|
|
573
|
+
critical: true
|
|
574
|
+
}, e);
|
|
575
|
+
}
|
|
576
|
+
return settleResult;
|
|
382
577
|
} finally {
|
|
383
578
|
this.inFlight.delete(dedupeKey);
|
|
384
579
|
}
|
|
@@ -406,4 +601,4 @@ var s402Facilitator = class {
|
|
|
406
601
|
};
|
|
407
602
|
|
|
408
603
|
//#endregion
|
|
409
|
-
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 };
|
|
604
|
+
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, getExtensionData, isValidAmount, isValidU64Amount, parseReceiptHeader, runExtensionHooks, s402Client, s402Error, s402ErrorCode, s402ExtensionRegistry, s402Facilitator, s402ResourceServer, setExtensionData, validateRequirementsShape };
|
|
@@ -60,6 +60,8 @@ interface s402ServerScheme {
|
|
|
60
60
|
*
|
|
61
61
|
* Critical: each scheme has its OWN verify logic.
|
|
62
62
|
* - Exact: signature recovery + dry-run simulation + balance check
|
|
63
|
+
* - Upto: deposit PTB validation + maxAmount match + deadline check
|
|
64
|
+
* - Prepaid: deposit PTB validation + rate/cap match
|
|
63
65
|
* - Stream: stream creation PTB validation + deposit check
|
|
64
66
|
* - Escrow: escrow creation PTB validation + arbiter/deadline check
|
|
65
67
|
* - Unlock: escrow validation (key release is separate PTB)
|
|
@@ -109,6 +111,20 @@ interface s402RouteConfig {
|
|
|
109
111
|
protocolFeeBps?: number;
|
|
110
112
|
/** Require on-chain receipt */
|
|
111
113
|
receiptRequired?: boolean;
|
|
114
|
+
upto?: {
|
|
115
|
+
maxAmount: string;
|
|
116
|
+
settlementDeadlineMs: string;
|
|
117
|
+
usageReportUrl?: string;
|
|
118
|
+
estimatedAmount?: string;
|
|
119
|
+
};
|
|
120
|
+
prepaid?: {
|
|
121
|
+
ratePerCall: string;
|
|
122
|
+
maxCalls?: string;
|
|
123
|
+
minDeposit: string;
|
|
124
|
+
withdrawalDelayMs: string; /** Provider Ed25519 pubkey (hex). Enables v0.2 signed receipt mode. @since v0.2 */
|
|
125
|
+
providerPubkey?: string; /** Dispute window in ms. Required when providerPubkey is set. @since v0.2 */
|
|
126
|
+
disputeWindowMs?: string;
|
|
127
|
+
};
|
|
112
128
|
stream?: {
|
|
113
129
|
ratePerSecond: string;
|
|
114
130
|
budgetCap: string;
|
|
@@ -124,14 +140,6 @@ interface s402RouteConfig {
|
|
|
124
140
|
encryptedContentId: string;
|
|
125
141
|
encryptionServiceId: string;
|
|
126
142
|
};
|
|
127
|
-
prepaid?: {
|
|
128
|
-
ratePerCall: string;
|
|
129
|
-
maxCalls?: string;
|
|
130
|
-
minDeposit: string;
|
|
131
|
-
withdrawalDelayMs: string; /** Provider Ed25519 pubkey (hex). Enables v0.2 signed receipt mode. @since v0.2 */
|
|
132
|
-
providerPubkey?: string; /** Dispute window in ms. Required when providerPubkey is set. @since v0.2 */
|
|
133
|
-
disputeWindowMs?: string;
|
|
134
|
-
};
|
|
135
143
|
}
|
|
136
144
|
//#endregion
|
|
137
145
|
export { s402ServerScheme as a, s402RouteConfig as i, s402DirectScheme as n, s402SettlementVerification as o, s402FacilitatorScheme as r, s402ClientScheme as t };
|
package/dist/test-utils.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { s402PaymentPayload, s402PaymentRequirements, s402SettleResponse, s402VerifyResponse } from "./types.mjs";
|
|
2
|
-
import { a as s402ServerScheme, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-
|
|
2
|
+
import { a as s402ServerScheme, r as s402FacilitatorScheme, t as s402ClientScheme } from "./scheme-m-uk4zyH.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/test-utils.d.ts
|
|
5
5
|
/**
|
package/dist/types.d.mts
CHANGED
|
@@ -3,8 +3,14 @@ import { s402ErrorCodeType } from "./errors.mjs";
|
|
|
3
3
|
//#region src/types.d.ts
|
|
4
4
|
/** Current protocol version. Always lowercase s. */
|
|
5
5
|
declare const S402_VERSION: "1";
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The six s402 payment schemes, ordered by complexity:
|
|
8
|
+
*
|
|
9
|
+
* TIER 1 — Single Payment: exact (fixed amount), upto (variable amount)
|
|
10
|
+
* TIER 2 — Persistent Balance: prepaid (multi-claim), stream (time-based)
|
|
11
|
+
* TIER 3 — Conditional Release: escrow (arbiter), unlock (encryption)
|
|
12
|
+
*/
|
|
13
|
+
type s402Scheme = 'exact' | 'upto' | 'prepaid' | 'stream' | 'escrow' | 'unlock';
|
|
8
14
|
/** Settlement mode: facilitator-mediated or direct on-chain */
|
|
9
15
|
type s402SettlementMode = 'facilitator' | 'direct';
|
|
10
16
|
/**
|
|
@@ -64,14 +70,18 @@ interface s402PaymentRequirements {
|
|
|
64
70
|
settlementMode?: s402SettlementMode;
|
|
65
71
|
/** When these requirements expire (Unix timestamp ms). Facilitator MUST reject after this. */
|
|
66
72
|
expiresAt?: number;
|
|
73
|
+
/** Extra fields for upto scheme (usage-based, variable settlement) */
|
|
74
|
+
upto?: s402UptoExtra;
|
|
75
|
+
/** Extra fields for prepaid scheme */
|
|
76
|
+
prepaid?: s402PrepaidExtra;
|
|
67
77
|
/** Extra fields for stream scheme */
|
|
68
78
|
stream?: s402StreamExtra;
|
|
69
79
|
/** Extra fields for escrow scheme */
|
|
70
80
|
escrow?: s402EscrowExtra;
|
|
71
81
|
/** Extra fields for unlock scheme (pay-to-decrypt encrypted content) */
|
|
72
82
|
unlock?: s402UnlockExtra;
|
|
73
|
-
/**
|
|
74
|
-
|
|
83
|
+
/** Settlement overrides (used by upto scheme — server provides actual amount at settle-time) */
|
|
84
|
+
settlementOverrides?: s402SettlementOverrides;
|
|
75
85
|
/**
|
|
76
86
|
* Arbitrary extension data (forward-compatible extensibility).
|
|
77
87
|
*
|
|
@@ -83,34 +93,57 @@ interface s402PaymentRequirements {
|
|
|
83
93
|
*/
|
|
84
94
|
extensions?: Record<string, unknown>;
|
|
85
95
|
}
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Upto-specific requirements (usage-based, variable settlement).
|
|
98
|
+
*
|
|
99
|
+
* The client authorizes up to `maxAmount`; the facilitator settles the actual
|
|
100
|
+
* amount (provided by the server via `settlementOverrides`) at settlement time.
|
|
101
|
+
* Remainder is returned to the payer on-chain.
|
|
102
|
+
*
|
|
103
|
+
* TRUST MODEL: The client can bound its exposure via `settlementCeiling` in the
|
|
104
|
+
* payment payload — an on-chain-enforced cap tighter than `maxAmount`. The server
|
|
105
|
+
* advertises `estimatedAmount` so the client can set a tight ceiling (e.g., 1.2x
|
|
106
|
+
* the estimate). Without `settlementCeiling`, the facilitator can settle up to
|
|
107
|
+
* `maxAmount`. See ADR-003 §Decision 3 and §Decision 8.
|
|
108
|
+
*
|
|
109
|
+
* SEMANTIC CLARITY: `amount` on the parent `s402PaymentRequirements` is the
|
|
110
|
+
* EXACT price for the `exact` scheme. For `upto`, `maxAmount` here is the
|
|
111
|
+
* ceiling — the two are intentionally separate fields to avoid the semantic
|
|
112
|
+
* overloading that x402 suffers from.
|
|
113
|
+
*/
|
|
114
|
+
interface s402UptoExtra {
|
|
115
|
+
/** Maximum authorized amount in base units. Client deposits this; actual may be less. */
|
|
116
|
+
maxAmount: string;
|
|
117
|
+
/**
|
|
118
|
+
* Deadline for settlement in milliseconds since epoch.
|
|
119
|
+
* After this time, the payer can reclaim the full deposit via `expire()`.
|
|
120
|
+
* Must be in the future at verify-time. Facilitator MUST reject expired deposits.
|
|
121
|
+
*/
|
|
122
|
+
settlementDeadlineMs: string;
|
|
123
|
+
/** Optional URL where the client can query usage/metering data (informational) */
|
|
124
|
+
usageReportUrl?: string;
|
|
125
|
+
/**
|
|
126
|
+
* Server's estimated cost in base units (advisory, optional).
|
|
127
|
+
* Helps the client set a tight `settlementCeiling` in the payload.
|
|
128
|
+
* Must be ≤ maxAmount when present. Not enforced on-chain — purely informational.
|
|
129
|
+
*/
|
|
130
|
+
estimatedAmount?: string;
|
|
105
131
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Settlement overrides — server provides the actual amount to the facilitator.
|
|
134
|
+
*
|
|
135
|
+
* Used by the `upto` scheme: the resource server tells the facilitator how much
|
|
136
|
+
* of the authorized maximum to actually charge, based on observed usage.
|
|
137
|
+
* Threaded via `requirements.settlementOverrides` so the facilitator's `process()`
|
|
138
|
+
* signature (payload, requirements) doesn't need to change.
|
|
139
|
+
*
|
|
140
|
+
* TRUST MODEL: The server is trusted to report honest usage. The facilitator
|
|
141
|
+
* enforces `actualAmount <= maxAmount` but cannot verify usage independently.
|
|
142
|
+
* On-chain events provide an audit trail for dispute resolution.
|
|
143
|
+
*/
|
|
144
|
+
interface s402SettlementOverrides {
|
|
145
|
+
/** Actual amount to settle in base units. Must be ≤ maxAmount from UptoExtra. */
|
|
146
|
+
actualAmount: string;
|
|
114
147
|
}
|
|
115
148
|
/**
|
|
116
149
|
* Prepaid-specific requirements.
|
|
@@ -152,6 +185,35 @@ interface s402PrepaidExtra {
|
|
|
152
185
|
*/
|
|
153
186
|
disputeWindowMs?: string;
|
|
154
187
|
}
|
|
188
|
+
/** Stream-specific requirements */
|
|
189
|
+
interface s402StreamExtra {
|
|
190
|
+
/** Rate in base units per second */
|
|
191
|
+
ratePerSecond: string;
|
|
192
|
+
/** Maximum budget cap in base units */
|
|
193
|
+
budgetCap: string;
|
|
194
|
+
/** Minimum initial deposit in base units */
|
|
195
|
+
minDeposit: string;
|
|
196
|
+
/** URL for stream status checks (phase 2) */
|
|
197
|
+
streamSetupUrl?: string;
|
|
198
|
+
}
|
|
199
|
+
/** Escrow-specific requirements */
|
|
200
|
+
interface s402EscrowExtra {
|
|
201
|
+
/** Seller/payee address */
|
|
202
|
+
seller: string;
|
|
203
|
+
/** Arbiter address for dispute resolution */
|
|
204
|
+
arbiter?: string;
|
|
205
|
+
/** Escrow deadline in milliseconds since epoch */
|
|
206
|
+
deadlineMs: string;
|
|
207
|
+
}
|
|
208
|
+
/** Unlock-specific requirements (pay-to-decrypt encrypted content) */
|
|
209
|
+
interface s402UnlockExtra {
|
|
210
|
+
/** Encryption ID for key servers */
|
|
211
|
+
encryptionId: string;
|
|
212
|
+
/** Content identifier for the encrypted blob (e.g., Walrus blob ID, IPFS CID) */
|
|
213
|
+
encryptedContentId: string;
|
|
214
|
+
/** Identifier for the encryption service or module (e.g., Sui package ID, EVM contract address) */
|
|
215
|
+
encryptionServiceId: string;
|
|
216
|
+
}
|
|
155
217
|
/** Mandate requirements in a 402 response — tells client what mandate is needed */
|
|
156
218
|
interface s402MandateRequirements {
|
|
157
219
|
/** Whether a mandate is required (true) or optional (false = speeds up if present) */
|
|
@@ -193,6 +255,46 @@ interface s402ExactPayload extends s402PaymentPayloadBase {
|
|
|
193
255
|
signature: string;
|
|
194
256
|
};
|
|
195
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Upto payment: signed deposit transaction for variable-amount settlement.
|
|
260
|
+
*
|
|
261
|
+
* The client deposits `maxAmount` into an on-chain UptoDeposit proxy.
|
|
262
|
+
* The facilitator later calls `settle(actual_amount)` where
|
|
263
|
+
* `actual ≤ min(maxAmount, settlementCeiling)`, returning the remainder
|
|
264
|
+
* to the payer. If settlement doesn't happen before the deadline, the
|
|
265
|
+
* payer can reclaim via `expire()`.
|
|
266
|
+
*/
|
|
267
|
+
interface s402UptoPayload extends s402PaymentPayloadBase {
|
|
268
|
+
scheme: 'upto';
|
|
269
|
+
payload: {
|
|
270
|
+
/** Base64-encoded signed deposit transaction (creates UptoDeposit on-chain) */transaction: string; /** Base64-encoded signature */
|
|
271
|
+
signature: string; /** Maximum authorized amount (must match requirements.upto.maxAmount) */
|
|
272
|
+
maxAmount: string;
|
|
273
|
+
/**
|
|
274
|
+
* Client-chosen settlement ceiling (optional, on-chain enforced).
|
|
275
|
+
* The Move contract rejects settlements where `actualAmount > settlementCeiling`.
|
|
276
|
+
* Must satisfy: `1 <= settlementCeiling <= maxAmount`.
|
|
277
|
+
* Omit to allow settlement up to `maxAmount` (backwards compatible).
|
|
278
|
+
*
|
|
279
|
+
* Servers SHOULD check this before serving expensive resources — if
|
|
280
|
+
* `settlementCeiling < estimatedCost`, respond with an updated 402.
|
|
281
|
+
*/
|
|
282
|
+
settlementCeiling?: string;
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Prepaid payment: agent deposits into a PrepaidBalance shared object.
|
|
287
|
+
* This is the deposit phase only — claims are provider-initiated (not via HTTP 402).
|
|
288
|
+
*/
|
|
289
|
+
interface s402PrepaidPayload extends s402PaymentPayloadBase {
|
|
290
|
+
scheme: 'prepaid';
|
|
291
|
+
payload: {
|
|
292
|
+
/** Base64-encoded deposit PTB */transaction: string; /** Agent's signature */
|
|
293
|
+
signature: string; /** Committed rate per call (must match requirements) */
|
|
294
|
+
ratePerCall: string; /** Committed max calls cap (must match requirements) */
|
|
295
|
+
maxCalls?: string;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
196
298
|
/** Stream payment: signed stream creation transaction */
|
|
197
299
|
interface s402StreamPayload extends s402PaymentPayloadBase {
|
|
198
300
|
scheme: 'stream';
|
|
@@ -226,21 +328,8 @@ interface s402UnlockPayload extends s402PaymentPayloadBase {
|
|
|
226
328
|
encryptionId: string;
|
|
227
329
|
};
|
|
228
330
|
}
|
|
229
|
-
/**
|
|
230
|
-
* Prepaid payment: agent deposits into a PrepaidBalance shared object.
|
|
231
|
-
* This is the deposit phase only — claims are provider-initiated (not via HTTP 402).
|
|
232
|
-
*/
|
|
233
|
-
interface s402PrepaidPayload extends s402PaymentPayloadBase {
|
|
234
|
-
scheme: 'prepaid';
|
|
235
|
-
payload: {
|
|
236
|
-
/** Base64-encoded deposit PTB */transaction: string; /** Agent's signature */
|
|
237
|
-
signature: string; /** Committed rate per call (must match requirements) */
|
|
238
|
-
ratePerCall: string; /** Committed max calls cap (must match requirements) */
|
|
239
|
-
maxCalls?: string;
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
331
|
/** Discriminated union of all payment payloads */
|
|
243
|
-
type s402PaymentPayload = s402ExactPayload | s402StreamPayload | s402EscrowPayload | s402UnlockPayload
|
|
332
|
+
type s402PaymentPayload = s402ExactPayload | s402UptoPayload | s402PrepaidPayload | s402StreamPayload | s402EscrowPayload | s402UnlockPayload;
|
|
244
333
|
interface s402SettleResponse {
|
|
245
334
|
/** Whether settlement was successful */
|
|
246
335
|
success: boolean;
|
|
@@ -250,12 +339,16 @@ interface s402SettleResponse {
|
|
|
250
339
|
receiptId?: string;
|
|
251
340
|
/** Time to finality in milliseconds */
|
|
252
341
|
finalityMs?: number;
|
|
342
|
+
/** Actual amount settled in base units (for upto scheme — fixes x402's opacity) */
|
|
343
|
+
actualAmount?: string;
|
|
344
|
+
/** UptoDeposit object ID (for upto scheme) */
|
|
345
|
+
depositId?: string;
|
|
346
|
+
/** PrepaidBalance object ID (for prepaid scheme) */
|
|
347
|
+
balanceId?: string;
|
|
253
348
|
/** Stream object ID (for stream scheme) */
|
|
254
349
|
streamId?: string;
|
|
255
350
|
/** Escrow object ID (for escrow scheme) */
|
|
256
351
|
escrowId?: string;
|
|
257
|
-
/** PrepaidBalance object ID (for prepaid scheme) */
|
|
258
|
-
balanceId?: string;
|
|
259
352
|
/** Error message if settlement failed */
|
|
260
353
|
error?: string;
|
|
261
354
|
/** Typed error code for programmatic failure handling */
|
|
@@ -356,4 +449,4 @@ declare const S402_HEADERS: {
|
|
|
356
449
|
readonly STREAM_ID: "x-stream-id";
|
|
357
450
|
};
|
|
358
451
|
//#endregion
|
|
359
|
-
export { 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 };
|
|
452
|
+
export { S402_HEADERS, S402_VERSION, s402Discovery, s402EscrowExtra, s402EscrowPayload, s402ExactPayload, s402Mandate, s402MandateRequirements, s402PaymentPayload, s402PaymentPayloadBase, s402PaymentRequirements, s402PaymentSession, s402PrepaidExtra, s402PrepaidPayload, s402RegistryQuery, s402Scheme, s402ServiceEntry, s402SettleResponse, s402SettlementMode, s402SettlementOverrides, s402StreamExtra, s402StreamPayload, s402UnlockExtra, s402UnlockPayload, s402UptoExtra, s402UptoPayload, s402VerifyResponse };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s402",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "s402 — Chain-agnostic HTTP 402 wire format. Types, HTTP encoding, and scheme registry for
|
|
5
|
+
"description": "s402 — Chain-agnostic HTTP 402 wire format. Types, HTTP encoding, and scheme registry for six 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": {
|
|
@@ -89,6 +89,13 @@
|
|
|
89
89
|
},
|
|
90
90
|
"default": "./dist/receipts.mjs"
|
|
91
91
|
},
|
|
92
|
+
"./extensions": {
|
|
93
|
+
"import": {
|
|
94
|
+
"types": "./dist/extensions.d.mts",
|
|
95
|
+
"default": "./dist/extensions.mjs"
|
|
96
|
+
},
|
|
97
|
+
"default": "./dist/extensions.mjs"
|
|
98
|
+
},
|
|
92
99
|
"./test-utils": {
|
|
93
100
|
"import": {
|
|
94
101
|
"types": "./dist/test-utils.d.mts",
|