@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.
@@ -0,0 +1,223 @@
1
+ import { noopLogger, FacilitatorHttpError } from './chunk-RAESVNQG.js';
2
+
3
+ // src/facilitator/client.ts
4
+ var DEFAULT_TIMEOUT_MS = 3e4;
5
+ var DEFAULT_RETRY = { attempts: 3, backoffMs: 200 };
6
+ var IDEMPOTENCY_KEY_HEADER = "x-x402b-idempotency-key";
7
+ function createFacilitatorClient(opts) {
8
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
9
+ if (fetchImpl === void 0) {
10
+ throw new Error(
11
+ "createFacilitatorClient: no `fetch` implementation available. Pass `opts.fetch` explicitly."
12
+ );
13
+ }
14
+ const baseUrl = opts.url.replace(/\/+$/, "");
15
+ const baseHeaders = { "content-type": "application/json", ...opts.headers ?? {} };
16
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
17
+ const retry = opts.retry ?? DEFAULT_RETRY;
18
+ if (!Number.isInteger(retry.attempts) || retry.attempts < 1) {
19
+ throw new Error("createFacilitatorClient: retry.attempts must be an integer >= 1");
20
+ }
21
+ if (!Number.isFinite(retry.backoffMs) || retry.backoffMs < 0) {
22
+ throw new Error("createFacilitatorClient: retry.backoffMs must be a finite number >= 0");
23
+ }
24
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
25
+ throw new Error("createFacilitatorClient: timeoutMs must be a finite number > 0");
26
+ }
27
+ const newIdempotencyKey = opts.idempotencyKey ?? (() => globalThis.crypto.randomUUID());
28
+ const setTimeoutImpl = opts.setTimeout ?? setTimeout;
29
+ const clearTimeoutImpl = opts.clearTimeout ?? clearTimeout;
30
+ const logger = opts.logger ?? noopLogger;
31
+ const postOnce = async (path, body, validate, extraHeaders) => {
32
+ const controller = new AbortController();
33
+ const timer = setTimeoutImpl(() => controller.abort(), timeoutMs);
34
+ const headers = { ...baseHeaders, ...extraHeaders };
35
+ let res;
36
+ try {
37
+ res = await fetchImpl(`${baseUrl}${path}`, {
38
+ method: "POST",
39
+ headers,
40
+ body: JSON.stringify(body),
41
+ signal: controller.signal
42
+ });
43
+ } catch (cause) {
44
+ const aborted = controller.signal.aborted;
45
+ logger.warn(aborted ? "facilitator request timed out" : "facilitator network error", {
46
+ path,
47
+ timeoutMs: aborted ? timeoutMs : void 0,
48
+ error: cause instanceof Error ? cause.message : String(cause)
49
+ });
50
+ throw new FacilitatorHttpError(
51
+ aborted ? `facilitator request timed out after ${timeoutMs}ms (${path})` : `facilitator network error (${path})`,
52
+ {
53
+ code: aborted ? "TIMEOUT" : "NETWORK_ERROR",
54
+ cause
55
+ }
56
+ );
57
+ } finally {
58
+ clearTimeoutImpl(timer);
59
+ }
60
+ let text;
61
+ try {
62
+ text = await res.text();
63
+ } catch (cause) {
64
+ throw new FacilitatorHttpError(`facilitator response body could not be read (${path})`, {
65
+ code: "BAD_RESPONSE_BODY",
66
+ status: res.status,
67
+ cause
68
+ });
69
+ }
70
+ let parsed;
71
+ try {
72
+ parsed = text.length === 0 ? null : JSON.parse(text);
73
+ } catch (cause) {
74
+ throw new FacilitatorHttpError(`facilitator returned non-JSON body (${path})`, {
75
+ code: "BAD_RESPONSE_BODY",
76
+ status: res.status,
77
+ cause
78
+ });
79
+ }
80
+ if (!res.ok) {
81
+ if (res.status === 400 && isFailureBranch(parsed) && validate(parsed)) {
82
+ return parsed;
83
+ }
84
+ const facilitatorCode = extractFacilitatorCode(parsed);
85
+ logger.warn("facilitator HTTP non-2xx", {
86
+ path,
87
+ status: res.status,
88
+ facilitatorCode,
89
+ reason: reasonString(parsed)
90
+ });
91
+ throw new FacilitatorHttpError(
92
+ `facilitator HTTP ${res.status} (${path}): ${reasonString(parsed) ?? text}`,
93
+ {
94
+ code: "BAD_HTTP_STATUS",
95
+ status: res.status,
96
+ ...facilitatorCode !== void 0 ? { facilitatorCode } : {}
97
+ }
98
+ );
99
+ }
100
+ if (!validate(parsed)) {
101
+ throw new FacilitatorHttpError(`facilitator returned unexpected body shape (${path})`, {
102
+ code: "BAD_RESPONSE_BODY",
103
+ status: res.status
104
+ });
105
+ }
106
+ return parsed;
107
+ };
108
+ const post = async (path, body, validate, extraHeaders = {}) => {
109
+ let lastError;
110
+ for (let attempt = 0; attempt < retry.attempts; attempt += 1) {
111
+ if (attempt > 0) {
112
+ await sleep(retry.backoffMs * attempt, setTimeoutImpl);
113
+ }
114
+ try {
115
+ return await postOnce(path, body, validate, extraHeaders);
116
+ } catch (e) {
117
+ if (e instanceof FacilitatorHttpError && isRetryable(e)) {
118
+ lastError = e;
119
+ continue;
120
+ }
121
+ throw e;
122
+ }
123
+ }
124
+ throw lastError;
125
+ };
126
+ return {
127
+ verify: (input) => post("/verify", input, isVerifyResult),
128
+ settle: (input) => post("/settle", input, isSettleResult, {
129
+ [IDEMPOTENCY_KEY_HEADER]: newIdempotencyKey()
130
+ }),
131
+ performAction: (input) => post(
132
+ `/perform-action?action=${encodeURIComponent(input.action)}`,
133
+ input,
134
+ isPerformActionResult
135
+ ),
136
+ async healthCheck() {
137
+ const controller = new AbortController();
138
+ const timer = setTimeoutImpl(() => controller.abort(), timeoutMs);
139
+ let res;
140
+ try {
141
+ res = await fetchImpl(`${baseUrl}/healthz`, {
142
+ method: "GET",
143
+ headers: { ...opts.headers ?? {} },
144
+ signal: controller.signal
145
+ });
146
+ } catch (cause) {
147
+ const aborted = controller.signal.aborted;
148
+ throw new FacilitatorHttpError(
149
+ aborted ? `facilitator request timed out after ${timeoutMs}ms (/healthz)` : "facilitator network error (/healthz)",
150
+ {
151
+ code: aborted ? "TIMEOUT" : "NETWORK_ERROR",
152
+ cause
153
+ }
154
+ );
155
+ } finally {
156
+ clearTimeoutImpl(timer);
157
+ }
158
+ if (!res.ok) {
159
+ throw new FacilitatorHttpError(`facilitator HTTP ${res.status} (/healthz)`, {
160
+ code: "BAD_HTTP_STATUS",
161
+ status: res.status
162
+ });
163
+ }
164
+ }
165
+ };
166
+ }
167
+ function isRetryable(e) {
168
+ if (e.code === "NETWORK_ERROR" || e.code === "TIMEOUT") return true;
169
+ if (e.code === "BAD_HTTP_STATUS" && e.status !== void 0 && e.status >= 500) return true;
170
+ return false;
171
+ }
172
+ function sleep(ms, setTimeoutImpl) {
173
+ if (ms <= 0) return Promise.resolve();
174
+ return new Promise((resolve) => {
175
+ setTimeoutImpl(resolve, ms);
176
+ });
177
+ }
178
+ function isObject(v) {
179
+ return v !== null && typeof v === "object";
180
+ }
181
+ function isFailureBranch(v) {
182
+ return isObject(v) && v.ok === false && typeof v.code === "string" && typeof v.reason === "string";
183
+ }
184
+ function isVerifyResult(parsed) {
185
+ if (!isObject(parsed)) return false;
186
+ if (parsed.ok === true) return true;
187
+ return isFailureBranch(parsed);
188
+ }
189
+ function isSettleResult(parsed) {
190
+ if (!isObject(parsed)) return false;
191
+ if (parsed.ok === true) {
192
+ return typeof parsed.exchangeId === "string" && typeof parsed.txHash === "string";
193
+ }
194
+ return isFailureBranch(parsed);
195
+ }
196
+ function isPerformActionResult(parsed) {
197
+ if (!isObject(parsed)) return false;
198
+ if (parsed.ok === true) {
199
+ if (typeof parsed.txHash !== "string") return false;
200
+ if (parsed.newExchangeState !== void 0 && typeof parsed.newExchangeState !== "string") {
201
+ return false;
202
+ }
203
+ if (parsed.newDisputeState !== void 0 && typeof parsed.newDisputeState !== "string") {
204
+ return false;
205
+ }
206
+ return true;
207
+ }
208
+ return isFailureBranch(parsed);
209
+ }
210
+ function extractFacilitatorCode(body) {
211
+ if (body === null || typeof body !== "object") return void 0;
212
+ const code = body.code;
213
+ return typeof code === "string" ? code : void 0;
214
+ }
215
+ function reasonString(body) {
216
+ if (body === null || typeof body !== "object") return void 0;
217
+ const reason = body.reason;
218
+ return typeof reason === "string" ? reason : void 0;
219
+ }
220
+
221
+ export { IDEMPOTENCY_KEY_HEADER, createFacilitatorClient };
222
+ //# sourceMappingURL=chunk-OMJEQEOF.js.map
223
+ //# sourceMappingURL=chunk-OMJEQEOF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/facilitator/client.ts"],"names":[],"mappings":"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","file":"chunk-OMJEQEOF.js","sourcesContent":["// HTTP client for the Boson facilitator service. The facilitator runs\n// as a remote process implementing the three endpoints in\n// docs/boson-impl-07-facilitator.md (`/verify`, `/settle`,\n// `/perform-action?action=<action>`); this module wraps `fetch` with the typed\n// request/response shapes exported by `@bosonprotocol/x402-facilitator`.\n//\n// Domain results (`{ ok: true, ... }` or `{ ok: false, code, reason }`)\n// are returned verbatim — both shapes are answers the caller branches\n// on. The facilitator-express adapter emits domain failures over HTTP\n// 400 (so curl users still see a 4xx), but the wire body is still a\n// well-formed result; we recognise that shape on HTTP 400 responses and\n// hand it back rather than throwing. HTTP-transport failures (network\n// down, non-JSON body, schema-mismatched body) throw\n// `FacilitatorHttpError` so the composition layer can distinguish\n// \"facilitator said no\" from \"we couldn't reach the facilitator\".\n//\n// Transport hardening: every request enforces a per-attempt timeout via\n// `AbortController` and retries `NETWORK_ERROR` / `TIMEOUT` / 5xx with\n// linear backoff. `/settle` calls additionally carry a stable\n// `x-x402b-idempotency-key` header so a facilitator-side dedup table\n// (separate package) can recognise a retried request as the same\n// underlying intent.\n\nimport type {\n FacilitatorErrorCode,\n FacilitatorPerformActionInput,\n FacilitatorPerformActionResult,\n FacilitatorSettleInput,\n FacilitatorSettleResult,\n FacilitatorVerifyInput,\n FacilitatorVerifyResult,\n} from \"@bosonprotocol/x402-facilitator\";\n\nimport { FacilitatorHttpError } from \"./errors.js\";\nimport { noopLogger, type Logger } from \"../logger.js\";\n\nexport type FetchLike = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n text: () => Promise<string>;\n json?: () => Promise<unknown>;\n}>;\n\nexport interface FacilitatorRetryOptions {\n /** Total attempt budget (initial try + retries). Default 3. */\n attempts: number;\n /** Linear-backoff multiplier in ms: sleep `backoffMs * attemptIndex` between attempts. Default 200. */\n backoffMs: number;\n}\n\nexport interface CreateFacilitatorClientOptions {\n /** Base URL of the facilitator service (no trailing slash needed). */\n url: string;\n /** Optional `fetch` override — defaults to the global `fetch`. Useful for tests + non-`fetch` runtimes. */\n fetch?: FetchLike;\n /** Optional headers attached to every request — e.g. an `Authorization` for hosted facilitators. */\n headers?: Record<string, string>;\n /**\n * Per-attempt timeout in ms. Default 30_000. The `AbortController`\n * aborts in-flight fetches when this elapses; the error surfaces as\n * `FacilitatorHttpError` with code `TIMEOUT` and is retried under\n * the same policy as `NETWORK_ERROR`.\n */\n timeoutMs?: number;\n /**\n * Retry policy applied to `NETWORK_ERROR`, `TIMEOUT`, and HTTP 5xx\n * responses. Defaults to `{ attempts: 3, backoffMs: 200 }` — three\n * attempts total at 0/200/400 ms. Set `{ attempts: 1, backoffMs: 0 }`\n * to disable.\n */\n retry?: FacilitatorRetryOptions;\n /**\n * Factory for the `x-x402b-idempotency-key` header attached to\n * `/settle` calls. Called once per logical request — *not* once per\n * retry attempt — so the facilitator's dedup table can recognise\n * retries of the same intent. Defaults to `crypto.randomUUID`.\n */\n idempotencyKey?: () => string;\n /** Override for `setTimeout` — tests use this to skip real backoff sleeps. */\n setTimeout?: typeof setTimeout;\n /** Override for `clearTimeout` — paired with `setTimeout`. */\n clearTimeout?: typeof clearTimeout;\n /** Optional structured logger. Defaults to no-op. Receives `warn` on errored requests; `debug` on each call. */\n logger?: Logger;\n}\n\nexport interface FacilitatorClient {\n verify(input: FacilitatorVerifyInput): Promise<FacilitatorVerifyResult>;\n settle(input: FacilitatorSettleInput): Promise<FacilitatorSettleResult>;\n performAction(input: FacilitatorPerformActionInput): Promise<FacilitatorPerformActionResult>;\n /**\n * GET `/healthz` liveness probe. Resolves on any 2xx; throws on\n * network error or non-2xx. Designed for the server SDK's\n * `healthCheck()` helper — hosts mount that one behind whatever\n * `/healthz` route their framework uses.\n */\n healthCheck(): Promise<void>;\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_RETRY: FacilitatorRetryOptions = { attempts: 3, backoffMs: 200 };\n\n/** Idempotency-key header name. Stable so a facilitator-side dedup table can recognise it. */\nexport const IDEMPOTENCY_KEY_HEADER = \"x-x402b-idempotency-key\";\n\n/**\n * Construct a typed HTTP client for the facilitator. The returned\n * methods stringify the input as JSON and POST to the matching path\n * on the configured URL. Bodies matching the well-formed result shape\n * (whether HTTP 2xx success or HTTP 400 domain rejection) are returned\n * verbatim; transport failures (network, non-JSON body,\n * schema-mismatched body) raise `FacilitatorHttpError`.\n */\nexport function createFacilitatorClient(opts: CreateFacilitatorClientOptions): FacilitatorClient {\n const fetchImpl = opts.fetch ?? (globalThis.fetch as FetchLike | undefined);\n if (fetchImpl === undefined) {\n throw new Error(\n \"createFacilitatorClient: no `fetch` implementation available. Pass `opts.fetch` explicitly.\",\n );\n }\n const baseUrl = opts.url.replace(/\\/+$/, \"\");\n const baseHeaders = { \"content-type\": \"application/json\", ...(opts.headers ?? {}) };\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const retry = opts.retry ?? DEFAULT_RETRY;\n // Defensive validation: `x402bServerConfigSchema` enforces these\n // bounds when the client is built via `createX402bServer`, but\n // `createFacilitatorClient` is also exported for direct use — a bad\n // `attempts` would silently fall through the retry loop and throw\n // `undefined` instead of a `FacilitatorHttpError`.\n if (!Number.isInteger(retry.attempts) || retry.attempts < 1) {\n throw new Error(\"createFacilitatorClient: retry.attempts must be an integer >= 1\");\n }\n if (!Number.isFinite(retry.backoffMs) || retry.backoffMs < 0) {\n throw new Error(\"createFacilitatorClient: retry.backoffMs must be a finite number >= 0\");\n }\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new Error(\"createFacilitatorClient: timeoutMs must be a finite number > 0\");\n }\n const newIdempotencyKey = opts.idempotencyKey ?? (() => globalThis.crypto.randomUUID());\n const setTimeoutImpl = opts.setTimeout ?? setTimeout;\n const clearTimeoutImpl = opts.clearTimeout ?? clearTimeout;\n const logger: Logger = opts.logger ?? noopLogger;\n\n const postOnce = async <Req, Res>(\n path: string,\n body: Req,\n validate: (parsed: unknown) => parsed is Res,\n extraHeaders: Record<string, string>,\n ): Promise<Res> => {\n const controller = new AbortController();\n const timer = setTimeoutImpl(() => controller.abort(), timeoutMs);\n const headers = { ...baseHeaders, ...extraHeaders };\n\n let res: Awaited<ReturnType<FetchLike>>;\n try {\n res = await fetchImpl(`${baseUrl}${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n } catch (cause) {\n const aborted = controller.signal.aborted;\n logger.warn(aborted ? \"facilitator request timed out\" : \"facilitator network error\", {\n path,\n timeoutMs: aborted ? timeoutMs : undefined,\n error: cause instanceof Error ? cause.message : String(cause),\n });\n throw new FacilitatorHttpError(\n aborted\n ? `facilitator request timed out after ${timeoutMs}ms (${path})`\n : `facilitator network error (${path})`,\n {\n code: aborted ? \"TIMEOUT\" : \"NETWORK_ERROR\",\n cause,\n },\n );\n } finally {\n clearTimeoutImpl(timer);\n }\n\n let text: string;\n try {\n text = await res.text();\n } catch (cause) {\n throw new FacilitatorHttpError(`facilitator response body could not be read (${path})`, {\n code: \"BAD_RESPONSE_BODY\",\n status: res.status,\n cause,\n });\n }\n\n let parsed: unknown;\n try {\n parsed = text.length === 0 ? null : JSON.parse(text);\n } catch (cause) {\n throw new FacilitatorHttpError(`facilitator returned non-JSON body (${path})`, {\n code: \"BAD_RESPONSE_BODY\",\n status: res.status,\n cause,\n });\n }\n\n if (!res.ok) {\n // The facilitator-express adapter returns domain failures as\n // HTTP 400 with the `{ok:false, code, reason}` body. Detect that\n // shape and surface it as the typed result — every endpoint's\n // `Res` union includes the same `{ok:false, code, reason}`\n // variant, so the cast through `validate` (which also accepts\n // that shape) preserves type-safety. Any other non-2xx status is\n // a genuine transport failure even if its body happens to look\n // like a facilitator result.\n if (res.status === 400 && isFailureBranch(parsed) && validate(parsed)) {\n return parsed;\n }\n const facilitatorCode = extractFacilitatorCode(parsed);\n logger.warn(\"facilitator HTTP non-2xx\", {\n path,\n status: res.status,\n facilitatorCode,\n reason: reasonString(parsed),\n });\n throw new FacilitatorHttpError(\n `facilitator HTTP ${res.status} (${path}): ${reasonString(parsed) ?? text}`,\n {\n code: \"BAD_HTTP_STATUS\",\n status: res.status,\n ...(facilitatorCode !== undefined ? { facilitatorCode } : {}),\n },\n );\n }\n\n if (!validate(parsed)) {\n throw new FacilitatorHttpError(`facilitator returned unexpected body shape (${path})`, {\n code: \"BAD_RESPONSE_BODY\",\n status: res.status,\n });\n }\n return parsed;\n };\n\n const post = async <Req, Res>(\n path: string,\n body: Req,\n validate: (parsed: unknown) => parsed is Res,\n extraHeaders: Record<string, string> = {},\n ): Promise<Res> => {\n let lastError: FacilitatorHttpError | undefined;\n for (let attempt = 0; attempt < retry.attempts; attempt += 1) {\n if (attempt > 0) {\n await sleep(retry.backoffMs * attempt, setTimeoutImpl);\n }\n try {\n return await postOnce(path, body, validate, extraHeaders);\n } catch (e) {\n if (e instanceof FacilitatorHttpError && isRetryable(e)) {\n lastError = e;\n continue;\n }\n throw e;\n }\n }\n // Non-null assertion: the loop above sets `lastError` on every retryable\n // failure and only falls through after the final attempt also failed.\n throw lastError as FacilitatorHttpError;\n };\n\n return {\n verify: (input) =>\n post<FacilitatorVerifyInput, FacilitatorVerifyResult>(\"/verify\", input, isVerifyResult),\n settle: (input) =>\n post<FacilitatorSettleInput, FacilitatorSettleResult>(\"/settle\", input, isSettleResult, {\n [IDEMPOTENCY_KEY_HEADER]: newIdempotencyKey(),\n }),\n performAction: (input) =>\n post<FacilitatorPerformActionInput, FacilitatorPerformActionResult>(\n `/perform-action?action=${encodeURIComponent(input.action)}`,\n input,\n isPerformActionResult,\n ),\n async healthCheck() {\n // GET /healthz — body is ignored. Any 2xx is healthy; anything\n // else (network error, timeout, non-2xx) raises FacilitatorHttpError\n // so the `createHealthCheck` helper maps to \"down\". The abort\n // timer mirrors `postOnce` so a stuck connection can't hang the\n // host's /healthz route indefinitely. `opts.headers` (e.g.\n // `Authorization` for hosted facilitators) must ride along here\n // too — otherwise normal POSTs would succeed while /healthz\n // would always 401 → \"down\".\n const controller = new AbortController();\n const timer = setTimeoutImpl(() => controller.abort(), timeoutMs);\n let res: Awaited<ReturnType<FetchLike>>;\n try {\n res = await fetchImpl(`${baseUrl}/healthz`, {\n method: \"GET\",\n headers: { ...(opts.headers ?? {}) },\n signal: controller.signal,\n });\n } catch (cause) {\n const aborted = controller.signal.aborted;\n throw new FacilitatorHttpError(\n aborted\n ? `facilitator request timed out after ${timeoutMs}ms (/healthz)`\n : \"facilitator network error (/healthz)\",\n {\n code: aborted ? \"TIMEOUT\" : \"NETWORK_ERROR\",\n cause,\n },\n );\n } finally {\n clearTimeoutImpl(timer);\n }\n if (!res.ok) {\n throw new FacilitatorHttpError(`facilitator HTTP ${res.status} (/healthz)`, {\n code: \"BAD_HTTP_STATUS\",\n status: res.status,\n });\n }\n },\n };\n}\n\nfunction isRetryable(e: FacilitatorHttpError): boolean {\n if (e.code === \"NETWORK_ERROR\" || e.code === \"TIMEOUT\") return true;\n if (e.code === \"BAD_HTTP_STATUS\" && e.status !== undefined && e.status >= 500) return true;\n return false;\n}\n\nfunction sleep(ms: number, setTimeoutImpl: typeof setTimeout): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise((resolve) => {\n setTimeoutImpl(resolve, ms);\n });\n}\n\n// --- Response-shape type guards ----------------------------------------\n//\n// A buggy or malicious facilitator could return any 2xx JSON; without a\n// runtime check, `parsed as Res` would silently slip an unexpected shape\n// past the type system and the convenience handlers (PR 4) would\n// dereference fields that aren't there. These guards mirror the\n// discriminated unions in `@bosonprotocol/x402-facilitator`'s types and\n// fail any response that doesn't fit — surfaced as\n// `BAD_RESPONSE_BODY` so the caller treats it as a transport-layer\n// failure rather than a domain answer.\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === \"object\";\n}\n\n/**\n * Failure branch is identical across all three endpoints. Accepts\n * `unknown` so the non-2xx path can pre-check the parsed body without\n * narrowing first.\n */\nfunction isFailureBranch(v: unknown): v is { ok: false; code: string; reason: string } {\n return (\n isObject(v) && v.ok === false && typeof v.code === \"string\" && typeof v.reason === \"string\"\n );\n}\n\nfunction isVerifyResult(parsed: unknown): parsed is FacilitatorVerifyResult {\n if (!isObject(parsed)) return false;\n if (parsed.ok === true) return true; // `{ ok: true }` carries no further fields\n return isFailureBranch(parsed);\n}\n\nfunction isSettleResult(parsed: unknown): parsed is FacilitatorSettleResult {\n if (!isObject(parsed)) return false;\n if (parsed.ok === true) {\n return typeof parsed.exchangeId === \"string\" && typeof parsed.txHash === \"string\";\n }\n return isFailureBranch(parsed);\n}\n\nfunction isPerformActionResult(parsed: unknown): parsed is FacilitatorPerformActionResult {\n if (!isObject(parsed)) return false;\n if (parsed.ok === true) {\n if (typeof parsed.txHash !== \"string\") return false;\n // Entity-keyed actions (e.g. `boson-withdrawFunds`) return just\n // `{ ok: true, txHash }` — no exchange state transition. Accept\n // that shape; only require `newExchangeState` when present.\n if (parsed.newExchangeState !== undefined && typeof parsed.newExchangeState !== \"string\") {\n return false;\n }\n if (parsed.newDisputeState !== undefined && typeof parsed.newDisputeState !== \"string\") {\n return false;\n }\n return true;\n }\n return isFailureBranch(parsed);\n}\n\nfunction extractFacilitatorCode(body: unknown): FacilitatorErrorCode | undefined {\n if (body === null || typeof body !== \"object\") return undefined;\n const code = (body as { code?: unknown }).code;\n return typeof code === \"string\" ? (code as FacilitatorErrorCode) : undefined;\n}\n\nfunction reasonString(body: unknown): string | undefined {\n if (body === null || typeof body !== \"object\") return undefined;\n const reason = (body as { reason?: unknown }).reason;\n return typeof reason === \"string\" ? reason : undefined;\n}\n"]}
@@ -9,6 +9,18 @@ var FacilitatorHttpError = class extends Error {
9
9
  }
