@bosonprotocol/x402-server 0.2.0 → 0.3.0-alpha-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/dist/esm/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { buildPaymentRequirements, signFullOffer } from './chunk-GBPU373M.js';
2
2
  export { buildPaymentRequirements, signFullOffer } from './chunk-GBPU373M.js';
3
- import { createFacilitatorClient } from './chunk-EAKQ4EFZ.js';
4
- export { createFacilitatorClient } from './chunk-EAKQ4EFZ.js';
5
- import { handleGetAvailableFunds, handleWithdrawFunds, handleDisputeEscalate, handleDisputeRetract, handleDisputeResolve, handleDisputeRaise, handleComplete, handleRedeem, handleCommitAndRedeem, handleCommit, stampFacilitatorEndpoints } from './chunk-Y5HFLCAT.js';
6
- export { ADDRESS_RE, DECIMAL_UINT_RE, HEX_BYTES_RE, emitNextActions, handleCommit, handleCommitAndRedeem, handleComplete, handleDisputeEscalate, handleDisputeRaise, handleDisputeResolve, handleDisputeRetract, handleGetAvailableFunds, handlePerformAction, handleRedeem, handleWithdrawFunds, handlerErr, handlerOk, plainHandlerOk, resolveEntityId } from './chunk-Y5HFLCAT.js';
7
- export { FacilitatorHttpError } from './chunk-PU5Y7FZG.js';
3
+ import { createFacilitatorClient } from './chunk-OMJEQEOF.js';
4
+ export { IDEMPOTENCY_KEY_HEADER, createFacilitatorClient } from './chunk-OMJEQEOF.js';
5
+ import { handleGetAvailableFunds, handleWithdrawFunds, handleDisputeEscalate, handleDisputeRetract, handleDisputeResolve, handleDisputeRaise, handleComplete, handleRedeem, handleCommitAndRedeem, handleCommit, stampFacilitatorEndpoints } from './chunk-SHYOIKYU.js';
6
+ export { ADDRESS_RE, DECIMAL_UINT_RE, HEX_BYTES_RE, emitNextActions, handleCommit, handleCommitAndRedeem, handleComplete, handleDisputeEscalate, handleDisputeRaise, handleDisputeResolve, handleDisputeRetract, handleGetAvailableFunds, handlePerformAction, handleRedeem, handleWithdrawFunds, handlerErr, handlerOk, plainHandlerOk, resolveEntityId } from './chunk-SHYOIKYU.js';
7
+ import { noopLogger } from './chunk-RAESVNQG.js';
8
+ export { FacilitatorHttpError, noopLogger } from './chunk-RAESVNQG.js';
8
9
  import './chunk-WU7QJ7YP.js';
9
10
  export { verifyExchange, verifyExchangeSnapshot } from './chunk-4NQ3VKC3.js';
10
11
  import './chunk-DJMCSCBE.js';
@@ -36,6 +37,31 @@ function createReadOnlyWeb3LibStub() {
36
37
  getCurrentTimeMs: () => Promise.reject(unreachable("getCurrentTimeMs"))
37
38
  };
38
39
  }
40
+
41
+ // src/store.ts
42
+ function mapAsStore(m) {
43
+ return {
44
+ async get(key) {
45
+ return m.get(key);
46
+ },
47
+ async set(key, value) {
48
+ m.set(key, value);
49
+ },
50
+ async delete(key) {
51
+ m.delete(key);
52
+ },
53
+ async *entries() {
54
+ for (const e of m.entries()) yield e;
55
+ }
56
+ };
57
+ }
58
+ function isStore(value) {
59
+ if (value === null || typeof value !== "object") return false;
60
+ const v = value;
61
+ return typeof v.get === "function" && typeof v.set === "function" && typeof v.delete === "function" && typeof v.entries === "function";
62
+ }
63
+
64
+ // src/config.ts
39
65
  var httpUrlSchema = z.string().url().refine((url) => url.startsWith("http://") || url.startsWith("https://"), {
40
66
  message: "must be an http(s) URL"
41
67
  });
