@relai-fi/x402 0.5.7 → 0.5.9
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 +104 -0
- package/dist/client.cjs +587 -13
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +64 -3
- package/dist/client.d.ts +64 -3
- package/dist/client.js +587 -13
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +864 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +862 -38
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +592 -14
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +7 -2
- package/dist/react/index.d.ts +7 -2
- package/dist/react/index.js +592 -14
- package/dist/react/index.js.map +1 -1
- package/dist/server.cjs +273 -25
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +17 -2
- package/dist/server.d.ts +17 -2
- package/dist/server.js +273 -25
- package/dist/server.js.map +1 -1
- package/dist/{types-C-2GNyMh.d.cts → types-CWtUxi3l.d.cts} +12 -1
- package/dist/{types-C-2GNyMh.d.ts → types-CWtUxi3l.d.ts} +12 -1
- package/package.json +1 -1
package/dist/client.cjs
CHANGED
|
@@ -110,12 +110,32 @@ function createX402Client(config) {
|
|
|
110
110
|
wallets = {},
|
|
111
111
|
wallet: legacyWallet,
|
|
112
112
|
facilitatorUrl = RELAI_FACILITATOR_URL,
|
|
113
|
+
relayWs,
|
|
113
114
|
preferredNetwork,
|
|
114
115
|
solanaRpcUrl = "https://api.mainnet-beta.solana.com",
|
|
115
116
|
evmRpcUrls = {},
|
|
116
117
|
maxAmountAtomic,
|
|
118
|
+
integritas,
|
|
117
119
|
verbose = false
|
|
118
120
|
} = config;
|
|
121
|
+
const relayWsEnabled = relayWs?.enabled === true;
|
|
122
|
+
const relayWsPreflightTimeoutMs = relayWs?.preflightTimeoutMs ?? 5e3;
|
|
123
|
+
const relayWsPaymentTimeoutMs = relayWs?.paymentTimeoutMs ?? 1e4;
|
|
124
|
+
const relayWsFallbackToHttp = relayWs?.fallbackToHttp ?? true;
|
|
125
|
+
const defaultIntegritas = normalizeIntegritasOptions(integritas);
|
|
126
|
+
const relayWsReservedSubdomains = /* @__PURE__ */ new Set([
|
|
127
|
+
"www",
|
|
128
|
+
"api",
|
|
129
|
+
"localhost",
|
|
130
|
+
"admin",
|
|
131
|
+
"app",
|
|
132
|
+
"dashboard",
|
|
133
|
+
"docs",
|
|
134
|
+
"documentation",
|
|
135
|
+
"status",
|
|
136
|
+
"blog",
|
|
137
|
+
"facilitator"
|
|
138
|
+
]);
|
|
119
139
|
const log = verbose ? console.log.bind(console, "[relai-x402]") : () => {
|
|
120
140
|
};
|
|
121
141
|
const effectiveWallets = { ...wallets };
|
|
@@ -126,6 +146,408 @@ function createX402Client(config) {
|
|
|
126
146
|
effectiveWallets.solana?.publicKey && effectiveWallets.solana?.signTransaction
|
|
127
147
|
);
|
|
128
148
|
if (hasSolanaWallet) log("Solana wallet ready");
|
|
149
|
+
function isRecord(value) {
|
|
150
|
+
return typeof value === "object" && value !== null;
|
|
151
|
+
}
|
|
152
|
+
function parseJsonSafe(value) {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(value);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function addSocketListener(socket, eventName, listener) {
|
|
160
|
+
if (socket.addEventListener) {
|
|
161
|
+
socket.addEventListener(eventName, listener);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (socket.on) {
|
|
165
|
+
socket.on(eventName, listener);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function removeSocketListener(socket, eventName, listener) {
|
|
169
|
+
if (socket.removeEventListener) {
|
|
170
|
+
socket.removeEventListener(eventName, listener);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (socket.off) {
|
|
174
|
+
socket.off(eventName, listener);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (socket.removeListener) {
|
|
178
|
+
socket.removeListener(eventName, listener);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function resolveRelayWsUrl(relayUrl) {
|
|
182
|
+
if (relayWs?.wsUrl && relayWs.wsUrl.trim() !== "") {
|
|
183
|
+
return relayWs.wsUrl.trim();
|
|
184
|
+
}
|
|
185
|
+
const parsedRelayUrl = new URL(relayUrl);
|
|
186
|
+
const wsProtocol = parsedRelayUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
187
|
+
return `${wsProtocol}//${parsedRelayUrl.host}/api/ws/relay`;
|
|
188
|
+
}
|
|
189
|
+
function resolveRelayWhitelabel(parsedRelayUrl) {
|
|
190
|
+
const hostParts = parsedRelayUrl.hostname.toLowerCase().split(".").filter(Boolean);
|
|
191
|
+
if (hostParts.length < 2) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const candidate = decodeURIComponent(hostParts[0] || "").trim();
|
|
195
|
+
if (!candidate) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (relayWsReservedSubdomains.has(candidate.toLowerCase())) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const lastPart = hostParts[hostParts.length - 1];
|
|
202
|
+
const secondLastPart = hostParts[hostParts.length - 2];
|
|
203
|
+
const isX402WhitelabelHost = hostParts.length >= 3 && secondLastPart === "x402" && lastPart === "fi";
|
|
204
|
+
const isLocalWhitelabelHost = hostParts.length === 2 && lastPart === "localhost";
|
|
205
|
+
if (!isX402WhitelabelHost && !isLocalWhitelabelHost) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return candidate;
|
|
209
|
+
}
|
|
210
|
+
function resolveRelayTarget(relayUrl) {
|
|
211
|
+
const parsedRelayUrl = new URL(relayUrl);
|
|
212
|
+
const match = parsedRelayUrl.pathname.match(/\/relay\/([^/]+)(\/.*)?$/);
|
|
213
|
+
if (match) {
|
|
214
|
+
const apiId = decodeURIComponent(match[1]);
|
|
215
|
+
const pathPart2 = match[2] || "/";
|
|
216
|
+
return {
|
|
217
|
+
apiId,
|
|
218
|
+
path: `${pathPart2}${parsedRelayUrl.search || ""}`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const whitelabel = resolveRelayWhitelabel(parsedRelayUrl);
|
|
222
|
+
if (!whitelabel) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`[relai-x402] Unsupported relay URL format for WS transport: ${relayUrl}. Expected /relay/:apiId/... or <whitelabel>.x402.fi/...`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const pathPart = parsedRelayUrl.pathname && parsedRelayUrl.pathname !== "" ? parsedRelayUrl.pathname : "/";
|
|
228
|
+
return {
|
|
229
|
+
apiId: whitelabel,
|
|
230
|
+
path: `${pathPart}${parsedRelayUrl.search || ""}`
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function isRelayRequestUrl(requestUrl) {
|
|
234
|
+
try {
|
|
235
|
+
resolveRelayTarget(requestUrl);
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function headersToRecord(headersInit) {
|
|
242
|
+
if (!headersInit) return {};
|
|
243
|
+
const output = {};
|
|
244
|
+
if (typeof Headers !== "undefined" && headersInit instanceof Headers) {
|
|
245
|
+
headersInit.forEach((value, key) => {
|
|
246
|
+
output[key] = value;
|
|
247
|
+
});
|
|
248
|
+
return output;
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(headersInit)) {
|
|
251
|
+
for (const [key, value] of headersInit) {
|
|
252
|
+
output[key] = value;
|
|
253
|
+
}
|
|
254
|
+
return output;
|
|
255
|
+
}
|
|
256
|
+
for (const [key, value] of Object.entries(headersInit)) {
|
|
257
|
+
if (typeof value === "string") {
|
|
258
|
+
output[key] = value;
|
|
259
|
+
} else if (Array.isArray(value)) {
|
|
260
|
+
output[key] = value.join(", ");
|
|
261
|
+
} else if (value !== void 0 && value !== null) {
|
|
262
|
+
output[key] = String(value);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return output;
|
|
266
|
+
}
|
|
267
|
+
function hasHeaderCaseInsensitive(headers, headerName) {
|
|
268
|
+
const normalized = headerName.toLowerCase();
|
|
269
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === normalized);
|
|
270
|
+
}
|
|
271
|
+
function normalizeIntegritasFlow(value) {
|
|
272
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
273
|
+
if (normalized === "single") return "single";
|
|
274
|
+
if (normalized === "dual") return "dual";
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
function normalizeIntegritasOptions(value) {
|
|
278
|
+
if (value === true) return { enabled: true };
|
|
279
|
+
if (value === false || value == null) return { enabled: false };
|
|
280
|
+
const flow = normalizeIntegritasFlow(value.flow);
|
|
281
|
+
const enabled = typeof value.enabled === "boolean" ? value.enabled : true;
|
|
282
|
+
return {
|
|
283
|
+
enabled,
|
|
284
|
+
...flow ? { flow } : {}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function resolveIntegritasOptions(override) {
|
|
288
|
+
if (override === void 0) {
|
|
289
|
+
return defaultIntegritas;
|
|
290
|
+
}
|
|
291
|
+
if (typeof override === "boolean") {
|
|
292
|
+
return {
|
|
293
|
+
enabled: override,
|
|
294
|
+
...override && defaultIntegritas.flow ? { flow: defaultIntegritas.flow } : {}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const flow = normalizeIntegritasFlow(override.flow) || defaultIntegritas.flow;
|
|
298
|
+
const enabled = typeof override.enabled === "boolean" ? override.enabled : defaultIntegritas.enabled;
|
|
299
|
+
return {
|
|
300
|
+
enabled,
|
|
301
|
+
...enabled && flow ? { flow } : {}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function stripInternalInit(init) {
|
|
305
|
+
if (!init) return void 0;
|
|
306
|
+
const { x402: _x402, ...requestInit } = init;
|
|
307
|
+
return requestInit;
|
|
308
|
+
}
|
|
309
|
+
function applyIntegritasHeaders(headers, options) {
|
|
310
|
+
if (!options.enabled) return headers;
|
|
311
|
+
if (!hasHeaderCaseInsensitive(headers, "x-integritas")) {
|
|
312
|
+
headers["X-Integritas"] = "true";
|
|
313
|
+
}
|
|
314
|
+
if (options.flow && !hasHeaderCaseInsensitive(headers, "x-integritas-flow")) {
|
|
315
|
+
headers["X-Integritas-Flow"] = options.flow;
|
|
316
|
+
}
|
|
317
|
+
return headers;
|
|
318
|
+
}
|
|
319
|
+
function getRequestMethod(input, init) {
|
|
320
|
+
const inputMethod = input instanceof Request ? input.method : void 0;
|
|
321
|
+
return (init?.method || inputMethod || "GET").toUpperCase();
|
|
322
|
+
}
|
|
323
|
+
async function bodyInitToWsPayload(bodyInit) {
|
|
324
|
+
if (bodyInit === void 0 || bodyInit === null) {
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
327
|
+
if (typeof bodyInit === "string") {
|
|
328
|
+
const parsed = parseJsonSafe(bodyInit);
|
|
329
|
+
return parsed === null ? bodyInit : parsed;
|
|
330
|
+
}
|
|
331
|
+
if (typeof URLSearchParams !== "undefined" && bodyInit instanceof URLSearchParams) {
|
|
332
|
+
return bodyInit.toString();
|
|
333
|
+
}
|
|
334
|
+
if (typeof FormData !== "undefined" && bodyInit instanceof FormData) {
|
|
335
|
+
const entries = {};
|
|
336
|
+
for (const [key, value] of bodyInit.entries()) {
|
|
337
|
+
entries[key] = typeof value === "string" ? value : value.name;
|
|
338
|
+
}
|
|
339
|
+
return entries;
|
|
340
|
+
}
|
|
341
|
+
if (typeof Blob !== "undefined" && bodyInit instanceof Blob) {
|
|
342
|
+
const text = await bodyInit.text();
|
|
343
|
+
if (!text) return void 0;
|
|
344
|
+
const parsed = parseJsonSafe(text);
|
|
345
|
+
return parsed === null ? text : parsed;
|
|
346
|
+
}
|
|
347
|
+
if (bodyInit instanceof ArrayBuffer) {
|
|
348
|
+
return Array.from(new Uint8Array(bodyInit));
|
|
349
|
+
}
|
|
350
|
+
if (ArrayBuffer.isView(bodyInit)) {
|
|
351
|
+
return Array.from(new Uint8Array(bodyInit.buffer, bodyInit.byteOffset, bodyInit.byteLength));
|
|
352
|
+
}
|
|
353
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(bodyInit)) {
|
|
354
|
+
return Array.from(bodyInit.values());
|
|
355
|
+
}
|
|
356
|
+
if (isRecord(bodyInit)) {
|
|
357
|
+
return bodyInit;
|
|
358
|
+
}
|
|
359
|
+
return String(bodyInit);
|
|
360
|
+
}
|
|
361
|
+
async function resolveRequestBody(input, init) {
|
|
362
|
+
if (init && Object.prototype.hasOwnProperty.call(init, "body")) {
|
|
363
|
+
return bodyInitToWsPayload(init.body);
|
|
364
|
+
}
|
|
365
|
+
if (input instanceof Request) {
|
|
366
|
+
const method = getRequestMethod(input, init);
|
|
367
|
+
if (method === "GET" || method === "HEAD") {
|
|
368
|
+
return void 0;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const cloned = input.clone();
|
|
372
|
+
const text = await cloned.text();
|
|
373
|
+
if (!text) return void 0;
|
|
374
|
+
const parsed = parseJsonSafe(text);
|
|
375
|
+
return parsed === null ? text : parsed;
|
|
376
|
+
} catch {
|
|
377
|
+
return void 0;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
function getRequestHeaders(input, init) {
|
|
383
|
+
const fromInput = input instanceof Request ? headersToRecord(input.headers) : {};
|
|
384
|
+
const fromInit = headersToRecord(init?.headers);
|
|
385
|
+
const merged = {
|
|
386
|
+
...fromInput,
|
|
387
|
+
...fromInit
|
|
388
|
+
};
|
|
389
|
+
if (!merged.Accept && !merged.accept) {
|
|
390
|
+
merged.Accept = "application/json";
|
|
391
|
+
}
|
|
392
|
+
return merged;
|
|
393
|
+
}
|
|
394
|
+
function toMessageString(data) {
|
|
395
|
+
if (typeof data === "string") {
|
|
396
|
+
return data;
|
|
397
|
+
}
|
|
398
|
+
if (isRecord(data) && typeof data.data !== "undefined") {
|
|
399
|
+
return toMessageString(data.data);
|
|
400
|
+
}
|
|
401
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
|
|
402
|
+
return data.toString("utf8");
|
|
403
|
+
}
|
|
404
|
+
if (data instanceof ArrayBuffer) {
|
|
405
|
+
const bytes = new Uint8Array(data);
|
|
406
|
+
if (typeof Buffer !== "undefined") {
|
|
407
|
+
return Buffer.from(bytes).toString("utf8");
|
|
408
|
+
}
|
|
409
|
+
if (typeof TextDecoder !== "undefined") {
|
|
410
|
+
return new TextDecoder().decode(bytes);
|
|
411
|
+
}
|
|
412
|
+
throw new Error("Unsupported WebSocket message data type");
|
|
413
|
+
}
|
|
414
|
+
if (ArrayBuffer.isView(data)) {
|
|
415
|
+
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
416
|
+
if (typeof Buffer !== "undefined") {
|
|
417
|
+
return Buffer.from(bytes).toString("utf8");
|
|
418
|
+
}
|
|
419
|
+
if (typeof TextDecoder !== "undefined") {
|
|
420
|
+
return new TextDecoder().decode(bytes);
|
|
421
|
+
}
|
|
422
|
+
throw new Error("Unsupported WebSocket message data type");
|
|
423
|
+
}
|
|
424
|
+
throw new Error("Unsupported WebSocket message data type");
|
|
425
|
+
}
|
|
426
|
+
function getWebSocketFactory() {
|
|
427
|
+
if (relayWs?.webSocketFactory) {
|
|
428
|
+
return relayWs.webSocketFactory;
|
|
429
|
+
}
|
|
430
|
+
if (typeof WebSocket !== "undefined") {
|
|
431
|
+
return (wsUrl) => new WebSocket(wsUrl);
|
|
432
|
+
}
|
|
433
|
+
throw new Error(
|
|
434
|
+
"[relai-x402] WebSocket is not available in this runtime. Provide relayWs.webSocketFactory."
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
async function relayCallOverWebSocket(request) {
|
|
438
|
+
const wsFactory = getWebSocketFactory();
|
|
439
|
+
const wsUrl = resolveRelayWsUrl(request.relayUrl);
|
|
440
|
+
const target = resolveRelayTarget(request.relayUrl);
|
|
441
|
+
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
442
|
+
const socket = wsFactory(wsUrl);
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
let settled = false;
|
|
445
|
+
const settleResolve = (value) => {
|
|
446
|
+
if (settled) return;
|
|
447
|
+
settled = true;
|
|
448
|
+
cleanup();
|
|
449
|
+
try {
|
|
450
|
+
socket.close();
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
resolve(value);
|
|
454
|
+
};
|
|
455
|
+
const settleReject = (error) => {
|
|
456
|
+
if (settled) return;
|
|
457
|
+
settled = true;
|
|
458
|
+
cleanup();
|
|
459
|
+
try {
|
|
460
|
+
socket.close();
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
reject(error);
|
|
464
|
+
};
|
|
465
|
+
const timeoutId = setTimeout(() => {
|
|
466
|
+
settleReject(new Error(`[relai-x402] Timed out waiting for WS relay response after ${request.timeoutMs}ms`));
|
|
467
|
+
}, request.timeoutMs);
|
|
468
|
+
const cleanup = () => {
|
|
469
|
+
clearTimeout(timeoutId);
|
|
470
|
+
removeSocketListener(socket, "open", onOpen);
|
|
471
|
+
removeSocketListener(socket, "message", onMessage);
|
|
472
|
+
removeSocketListener(socket, "error", onError);
|
|
473
|
+
removeSocketListener(socket, "close", onClose);
|
|
474
|
+
};
|
|
475
|
+
const onOpen = () => {
|
|
476
|
+
const envelope = {
|
|
477
|
+
id: requestId,
|
|
478
|
+
method: "relay.call",
|
|
479
|
+
params: {
|
|
480
|
+
apiId: target.apiId,
|
|
481
|
+
path: target.path,
|
|
482
|
+
requestMethod: request.requestMethod,
|
|
483
|
+
requestHeaders: request.requestHeaders,
|
|
484
|
+
...request.requestBody !== void 0 ? { requestBody: request.requestBody } : {}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
if (request.paymentPayload !== void 0) {
|
|
488
|
+
envelope.payment = request.paymentPayload;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
socket.send(JSON.stringify(envelope));
|
|
492
|
+
} catch {
|
|
493
|
+
settleReject(new Error("[relai-x402] Failed to send WS relay request"));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const onMessage = (...args) => {
|
|
497
|
+
const payload = args.length > 0 ? args[0] : void 0;
|
|
498
|
+
let parsed;
|
|
499
|
+
try {
|
|
500
|
+
parsed = parseJsonSafe(toMessageString(payload));
|
|
501
|
+
} catch {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!isRecord(parsed)) return;
|
|
505
|
+
const responseId = typeof parsed.id === "string" || typeof parsed.id === "number" ? String(parsed.id) : "";
|
|
506
|
+
if (responseId !== requestId) return;
|
|
507
|
+
settleResolve(parsed);
|
|
508
|
+
};
|
|
509
|
+
const onError = () => {
|
|
510
|
+
settleReject(new Error("[relai-x402] WebSocket relay transport error"));
|
|
511
|
+
};
|
|
512
|
+
const onClose = () => {
|
|
513
|
+
settleReject(new Error("[relai-x402] WebSocket relay connection closed before response"));
|
|
514
|
+
};
|
|
515
|
+
addSocketListener(socket, "open", onOpen);
|
|
516
|
+
addSocketListener(socket, "message", onMessage);
|
|
517
|
+
addSocketListener(socket, "error", onError);
|
|
518
|
+
addSocketListener(socket, "close", onClose);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
function extractPaymentRequirementsFromWsError(error) {
|
|
522
|
+
const candidates = [error.paymentRequired, error.data];
|
|
523
|
+
for (const candidate of candidates) {
|
|
524
|
+
if (!isRecord(candidate)) continue;
|
|
525
|
+
if (Array.isArray(candidate.accepts)) {
|
|
526
|
+
return candidate;
|
|
527
|
+
}
|
|
528
|
+
if (isRecord(candidate.paymentRequired) && Array.isArray(candidate.paymentRequired.accepts)) {
|
|
529
|
+
return candidate.paymentRequired;
|
|
530
|
+
}
|
|
531
|
+
if (isRecord(candidate.data) && Array.isArray(candidate.data.accepts)) {
|
|
532
|
+
return candidate.data;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
function buildWsResponse(wsResponse) {
|
|
538
|
+
const statusFromMetadata = isRecord(wsResponse.metadata) && typeof wsResponse.metadata.status === "number" ? wsResponse.metadata.status : 200;
|
|
539
|
+
const status = Number.isInteger(statusFromMetadata) && statusFromMetadata >= 100 && statusFromMetadata <= 599 ? statusFromMetadata : 200;
|
|
540
|
+
const headers = new Headers();
|
|
541
|
+
headers.set("Content-Type", "application/json");
|
|
542
|
+
if (wsResponse.paymentResponse !== void 0) {
|
|
543
|
+
headers.set("PAYMENT-RESPONSE", encodeBase64Json(wsResponse.paymentResponse));
|
|
544
|
+
}
|
|
545
|
+
const bodyPayload = wsResponse.result === void 0 ? null : wsResponse.result;
|
|
546
|
+
return new Response(JSON.stringify(bodyPayload), {
|
|
547
|
+
status,
|
|
548
|
+
headers
|
|
549
|
+
});
|
|
550
|
+
}
|
|
129
551
|
function selectAccept(accepts) {
|
|
130
552
|
if (preferredNetwork) {
|
|
131
553
|
const caip2 = NETWORK_CAIP2[preferredNetwork];
|
|
@@ -237,7 +659,7 @@ function createX402Client(config) {
|
|
|
237
659
|
amount: paymentAmount
|
|
238
660
|
}
|
|
239
661
|
};
|
|
240
|
-
return
|
|
662
|
+
return encodeBase64Json(paymentPayload);
|
|
241
663
|
}
|
|
242
664
|
async function buildEvmPayment(accept, requirements, url) {
|
|
243
665
|
const evmWallet = effectiveWallets.evm;
|
|
@@ -305,7 +727,7 @@ function createX402Client(config) {
|
|
|
305
727
|
},
|
|
306
728
|
facilitatorUrl
|
|
307
729
|
};
|
|
308
|
-
return
|
|
730
|
+
return encodeBase64Json(paymentPayload);
|
|
309
731
|
}
|
|
310
732
|
async function buildSolanaPayment(accept, requirements, url) {
|
|
311
733
|
const solWallet = effectiveWallets.solana;
|
|
@@ -369,21 +791,173 @@ function createX402Client(config) {
|
|
|
369
791
|
transaction: serializedTx
|
|
370
792
|
}
|
|
371
793
|
};
|
|
372
|
-
return
|
|
794
|
+
return encodeBase64Json(paymentPayload);
|
|
795
|
+
}
|
|
796
|
+
function encodeBase64Json(payload) {
|
|
797
|
+
const serialized = JSON.stringify(payload);
|
|
798
|
+
if (typeof Buffer !== "undefined") {
|
|
799
|
+
return Buffer.from(serialized, "utf8").toString("base64");
|
|
800
|
+
}
|
|
801
|
+
if (typeof btoa !== "undefined") {
|
|
802
|
+
return btoa(serialized);
|
|
803
|
+
}
|
|
804
|
+
throw new Error("[relai-x402] Base64 encoding is not available in this runtime");
|
|
805
|
+
}
|
|
806
|
+
function decodeBase64Json(encoded) {
|
|
807
|
+
try {
|
|
808
|
+
const normalized = encoded.trim().replace(/-/g, "+").replace(/_/g, "/");
|
|
809
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
810
|
+
const decoded = typeof Buffer !== "undefined" ? Buffer.from(padded, "base64").toString("utf8") : atob(padded);
|
|
811
|
+
return JSON.parse(decoded);
|
|
812
|
+
} catch {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function parsePaymentRequiredHeader(response) {
|
|
817
|
+
const headerValue = response.headers.get("payment-required") || response.headers.get("PAYMENT-REQUIRED") || response.headers.get("x-payment-required") || response.headers.get("X-PAYMENT-REQUIRED");
|
|
818
|
+
if (!headerValue) return null;
|
|
819
|
+
const trimmed = headerValue.trim();
|
|
820
|
+
if (!trimmed) return null;
|
|
821
|
+
try {
|
|
822
|
+
return JSON.parse(trimmed);
|
|
823
|
+
} catch {
|
|
824
|
+
return decodeBase64Json(trimmed);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function getAccepts(requirements) {
|
|
828
|
+
if (!requirements || typeof requirements !== "object") {
|
|
829
|
+
return [];
|
|
830
|
+
}
|
|
831
|
+
if (Array.isArray(requirements.accepts)) {
|
|
832
|
+
return requirements.accepts;
|
|
833
|
+
}
|
|
834
|
+
if (requirements.paymentRequired && Array.isArray(requirements.paymentRequired.accepts)) {
|
|
835
|
+
return requirements.paymentRequired.accepts;
|
|
836
|
+
}
|
|
837
|
+
if (requirements.data && Array.isArray(requirements.data.accepts)) {
|
|
838
|
+
return requirements.data.accepts;
|
|
839
|
+
}
|
|
840
|
+
return [];
|
|
373
841
|
}
|
|
374
842
|
async function x402Fetch(input, init) {
|
|
375
843
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
376
844
|
log("Request:", url);
|
|
377
|
-
const
|
|
845
|
+
const requestInit = stripInternalInit(init);
|
|
846
|
+
const integritasOptions = resolveIntegritasOptions(init?.x402?.integritas);
|
|
847
|
+
const requestMethod = getRequestMethod(input, requestInit);
|
|
848
|
+
const requestHeaders = applyIntegritasHeaders(
|
|
849
|
+
getRequestHeaders(input, requestInit),
|
|
850
|
+
integritasOptions
|
|
851
|
+
);
|
|
852
|
+
const requestInitWithHeaders = {
|
|
853
|
+
...requestInit || {},
|
|
854
|
+
headers: requestHeaders
|
|
855
|
+
};
|
|
856
|
+
const requestBody = await resolveRequestBody(input, requestInitWithHeaders);
|
|
857
|
+
if (relayWsEnabled && isRelayRequestUrl(url)) {
|
|
858
|
+
let wsPaymentPhaseStarted = false;
|
|
859
|
+
try {
|
|
860
|
+
log("Using WebSocket relay transport");
|
|
861
|
+
const wsPreflightResponse = await relayCallOverWebSocket({
|
|
862
|
+
relayUrl: url,
|
|
863
|
+
requestMethod,
|
|
864
|
+
requestHeaders,
|
|
865
|
+
requestBody,
|
|
866
|
+
timeoutMs: relayWsPreflightTimeoutMs
|
|
867
|
+
});
|
|
868
|
+
if (!wsPreflightResponse.error) {
|
|
869
|
+
return buildWsResponse(wsPreflightResponse);
|
|
870
|
+
}
|
|
871
|
+
if (Number(wsPreflightResponse.error.code) !== 402) {
|
|
872
|
+
throw new Error(wsPreflightResponse.error.message || "[relai-x402] WebSocket relay request failed");
|
|
873
|
+
}
|
|
874
|
+
const wsRequirements = extractPaymentRequirementsFromWsError(wsPreflightResponse.error);
|
|
875
|
+
if (!wsRequirements) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
wsPreflightResponse.error.message || "[relai-x402] No payment requirements in WS 402 response"
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const wsAccepts = getAccepts(wsRequirements);
|
|
881
|
+
if (!wsAccepts.length) {
|
|
882
|
+
throw new Error("[relai-x402] No payment options in WS 402 response");
|
|
883
|
+
}
|
|
884
|
+
if (wsAccepts.length > 1) {
|
|
885
|
+
throw new Error(
|
|
886
|
+
"[relai-x402] WS relay currently supports a single payment payload; use HTTP flow for multi-accept payments"
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
const wsSelected = selectAccept(wsAccepts);
|
|
890
|
+
if (!wsSelected) {
|
|
891
|
+
const networks = wsAccepts.map((a) => a.network).join(", ");
|
|
892
|
+
throw new Error(`[relai-x402] No wallet available for WS networks: ${networks}`);
|
|
893
|
+
}
|
|
894
|
+
const { accept: accept2, chain: chain2 } = wsSelected;
|
|
895
|
+
const amount2 = accept2.amount || accept2.maxAmountRequired;
|
|
896
|
+
if (maxAmountAtomic && BigInt(amount2) > BigInt(maxAmountAtomic)) {
|
|
897
|
+
throw new Error(`[relai-x402] Amount ${amount2} exceeds max ${maxAmountAtomic}`);
|
|
898
|
+
}
|
|
899
|
+
wsPaymentPhaseStarted = true;
|
|
900
|
+
let paymentHeader = null;
|
|
901
|
+
if (chain2 === "solana" && hasSolanaWallet) {
|
|
902
|
+
paymentHeader = await buildSolanaPayment(accept2, wsRequirements, url);
|
|
903
|
+
} else if (chain2 === "evm") {
|
|
904
|
+
const evmNetwork = normalizeNetwork(accept2.network || "");
|
|
905
|
+
const usePermit = evmNetwork && PERMIT_NETWORKS.has(evmNetwork);
|
|
906
|
+
paymentHeader = usePermit ? await buildEvmPermitPayment(accept2, wsRequirements, url) : await buildEvmPayment(accept2, wsRequirements, url);
|
|
907
|
+
}
|
|
908
|
+
if (!paymentHeader) {
|
|
909
|
+
throw new Error("[relai-x402] Unexpected state - no WS payment handler matched");
|
|
910
|
+
}
|
|
911
|
+
const paymentPayload = decodeBase64Json(paymentHeader);
|
|
912
|
+
if (!paymentPayload) {
|
|
913
|
+
throw new Error("[relai-x402] Failed to decode payment payload for WS relay call");
|
|
914
|
+
}
|
|
915
|
+
const wsPaidResponse = await relayCallOverWebSocket({
|
|
916
|
+
relayUrl: url,
|
|
917
|
+
requestMethod,
|
|
918
|
+
requestHeaders,
|
|
919
|
+
requestBody,
|
|
920
|
+
paymentPayload,
|
|
921
|
+
timeoutMs: relayWsPaymentTimeoutMs
|
|
922
|
+
});
|
|
923
|
+
if (wsPaidResponse.error) {
|
|
924
|
+
throw new Error(wsPaidResponse.error.message || "[relai-x402] WebSocket paid relay request failed");
|
|
925
|
+
}
|
|
926
|
+
return buildWsResponse(wsPaidResponse);
|
|
927
|
+
} catch (wsError) {
|
|
928
|
+
const wsMessage = wsError instanceof Error ? wsError.message : String(wsError);
|
|
929
|
+
log("WebSocket relay transport failed:", wsMessage);
|
|
930
|
+
if (wsPaymentPhaseStarted) {
|
|
931
|
+
throw wsError instanceof Error ? wsError : new Error(`[relai-x402] ${wsMessage}`);
|
|
932
|
+
}
|
|
933
|
+
if (!relayWsFallbackToHttp) {
|
|
934
|
+
throw wsError instanceof Error ? wsError : new Error(`[relai-x402] ${wsMessage}`);
|
|
935
|
+
}
|
|
936
|
+
log("Falling back to HTTP x402 flow");
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const response = await fetch(input, requestInitWithHeaders);
|
|
378
940
|
if (response.status !== 402) return response;
|
|
379
941
|
log("Got 402 Payment Required");
|
|
380
|
-
let
|
|
942
|
+
let requirementsFromBody = null;
|
|
381
943
|
try {
|
|
382
|
-
|
|
944
|
+
requirementsFromBody = await response.clone().json();
|
|
383
945
|
} catch {
|
|
384
|
-
throw new Error("[relai-x402] Failed to parse 402 response body");
|
|
385
946
|
}
|
|
386
|
-
const
|
|
947
|
+
const requirementsFromHeader = parsePaymentRequiredHeader(response);
|
|
948
|
+
let requirements = requirementsFromBody;
|
|
949
|
+
let accepts = getAccepts(requirementsFromBody);
|
|
950
|
+
if (!accepts.length && requirementsFromHeader) {
|
|
951
|
+
const headerAccepts = getAccepts(requirementsFromHeader);
|
|
952
|
+
if (headerAccepts.length || !requirements || typeof requirements !== "object") {
|
|
953
|
+
requirements = requirementsFromHeader;
|
|
954
|
+
accepts = headerAccepts;
|
|
955
|
+
log("402 body missing accepts; using PAYMENT-REQUIRED header fallback");
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (!requirements || typeof requirements !== "object") {
|
|
959
|
+
throw new Error("[relai-x402] Failed to parse 402 response body/header");
|
|
960
|
+
}
|
|
387
961
|
if (!accepts.length) throw new Error("[relai-x402] No payment options in 402 response");
|
|
388
962
|
const selected = selectAccept(accepts);
|
|
389
963
|
if (!selected) {
|
|
@@ -400,9 +974,9 @@ function createX402Client(config) {
|
|
|
400
974
|
const paymentHeader = await buildSolanaPayment(accept, requirements, url);
|
|
401
975
|
log("Retrying with X-PAYMENT header (Solana)");
|
|
402
976
|
return fetch(input, {
|
|
403
|
-
...
|
|
977
|
+
...requestInitWithHeaders,
|
|
404
978
|
headers: {
|
|
405
|
-
...
|
|
979
|
+
...requestHeaders,
|
|
406
980
|
"X-PAYMENT": paymentHeader
|
|
407
981
|
}
|
|
408
982
|
});
|
|
@@ -413,14 +987,14 @@ function createX402Client(config) {
|
|
|
413
987
|
const paymentHeader = usePermit ? await buildEvmPermitPayment(accept, requirements, url) : await buildEvmPayment(accept, requirements, url);
|
|
414
988
|
log("Retrying with X-PAYMENT header");
|
|
415
989
|
return fetch(input, {
|
|
416
|
-
...
|
|
990
|
+
...requestInitWithHeaders,
|
|
417
991
|
headers: {
|
|
418
|
-
...
|
|
992
|
+
...requestHeaders,
|
|
419
993
|
"X-PAYMENT": paymentHeader
|
|
420
994
|
}
|
|
421
995
|
});
|
|
422
996
|
}
|
|
423
|
-
throw new Error("[relai-x402] Unexpected state
|
|
997
|
+
throw new Error("[relai-x402] Unexpected state - no payment handler matched");
|
|
424
998
|
}
|
|
425
999
|
return { fetch: x402Fetch };
|
|
426
1000
|
}
|