10
10
  };
11
11
 
12
- export { FacilitatorHttpError };
13
- //# sourceMappingURL=chunk-PU5Y7FZG.js.map
14
- //# sourceMappingURL=chunk-PU5Y7FZG.js.map
12
+ // src/logger.ts
13
+ var noopLogger = {
14
+ debug: () => {
15
+ },
16
+ info: () => {
17
+ },
18
+ warn: () => {
19
+ },
20
+ error: () => {
21
+ }
22
+ };
23
+
24
+ export { FacilitatorHttpError, noopLogger };
25
+ //# sourceMappingURL=chunk-RAESVNQG.js.map
26
+ //# sourceMappingURL=chunk-RAESVNQG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/facilitator/errors.ts","../../src/logger.ts"],"names":[],"mappings":";AAqBO,IAAM,oBAAA,GAAN,cAAmC,KAAA,CAAM;AAAA,EAM9C,WAAA,CACE,SACA,IAAA,EAMA;AACA,IAAA,KAAA,CAAM,OAAA,EAAS,KAAK,KAAA,KAAU,MAAA,GAAY,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,EAAM,GAAI,MAAS,CAAA;AAC3E,IAAA,IAAA,CAAK,IAAA,GAAO,sBAAA;AACZ,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,MAAA,EAAW,IAAA,CAAK,SAAS,IAAA,CAAK,MAAA;AAClD,IAAA,IAAI,IAAA,CAAK,eAAA,KAAoB,MAAA,EAAW,IAAA,CAAK,kBAAkB,IAAA,CAAK,eAAA;AAAA,EACtE;AACF;;;AC3BO,IAAM,UAAA,GAAqB;AAAA,EAChC,OAAO,MAAM;AAAA,EAAC,CAAA;AAAA,EACd,MAAM,MAAM;AAAA,EAAC,CAAA;AAAA,EACb,MAAM,MAAM;AAAA,EAAC,CAAA;AAAA,EACb,OAAO,MAAM;AAAA,EAAC;AAChB","file":"chunk-RAESVNQG.js","sourcesContent":["// Typed error class for HTTP-level facilitator failures. Distinct from\n// the `{ ok: false, code }` shape the facilitator itself returns —\n// those are domain errors and surface in the typed result. This class\n// is thrown only when the HTTP transport fails (network error,\n// non-2xx response with an unparseable body, etc.).\n\nimport type { FacilitatorErrorCode } from \"@bosonprotocol/x402-facilitator\";\n\n/**\n * Stable, x402-server-side codes for HTTP-transport failures.\n * Distinct from the facilitator-side `FacilitatorErrorCode` enum so\n * callers can branch on \"the facilitator answered with X\" vs \"we\n * never reached the facilitator\".\n */\nexport type FacilitatorHttpErrorCode =\n | \"NETWORK_ERROR\"\n | \"BAD_HTTP_STATUS\"\n | \"BAD_RESPONSE_BODY\"\n | \"TIMEOUT\";\n\n/** Network- or transport-layer failure when calling the facilitator. */\nexport class FacilitatorHttpError extends Error {\n readonly code: FacilitatorHttpErrorCode;\n readonly status?: number;\n /** Original facilitator-side error code, if the body parsed cleanly. */\n readonly facilitatorCode?: FacilitatorErrorCode;\n\n constructor(\n message: string,\n init: {\n code: FacilitatorHttpErrorCode;\n status?: number;\n facilitatorCode?: FacilitatorErrorCode;\n cause?: unknown;\n },\n ) {\n super(message, init.cause !== undefined ? { cause: init.cause } : undefined);\n this.name = \"FacilitatorHttpError\";\n this.code = init.code;\n if (init.status !== undefined) this.status = init.status;\n if (init.facilitatorCode !== undefined) this.facilitatorCode = init.facilitatorCode;\n }\n}\n","// Minimal four-method logger interface threaded through the server.\n// Matches the de-facto shape every observability library in the JS\n// ecosystem (pino, winston, console, bunyan, …) so hosts adapt their\n// existing logger in one line. The default is `noopLogger`, which\n// discards every event — no overhead, no surprise output, opt-in\n// observability.\n\nexport interface Logger {\n debug(msg: string, meta?: Record<string, unknown>): void;\n info(msg: string, meta?: Record<string, unknown>): void;\n warn(msg: string, meta?: Record<string, unknown>): void;\n error(msg: string, meta?: Record<string, unknown>): void;\n}\n\n/** No-op logger — the default when the host doesn't supply one. */\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n};\n"]}
@@ -1,4 +1,4 @@
1
- import { FacilitatorHttpError } from './chunk-PU5Y7FZG.js';
1
+ import { noopLogger, FacilitatorHttpError } from './chunk-RAESVNQG.js';
2
2
  import { verifyExchange } from './chunk-4NQ3VKC3.js';