@@ -58,22 +84,40 @@ var coreSdkReadShallowSchema = z.object({
58
84
  var fulfillmentChannelShallowSchema = z.object({
59
85
  id: z.string().min(1),
60
86
  validate: z.function(),
61
- onCommit: z.function()
87
+ onCommit: z.function(),
88
+ // Optional delivery dispatch — present on real channels, omitted by
89
+ // hosts that deliver out-of-band.
90
+ onFulfill: z.function().optional()
62
91
  }).passthrough();
92
+ var asyncStoreSchema = () => z.custom(isStore, {
93
+ message: "must implement the async Store<V> interface (get/set/delete/entries)"
94
+ });
63
95
  var x402bServerConfigSchema = z.object({
64
96
  network: evmNetworkSchema,
65
97
  chainId: z.number().int().positive(),
66
98
  escrow: addressSchema,
67
99
  signer: sellerSignerSchema,
68
100
  facilitator: z.object({
69
- url: httpUrlSchema
101
+ url: httpUrlSchema,
102
+ timeoutMs: z.number().int().positive().optional(),
103
+ retry: z.object({
104
+ attempts: z.number().int().positive(),
105
+ backoffMs: z.number().int().nonnegative()
106
+ }).strict().optional(),
107
+ idempotencyKey: z.function().args().returns(z.string()).optional()
70
108
  }).strict(),
71
109
  channelRegistry: channelRegistryZodSchema,
72
110
  exchangeReader: exchangeReaderShallowSchema.optional(),
73
111
  subgraphUrl: httpUrlSchema.optional(),
74
112
  coreSdkRead: coreSdkReadShallowSchema.optional(),
75
- exchangeFulfillmentOptionStore: z.instanceof(Map).optional(),
76
- fulfillmentRecoveryStore: z.instanceof(Map).optional(),
113
+ exchangeFulfillmentOptionStore: asyncStoreSchema().optional(),
114
+ fulfillmentRecoveryStore: asyncStoreSchema().optional(),
115
+ logger: z.object({
116
+ debug: z.function(),
117
+ info: z.function(),
118
+ warn: z.function(),
119
+ error: z.function()
120
+ }).passthrough().optional(),
77
121
  fulfillmentChannels: z.array(fulfillmentChannelShallowSchema).superRefine((channels, ctx) => {
78
122
  const seen = /* @__PURE__ */ new Set();
79
123
  const duplicates = /* @__PURE__ */ new Set();
@@ -87,7 +131,8 @@ var x402bServerConfigSchema = z.object({
87
131
  message: `fulfillmentChannels has duplicate id(s): ${[...duplicates].join(", ")}`
88
132
  });
89
133
  }
90
- }).optional()
134
+ }).optional(),
135
+ mode: z.enum(["development", "production"]).optional()
91
136
  }).strict().superRefine((cfg, ctx) => {
92
137
  const networkChainId = Number(cfg.network.split(":")[1]);
93
138
  if (networkChainId !== cfg.chainId) {
@@ -97,6 +142,36 @@ var x402bServerConfigSchema = z.object({
97
142
  message: `chainId (${cfg.chainId}) must match network (${cfg.network})`
98
143
  });
99
144
  }
145
+ if (cfg.mode === "production") {
146
+ if (cfg.exchangeReader === void 0) {
147
+ ctx.addIssue({
148
+ code: z.ZodIssueCode.custom,
149
+ path: ["exchangeReader"],
150
+ message: "required when mode is 'production' (post-settle state verification)"
151
+ });
152
+ }
153
+ if (cfg.coreSdkRead === void 0 && cfg.subgraphUrl === void 0) {
154
+ ctx.addIssue({
155
+ code: z.ZodIssueCode.custom,
156
+ path: ["subgraphUrl"],
157
+ message: "one of `coreSdkRead` or `subgraphUrl` is required when mode is 'production' (read client for withdraw / available-funds)"
158
+ });
159
+ }
160
+ if (cfg.exchangeFulfillmentOptionStore === void 0) {
161
+ ctx.addIssue({
162
+ code: z.ZodIssueCode.custom,
163
+ path: ["exchangeFulfillmentOptionStore"],
164
+ message: "required when mode is 'production'; without a persistent store the Flow A redeem-time option gate silently relaxes after a restart"
165
+ });
166
+ }
167
+ if (cfg.fulfillmentRecoveryStore === void 0) {
168
+ ctx.addIssue({
169
+ code: z.ZodIssueCode.custom,
170
+ path: ["fulfillmentRecoveryStore"],
171
+ message: "required when mode is 'production'; without a persistent store, post-settle channel-onCommit failures lose their replay handle"
172
+ });
173
+ }
174
+ }
100
175
  });