3
3
  import { decodeXPaymentHeader, validatePaymentPayload } from './chunk-CGVXQX5V.js';
4
4
  import { ExchangeState, deriveNextActions } from '@bosonprotocol/x402-actions';
@@ -58,6 +58,21 @@ function emitNextActions(input, registry, facilitatorUrl) {
58
58
  };
59
59
  }
60
60
 
61
+ // src/handlers/fulfillment-result.ts
62
+ function serializeFulfillmentResult(result) {
63
+ if (result.kind === "inline") {
64
+ return {
65
+ kind: "inline",
66
+ body: Buffer.from(result.body).toString("base64"),
67
+ contentType: result.contentType
68
+ };
69
+ }
70
+ return {
71
+ kind: "async",
72
+ ...result.pointer !== void 0 ? { pointer: result.pointer } : {}
73
+ };
74
+ }
75
+
61
76
  // src/handlers/types.ts
62
77
  function handlerOk(body) {
63
78
  return { ok: true, status: 200, body };
@@ -85,6 +100,7 @@ async function handleCommitAndRedeem(input, ctx) {
85
100
  });
86
101
  }
87
102
  async function handleCommitImpl(input, ctx, expected) {
103
+ const logger = ctx.logger ?? noopLogger;
88
104
  const decoded = decodeXPaymentHeader(input.paymentHeader);
89
105
  if (!decoded.ok) {
90
106
  const status = decoded.code === "MISSING_HEADER" ? 402 : 400;
@@ -173,28 +189,38 @@ async function handleCommitImpl(input, ctx, expected) {
173
189
  );
174
190
  }
175
191
  if (expected.expectedState === ExchangeState.COMMITTED) {
176
- ctx.exchangeFulfillmentOptionStore.set(
192
+ await ctx.exchangeFulfillmentOptionStore.set(
177
193
  settleResult.exchangeId,
178
194
  input.requirements.fulfillment?.options.map((option) => option.id) ?? []
179
195
  );
180
196
  }
181
197
  const warnings = [];
198
+ let delivery;
182
199
  if (expected.expectedState === ExchangeState.REDEEMED && decoded.payload.fulfillment !== void 0 && decoded.payload.fulfillment.data !== void 0) {
183
200
  const pending = {
184
201
  exchangeId: settleResult.exchangeId,
185
202
  option: decoded.payload.fulfillment.option,
186
203
  data: decoded.payload.fulfillment.data,
187
204
  redeemer: decoded.payload.payload.buyer,
188
- recordedAt: Date.now()
205
+ recordedAt: Date.now(),
206
+ phase: "commit"
189
207
  };
190
- ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, pending);
208
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, pending);
209
+ logger.debug("x402-server: fulfillment recovery entry recorded (Flow B)", {
210
+ exchangeId: settleResult.exchangeId,
211
+ option: decoded.payload.fulfillment.option
212
+ });
191
213
  const channel = channelById.get(decoded.payload.fulfillment.option);
192
214
  if (channel === void 0) {
193
215
  const reason = "no channel adapter is registered";
194
- ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
216
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
195
217
  ...pending,
196
218
  error: reason
197
219
  });
220
+ logger.error("x402-server: Flow B channel adapter missing post-settle", {
221
+ exchangeId: settleResult.exchangeId,
222
+ option: decoded.payload.fulfillment.option
223
+ });
198
224
  warnings.push({
199
225
  code: "FULFILLMENT_COMMIT_DEFERRED",
200
226
  reason: "atomic redeem succeeded on-chain, but no channel adapter is registered",
@@ -207,13 +233,54 @@ async function handleCommitImpl(input, ctx, expected) {
207
233
  } else {
208
234
  try {
209
235
  await channel.onCommit(settleResult.exchangeId, decoded.payload.fulfillment.data);
210
- ctx.fulfillmentRecoveryStore.delete(settleResult.exchangeId);
236
+ if (channel.onFulfill !== void 0) {
237
+ const deliveryPending = {
238
+ ...pending,
239
+ phase: "delivery",
240
+ recordedAt: Date.now()
241
+ };
242
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, deliveryPending);
243
+ try {
244
+ delivery = serializeFulfillmentResult(await channel.onFulfill(settleResult.exchangeId));
245
+ await ctx.fulfillmentRecoveryStore.delete(settleResult.exchangeId);
246
+ logger.debug("x402-server: Flow B channel onCommit succeeded", {
247
+ exchangeId: settleResult.exchangeId,
248
+ option: decoded.payload.fulfillment.option
249
+ });
250
+ } catch (e) {
251
+ const reason = e instanceof Error ? e.message : String(e);
252
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
253
+ ...deliveryPending,
254
+ error: reason
255
+ });
256
+ warnings.push({
257
+ code: "FULFILLMENT_DELIVERY_DEFERRED",
258
+ reason: "atomic redeem succeeded on-chain, but the channel's delivery dispatch failed",
259
+ details: {
260
+ exchangeId: settleResult.exchangeId,
261
+ option: decoded.payload.fulfillment.option,
262
+ error: reason
263
+ }
264
+ });
265
+ }
266
+ } else {
267
+ await ctx.fulfillmentRecoveryStore.delete(settleResult.exchangeId);
268
+ logger.debug("x402-server: Flow B channel onCommit succeeded", {
269
+ exchangeId: settleResult.exchangeId,
270
+ option: decoded.payload.fulfillment.option
271
+ });
272
+ }
211
273
  } catch (e) {
212
274
  const reason = e instanceof Error ? e.message : String(e);
213
- ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
275
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
214
276
  ...pending,
215
277
  error: reason
216
278
  });