101
176
  function assertChannelRegistryEscrowMatch(config) {
102
177
  if (config.escrow.toLowerCase() !== config.channelRegistry.escrow.toLowerCase()) {
@@ -111,6 +186,52 @@ function asCoreSdkReadAdapter(coreSdk) {
111
186
  return coreSdk;
112
187
  }
113
188
 
189
+ // src/concurrency.ts
190
+ function createKeyedMutex() {
191
+ const chains = /* @__PURE__ */ new Map();
192
+ return {
193
+ runExclusive(key, fn) {
194
+ const prev = chains.get(key) ?? Promise.resolve();
195
+ const run = prev.then(fn, fn);
196
+ const tracked = run.catch(() => void 0);
197
+ chains.set(key, tracked);
198
+ tracked.then(() => {
199
+ if (chains.get(key) === tracked) chains.delete(key);
200
+ });
201
+ return run;
202
+ }
203
+ };
204
+ }
205
+
206
+ // src/health.ts
207
+ function createHealthCheck(deps) {
208
+ return async () => {
209
+ const facilitator = await probe(() => deps.facilitator.healthCheck()) ? "ok" : "down";
210
+ let readClient;
211
+ let readClientFailed = false;
212
+ if (typeof deps.coreSdkRead === "function") {
213
+ try {
214
+ readClient = deps.coreSdkRead();
215
+ } catch {
216
+ readClientFailed = true;
217
+ }
218
+ } else {
219
+ readClient = deps.coreSdkRead;
220
+ }
221
+ const subgraph = readClientFailed ? "down" : readClient === void 0 ? "n/a" : await probe(() => readClient.getSellersByAddress(ZERO_ADDRESS_PROBE)) ? "ok" : "down";
222
+ return { facilitator, subgraph };
223
+ };
224
+ }
225
+ async function probe(fn) {
226
+ try {
227
+ await fn();
228
+ return true;
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+ var ZERO_ADDRESS_PROBE = "0x0000000000000000000000000000000000000000";
234
+
114
235
  // src/server.ts
115
236
  function withFacilitatorEndpoints(requirements, facilitatorUrl) {
116
237
  return {
@@ -124,9 +245,23 @@ function withFacilitatorEndpoints(requirements, facilitatorUrl) {
124
245
  function createX402bServer(config) {
125
246
  const validated = x402bServerConfigSchema.parse(config);
126
247
  assertChannelRegistryEscrowMatch(validated);
127
- const facilitator = createFacilitatorClient({ url: validated.facilitator.url });
128
- const exchangeFulfillmentOptionStore = validated.exchangeFulfillmentOptionStore ?? /* @__PURE__ */ new Map();
129
- const fulfillmentRecoveryStore = validated.fulfillmentRecoveryStore ?? /* @__PURE__ */ new Map();
248
+ const logger = validated.logger ?? noopLogger;
249
+ logger.info("x402-server: createX402bServer", {
250
+ network: validated.network,
251
+ chainId: validated.chainId,
252
+ escrow: validated.escrow,
253
+ facilitatorUrl: validated.facilitator.url
254
+ });
255
+ const facilitator = createFacilitatorClient({
256
+ url: validated.facilitator.url,
257
+ logger,
258
+ ...validated.facilitator.timeoutMs !== void 0 ? { timeoutMs: validated.facilitator.timeoutMs } : {},
259
+ ...validated.facilitator.retry !== void 0 ? { retry: validated.facilitator.retry } : {},
260
+ ...validated.facilitator.idempotencyKey !== void 0 ? { idempotencyKey: validated.facilitator.idempotencyKey } : {}
261
+ });
262
+ const exchangeMutex = createKeyedMutex();
263
+ const exchangeFulfillmentOptionStore = validated.exchangeFulfillmentOptionStore ?? mapAsStore(/* @__PURE__ */ new Map());
264
+ const fulfillmentRecoveryStore = validated.fulfillmentRecoveryStore ?? mapAsStore(/* @__PURE__ */ new Map());
130
265
  validated.exchangeFulfillmentOptionStore = exchangeFulfillmentOptionStore;
131
266
  validated.fulfillmentRecoveryStore = fulfillmentRecoveryStore;
132
267
  const signOffer = (unsigned) => signFullOffer({
@@ -144,14 +279,10 @@ function createX402bServer(config) {
144
279
  return validated.exchangeReader;
145
280
  };
146
281
  let cachedCoreSdkRead;
147
- const requireCoreSdkRead = (action) => {
282
+ const getOrCreateCoreSdkRead = () => {
148
283
  if (validated.coreSdkRead !== void 0) return validated.coreSdkRead;
149
284
  if (cachedCoreSdkRead !== void 0) return cachedCoreSdkRead;
150
- if (validated.subgraphUrl === void 0) {
151
- throw new Error(
152
- `x402-server: handlers.${action}() requires either \`coreSdkRead\` or \`subgraphUrl\` in config (subgraph read step).`
153
- );
154
- }
285
+ if (validated.subgraphUrl === void 0) return void 0;
155
286
  cachedCoreSdkRead = asCoreSdkReadAdapter(
156
287
  new CoreSDK({
157
288
  web3Lib: createReadOnlyWeb3LibStub(),
@@ -162,10 +293,106 @@ function createX402bServer(config) {
162
293
  );
163
294
  return cachedCoreSdkRead;
164
295
  };
296
+ const requireCoreSdkRead = (action) => {
297
+ const client = getOrCreateCoreSdkRead();
298
+ if (client === void 0) {
299
+ throw new Error(
300
+ `x402-server: handlers.${action}() requires either \`coreSdkRead\` or \`subgraphUrl\` in config (subgraph read step).`
301
+ );
302
+ }
303
+ return client;
304
+ };
305
+ const recovery = {
306
+ async list() {
307
+ const entries = [];
308
+ for await (const [, value] of fulfillmentRecoveryStore.entries()) {
309
+ entries.push(value);
310
+ }
311
+ return entries;
312
+ },
313
+ async replay(exchangeId) {
314
+ const entry = await fulfillmentRecoveryStore.get(exchangeId);
315
+ if (entry === void 0) {
316
+ return { ok: false, reason: `no pending recovery entry for exchangeId '${exchangeId}'` };
317
+ }
318
+ const channels = validated.fulfillmentChannels ?? [];
319
+ const channel = channels.find((c) => c.id === entry.option);
320
+ if (channel === void 0) {
321
+ const reason = `no channel adapter is registered for option '${entry.option}'`;
322
+ await fulfillmentRecoveryStore.set(exchangeId, { ...entry, error: reason });
323
+ logger.warn("x402-server: recovery replay failed (no adapter)", {
324
+ exchangeId,
325
+ option: entry.option
326
+ });
327
+ return { ok: false, reason };
328
+ }
329
+ if (entry.phase === "delivery") {
330
+ if (channel.onFulfill === void 0) {
331
+ const reason = `channel '${entry.option}' has no onFulfill; cannot replay delivery phase`;
332
+ await fulfillmentRecoveryStore.set(exchangeId, { ...entry, error: reason });
333
+ logger.warn("x402-server: recovery replay failed (no onFulfill)", {
334
+ exchangeId,
335
+ option: entry.option
336
+ });
337
+ return { ok: false, reason };
338
+ }
339
+ try {
340
+ await channel.onFulfill(exchangeId);
341
+ await fulfillmentRecoveryStore.delete(exchangeId);
342
+ logger.info("x402-server: recovery replay succeeded", {
343
+ exchangeId,
344
+ option: entry.option,
345
+ phase: entry.phase
346
+ });
347
+ return { ok: true };
348
+ } catch (e) {
349
+ const reason = e instanceof Error ? e.message : String(e);
350
+ await fulfillmentRecoveryStore.set(exchangeId, { ...entry, error: reason });
351
+ logger.warn("x402-server: recovery replay failed (channel error)", {
352
+ exchangeId,
353
+ option: entry.option,
354
+ phase: entry.phase,
355
+ error: reason
356
+ });
357
+ return { ok: false, reason };
358
+ }
359
+ }
360
+ try {
361
+ await channel.onCommit(exchangeId, entry.data);
362
+ await fulfillmentRecoveryStore.delete(exchangeId);
363
+ logger.info("x402-server: recovery replay succeeded", {
364
+ exchangeId,
365
+ option: entry.option,
366
+ phase: entry.phase
367
+ });
368
+ return { ok: true };
369
+ } catch (e) {
370
+ const reason = e instanceof Error ? e.message : String(e);
371
+ await fulfillmentRecoveryStore.set(exchangeId, { ...entry, error: reason });
372
+ logger.warn("x402-server: recovery replay failed (channel error)", {
373
+ exchangeId,
374
+ option: entry.option,
375
+ phase: entry.phase,
376
+ error: reason
377
+ });
378
+ return { ok: false, reason };
379
+ }
380
+ }
381
+ };
382
+ const healthCheck = createHealthCheck({
383
+ facilitator,
384
+ // Materialise the lazy `subgraphUrl`-backed read client on first
385
+ // probe so a configured subgraph is actually checked (reporting
386
+ // `"ok"` / `"down"` rather than `"n/a"`). Only the
387
+ // "no `coreSdkRead` AND no `subgraphUrl`" case maps to `"n/a"`.
388
+ coreSdkRead: () => getOrCreateCoreSdkRead()
389
+ });
165
390
  return {
166
391
  config: validated,
167
392
  facilitator,
168
393
  signOffer,
394
+ recovery,
395
+ healthCheck,
169
396
  async buildPaymentRequirements(input) {
170
397
  const offer = "unsigned" in input.offer ? await signOffer(input.offer.unsigned) : input.offer;
171
398
  const requirements = buildPaymentRequirements({
@@ -188,47 +415,73 @@ function createX402bServer(config) {
188
415
  facilitator,
189
416
  exchangeReader: await requireReader("commit"),
190
417
  fulfillmentRecoveryStore,
191
- exchangeFulfillmentOptionStore
418
+ exchangeFulfillmentOptionStore,
419
+ logger
192
420
  }),
193
421
  commitAndRedeem: async (input) => handleCommitAndRedeem(input, {
194
422
  config: validated,
195
423
  facilitator,
196
424
  exchangeReader: await requireReader("commitAndRedeem"),
197
425
  fulfillmentRecoveryStore,
198
- exchangeFulfillmentOptionStore
199
- }),
200
- redeem: async (input) => handleRedeem(input, {
201
- config: validated,
202
- facilitator,
203
- exchangeReader: await requireReader("redeem"),
204
426
  exchangeFulfillmentOptionStore,
205
- fulfillmentRecoveryStore
206
- }),
207
- complete: async (input) => handleComplete(input, {
208
- config: validated,
209
- facilitator,
210
- exchangeReader: await requireReader("complete")
211
- }),
212
- disputeRaise: async (input) => handleDisputeRaise(input, {
213
- config: validated,
214
- facilitator,
215
- exchangeReader: await requireReader("disputeRaise")
216
- }),
217
- disputeResolve: async (input) => handleDisputeResolve(input, {
218
- config: validated,
219
- facilitator,
220
- exchangeReader: await requireReader("disputeResolve")
221
- }),
222
- disputeRetract: async (input) => handleDisputeRetract(input, {
223
- config: validated,
224
- facilitator,
225
- exchangeReader: await requireReader("disputeRetract")
226
- }),
227
- disputeEscalate: async (input) => handleDisputeEscalate(input, {
228
- config: validated,
229
- facilitator,
230
- exchangeReader: await requireReader("disputeEscalate")
427
+ logger
231
428
  }),
429
+ redeem: async (input) => exchangeMutex.runExclusive(
430
+ input.exchangeId,
431
+ async () => handleRedeem(input, {
432
+ config: validated,
433
+ facilitator,
434
+ exchangeReader: await requireReader("redeem"),
435
+ exchangeFulfillmentOptionStore,
436
+ fulfillmentRecoveryStore,
437
+ logger
438
+ })
439
+ ),
440
+ complete: async (input) => exchangeMutex.runExclusive(
441
+ input.exchangeId,
442
+ async () => handleComplete(input, {
443
+ config: validated,
444
+ facilitator,
445
+ exchangeReader: await requireReader("complete"),
446
+ logger
447
+ })
448
+ ),
449
+ disputeRaise: async (input) => exchangeMutex.runExclusive(
450
+ input.exchangeId,
451
+ async () => handleDisputeRaise(input, {
452
+ config: validated,
453
+ facilitator,
454
+ exchangeReader: await requireReader("disputeRaise"),
455
+ logger
456
+ })
457
+ ),
458
+ disputeResolve: async (input) => exchangeMutex.runExclusive(
459
+ input.exchangeId,
460
+ async () => handleDisputeResolve(input, {
461
+ config: validated,
462
+ facilitator,
463
+ exchangeReader: await requireReader("disputeResolve"),
464
+ logger
465
+ })
466
+ ),
467
+ disputeRetract: async (input) => exchangeMutex.runExclusive(
468
+ input.exchangeId,
469
+ async () => handleDisputeRetract(input, {
470
+ config: validated,
471
+ facilitator,
472
+ exchangeReader: await requireReader("disputeRetract"),
473
+ logger
474
+ })
475
+ ),
476
+ disputeEscalate: async (input) => exchangeMutex.runExclusive(
477
+ input.exchangeId,
478
+ async () => handleDisputeEscalate(input, {
479
+ config: validated,
480
+ facilitator,
481
+ exchangeReader: await requireReader("disputeEscalate"),
482
+ logger
483
+ })
484
+ ),
232
485
  withdrawFunds: async (input) => handleWithdrawFunds(input, {
233
486
  config: validated,
234
487
  facilitator,
@@ -256,6 +509,6 @@ function encodeXPaymentResponse(body) {
256
509
  }
257
510
  var X_PAYMENT_RESPONSE_HEADER = "X-PAYMENT-RESPONSE";
258
511
 
259
- export { X_PAYMENT_RESPONSE_HEADER, asCoreSdkReadAdapter, assertChannelRegistryEscrowMatch, createX402bServer, encodeXPaymentResponse, x402bServerConfigSchema };
512
+ export { X_PAYMENT_RESPONSE_HEADER, asCoreSdkReadAdapter, assertChannelRegistryEscrowMatch, createHealthCheck, createKeyedMutex, createX402bServer, encodeXPaymentResponse, isStore, mapAsStore, x402bServerConfigSchema };
260
513
  //# sourceMappingURL=index.js.map
261
514
  //# sourceMappingURL=index.js.map