279
+ logger.warn("x402-server: Flow B channel onCommit failed; recovery entry retained", {
280
+ exchangeId: settleResult.exchangeId,
281
+ option: decoded.payload.fulfillment.option,
282
+ error: reason
283
+ });
217
284
  warnings.push({
218
285
  code: "FULFILLMENT_COMMIT_DEFERRED",
219
286
  reason: "atomic redeem succeeded on-chain, but the channel adapter rejected the data",
@@ -238,7 +305,8 @@ async function handleCommitImpl(input, ctx, expected) {
238
305
  exchangeId: settleResult.exchangeId,
239
306
  txHash: settleResult.txHash,
240
307
  nextActions,
241
- ...warnings.length > 0 ? { warnings } : {}
308
+ ...warnings.length > 0 ? { warnings } : {},
309
+ ...delivery !== void 0 ? { fulfillment: delivery } : {}
242
310
  });
243
311
  }
244
312
  function buildExpectedFromRequirements(requirements, state) {
@@ -338,7 +406,7 @@ async function handleRedeem(input, ctx) {
338
406
  }
339
407
  let resolvedChannel;
340
408
  if (input.fulfillment !== void 0) {
341
- const advertisedOptions = ctx.exchangeFulfillmentOptionStore.get(input.exchangeId);
409
+ const advertisedOptions = await ctx.exchangeFulfillmentOptionStore.get(input.exchangeId);
342
410
  if (advertisedOptions !== void 0 && !advertisedOptions.includes(input.fulfillment.option)) {
343
411
  return handlerErr(
344
412
  400,
@@ -381,23 +449,40 @@ async function handleRedeem(input, ctx) {
381
449
  }
382
450
  const result = await handlePerformAction("boson-redeem", input, ctx);
383
451
  if (!result.ok) return result;
384
- let warning;
452
+ const warnings = [];
453
+ let delivery;
385
454
  if (resolvedChannel !== void 0 && input.fulfillment !== void 0) {
386
455
  const pending = {
387
456
  exchangeId: input.exchangeId,
388
457
  option: input.fulfillment.option,
389
458
  data: input.fulfillment.data,
390
459
  redeemer,
391
- recordedAt: Date.now()
460
+ recordedAt: Date.now(),
461
+ phase: "commit"
392
462
  };
393
- ctx.fulfillmentRecoveryStore.set(input.exchangeId, pending);
463
+ const logger = ctx.logger ?? noopLogger;
464
+ await ctx.fulfillmentRecoveryStore.set(input.exchangeId, pending);
465
+ logger.debug("x402-server: fulfillment recovery entry recorded (Flow A redeem)", {
466
+ exchangeId: input.exchangeId,
467
+ option: input.fulfillment.option
468
+ });
469
+ let onCommitOk = false;
394
470
  try {
395
471
  await resolvedChannel.onCommit(input.exchangeId, input.fulfillment.data);
396
- ctx.fulfillmentRecoveryStore.delete(input.exchangeId);
472
+ onCommitOk = true;
473
+ logger.debug("x402-server: Flow A channel onCommit succeeded", {
474
+ exchangeId: input.exchangeId,
475
+ option: input.fulfillment.option
476
+ });
397
477
  } catch (e) {
398
478
  const reason = errorMessage(e);
399
- ctx.fulfillmentRecoveryStore.set(input.exchangeId, { ...pending, error: reason });
400
- warning = {
479
+ await ctx.fulfillmentRecoveryStore.set(input.exchangeId, { ...pending, error: reason });
480
+ logger.warn("x402-server: Flow A channel onCommit failed; recovery entry retained", {
481
+ exchangeId: input.exchangeId,
482
+ option: input.fulfillment.option,
483
+ error: reason
484
+ });
485
+ warnings.push({
401
486
  code: "FULFILLMENT_UPDATE_DEFERRED",
402
487
  reason: "redeem succeeded on-chain, but the server could not persist the fulfillment update",
403
488
  details: {
@@ -405,16 +490,48 @@ async function handleRedeem(input, ctx) {
405
490
  option: input.fulfillment.option,
406
491
  error: reason
407
492
  }
408
- };
493
+ });
494
+ }
495
+ if (onCommitOk) {
496
+ if (resolvedChannel.onFulfill !== void 0) {
497
+ const deliveryPending = {
498
+ ...pending,
499
+ phase: "delivery",
500
+ recordedAt: Date.now()
501
+ };
502
+ await ctx.fulfillmentRecoveryStore.set(input.exchangeId, deliveryPending);
503
+ try {
504
+ delivery = serializeFulfillmentResult(await resolvedChannel.onFulfill(input.exchangeId));
505
+ await ctx.fulfillmentRecoveryStore.delete(input.exchangeId);
506
+ } catch (e) {
507
+ const reason = errorMessage(e);
508
+ await ctx.fulfillmentRecoveryStore.set(input.exchangeId, {
509
+ ...deliveryPending,
510
+ error: reason
511
+ });
512
+ warnings.push({
513
+ code: "FULFILLMENT_DELIVERY_DEFERRED",
514
+ reason: "redeem succeeded on-chain, but the fulfillment channel's delivery dispatch failed",
515
+ details: {
516
+ exchangeId: input.exchangeId,
517
+ option: input.fulfillment.option,
518
+ error: reason
519
+ }
520
+ });
521
+ }
522
+ } else {
523
+ await ctx.fulfillmentRecoveryStore.delete(input.exchangeId);
524
+ }
409
525
  }
410
526
  }
411
- ctx.exchangeFulfillmentOptionStore.delete(input.exchangeId);
412
- if (warning !== void 0) {
527
+ await ctx.exchangeFulfillmentOptionStore.delete(input.exchangeId);
528
+ if (warnings.length > 0 || delivery !== void 0) {
413
529
  return {
414
530
  ...result,
415
531
  body: {
416
532
  ...result.body,
417
- warnings: [...result.body.warnings ?? [], warning]
533
+ ...delivery !== void 0 ? { fulfillment: delivery } : {},
534
+ ...warnings.length > 0 ? { warnings: [...result.body.warnings ?? [], ...warnings] } : {}
418
535
  }
419
536
  };
420
537
  }
@@ -653,5 +770,5 @@ function omitErrorMetadata(resolved) {
653
770
  }
654
771
 
655
772
  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, stampFacilitatorEndpoints };
656
- //# sourceMappingURL=chunk-Y5HFLCAT.js.map
657
- //# sourceMappingURL=chunk-Y5HFLCAT.js.map
773
+ //# sourceMappingURL=chunk-SHYOIKYU.js.map
774
+ //# sourceMappingURL=chunk-SHYOIKYU.js.map