@agentcash/router 1.5.2 → 1.6.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/.claude/CLAUDE.md +116 -2
- package/README.md +34 -5
- package/dist/index.cjs +2173 -1079
- package/dist/index.d.cts +391 -276
- package/dist/index.d.ts +391 -276
- package/dist/index.js +2169 -1077
- package/package.json +11 -11
package/dist/index.cjs
CHANGED
|
@@ -105,6 +105,18 @@ function buildEvmExactOptions(accepts, price) {
|
|
|
105
105
|
payTo
|
|
106
106
|
}));
|
|
107
107
|
}
|
|
108
|
+
function buildEvmUptoOptions(accepts, price) {
|
|
109
|
+
return accepts.filter(
|
|
110
|
+
(accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
|
|
111
|
+
).map((accept) => ({
|
|
112
|
+
scheme: "upto",
|
|
113
|
+
network: accept.network,
|
|
114
|
+
payTo: accept.payTo,
|
|
115
|
+
price,
|
|
116
|
+
...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
|
|
117
|
+
...accept.extra ? { extra: accept.extra } : {}
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
108
120
|
var init_evm = __esm({
|
|
109
121
|
"src/protocols/x402/evm.ts"() {
|
|
110
122
|
"use strict";
|
|
@@ -289,40 +301,40 @@ async function createX402Server(config) {
|
|
|
289
301
|
facilitatorsByNetwork
|
|
290
302
|
};
|
|
291
303
|
}
|
|
292
|
-
function
|
|
304
|
+
function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
|
|
305
|
+
const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
|
|
306
|
+
return groups.map((group) => {
|
|
307
|
+
const inner = new HTTPFacilitatorClient(group.config);
|
|
308
|
+
const kinds = buildSupportedKinds(group);
|
|
309
|
+
return hardcodedSupportedClient(inner, kinds);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function hardcodedSupportedClient(inner, kinds) {
|
|
293
313
|
return {
|
|
294
314
|
verify: inner.verify.bind(inner),
|
|
295
315
|
settle: inner.settle.bind(inner),
|
|
296
|
-
getSupported: async () => ({
|
|
297
|
-
kinds,
|
|
298
|
-
extensions: [],
|
|
299
|
-
signers: {}
|
|
300
|
-
})
|
|
316
|
+
getSupported: async () => ({ kinds, extensions: [], signers: {} })
|
|
301
317
|
};
|
|
302
318
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
extra: {
|
|
314
|
-
features: {
|
|
315
|
-
xSettlementAccountSupported: true
|
|
316
|
-
}
|
|
319
|
+
function buildSupportedKinds(group) {
|
|
320
|
+
return group.networks.flatMap((network) => {
|
|
321
|
+
const exactKind = {
|
|
322
|
+
x402Version: 2,
|
|
323
|
+
scheme: "exact",
|
|
324
|
+
network,
|
|
325
|
+
...group.family === "solana" ? {
|
|
326
|
+
extra: {
|
|
327
|
+
features: {
|
|
328
|
+
xSettlementAccountSupported: true
|
|
317
329
|
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return [exactKind];
|
|
324
|
-
}
|
|
325
|
-
return
|
|
330
|
+
}
|
|
331
|
+
} : {}
|
|
332
|
+
};
|
|
333
|
+
const uptoKind = { x402Version: 2, scheme: "upto", network };
|
|
334
|
+
if (group.family === "evm") {
|
|
335
|
+
return [exactKind, uptoKind];
|
|
336
|
+
}
|
|
337
|
+
return [exactKind, uptoKind];
|
|
326
338
|
});
|
|
327
339
|
}
|
|
328
340
|
var init_server = __esm({
|
|
@@ -335,57 +347,6 @@ var init_server = __esm({
|
|
|
335
347
|
}
|
|
336
348
|
});
|
|
337
349
|
|
|
338
|
-
// src/upstash-rest.ts
|
|
339
|
-
var upstash_rest_exports = {};
|
|
340
|
-
__export(upstash_rest_exports, {
|
|
341
|
-
createUpstashRest: () => createUpstashRest
|
|
342
|
-
});
|
|
343
|
-
function createUpstashRest(url, token) {
|
|
344
|
-
const base = url.replace(/\/+$/, "");
|
|
345
|
-
const headers = { Authorization: `Bearer ${token}` };
|
|
346
|
-
async function get(key) {
|
|
347
|
-
const res = await fetch(`${base}/get/${key}`, { headers });
|
|
348
|
-
if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
|
|
349
|
-
const { result } = await res.json();
|
|
350
|
-
return result ?? null;
|
|
351
|
-
}
|
|
352
|
-
async function set(key, value) {
|
|
353
|
-
const res = await fetch(`${base}`, {
|
|
354
|
-
method: "POST",
|
|
355
|
-
headers: { ...headers, "Content-Type": "application/json" },
|
|
356
|
-
body: JSON.stringify(["SET", key, JSON.stringify(value)])
|
|
357
|
-
});
|
|
358
|
-
if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
|
|
359
|
-
return await res.json();
|
|
360
|
-
}
|
|
361
|
-
async function del(key) {
|
|
362
|
-
const res = await fetch(`${base}`, {
|
|
363
|
-
method: "POST",
|
|
364
|
-
headers: { ...headers, "Content-Type": "application/json" },
|
|
365
|
-
body: JSON.stringify(["DEL", key])
|
|
366
|
-
});
|
|
367
|
-
if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
|
|
368
|
-
return await res.json();
|
|
369
|
-
}
|
|
370
|
-
return {
|
|
371
|
-
get,
|
|
372
|
-
set,
|
|
373
|
-
del,
|
|
374
|
-
async update(key, fn) {
|
|
375
|
-
const current = await get(key);
|
|
376
|
-
const change = fn(current);
|
|
377
|
-
if (change.op === "set") await set(key, change.value);
|
|
378
|
-
if (change.op === "delete") await del(key);
|
|
379
|
-
return change.result;
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
var init_upstash_rest = __esm({
|
|
384
|
-
"src/upstash-rest.ts"() {
|
|
385
|
-
"use strict";
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
350
|
// src/index.ts
|
|
390
351
|
var index_exports = {};
|
|
391
352
|
__export(index_exports, {
|
|
@@ -402,14 +363,16 @@ __export(index_exports, {
|
|
|
402
363
|
TEMPO_USDC_CURRENCY: () => TEMPO_USDC_CURRENCY,
|
|
403
364
|
ZERO_EVM_ADDRESS: () => ZERO_EVM_ADDRESS,
|
|
404
365
|
consolePlugin: () => consolePlugin,
|
|
405
|
-
|
|
406
|
-
|
|
366
|
+
createKvEntitlementStore: () => createKvEntitlementStore,
|
|
367
|
+
createKvMppStore: () => createKvMppStore,
|
|
368
|
+
createKvNonceStore: () => createKvNonceStore,
|
|
407
369
|
createRouter: () => createRouter,
|
|
408
370
|
formatRouterConfigIssues: () => formatRouterConfigIssues,
|
|
409
371
|
getRouterConfigIssues: () => getRouterConfigIssues,
|
|
410
372
|
mppFromEnv: () => mppFromEnv,
|
|
411
373
|
paidOptionsForProtocols: () => paidOptionsForProtocols,
|
|
412
374
|
validateRouterConfig: () => validateRouterConfig,
|
|
375
|
+
withPrefix: () => withPrefix,
|
|
413
376
|
x402AcceptsFromEnv: () => x402AcceptsFromEnv
|
|
414
377
|
});
|
|
415
378
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -417,11 +380,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
417
380
|
// src/registry.ts
|
|
418
381
|
var RouteRegistry = class {
|
|
419
382
|
routes = /* @__PURE__ */ new Map();
|
|
420
|
-
// Internal map key includes the HTTP method so that POST and DELETE on the
|
|
421
|
-
// same path coexist. Within the same path+method, last-write-wins is still
|
|
422
|
-
// intentional — Next.js module loading order is non-deterministic during
|
|
423
|
-
// build and discovery stubs may register the same route in either order.
|
|
424
|
-
// Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
|
|
425
383
|
mapKey(entry) {
|
|
426
384
|
return `${entry.key}:${entry.method}`;
|
|
427
385
|
}
|
|
@@ -434,8 +392,6 @@ var RouteRegistry = class {
|
|
|
434
392
|
}
|
|
435
393
|
this.routes.set(k, entry);
|
|
436
394
|
}
|
|
437
|
-
// Accepts either a compound key ("site/domain:DELETE") or a path-only key
|
|
438
|
-
// ("site/domain") — path-only returns the first registered method for that path.
|
|
439
395
|
get(key) {
|
|
440
396
|
const direct = this.routes.get(key);
|
|
441
397
|
if (direct) return direct;
|
|
@@ -467,24 +423,17 @@ var RouteRegistry = class {
|
|
|
467
423
|
|
|
468
424
|
// src/headers.ts
|
|
469
425
|
var HEADERS = {
|
|
470
|
-
// ---- Standard HTTP ----
|
|
471
426
|
AUTHORIZATION: "Authorization",
|
|
472
427
|
WWW_AUTHENTICATE: "WWW-Authenticate",
|
|
473
|
-
// ---- Auth ----
|
|
474
428
|
API_KEY: "X-API-Key",
|
|
475
|
-
// ---- Request meta (used by plugin/observability) ----
|
|
476
429
|
WALLET_ADDRESS: "X-Wallet-Address",
|
|
477
430
|
CLIENT_ID: "X-Client-ID",
|
|
478
431
|
SESSION_ID: "X-Session-ID",
|
|
479
|
-
// ---- SIWX ----
|
|
480
432
|
SIWX: "SIGN-IN-WITH-X",
|
|
481
|
-
// ---- x402 (payment) ----
|
|
482
433
|
X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
|
|
483
|
-
/** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
|
|
484
434
|
X402_PAYMENT_LEGACY: "X-PAYMENT",
|
|
485
435
|
X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
|
|
486
436
|
X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
|
|
487
|
-
// ---- MPP (payment) ----
|
|
488
437
|
MPP_PAYMENT_RECEIPT: "Payment-Receipt"
|
|
489
438
|
};
|
|
490
439
|
var AUTH_SCHEME = {
|
|
@@ -567,11 +516,31 @@ function consolePlugin() {
|
|
|
567
516
|
};
|
|
568
517
|
}
|
|
569
518
|
|
|
519
|
+
// src/alert.ts
|
|
520
|
+
function createReporter(plugin, pluginCtx, route) {
|
|
521
|
+
return (level, message, meta) => {
|
|
522
|
+
firePluginHook(plugin, "onAlert", pluginCtx, {
|
|
523
|
+
level,
|
|
524
|
+
message,
|
|
525
|
+
route,
|
|
526
|
+
...meta ? { meta } : {}
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
570
531
|
// src/pipeline/context/preflight.ts
|
|
571
532
|
function preflight(routeEntry, handler, deps, request) {
|
|
572
533
|
const meta = buildMeta(request, routeEntry);
|
|
573
534
|
const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
|
|
574
|
-
return {
|
|
535
|
+
return {
|
|
536
|
+
routeEntry,
|
|
537
|
+
handler,
|
|
538
|
+
deps,
|
|
539
|
+
request,
|
|
540
|
+
meta,
|
|
541
|
+
pluginCtx,
|
|
542
|
+
report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
|
|
543
|
+
};
|
|
575
544
|
}
|
|
576
545
|
function buildMeta(request, routeEntry) {
|
|
577
546
|
return {
|
|
@@ -616,19 +585,38 @@ function validateBody(parsed, schema) {
|
|
|
616
585
|
};
|
|
617
586
|
}
|
|
618
587
|
|
|
588
|
+
// src/pipeline/context/fire-plugin-response.ts
|
|
589
|
+
function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
590
|
+
firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
|
|
591
|
+
statusCode: response.status,
|
|
592
|
+
statusText: response.statusText,
|
|
593
|
+
duration: Date.now() - ctx.meta.startTime,
|
|
594
|
+
contentType: response.headers.get("content-type"),
|
|
595
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
596
|
+
requestBody,
|
|
597
|
+
responseBody
|
|
598
|
+
});
|
|
599
|
+
if (response.status >= 400 && response.status !== 402) {
|
|
600
|
+
firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
|
|
601
|
+
status: response.status,
|
|
602
|
+
message: response.statusText || `HTTP ${response.status}`,
|
|
603
|
+
settled: false
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
619
608
|
// src/pipeline/context/parse-body.ts
|
|
620
|
-
async function parseBody(
|
|
621
|
-
if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
609
|
+
async function parseBody(ctx, request = ctx.request) {
|
|
610
|
+
if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
622
611
|
const raw = await bufferBody(request);
|
|
623
|
-
const result = validateBody(raw, routeEntry.bodySchema);
|
|
612
|
+
const result = validateBody(raw, ctx.routeEntry.bodySchema);
|
|
624
613
|
if (result.success) return { ok: true, data: result.data };
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
};
|
|
614
|
+
const response = import_server.NextResponse.json(
|
|
615
|
+
{ success: false, error: result.error, issues: result.issues },
|
|
616
|
+
{ status: 400 }
|
|
617
|
+
);
|
|
618
|
+
firePluginResponse(ctx, response);
|
|
619
|
+
return { ok: false, response };
|
|
632
620
|
}
|
|
633
621
|
|
|
634
622
|
// src/pipeline/context/parse-query.ts
|
|
@@ -654,28 +642,6 @@ function handlerFailureError(response) {
|
|
|
654
642
|
|
|
655
643
|
// src/pipeline/context/fail.ts
|
|
656
644
|
var import_server2 = require("next/server");
|
|
657
|
-
|
|
658
|
-
// src/pipeline/context/fire-plugin-response.ts
|
|
659
|
-
function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
660
|
-
firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
|
|
661
|
-
statusCode: response.status,
|
|
662
|
-
statusText: response.statusText,
|
|
663
|
-
duration: Date.now() - ctx.meta.startTime,
|
|
664
|
-
contentType: response.headers.get("content-type"),
|
|
665
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
666
|
-
requestBody,
|
|
667
|
-
responseBody
|
|
668
|
-
});
|
|
669
|
-
if (response.status >= 400 && response.status !== 402) {
|
|
670
|
-
firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
|
|
671
|
-
status: response.status,
|
|
672
|
-
message: response.statusText || `HTTP ${response.status}`,
|
|
673
|
-
settled: false
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// src/pipeline/context/fail.ts
|
|
679
645
|
function fail(ctx, status, message, requestBody) {
|
|
680
646
|
const response = import_server2.NextResponse.json({ success: false, error: message }, { status });
|
|
681
647
|
firePluginResponse(ctx, response, requestBody);
|
|
@@ -693,7 +659,7 @@ async function runValidate(ctx, body) {
|
|
|
693
659
|
}
|
|
694
660
|
}
|
|
695
661
|
|
|
696
|
-
// src/
|
|
662
|
+
// src/pipeline/flows/static/static-invoke.ts
|
|
697
663
|
var import_server3 = require("next/server");
|
|
698
664
|
|
|
699
665
|
// src/types.ts
|
|
@@ -705,23 +671,15 @@ var HttpError = class extends Error {
|
|
|
705
671
|
}
|
|
706
672
|
};
|
|
707
673
|
|
|
708
|
-
// src/
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const result = await handler(ctx);
|
|
712
|
-
if (result instanceof Response) return result;
|
|
713
|
-
return import_server3.NextResponse.json(result);
|
|
714
|
-
} catch (error) {
|
|
715
|
-
options.onError?.(error);
|
|
716
|
-
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
717
|
-
const message = error instanceof Error ? error.message : "Internal error";
|
|
718
|
-
return import_server3.NextResponse.json({ success: false, error: message }, { status });
|
|
719
|
-
}
|
|
674
|
+
// src/pipeline/flows/static/static-invoke.ts
|
|
675
|
+
function invokePaidStatic(ctx, wallet, account, body, payment) {
|
|
676
|
+
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
|
|
720
677
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
678
|
+
function invokeUnauthed(ctx, wallet, account, body) {
|
|
679
|
+
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, null));
|
|
680
|
+
}
|
|
681
|
+
function buildHandlerCtx(ctx, wallet, account, body, payment) {
|
|
682
|
+
return {
|
|
725
683
|
body,
|
|
726
684
|
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
727
685
|
request: ctx.request,
|
|
@@ -740,21 +698,45 @@ async function invoke(ctx, wallet, account, body, payment) {
|
|
|
740
698
|
},
|
|
741
699
|
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
742
700
|
};
|
|
701
|
+
}
|
|
702
|
+
async function runHandler(ctx, handlerCtx) {
|
|
703
|
+
let returned;
|
|
704
|
+
try {
|
|
705
|
+
returned = ctx.handler(handlerCtx);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
return errorResult(error);
|
|
708
|
+
}
|
|
709
|
+
if (isAsyncIterable(returned) && !isThenable(returned)) {
|
|
710
|
+
return errorResult(
|
|
711
|
+
new HttpError(
|
|
712
|
+
`route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
|
|
713
|
+
500
|
|
714
|
+
)
|
|
715
|
+
);
|
|
716
|
+
}
|
|
743
717
|
let rawResult;
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
718
|
+
try {
|
|
719
|
+
rawResult = await returned;
|
|
720
|
+
} catch (error) {
|
|
721
|
+
return errorResult(error);
|
|
722
|
+
}
|
|
723
|
+
const response = rawResult instanceof Response ? rawResult : import_server3.NextResponse.json(rawResult);
|
|
724
|
+
return { response, rawResult };
|
|
725
|
+
}
|
|
726
|
+
function errorResult(error) {
|
|
727
|
+
const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
|
|
728
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
729
|
+
return {
|
|
730
|
+
response: import_server3.NextResponse.json({ success: false, error: message }, { status }),
|
|
731
|
+
rawResult: void 0,
|
|
732
|
+
handlerError: error
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function isAsyncIterable(value) {
|
|
736
|
+
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
737
|
+
}
|
|
738
|
+
function isThenable(value) {
|
|
739
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
758
740
|
}
|
|
759
741
|
|
|
760
742
|
// src/pipeline/context/fire-provider-quota.ts
|
|
@@ -788,24 +770,24 @@ function computeQuotaLevel(remaining, warn, critical) {
|
|
|
788
770
|
return "healthy";
|
|
789
771
|
}
|
|
790
772
|
|
|
791
|
-
// src/pipeline/context/finalize.ts
|
|
773
|
+
// src/pipeline/context/finalize/response.ts
|
|
792
774
|
function finalize(ctx, response, rawResult, requestBody) {
|
|
793
775
|
fireProviderQuota(ctx, response, rawResult);
|
|
794
776
|
firePluginResponse(ctx, response, requestBody, rawResult);
|
|
795
777
|
return response;
|
|
796
778
|
}
|
|
797
779
|
|
|
798
|
-
// src/pipeline/context/
|
|
799
|
-
async function
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
780
|
+
// src/pipeline/context/grant-entitlement.ts
|
|
781
|
+
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
782
|
+
if (!ctx.routeEntry.siwxEnabled) return;
|
|
783
|
+
try {
|
|
784
|
+
await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
ctx.report(
|
|
787
|
+
"warn",
|
|
788
|
+
`Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`
|
|
789
|
+
);
|
|
804
790
|
}
|
|
805
|
-
const validateErr = await runValidate(ctx, body.data);
|
|
806
|
-
if (validateErr) return validateErr;
|
|
807
|
-
const result = await invoke(ctx, wallet, account, body.data, null);
|
|
808
|
-
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
809
791
|
}
|
|
810
792
|
|
|
811
793
|
// src/pipeline/context/settlement-context.ts
|
|
@@ -822,23 +804,6 @@ function settlementContext(ctx, scope) {
|
|
|
822
804
|
};
|
|
823
805
|
}
|
|
824
806
|
|
|
825
|
-
// src/pipeline/context/run-before-settle.ts
|
|
826
|
-
async function runBeforeSettle(ctx, scope) {
|
|
827
|
-
const hook = ctx.routeEntry.settlement?.beforeSettle;
|
|
828
|
-
if (!hook) return null;
|
|
829
|
-
try {
|
|
830
|
-
await hook(settlementContext(ctx, scope));
|
|
831
|
-
return null;
|
|
832
|
-
} catch (error) {
|
|
833
|
-
return fail(
|
|
834
|
-
ctx,
|
|
835
|
-
errorStatus(error, 500),
|
|
836
|
-
errorMessage(error, "Pre-settlement validation failed"),
|
|
837
|
-
scope.body
|
|
838
|
-
);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
807
|
// src/pipeline/context/run-settlement-error.ts
|
|
843
808
|
async function runSettlementError(ctx, scope, error, phase) {
|
|
844
809
|
const hook = ctx.routeEntry.settlement?.onSettlementError;
|
|
@@ -847,12 +812,7 @@ async function runSettlementError(ctx, scope, error, phase) {
|
|
|
847
812
|
await hook({ ...settlementContext(ctx, scope), error, phase });
|
|
848
813
|
} catch (hookError) {
|
|
849
814
|
const message = errorMessage(hookError, "Settlement error hook failed");
|
|
850
|
-
|
|
851
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
852
|
-
level: "error",
|
|
853
|
-
message: `Settlement error hook failed: ${message}`,
|
|
854
|
-
route: ctx.routeEntry.key
|
|
855
|
-
});
|
|
815
|
+
ctx.report("error", `Settlement error hook failed: ${message}`);
|
|
856
816
|
}
|
|
857
817
|
}
|
|
858
818
|
|
|
@@ -864,81 +824,145 @@ async function runAfterSettle(ctx, scope) {
|
|
|
864
824
|
await hook(settlementContext(ctx, scope));
|
|
865
825
|
} catch (error) {
|
|
866
826
|
const message = errorMessage(error, "Post-settlement hook failed");
|
|
867
|
-
|
|
868
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
869
|
-
level: "error",
|
|
870
|
-
message: `Post-settlement hook failed: ${message}`,
|
|
871
|
-
route: ctx.routeEntry.key
|
|
872
|
-
});
|
|
827
|
+
ctx.report("error", `Post-settlement hook failed: ${message}`);
|
|
873
828
|
await runSettlementError(ctx, scope, error, "afterSettle");
|
|
874
829
|
}
|
|
875
830
|
}
|
|
876
831
|
|
|
877
|
-
// src/pipeline/context/
|
|
878
|
-
async function
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
route: ctx.routeEntry.key
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// src/pipeline/context/grant-entitlement.ts
|
|
895
|
-
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
896
|
-
if (!ctx.routeEntry.siwxEnabled) return;
|
|
897
|
-
try {
|
|
898
|
-
await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
|
|
899
|
-
} catch (error) {
|
|
900
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
901
|
-
level: "warn",
|
|
902
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
903
|
-
route: ctx.routeEntry.key
|
|
904
|
-
});
|
|
905
|
-
}
|
|
832
|
+
// src/pipeline/context/finalize/epilogue.ts
|
|
833
|
+
async function runPostSettleEpilogue(args) {
|
|
834
|
+
const { ctx, strategy, wallet, settle, afterSettleScope, rawResult, body } = args;
|
|
835
|
+
await grantEntitlementIfSiwx(ctx, wallet);
|
|
836
|
+
firePluginHook(ctx.deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
|
|
837
|
+
protocol: strategy.protocol,
|
|
838
|
+
payer: wallet,
|
|
839
|
+
transaction: settle.settledPayment.transaction ?? "",
|
|
840
|
+
network: settle.settledPayment.network
|
|
841
|
+
});
|
|
842
|
+
await runAfterSettle(ctx, afterSettleScope);
|
|
843
|
+
return finalize(ctx, settle.response, rawResult, body);
|
|
906
844
|
}
|
|
907
845
|
|
|
908
|
-
// src/pipeline/context/
|
|
909
|
-
async function
|
|
910
|
-
const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
|
|
911
|
-
const { request, routeEntry, deps } = ctx;
|
|
846
|
+
// src/pipeline/context/finalize/request.ts
|
|
847
|
+
async function settleAndFinalizeRequest(args) {
|
|
848
|
+
const { ctx, strategy, verifyOutcome, scope, rawResult, body, billedAmount, onSettleError } = args;
|
|
849
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
912
850
|
const settle = await strategy.settle({
|
|
913
851
|
request,
|
|
914
852
|
response: scope.response,
|
|
915
853
|
payment: verifyOutcome.payment,
|
|
916
854
|
token: verifyOutcome.token,
|
|
917
855
|
routeEntry,
|
|
918
|
-
deps
|
|
856
|
+
deps,
|
|
857
|
+
billedAmount,
|
|
858
|
+
report
|
|
919
859
|
});
|
|
920
860
|
if (!settle.ok) {
|
|
921
861
|
if (onSettleError) await onSettleError(settle.error, settle.failMessage);
|
|
922
862
|
return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
|
|
923
863
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
864
|
+
return runPostSettleEpilogue({
|
|
865
|
+
ctx,
|
|
866
|
+
strategy,
|
|
867
|
+
wallet: verifyOutcome.wallet,
|
|
868
|
+
settle,
|
|
869
|
+
afterSettleScope: {
|
|
870
|
+
...scope,
|
|
871
|
+
payment: settle.settledPayment,
|
|
872
|
+
response: settle.response
|
|
873
|
+
},
|
|
874
|
+
rawResult,
|
|
875
|
+
body
|
|
930
876
|
});
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/pipeline/context/finalize/stream.ts
|
|
880
|
+
async function settleAndFinalizeStream(args) {
|
|
881
|
+
const { ctx, strategy, verifyOutcome, source, account, body, bindChannelCharge } = args;
|
|
882
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
883
|
+
if (!strategy.settleStream) {
|
|
884
|
+
return fail(ctx, 500, `${strategy.protocol} does not support streaming handlers`, body);
|
|
885
|
+
}
|
|
886
|
+
const settle = await strategy.settleStream({
|
|
887
|
+
request,
|
|
888
|
+
source,
|
|
889
|
+
payment: verifyOutcome.payment,
|
|
890
|
+
token: verifyOutcome.token,
|
|
891
|
+
routeEntry,
|
|
892
|
+
deps,
|
|
893
|
+
bindChannelCharge,
|
|
894
|
+
report
|
|
935
895
|
});
|
|
936
|
-
|
|
896
|
+
if (!settle.ok) {
|
|
897
|
+
return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
|
|
898
|
+
}
|
|
899
|
+
return runPostSettleEpilogue({
|
|
900
|
+
ctx,
|
|
901
|
+
strategy,
|
|
902
|
+
wallet: verifyOutcome.wallet,
|
|
903
|
+
settle,
|
|
904
|
+
afterSettleScope: {
|
|
905
|
+
wallet: verifyOutcome.wallet,
|
|
906
|
+
account,
|
|
907
|
+
body,
|
|
908
|
+
payment: settle.settledPayment,
|
|
909
|
+
response: settle.response,
|
|
910
|
+
rawResult: void 0
|
|
911
|
+
},
|
|
912
|
+
rawResult: void 0,
|
|
913
|
+
body
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/pipeline/context/run-handler-only.ts
|
|
918
|
+
async function runHandlerOnly(ctx, wallet, account) {
|
|
919
|
+
const body = await parseBody(ctx);
|
|
920
|
+
if (!body.ok) return body.response;
|
|
921
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
922
|
+
if (validateErr) return validateErr;
|
|
923
|
+
const result = await invokeUnauthed(ctx, wallet, account, body.data);
|
|
924
|
+
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/pipeline/context/run-before-settle.ts
|
|
928
|
+
async function runBeforeSettle(ctx, scope) {
|
|
929
|
+
const hook = ctx.routeEntry.settlement?.beforeSettle;
|
|
930
|
+
if (!hook) return null;
|
|
931
|
+
try {
|
|
932
|
+
await hook(settlementContext(ctx, scope));
|
|
933
|
+
return null;
|
|
934
|
+
} catch (error) {
|
|
935
|
+
return fail(
|
|
936
|
+
ctx,
|
|
937
|
+
errorStatus(error, 500),
|
|
938
|
+
errorMessage(error, "Pre-settlement validation failed"),
|
|
939
|
+
scope.body
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/pipeline/context/run-settled-handler-error.ts
|
|
945
|
+
async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
|
|
946
|
+
const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
|
|
947
|
+
if (!hook) return;
|
|
948
|
+
try {
|
|
949
|
+
await hook({ ...settlementContext(ctx, scope), error });
|
|
950
|
+
} catch (hookError) {
|
|
951
|
+
const message = errorMessage(hookError, "Settled handler error hook failed");
|
|
952
|
+
ctx.report("error", `Settled handler error hook failed: ${message}`);
|
|
953
|
+
}
|
|
937
954
|
}
|
|
938
955
|
|
|
939
956
|
// src/auth/normalize-wallet.ts
|
|
940
957
|
function normalizeWalletAddress(address) {
|
|
941
|
-
|
|
958
|
+
const isEvm = /^0x/i.test(address);
|
|
959
|
+
return isEvm ? normalizeEvmWalletAddress(address) : normalizeSolanaWalletAddress(address);
|
|
960
|
+
}
|
|
961
|
+
function normalizeEvmWalletAddress(address) {
|
|
962
|
+
return address.toLowerCase();
|
|
963
|
+
}
|
|
964
|
+
function normalizeSolanaWalletAddress(address) {
|
|
965
|
+
return address;
|
|
942
966
|
}
|
|
943
967
|
|
|
944
968
|
// src/auth/siwx.ts
|
|
@@ -1018,20 +1042,18 @@ function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
|
|
|
1018
1042
|
return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
|
|
1019
1043
|
}
|
|
1020
1044
|
|
|
1021
|
-
// src/pipeline/context/
|
|
1022
|
-
function
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
if (errors.length === 0) return null;
|
|
1034
|
-
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
1045
|
+
// src/pipeline/context/resolve-early-body.ts
|
|
1046
|
+
async function resolveEarlyBody(args) {
|
|
1047
|
+
const { ctx, pricing, incomingStrategy } = args;
|
|
1048
|
+
if (!shouldParseBodyEarly(incomingStrategy, ctx.routeEntry, pricing)) {
|
|
1049
|
+
return { ok: true, earlyBody: void 0 };
|
|
1050
|
+
}
|
|
1051
|
+
const earlyClone = ctx.request.clone();
|
|
1052
|
+
const earlyResult = await parseBody(ctx, earlyClone);
|
|
1053
|
+
if (!earlyResult.ok) return { ok: false, response: earlyResult.response };
|
|
1054
|
+
const validateErr = await runValidate(ctx, earlyResult.data);
|
|
1055
|
+
if (validateErr) return { ok: false, response: validateErr };
|
|
1056
|
+
return { ok: true, earlyBody: earlyResult.data };
|
|
1035
1057
|
}
|
|
1036
1058
|
|
|
1037
1059
|
// src/auth/api-key.ts
|
|
@@ -1052,6 +1074,39 @@ function extractBearerToken(header) {
|
|
|
1052
1074
|
return null;
|
|
1053
1075
|
}
|
|
1054
1076
|
|
|
1077
|
+
// src/pipeline/context/run-api-key-gate.ts
|
|
1078
|
+
async function runApiKeyGate(ctx) {
|
|
1079
|
+
const { request, routeEntry, deps } = ctx;
|
|
1080
|
+
if (!routeEntry.apiKeyResolver) return { ok: true, account: void 0 };
|
|
1081
|
+
const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
1082
|
+
if (!apiKeyResult.valid) {
|
|
1083
|
+
return { ok: false, response: fail(ctx, 401, "Invalid or missing API key") };
|
|
1084
|
+
}
|
|
1085
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
1086
|
+
authMode: "apiKey",
|
|
1087
|
+
wallet: null,
|
|
1088
|
+
route: routeEntry.key,
|
|
1089
|
+
account: apiKeyResult.account
|
|
1090
|
+
});
|
|
1091
|
+
return { ok: true, account: apiKeyResult.account };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/pipeline/context/protocol-init-error.ts
|
|
1095
|
+
function protocolInitError(routeEntry, deps) {
|
|
1096
|
+
if (!routeEntry.pricing) return null;
|
|
1097
|
+
const errors = [];
|
|
1098
|
+
for (const protocol of routeEntry.protocols) {
|
|
1099
|
+
if (protocol === "x402" && deps.x402InitError) {
|
|
1100
|
+
errors.push(`x402: ${deps.x402InitError}`);
|
|
1101
|
+
}
|
|
1102
|
+
if (protocol === "mpp" && deps.mppInitError) {
|
|
1103
|
+
errors.push(`mpp: ${deps.mppInitError}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (errors.length === 0) return null;
|
|
1107
|
+
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1055
1110
|
// src/pipeline/flows/api-key-only.ts
|
|
1056
1111
|
async function runApiKeyOnlyFlow(ctx) {
|
|
1057
1112
|
if (!ctx.routeEntry.apiKeyResolver) {
|
|
@@ -1221,13 +1276,22 @@ function selectPricing(raw, deps = {}) {
|
|
|
1221
1276
|
// src/protocols/mpp/credential.ts
|
|
1222
1277
|
var import_mppx = require("mppx");
|
|
1223
1278
|
var import_viem = require("viem");
|
|
1279
|
+
var SESSION_ACTIONS = /* @__PURE__ */ new Set(["open", "topUp", "voucher", "close"]);
|
|
1224
1280
|
function readMppCredential(request) {
|
|
1225
1281
|
const credential = import_mppx.Credential.fromRequest(request);
|
|
1226
1282
|
if (!credential) return null;
|
|
1227
1283
|
const wallet = walletFromDid(credential.source ?? "");
|
|
1228
|
-
const
|
|
1284
|
+
const payload = credential.payload;
|
|
1285
|
+
const rawType = payload?.type;
|
|
1229
1286
|
const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
|
|
1230
|
-
|
|
1287
|
+
const rawAction = payload?.action;
|
|
1288
|
+
const sessionAction = typeof rawAction === "string" && SESSION_ACTIONS.has(rawAction) ? rawAction : void 0;
|
|
1289
|
+
return {
|
|
1290
|
+
credential,
|
|
1291
|
+
wallet,
|
|
1292
|
+
payloadType,
|
|
1293
|
+
...sessionAction ? { sessionAction } : {}
|
|
1294
|
+
};
|
|
1231
1295
|
}
|
|
1232
1296
|
function walletFromDid(rawSource) {
|
|
1233
1297
|
const parts = rawSource.split(":");
|
|
@@ -1235,6 +1299,168 @@ function walletFromDid(rawSource) {
|
|
|
1235
1299
|
return normalizeWalletAddress((0, import_viem.isAddress)(last) ? (0, import_viem.getAddress)(last) : rawSource);
|
|
1236
1300
|
}
|
|
1237
1301
|
|
|
1302
|
+
// src/protocols/mpp/session-mode.ts
|
|
1303
|
+
async function verifySessionMode(args, info) {
|
|
1304
|
+
const { request, deps, price, routeEntry } = args;
|
|
1305
|
+
if (!deps.mppx?.sessionRequest || !deps.mppx?.sessionStream || !deps.mppSessionConfig) {
|
|
1306
|
+
return {
|
|
1307
|
+
ok: false,
|
|
1308
|
+
kind: "config",
|
|
1309
|
+
message: "MPP sessions not configured on this server (set RouterConfig.mpp.session)"
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
const tickCost = routeEntry.tickCost;
|
|
1313
|
+
const unitType = routeEntry.unitType;
|
|
1314
|
+
const streaming = routeEntry.streaming === true;
|
|
1315
|
+
const middleware = streaming ? deps.mppx.sessionStream : deps.mppx.sessionRequest;
|
|
1316
|
+
const middlewareRequest = isChannelOnlyAction(info, request) ? new Request(request.url, { method: request.method, headers: request.headers }) : request;
|
|
1317
|
+
let result;
|
|
1318
|
+
try {
|
|
1319
|
+
result = await middleware({
|
|
1320
|
+
amount: tickCost,
|
|
1321
|
+
unitType,
|
|
1322
|
+
suggestedDeposit: price,
|
|
1323
|
+
...streaming ? { meta: { streaming: "true" } } : {}
|
|
1324
|
+
})(middlewareRequest);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1327
|
+
return {
|
|
1328
|
+
ok: false,
|
|
1329
|
+
kind: "config",
|
|
1330
|
+
message: `MPP session verify failed: ${message}`
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
if (result.status === 402) {
|
|
1334
|
+
const failure = await readMppxProblemDetails(result.challenge);
|
|
1335
|
+
return { ok: false, kind: "invalid", failure };
|
|
1336
|
+
}
|
|
1337
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1338
|
+
const payment = {
|
|
1339
|
+
protocol: "mpp",
|
|
1340
|
+
status: "verified",
|
|
1341
|
+
payer: info.wallet,
|
|
1342
|
+
amount: price,
|
|
1343
|
+
network: "tempo:4217",
|
|
1344
|
+
...mppRecipient ? { recipient: mppRecipient } : {}
|
|
1345
|
+
};
|
|
1346
|
+
const token = {
|
|
1347
|
+
mode: "session",
|
|
1348
|
+
streaming,
|
|
1349
|
+
sessionResult: result,
|
|
1350
|
+
info,
|
|
1351
|
+
tickCost
|
|
1352
|
+
};
|
|
1353
|
+
return {
|
|
1354
|
+
ok: true,
|
|
1355
|
+
wallet: info.wallet,
|
|
1356
|
+
payment,
|
|
1357
|
+
token,
|
|
1358
|
+
alreadySettled: false
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
async function settleSessionMode(args) {
|
|
1362
|
+
const { request, response, payment, token, billedAmount } = args;
|
|
1363
|
+
const sessionToken = token;
|
|
1364
|
+
if (isChannelOnlyAction(sessionToken.info, request)) {
|
|
1365
|
+
const wrapped2 = sessionToken.sessionResult.withReceipt(
|
|
1366
|
+
new Response(null, { status: 200 })
|
|
1367
|
+
);
|
|
1368
|
+
return {
|
|
1369
|
+
ok: true,
|
|
1370
|
+
response: wrapped2,
|
|
1371
|
+
settledPayment: { ...payment, status: "settled", amount: billedAmount }
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
if (sessionToken.streaming) {
|
|
1375
|
+
return {
|
|
1376
|
+
ok: false,
|
|
1377
|
+
error: new Error("streaming session content settled via request path"),
|
|
1378
|
+
failMessage: "streaming session content settled via request path",
|
|
1379
|
+
failStatus: 500
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
const wrapped = sessionToken.sessionResult.withReceipt(
|
|
1383
|
+
response
|
|
1384
|
+
);
|
|
1385
|
+
wrapped.headers.set("Cache-Control", "private");
|
|
1386
|
+
const receiptHeader = wrapped.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
|
|
1387
|
+
const settledPayment = {
|
|
1388
|
+
...payment,
|
|
1389
|
+
status: "settled",
|
|
1390
|
+
amount: billedAmount,
|
|
1391
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1392
|
+
};
|
|
1393
|
+
return { ok: true, response: wrapped, settledPayment };
|
|
1394
|
+
}
|
|
1395
|
+
async function buildSessionChallenge(args) {
|
|
1396
|
+
const { request, deps, suggestedDeposit, routeEntry, report } = args;
|
|
1397
|
+
if (!deps.mppSessionConfig) return {};
|
|
1398
|
+
const streaming = routeEntry.streaming === true;
|
|
1399
|
+
const middleware = streaming ? deps.mppx?.sessionStream : deps.mppx?.sessionRequest;
|
|
1400
|
+
if (!middleware) return {};
|
|
1401
|
+
const tickCost = routeEntry.tickCost;
|
|
1402
|
+
const unitType = routeEntry.unitType;
|
|
1403
|
+
try {
|
|
1404
|
+
const result = await middleware({
|
|
1405
|
+
amount: tickCost,
|
|
1406
|
+
unitType,
|
|
1407
|
+
suggestedDeposit,
|
|
1408
|
+
...streaming ? { meta: { streaming: "true" } } : {}
|
|
1409
|
+
})(request);
|
|
1410
|
+
if (result.status === 402) {
|
|
1411
|
+
const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
1412
|
+
if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
|
|
1413
|
+
}
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
report(
|
|
1416
|
+
"warn",
|
|
1417
|
+
`MPP session challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1418
|
+
);
|
|
1419
|
+
throw err;
|
|
1420
|
+
}
|
|
1421
|
+
return {};
|
|
1422
|
+
}
|
|
1423
|
+
function isChannelOnlyAction(info, request) {
|
|
1424
|
+
const action = info.sessionAction;
|
|
1425
|
+
if (!action) return false;
|
|
1426
|
+
if (action === "close" || action === "topUp") return true;
|
|
1427
|
+
if ((action === "open" || action === "voucher") && !hasRequestBody(request)) return true;
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
async function readMppxProblemDetails(challenge) {
|
|
1431
|
+
let body;
|
|
1432
|
+
try {
|
|
1433
|
+
body = await challenge.clone().text();
|
|
1434
|
+
} catch {
|
|
1435
|
+
return { reason: "mpp_session_invalid" };
|
|
1436
|
+
}
|
|
1437
|
+
if (!body) return { reason: "mpp_session_invalid" };
|
|
1438
|
+
let parsed;
|
|
1439
|
+
try {
|
|
1440
|
+
parsed = JSON.parse(body);
|
|
1441
|
+
} catch {
|
|
1442
|
+
return { reason: "mpp_session_invalid", message: body.slice(0, 500) };
|
|
1443
|
+
}
|
|
1444
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1445
|
+
return { reason: "mpp_session_invalid" };
|
|
1446
|
+
}
|
|
1447
|
+
const details = parsed;
|
|
1448
|
+
const typeUri = typeof details.type === "string" ? details.type : void 0;
|
|
1449
|
+
const slug = typeUri ? typeUri.split("/").pop() : void 0;
|
|
1450
|
+
const reason = slug ? slug.replace(/-/g, "_") : "mpp_session_invalid";
|
|
1451
|
+
const message = typeof details.detail === "string" && details.detail.length > 0 ? details.detail : typeof details.title === "string" ? details.title : void 0;
|
|
1452
|
+
return message ? { reason, message } : { reason };
|
|
1453
|
+
}
|
|
1454
|
+
function hasRequestBody(request) {
|
|
1455
|
+
const cl = request.headers.get("content-length");
|
|
1456
|
+
if (cl !== null) {
|
|
1457
|
+
const n = Number.parseInt(cl.trim(), 10);
|
|
1458
|
+
return Number.isFinite(n) && n > 0;
|
|
1459
|
+
}
|
|
1460
|
+
if (request.headers.get("transfer-encoding") !== null) return true;
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1238
1464
|
// src/protocols/mpp/transaction-mode.ts
|
|
1239
1465
|
var import_tempo = require("viem/tempo");
|
|
1240
1466
|
var import_actions = require("viem/actions");
|
|
@@ -1262,7 +1488,7 @@ async function readChallengeReason(challenge) {
|
|
|
1262
1488
|
|
|
1263
1489
|
// src/protocols/mpp/transaction-mode.ts
|
|
1264
1490
|
async function verifyTxMode(args, info) {
|
|
1265
|
-
const { deps, price,
|
|
1491
|
+
const { deps, price, report } = args;
|
|
1266
1492
|
if (!deps.tempoClient) {
|
|
1267
1493
|
return {
|
|
1268
1494
|
ok: false,
|
|
@@ -1280,7 +1506,7 @@ async function verifyTxMode(args, info) {
|
|
|
1280
1506
|
});
|
|
1281
1507
|
} catch (err) {
|
|
1282
1508
|
const message = err instanceof Error ? err.message : String(err);
|
|
1283
|
-
|
|
1509
|
+
report("warn", `MPP simulation failed: ${message}`);
|
|
1284
1510
|
return { ok: false, kind: "invalid" };
|
|
1285
1511
|
}
|
|
1286
1512
|
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
@@ -1301,7 +1527,7 @@ async function verifyTxMode(args, info) {
|
|
|
1301
1527
|
};
|
|
1302
1528
|
}
|
|
1303
1529
|
async function settleTxMode(args) {
|
|
1304
|
-
const { request, response, payment, deps,
|
|
1530
|
+
const { request, response, payment, deps, report } = args;
|
|
1305
1531
|
if (!deps.mppx) {
|
|
1306
1532
|
return {
|
|
1307
1533
|
ok: false,
|
|
@@ -1315,7 +1541,7 @@ async function settleTxMode(args) {
|
|
|
1315
1541
|
result = await deps.mppx.charge({ amount: payment.amount })(request);
|
|
1316
1542
|
} catch (err) {
|
|
1317
1543
|
const message = err instanceof Error ? err.message : String(err);
|
|
1318
|
-
|
|
1544
|
+
report("error", `MPP broadcast failed after handler: ${message}`);
|
|
1319
1545
|
return {
|
|
1320
1546
|
ok: false,
|
|
1321
1547
|
error: err,
|
|
@@ -1332,7 +1558,7 @@ async function settleTxMode(args) {
|
|
|
1332
1558
|
mppResult: result,
|
|
1333
1559
|
challenge: result.challenge
|
|
1334
1560
|
});
|
|
1335
|
-
|
|
1561
|
+
report("error", `MPP payment failed after handler: ${detail}`);
|
|
1336
1562
|
return {
|
|
1337
1563
|
ok: false,
|
|
1338
1564
|
error: settlementError,
|
|
@@ -1355,10 +1581,10 @@ async function settleTxMode(args) {
|
|
|
1355
1581
|
|
|
1356
1582
|
// src/protocols/mpp/hash-mode.ts
|
|
1357
1583
|
async function verifyHashMode(args, info) {
|
|
1358
|
-
const { deps, price,
|
|
1584
|
+
const { deps, price, request, report } = args;
|
|
1359
1585
|
if (!deps.mppx) {
|
|
1360
1586
|
const reason = deps.mppInitError ? `MPP initialization failed: ${deps.mppInitError}` : "MPP not initialized \u2014 ensure mppx is installed and mpp config (secretKey, currency, recipient) is correct";
|
|
1361
|
-
|
|
1587
|
+
report("error", reason);
|
|
1362
1588
|
return { ok: false, kind: "config", message: reason };
|
|
1363
1589
|
}
|
|
1364
1590
|
let chargeResult;
|
|
@@ -1366,13 +1592,13 @@ async function verifyHashMode(args, info) {
|
|
|
1366
1592
|
chargeResult = await deps.mppx.charge({ amount: price })(request);
|
|
1367
1593
|
} catch (err) {
|
|
1368
1594
|
const message = err instanceof Error ? err.message : String(err);
|
|
1369
|
-
|
|
1595
|
+
report("error", `MPP charge failed: ${message}`);
|
|
1370
1596
|
return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
|
|
1371
1597
|
}
|
|
1372
1598
|
if (chargeResult.status === 402) {
|
|
1373
1599
|
const reason = await readChallengeReason(chargeResult.challenge);
|
|
1374
1600
|
const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
|
|
1375
|
-
|
|
1601
|
+
report("warn", `MPP credential rejected: ${detail}`);
|
|
1376
1602
|
return { ok: false, kind: "invalid" };
|
|
1377
1603
|
}
|
|
1378
1604
|
const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
|
|
@@ -1417,9 +1643,20 @@ var mppStrategy = {
|
|
|
1417
1643
|
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
1418
1644
|
return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
|
|
1419
1645
|
},
|
|
1646
|
+
preflight(request, _routeEntry) {
|
|
1647
|
+
const info = readMppCredential(request);
|
|
1648
|
+
if (!info?.sessionAction) return null;
|
|
1649
|
+
if (!isChannelOnlyAction(info, request)) return null;
|
|
1650
|
+
return { skipBody: true, skipHandler: true };
|
|
1651
|
+
},
|
|
1420
1652
|
async verify(args) {
|
|
1421
1653
|
const info = readMppCredential(args.request);
|
|
1422
1654
|
if (!info) return { ok: false, kind: "invalid" };
|
|
1655
|
+
if (args.routeEntry.dynamicPrice) {
|
|
1656
|
+
if (!info.sessionAction) return { ok: false, kind: "invalid" };
|
|
1657
|
+
return verifySessionMode(args, info);
|
|
1658
|
+
}
|
|
1659
|
+
if (info.sessionAction) return { ok: false, kind: "invalid" };
|
|
1423
1660
|
if (info.payloadType === "transaction" && args.deps.tempoClient) {
|
|
1424
1661
|
return verifyTxMode(args, info);
|
|
1425
1662
|
}
|
|
@@ -1427,28 +1664,88 @@ var mppStrategy = {
|
|
|
1427
1664
|
},
|
|
1428
1665
|
async settle(args) {
|
|
1429
1666
|
const token = args.token;
|
|
1667
|
+
if (token.mode === "session") return settleSessionMode(args);
|
|
1430
1668
|
if (token.mode === "transaction") return settleTxMode(args);
|
|
1431
1669
|
return settleHashMode(args);
|
|
1432
1670
|
},
|
|
1671
|
+
async settleStream(args) {
|
|
1672
|
+
const token = args.token;
|
|
1673
|
+
if (token.mode !== "session" || !token.streaming) {
|
|
1674
|
+
return {
|
|
1675
|
+
ok: false,
|
|
1676
|
+
error: new Error("streaming requires a streaming-mode MPP session credential"),
|
|
1677
|
+
failMessage: "streaming requires a streaming-mode MPP session credential",
|
|
1678
|
+
failStatus: 400
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
const sessionToken = token;
|
|
1682
|
+
const sseResult = sessionToken.sessionResult;
|
|
1683
|
+
const { bindChannelCharge, source: handlerStream } = args;
|
|
1684
|
+
async function* forwardHandlerStreamWithChannelDebit(channel) {
|
|
1685
|
+
bindChannelCharge(channel.charge);
|
|
1686
|
+
try {
|
|
1687
|
+
for await (const chunk of handlerStream) {
|
|
1688
|
+
yield typeof chunk === "string" ? chunk : JSON.stringify(chunk);
|
|
1689
|
+
}
|
|
1690
|
+
} finally {
|
|
1691
|
+
bindChannelCharge(null);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const sse = sseResult.withReceipt(forwardHandlerStreamWithChannelDebit);
|
|
1695
|
+
sse.headers.set("Cache-Control", "private");
|
|
1696
|
+
const settledPayment = {
|
|
1697
|
+
...args.payment,
|
|
1698
|
+
status: "settled",
|
|
1699
|
+
amount: args.payment.amount
|
|
1700
|
+
};
|
|
1701
|
+
return { ok: true, response: sse, settledPayment };
|
|
1702
|
+
},
|
|
1433
1703
|
async buildChallenge(args) {
|
|
1434
1704
|
if (!args.deps.mppx) return {};
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
);
|
|
1445
|
-
throw err;
|
|
1705
|
+
const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
|
|
1706
|
+
if (args.routeEntry.dynamicPrice && sessionsConfigured) {
|
|
1707
|
+
const tickCost = args.routeEntry.tickCost;
|
|
1708
|
+
const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
|
|
1709
|
+
const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
|
|
1710
|
+
return buildSessionChallenge({
|
|
1711
|
+
...args,
|
|
1712
|
+
suggestedDeposit
|
|
1713
|
+
});
|
|
1446
1714
|
}
|
|
1447
|
-
return
|
|
1715
|
+
return buildChargeChallenge(args);
|
|
1448
1716
|
}
|
|
1449
1717
|
};
|
|
1718
|
+
function multiplyDecimal(decimal, factor) {
|
|
1719
|
+
if (!Number.isFinite(factor) || factor <= 0) return decimal;
|
|
1720
|
+
const [whole, fraction = ""] = decimal.split(".");
|
|
1721
|
+
const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
|
|
1722
|
+
const decimals = fraction.length;
|
|
1723
|
+
if (decimals === 0) return scaled;
|
|
1724
|
+
const padded = scaled.padStart(decimals + 1, "0");
|
|
1725
|
+
const intPart = padded.slice(0, padded.length - decimals);
|
|
1726
|
+
const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
|
|
1727
|
+
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
1728
|
+
}
|
|
1729
|
+
async function buildChargeChallenge(args) {
|
|
1730
|
+
if (!args.deps.mppx) return {};
|
|
1731
|
+
try {
|
|
1732
|
+
const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
|
|
1733
|
+
if (result.status === 402) {
|
|
1734
|
+
const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
1735
|
+
if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
|
|
1736
|
+
}
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
args.report(
|
|
1739
|
+
"warn",
|
|
1740
|
+
`MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1741
|
+
);
|
|
1742
|
+
throw err;
|
|
1743
|
+
}
|
|
1744
|
+
return {};
|
|
1745
|
+
}
|
|
1450
1746
|
|
|
1451
1747
|
// src/protocols/x402/strategy.ts
|
|
1748
|
+
var import_evm3 = require("@x402/evm");
|
|
1452
1749
|
init_accepts();
|
|
1453
1750
|
|
|
1454
1751
|
// src/protocols/x402/challenge.ts
|
|
@@ -1458,20 +1755,27 @@ init_solana();
|
|
|
1458
1755
|
// src/protocols/x402/requirements.ts
|
|
1459
1756
|
init_evm();
|
|
1460
1757
|
init_solana();
|
|
1461
|
-
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
1462
|
-
const
|
|
1758
|
+
async function buildExpectedRequirements(server, request, price, accepts, report) {
|
|
1759
|
+
const sdkRequirements = await buildSdkHandledRequirements(
|
|
1760
|
+
server,
|
|
1761
|
+
request,
|
|
1762
|
+
price,
|
|
1763
|
+
accepts,
|
|
1764
|
+
report
|
|
1765
|
+
);
|
|
1463
1766
|
const customRequirements = buildCustomRequirements(price, accepts);
|
|
1464
|
-
return [...
|
|
1767
|
+
return [...sdkRequirements, ...customRequirements];
|
|
1465
1768
|
}
|
|
1466
|
-
async function
|
|
1467
|
-
const
|
|
1769
|
+
async function buildSdkHandledRequirements(server, request, price, accepts, report) {
|
|
1770
|
+
const groups = [
|
|
1468
1771
|
buildEvmExactOptions(accepts, price),
|
|
1772
|
+
buildEvmUptoOptions(accepts, price),
|
|
1469
1773
|
buildSolanaExactOptions(accepts, price)
|
|
1470
1774
|
].filter((options) => options.length > 0);
|
|
1471
|
-
if (
|
|
1775
|
+
if (groups.length === 0) return [];
|
|
1472
1776
|
const requirements = [];
|
|
1473
1777
|
const failures = [];
|
|
1474
|
-
for (const options of
|
|
1778
|
+
for (const options of groups) {
|
|
1475
1779
|
try {
|
|
1476
1780
|
requirements.push(
|
|
1477
1781
|
...await server.buildPaymentRequirementsFromOptions(options, { request })
|
|
@@ -1479,21 +1783,31 @@ async function buildExactRequirements(server, request, price, accepts) {
|
|
|
1479
1783
|
} catch (error) {
|
|
1480
1784
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1481
1785
|
failures.push(err);
|
|
1482
|
-
if (
|
|
1786
|
+
if (groups.length === 1) {
|
|
1483
1787
|
throw err;
|
|
1484
1788
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1789
|
+
report?.(
|
|
1790
|
+
"warn",
|
|
1791
|
+
`Failed to build x402 ${options[0]?.scheme} requirements for ${options[0]?.network}: ${err.message}`
|
|
1487
1792
|
);
|
|
1488
1793
|
}
|
|
1489
1794
|
}
|
|
1490
1795
|
if (requirements.length > 0) {
|
|
1491
1796
|
return requirements;
|
|
1492
1797
|
}
|
|
1493
|
-
throw failures[0] ?? new Error("Failed to build x402
|
|
1798
|
+
throw failures[0] ?? new Error("Failed to build x402 SDK-handled requirements");
|
|
1494
1799
|
}
|
|
1495
1800
|
function buildCustomRequirements(price, accepts) {
|
|
1496
|
-
return accepts.filter((accept) => accept
|
|
1801
|
+
return accepts.filter((accept) => !isSdkHandled(accept)).map((accept) => buildCustomRequirement(price, accept));
|
|
1802
|
+
}
|
|
1803
|
+
function isSdkHandled(accept) {
|
|
1804
|
+
if (isEvmNetwork(accept.network)) {
|
|
1805
|
+
return accept.scheme === "exact" || accept.scheme === "upto";
|
|
1806
|
+
}
|
|
1807
|
+
if (isSolanaRequirement({ network: accept.network })) {
|
|
1808
|
+
return accept.scheme === "exact";
|
|
1809
|
+
}
|
|
1810
|
+
return false;
|
|
1497
1811
|
}
|
|
1498
1812
|
function buildCustomRequirement(price, accept) {
|
|
1499
1813
|
if (!accept.asset) {
|
|
@@ -1527,7 +1841,7 @@ function decimalToAtomicUnits(amount, decimals) {
|
|
|
1527
1841
|
|
|
1528
1842
|
// src/protocols/x402/challenge.ts
|
|
1529
1843
|
async function buildX402Challenge(opts) {
|
|
1530
|
-
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
|
|
1844
|
+
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions, report } = opts;
|
|
1531
1845
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
1532
1846
|
const resource = buildChallengeResource(request, routeEntry);
|
|
1533
1847
|
const requirements = await buildChallengeRequirements(
|
|
@@ -1536,7 +1850,8 @@ async function buildX402Challenge(opts) {
|
|
|
1536
1850
|
price,
|
|
1537
1851
|
accepts,
|
|
1538
1852
|
resource,
|
|
1539
|
-
facilitatorsByNetwork
|
|
1853
|
+
facilitatorsByNetwork,
|
|
1854
|
+
report
|
|
1540
1855
|
);
|
|
1541
1856
|
const paymentRequired = await server.createPaymentRequiredResponse(
|
|
1542
1857
|
requirements,
|
|
@@ -1547,13 +1862,13 @@ async function buildX402Challenge(opts) {
|
|
|
1547
1862
|
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1548
1863
|
return { encoded, requirements };
|
|
1549
1864
|
}
|
|
1550
|
-
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
|
|
1551
|
-
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1865
|
+
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork, report) {
|
|
1866
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
|
|
1552
1867
|
if (!needsFacilitatorEnrichment(accepts)) return requirements;
|
|
1553
|
-
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
|
|
1868
|
+
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report);
|
|
1554
1869
|
}
|
|
1555
1870
|
function needsFacilitatorEnrichment(accepts) {
|
|
1556
|
-
return
|
|
1871
|
+
return hasSolanaAccepts(accepts);
|
|
1557
1872
|
}
|
|
1558
1873
|
async function enrichGroup(group, resource) {
|
|
1559
1874
|
const accepted = await enrichRequirementsWithFacilitatorAccepts(
|
|
@@ -1568,7 +1883,7 @@ async function enrichGroup(group, resource) {
|
|
|
1568
1883
|
}
|
|
1569
1884
|
return accepted;
|
|
1570
1885
|
}
|
|
1571
|
-
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
|
|
1886
|
+
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report) {
|
|
1572
1887
|
const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
|
|
1573
1888
|
if (groups.length === 0) return requirements;
|
|
1574
1889
|
const results = await Promise.all(
|
|
@@ -1578,8 +1893,9 @@ async function enrichChallengeRequirements(requirements, resource, facilitatorsB
|
|
|
1578
1893
|
} catch (err) {
|
|
1579
1894
|
const label = group.facilitator.url ?? group.facilitator.network;
|
|
1580
1895
|
const reason = err instanceof Error ? err.message : String(err);
|
|
1581
|
-
|
|
1582
|
-
|
|
1896
|
+
report?.(
|
|
1897
|
+
"warn",
|
|
1898
|
+
`${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
|
|
1583
1899
|
);
|
|
1584
1900
|
return { success: false, group };
|
|
1585
1901
|
}
|
|
@@ -1632,7 +1948,7 @@ function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
|
|
|
1632
1948
|
return facilitator;
|
|
1633
1949
|
}
|
|
1634
1950
|
function requiresFacilitatorEnrichment(requirement) {
|
|
1635
|
-
return
|
|
1951
|
+
return isSolanaRequirement(requirement);
|
|
1636
1952
|
}
|
|
1637
1953
|
function buildChallengeResource(request, routeEntry) {
|
|
1638
1954
|
return {
|
|
@@ -1644,33 +1960,68 @@ function buildChallengeResource(request, routeEntry) {
|
|
|
1644
1960
|
}
|
|
1645
1961
|
|
|
1646
1962
|
// src/protocols/x402/settle.ts
|
|
1647
|
-
async function settleX402Payment(server, payload, requirements) {
|
|
1963
|
+
async function settleX402Payment(server, payload, requirements, amountOverride) {
|
|
1648
1964
|
const { encodePaymentResponseHeader } = await import("@x402/core/http");
|
|
1965
|
+
if (amountOverride?.amount !== void 0) {
|
|
1966
|
+
const upstreamTaggedAmount = tagBareDecimalAsDollars(amountOverride.amount);
|
|
1967
|
+
const result2 = await server.settlePayment(payload, requirements, void 0, void 0, {
|
|
1968
|
+
amount: upstreamTaggedAmount
|
|
1969
|
+
});
|
|
1970
|
+
return {
|
|
1971
|
+
encoded: encodePaymentResponseHeader(result2),
|
|
1972
|
+
result: result2
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1649
1975
|
const result = await server.settlePayment(payload, requirements);
|
|
1650
|
-
|
|
1651
|
-
|
|
1976
|
+
return {
|
|
1977
|
+
encoded: encodePaymentResponseHeader(result),
|
|
1978
|
+
result
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
function tagBareDecimalAsDollars(amount) {
|
|
1982
|
+
if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
|
|
1983
|
+
return amount;
|
|
1652
1984
|
}
|
|
1653
1985
|
|
|
1654
1986
|
// src/protocols/x402/verify.ts
|
|
1987
|
+
var import_types2 = require("@x402/core/types");
|
|
1655
1988
|
async function verifyX402Payment(opts) {
|
|
1656
|
-
const { server, request, price, accepts } = opts;
|
|
1989
|
+
const { server, request, price, accepts, report } = opts;
|
|
1657
1990
|
const payload = await readPaymentPayload(request);
|
|
1658
1991
|
if (!payload) return null;
|
|
1659
|
-
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1992
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
|
|
1660
1993
|
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
1994
|
+
const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
|
|
1661
1995
|
if (!matching) {
|
|
1662
|
-
return invalidPaymentVerification(
|
|
1996
|
+
return invalidPaymentVerification({
|
|
1997
|
+
reason: "requirements_mismatch",
|
|
1998
|
+
message: "Signed payment requirements did not match any server-built requirement",
|
|
1999
|
+
...accepted ? { accepted } : {}
|
|
2000
|
+
});
|
|
1663
2001
|
}
|
|
1664
2002
|
let verify;
|
|
1665
2003
|
try {
|
|
1666
2004
|
verify = await server.verifyPayment(payload, matching);
|
|
1667
2005
|
} catch (err) {
|
|
1668
|
-
|
|
1669
|
-
|
|
2006
|
+
if (err instanceof import_types2.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
|
|
2007
|
+
return invalidPaymentVerification({
|
|
2008
|
+
reason: err.invalidReason ?? "verify_error",
|
|
2009
|
+
...err.invalidMessage ? { message: err.invalidMessage } : {},
|
|
2010
|
+
...err.payer ? { payer: err.payer } : {},
|
|
2011
|
+
...accepted ? { accepted } : {}
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
1670
2014
|
throw err;
|
|
1671
2015
|
}
|
|
1672
|
-
if (!verify.isValid)
|
|
1673
|
-
|
|
2016
|
+
if (!verify.isValid) {
|
|
2017
|
+
return invalidPaymentVerification({
|
|
2018
|
+
reason: verify.invalidReason ?? "unknown",
|
|
2019
|
+
...verify.invalidMessage ? { message: verify.invalidMessage } : {},
|
|
2020
|
+
...verify.payer ? { payer: verify.payer } : {},
|
|
2021
|
+
...accepted ? { accepted } : {}
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
if (!verify.payer) {
|
|
1674
2025
|
throw new Error("x402 verification succeeded without a payer address");
|
|
1675
2026
|
}
|
|
1676
2027
|
return {
|
|
@@ -1702,11 +2053,36 @@ async function readPaymentPayload(request) {
|
|
|
1702
2053
|
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
1703
2054
|
return decodePaymentSignatureHeader(paymentHeader);
|
|
1704
2055
|
}
|
|
1705
|
-
function invalidPaymentVerification() {
|
|
1706
|
-
return {
|
|
2056
|
+
function invalidPaymentVerification(failure) {
|
|
2057
|
+
return {
|
|
2058
|
+
valid: false,
|
|
2059
|
+
payload: null,
|
|
2060
|
+
requirements: null,
|
|
2061
|
+
payer: null,
|
|
2062
|
+
...failure ? { failure } : {}
|
|
2063
|
+
};
|
|
1707
2064
|
}
|
|
1708
2065
|
|
|
1709
2066
|
// src/protocols/x402/strategy.ts
|
|
2067
|
+
function formatVerifyFailureMessage(failure) {
|
|
2068
|
+
if (failure.reason === "permit2_allowance_required") {
|
|
2069
|
+
const wallet = failure.payer ?? "<the payer wallet>";
|
|
2070
|
+
const asset = failure.accepted?.asset ?? "<the asset>";
|
|
2071
|
+
const amount = failure.accepted?.amount ?? "<the required amount>";
|
|
2072
|
+
const network = failure.accepted?.network ?? "<the payment network>";
|
|
2073
|
+
return [
|
|
2074
|
+
`Payment rejected: In order for Upto to charge, the wallet ${wallet} MUST approve Permit2 to spend ${asset} on ${network}.`,
|
|
2075
|
+
`Required call (one-time, on-chain): ${asset}.approve(${import_evm3.PERMIT2_ADDRESS}, MAX_UINT256) from ${wallet}.`,
|
|
2076
|
+
`Permit2 contract address: ${import_evm3.PERMIT2_ADDRESS}.`,
|
|
2077
|
+
`Minimum allowance for this request: ${amount} (smallest units of ${asset}); use MAX_UINT256 to avoid re-approving on every future call.`,
|
|
2078
|
+
`Alternative without an on-chain transaction: the merchant can adopt the EIP-2612 gas-sponsoring extension (https://docs.x402.org/extensions/eip2612-gas-sponsoring).`
|
|
2079
|
+
].join(" ");
|
|
2080
|
+
}
|
|
2081
|
+
if (failure.message) {
|
|
2082
|
+
return `Payment rejected (${failure.reason}): ${failure.message}`;
|
|
2083
|
+
}
|
|
2084
|
+
return `Payment rejected: ${failure.reason}`;
|
|
2085
|
+
}
|
|
1710
2086
|
var x402Strategy = {
|
|
1711
2087
|
protocol: "x402",
|
|
1712
2088
|
detects(request) {
|
|
@@ -1714,114 +2090,123 @@ var x402Strategy = {
|
|
|
1714
2090
|
request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
|
|
1715
2091
|
);
|
|
1716
2092
|
},
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
payment,
|
|
1753
|
-
token: {
|
|
1754
|
-
payload: verifyResult.payload,
|
|
1755
|
-
requirements: verifyResult.requirements
|
|
1756
|
-
}
|
|
1757
|
-
};
|
|
1758
|
-
},
|
|
1759
|
-
async settle(args) {
|
|
1760
|
-
const { response, payment, token, deps } = args;
|
|
1761
|
-
const x402Token = token;
|
|
1762
|
-
try {
|
|
1763
|
-
const settle = await settleX402Payment(
|
|
1764
|
-
deps.x402Server,
|
|
1765
|
-
x402Token.payload,
|
|
1766
|
-
x402Token.requirements
|
|
1767
|
-
);
|
|
1768
|
-
if (!settle.result?.success) {
|
|
1769
|
-
const reason = settle.result?.errorReason || "x402 settlement returned success=false";
|
|
1770
|
-
const error = new Error(reason);
|
|
1771
|
-
error.errorReason = reason;
|
|
1772
|
-
throw error;
|
|
1773
|
-
}
|
|
1774
|
-
response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
|
|
1775
|
-
response.headers.set("Cache-Control", "private");
|
|
1776
|
-
const transaction = String(settle.result?.transaction ?? "");
|
|
1777
|
-
const settledPayment = {
|
|
1778
|
-
...payment,
|
|
1779
|
-
status: "settled",
|
|
1780
|
-
...transaction ? { transaction } : {}
|
|
2093
|
+
verify: (args) => verifyX402(args),
|
|
2094
|
+
settle: (args) => settleX402(args),
|
|
2095
|
+
buildChallenge: (args) => buildX402ChallengeContribution(args)
|
|
2096
|
+
};
|
|
2097
|
+
async function verifyX402(args) {
|
|
2098
|
+
const { request, body, price, routeEntry, deps, report } = args;
|
|
2099
|
+
if (!deps.x402Server) {
|
|
2100
|
+
const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
|
|
2101
|
+
report("error", reason);
|
|
2102
|
+
return { ok: false, kind: "config", message: reason };
|
|
2103
|
+
}
|
|
2104
|
+
const accepts = await resolveX402Accepts(
|
|
2105
|
+
request,
|
|
2106
|
+
routeEntry,
|
|
2107
|
+
deps.x402Accepts,
|
|
2108
|
+
deps.payeeAddress,
|
|
2109
|
+
body
|
|
2110
|
+
);
|
|
2111
|
+
const verifyResult = await verifyX402Payment({
|
|
2112
|
+
server: deps.x402Server,
|
|
2113
|
+
request,
|
|
2114
|
+
price,
|
|
2115
|
+
accepts,
|
|
2116
|
+
report
|
|
2117
|
+
});
|
|
2118
|
+
if (!verifyResult?.valid) {
|
|
2119
|
+
const failure = verifyResult?.failure;
|
|
2120
|
+
if (failure) {
|
|
2121
|
+
return {
|
|
2122
|
+
ok: false,
|
|
2123
|
+
kind: "invalid",
|
|
2124
|
+
failure: {
|
|
2125
|
+
reason: failure.reason,
|
|
2126
|
+
message: formatVerifyFailureMessage(failure)
|
|
2127
|
+
}
|
|
1781
2128
|
};
|
|
1782
|
-
return { ok: true, response, settledPayment };
|
|
1783
|
-
} catch (err) {
|
|
1784
|
-
const errObj = err;
|
|
1785
|
-
console.error("Settlement failed", {
|
|
1786
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1787
|
-
route: args.routeEntry.key,
|
|
1788
|
-
network: payment.network,
|
|
1789
|
-
errorReason: errObj.errorReason,
|
|
1790
|
-
facilitatorStatus: errObj.response?.status,
|
|
1791
|
-
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
1792
|
-
});
|
|
1793
|
-
return { ok: false, error: err, failMessage: "Settlement failed" };
|
|
1794
2129
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
}
|
|
1815
|
-
|
|
2130
|
+
return { ok: false, kind: "invalid" };
|
|
2131
|
+
}
|
|
2132
|
+
const wallet = normalizeWalletAddress(verifyResult.payer);
|
|
2133
|
+
const { network, payTo } = verifyResult.requirements;
|
|
2134
|
+
const payment = {
|
|
2135
|
+
protocol: "x402",
|
|
2136
|
+
status: "verified",
|
|
2137
|
+
payer: wallet,
|
|
2138
|
+
amount: price,
|
|
2139
|
+
network,
|
|
2140
|
+
...payTo ? { recipient: payTo } : {}
|
|
2141
|
+
};
|
|
2142
|
+
return {
|
|
2143
|
+
ok: true,
|
|
2144
|
+
wallet,
|
|
2145
|
+
payment,
|
|
2146
|
+
token: {
|
|
2147
|
+
payload: verifyResult.payload,
|
|
2148
|
+
requirements: verifyResult.requirements
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
async function settleX402(args) {
|
|
2153
|
+
const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
|
|
2154
|
+
const { payload, requirements } = token;
|
|
2155
|
+
const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
|
|
2156
|
+
try {
|
|
2157
|
+
const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
|
|
2158
|
+
if (!settle.result?.success) {
|
|
2159
|
+
throw Object.assign(
|
|
2160
|
+
new Error(settle.result?.errorReason ?? "x402 settlement returned success=false"),
|
|
2161
|
+
{ errorReason: settle.result?.errorReason }
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
|
|
2165
|
+
response.headers.set("Cache-Control", "private");
|
|
2166
|
+
const transaction = String(settle.result.transaction ?? "");
|
|
2167
|
+
const settledPayment = {
|
|
2168
|
+
...payment,
|
|
2169
|
+
status: "settled",
|
|
2170
|
+
amount: billedAmount,
|
|
2171
|
+
...transaction ? { transaction } : {}
|
|
2172
|
+
};
|
|
2173
|
+
return { ok: true, response, settledPayment };
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
reportSettleFailure(report, err, payment.network);
|
|
2176
|
+
return { ok: false, error: err, failMessage: "Settlement failed" };
|
|
1816
2177
|
}
|
|
1817
|
-
};
|
|
1818
|
-
function getRequirementNetwork(requirements, fallback) {
|
|
1819
|
-
const network = requirements?.network;
|
|
1820
|
-
return typeof network === "string" ? network : fallback;
|
|
1821
2178
|
}
|
|
1822
|
-
function
|
|
1823
|
-
const
|
|
1824
|
-
|
|
2179
|
+
async function buildX402ChallengeContribution(args) {
|
|
2180
|
+
const { request, routeEntry, body, price, extensions, deps, report } = args;
|
|
2181
|
+
if (!deps.x402Server) return {};
|
|
2182
|
+
const accepts = await resolveX402Accepts(
|
|
2183
|
+
request,
|
|
2184
|
+
routeEntry,
|
|
2185
|
+
deps.x402Accepts,
|
|
2186
|
+
deps.payeeAddress,
|
|
2187
|
+
body
|
|
2188
|
+
);
|
|
2189
|
+
const { encoded } = await buildX402Challenge({
|
|
2190
|
+
server: deps.x402Server,
|
|
2191
|
+
routeEntry,
|
|
2192
|
+
request,
|
|
2193
|
+
price,
|
|
2194
|
+
accepts,
|
|
2195
|
+
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
2196
|
+
extensions,
|
|
2197
|
+
report
|
|
2198
|
+
});
|
|
2199
|
+
return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
|
|
2200
|
+
}
|
|
2201
|
+
function reportSettleFailure(report, err, network) {
|
|
2202
|
+
const facilitator = err ?? {};
|
|
2203
|
+
report("error", "Settlement failed", {
|
|
2204
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2205
|
+
network,
|
|
2206
|
+
errorReason: facilitator.errorReason,
|
|
2207
|
+
facilitatorStatus: facilitator.response?.status,
|
|
2208
|
+
facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
|
|
2209
|
+
});
|
|
1825
2210
|
}
|
|
1826
2211
|
|
|
1827
2212
|
// src/protocols/detect.ts
|
|
@@ -1855,23 +2240,75 @@ function getAllowedStrategies(allowed) {
|
|
|
1855
2240
|
return allowed.map((name) => STRATEGIES[name]);
|
|
1856
2241
|
}
|
|
1857
2242
|
|
|
1858
|
-
// src/pipeline/
|
|
2243
|
+
// src/pipeline/flows/build402.ts
|
|
1859
2244
|
var import_server4 = require("next/server");
|
|
1860
|
-
|
|
1861
|
-
|
|
2245
|
+
|
|
2246
|
+
// src/pipeline/challenge-extensions.ts
|
|
2247
|
+
async function buildChallengeExtensions(ctx) {
|
|
2248
|
+
const { routeEntry } = ctx;
|
|
2249
|
+
let extensions;
|
|
1862
2250
|
try {
|
|
1863
|
-
|
|
2251
|
+
const { z } = await import("zod");
|
|
2252
|
+
const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
|
|
2253
|
+
const toJSON = (schema) => z.toJSONSchema(schema, {
|
|
2254
|
+
target: "draft-2020-12",
|
|
2255
|
+
unrepresentable: "any"
|
|
2256
|
+
});
|
|
2257
|
+
const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
|
|
2258
|
+
const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
|
|
2259
|
+
if (inputSchema) {
|
|
2260
|
+
const config = {
|
|
2261
|
+
method: routeEntry.method,
|
|
2262
|
+
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
2263
|
+
inputSchema
|
|
2264
|
+
};
|
|
2265
|
+
if (routeEntry.inputExample !== void 0) {
|
|
2266
|
+
config.input = routeEntry.inputExample;
|
|
2267
|
+
}
|
|
2268
|
+
if (outputSchema && routeEntry.outputExample !== void 0) {
|
|
2269
|
+
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
2270
|
+
}
|
|
2271
|
+
extensions = declareDiscoveryExtension(config);
|
|
2272
|
+
}
|
|
1864
2273
|
} catch (err) {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
);
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2274
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2275
|
+
level: "warn",
|
|
2276
|
+
message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2277
|
+
route: routeEntry.key
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
if (routeEntry.siwxEnabled) {
|
|
2281
|
+
try {
|
|
2282
|
+
const siwxExtension = await buildSIWXExtension();
|
|
2283
|
+
if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
|
|
2284
|
+
extensions = {
|
|
2285
|
+
...extensions ?? {},
|
|
2286
|
+
...siwxExtension
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
} catch {
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return extensions;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// src/pipeline/flows/build402.ts
|
|
2296
|
+
async function build402(ctx, pricing, body, failure) {
|
|
2297
|
+
let challengePrice;
|
|
2298
|
+
try {
|
|
2299
|
+
challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
|
|
2300
|
+
} catch (err) {
|
|
2301
|
+
const message = errorMessage(err, "Price calculation failed");
|
|
2302
|
+
const errorResponse = import_server4.NextResponse.json(
|
|
2303
|
+
{ success: false, error: message },
|
|
2304
|
+
{ status: errorStatus(err, 500) }
|
|
2305
|
+
);
|
|
2306
|
+
firePluginResponse(ctx, errorResponse);
|
|
2307
|
+
return errorResponse;
|
|
2308
|
+
}
|
|
2309
|
+
const extensions = await buildChallengeExtensions(ctx);
|
|
2310
|
+
const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
|
|
2311
|
+
const response = new import_server4.NextResponse(responseBody, {
|
|
1875
2312
|
status: 402,
|
|
1876
2313
|
headers: {
|
|
1877
2314
|
"Content-Type": "application/json",
|
|
@@ -1886,7 +2323,8 @@ async function build402(ctx, pricing, body) {
|
|
|
1886
2323
|
body,
|
|
1887
2324
|
price: challengePrice,
|
|
1888
2325
|
extensions,
|
|
1889
|
-
deps: ctx.deps
|
|
2326
|
+
deps: ctx.deps,
|
|
2327
|
+
report: ctx.report
|
|
1890
2328
|
});
|
|
1891
2329
|
if (contribution.headers) {
|
|
1892
2330
|
for (const [name, value] of Object.entries(contribution.headers)) {
|
|
@@ -1895,11 +2333,7 @@ async function build402(ctx, pricing, body) {
|
|
|
1895
2333
|
}
|
|
1896
2334
|
} catch (err) {
|
|
1897
2335
|
const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
|
|
1898
|
-
|
|
1899
|
-
level: "critical",
|
|
1900
|
-
message,
|
|
1901
|
-
route: ctx.routeEntry.key
|
|
1902
|
-
});
|
|
2336
|
+
ctx.report("critical", message);
|
|
1903
2337
|
if (strategy.protocol === "x402") {
|
|
1904
2338
|
const errorResponse = import_server4.NextResponse.json(
|
|
1905
2339
|
{ success: false, error: message },
|
|
@@ -1913,97 +2347,313 @@ async function build402(ctx, pricing, body) {
|
|
|
1913
2347
|
firePluginResponse(ctx, response);
|
|
1914
2348
|
return response;
|
|
1915
2349
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2350
|
+
|
|
2351
|
+
// src/pipeline/flows/dynamic/dynamic-body-and-price.ts
|
|
2352
|
+
async function resolveDynamicBodyAndPrice(args) {
|
|
2353
|
+
const { ctx, pricing, skipBody } = args;
|
|
2354
|
+
if (skipBody) {
|
|
2355
|
+
return {
|
|
2356
|
+
ok: true,
|
|
2357
|
+
parsedBody: void 0,
|
|
2358
|
+
price: surrogatePriceForSkippedBody(ctx.routeEntry)
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
const body = await parseBody(ctx);
|
|
2362
|
+
if (!body.ok) return { ok: false, response: body.response };
|
|
2363
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
2364
|
+
if (validateErr) {
|
|
2365
|
+
return { ok: false, response: validateErr };
|
|
2366
|
+
}
|
|
2367
|
+
if (!pricing) {
|
|
2368
|
+
return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
|
|
2369
|
+
}
|
|
1919
2370
|
try {
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
const toJSON = (schema) => z.toJSONSchema(schema, {
|
|
1923
|
-
target: "draft-2020-12",
|
|
1924
|
-
unrepresentable: "any"
|
|
1925
|
-
});
|
|
1926
|
-
const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
|
|
1927
|
-
const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
|
|
1928
|
-
if (inputSchema) {
|
|
1929
|
-
const config = {
|
|
1930
|
-
method: routeEntry.method,
|
|
1931
|
-
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
1932
|
-
inputSchema
|
|
1933
|
-
};
|
|
1934
|
-
if (routeEntry.inputExample !== void 0) {
|
|
1935
|
-
config.input = routeEntry.inputExample;
|
|
1936
|
-
}
|
|
1937
|
-
if (outputSchema && routeEntry.outputExample !== void 0) {
|
|
1938
|
-
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
1939
|
-
}
|
|
1940
|
-
extensions = declareDiscoveryExtension(config);
|
|
1941
|
-
}
|
|
2371
|
+
const price = await pricing.quote(body.data);
|
|
2372
|
+
return { ok: true, parsedBody: body.data, price };
|
|
1942
2373
|
} catch (err) {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2374
|
+
return {
|
|
2375
|
+
ok: false,
|
|
2376
|
+
response: fail(
|
|
2377
|
+
ctx,
|
|
2378
|
+
errorStatus(err, 500),
|
|
2379
|
+
errorMessage(err, "Price calculation failed"),
|
|
2380
|
+
body.data
|
|
2381
|
+
)
|
|
2382
|
+
};
|
|
1948
2383
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2384
|
+
}
|
|
2385
|
+
function surrogatePriceForSkippedBody(routeEntry) {
|
|
2386
|
+
return routeEntry.maxPrice ?? routeEntry.minPrice ?? "0";
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
|
|
2390
|
+
var import_server5 = require("next/server");
|
|
2391
|
+
async function runDynamicChannelMgmtFlow(args) {
|
|
2392
|
+
const { ctx, strategy, account, pricing, skipBody } = args;
|
|
2393
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2394
|
+
const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
|
|
2395
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2396
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
2397
|
+
const verifyOutcome = await strategy.verify({
|
|
2398
|
+
request,
|
|
2399
|
+
body: parsedBody,
|
|
2400
|
+
price,
|
|
2401
|
+
routeEntry,
|
|
2402
|
+
deps,
|
|
2403
|
+
report
|
|
2404
|
+
});
|
|
2405
|
+
if (verifyOutcome.ok === false) {
|
|
2406
|
+
if (verifyOutcome.kind === "config") {
|
|
2407
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
1959
2408
|
}
|
|
2409
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
1960
2410
|
}
|
|
1961
|
-
|
|
2411
|
+
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2412
|
+
firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
|
|
2413
|
+
protocol: strategy.protocol,
|
|
2414
|
+
payer: verifyOutcome.wallet,
|
|
2415
|
+
amount: price,
|
|
2416
|
+
network: verifyOutcome.payment.network
|
|
2417
|
+
});
|
|
2418
|
+
const synthetic = new import_server5.NextResponse(null, { status: 200 });
|
|
2419
|
+
const settleScope = {
|
|
2420
|
+
wallet: verifyOutcome.wallet,
|
|
2421
|
+
account,
|
|
2422
|
+
body: parsedBody,
|
|
2423
|
+
payment: verifyOutcome.payment,
|
|
2424
|
+
response: synthetic,
|
|
2425
|
+
rawResult: void 0
|
|
2426
|
+
};
|
|
2427
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2428
|
+
if (beforeErr) return beforeErr;
|
|
2429
|
+
return settleAndFinalizeRequest({
|
|
2430
|
+
ctx,
|
|
2431
|
+
strategy,
|
|
2432
|
+
verifyOutcome,
|
|
2433
|
+
scope: settleScope,
|
|
2434
|
+
rawResult: void 0,
|
|
2435
|
+
body: parsedBody,
|
|
2436
|
+
billedAmount: "0",
|
|
2437
|
+
onSettleError: async (error, failMessage) => {
|
|
2438
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2439
|
+
report("critical", `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`);
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
1962
2442
|
}
|
|
1963
2443
|
|
|
1964
|
-
// src/pipeline/flows/
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
wallet: null,
|
|
1975
|
-
route: routeEntry.key,
|
|
1976
|
-
account
|
|
2444
|
+
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2445
|
+
var import_server6 = require("next/server");
|
|
2446
|
+
|
|
2447
|
+
// src/pricing/atomic.ts
|
|
2448
|
+
var USDC_DECIMALS = 6;
|
|
2449
|
+
function decimalToAtomic(amount) {
|
|
2450
|
+
const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
|
|
2451
|
+
if (!m) {
|
|
2452
|
+
throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
|
|
2453
|
+
status: 400
|
|
1977
2454
|
});
|
|
1978
2455
|
}
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2456
|
+
const whole = m[1];
|
|
2457
|
+
const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
|
|
2458
|
+
return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
|
|
2459
|
+
}
|
|
2460
|
+
function atomicToDecimal(atomic) {
|
|
2461
|
+
const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
|
|
2462
|
+
const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
|
|
2463
|
+
if (fraction === 0n) return whole.toString();
|
|
2464
|
+
const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
|
|
2465
|
+
return `${whole}.${fractionStr}`;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// src/pricing/charge-context.ts
|
|
2469
|
+
function createChargeContext(args) {
|
|
2470
|
+
const { tickCost, maxPrice, route } = args;
|
|
2471
|
+
const tickAtomic = decimalToAtomic(tickCost);
|
|
2472
|
+
if (tickAtomic <= 0n) {
|
|
2473
|
+
throw new Error(`route '${route}': tickCost '${tickCost}' must be a positive decimal string`);
|
|
2474
|
+
}
|
|
2475
|
+
const capAtomic = maxPrice !== void 0 ? decimalToAtomic(maxPrice) : null;
|
|
2476
|
+
let ticks = 0;
|
|
2477
|
+
let atomic = 0n;
|
|
2478
|
+
let channelCharge = null;
|
|
2479
|
+
const charge = async () => {
|
|
2480
|
+
const nextAtomic = atomic + tickAtomic;
|
|
2481
|
+
if (capAtomic !== null && nextAtomic > capAtomic) {
|
|
2482
|
+
throw Object.assign(
|
|
2483
|
+
new Error(
|
|
2484
|
+
`route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
|
|
2485
|
+
),
|
|
2486
|
+
{ status: 400, code: "CHARGE_OVER_CAP" }
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
ticks += 1;
|
|
2490
|
+
atomic = nextAtomic;
|
|
2491
|
+
if (channelCharge) await channelCharge();
|
|
1986
2492
|
};
|
|
2493
|
+
return {
|
|
2494
|
+
charge,
|
|
2495
|
+
bindChannelCharge: (fn) => {
|
|
2496
|
+
channelCharge = fn;
|
|
2497
|
+
},
|
|
2498
|
+
tickCount: () => ticks,
|
|
2499
|
+
atomicTotal: () => atomic
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2504
|
+
async function invokeDynamic(ctx, wallet, account, body, payment) {
|
|
2505
|
+
const streaming = ctx.routeEntry.streaming === true;
|
|
2506
|
+
const chargeContext = streaming ? createChargeContext({
|
|
2507
|
+
tickCost: ctx.routeEntry.tickCost,
|
|
2508
|
+
maxPrice: ctx.routeEntry.maxPrice,
|
|
2509
|
+
route: ctx.routeEntry.key
|
|
2510
|
+
}) : null;
|
|
2511
|
+
const baseHandlerCtx = {
|
|
2512
|
+
body,
|
|
2513
|
+
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
2514
|
+
request: ctx.request,
|
|
2515
|
+
requestId: ctx.meta.requestId,
|
|
2516
|
+
route: ctx.routeEntry.key,
|
|
2517
|
+
wallet,
|
|
2518
|
+
payment,
|
|
2519
|
+
account,
|
|
2520
|
+
alert(level, message, alertMeta) {
|
|
2521
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2522
|
+
level,
|
|
2523
|
+
message,
|
|
2524
|
+
route: ctx.routeEntry.key,
|
|
2525
|
+
meta: alertMeta
|
|
2526
|
+
});
|
|
2527
|
+
},
|
|
2528
|
+
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
2529
|
+
};
|
|
2530
|
+
const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
|
|
2531
|
+
let returned;
|
|
2532
|
+
try {
|
|
2533
|
+
returned = ctx.handler(handlerCtx);
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
return errorResult2(error, chargeContext);
|
|
2536
|
+
}
|
|
2537
|
+
if (isAsyncIterable2(returned) && !isThenable2(returned)) {
|
|
2538
|
+
if (!chargeContext) {
|
|
2539
|
+
return errorResult2(
|
|
2540
|
+
new HttpError(
|
|
2541
|
+
"route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
|
|
2542
|
+
500
|
|
2543
|
+
),
|
|
2544
|
+
null
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
return {
|
|
2548
|
+
kind: "stream",
|
|
2549
|
+
source: returned,
|
|
2550
|
+
chargeContext
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
let rawResult;
|
|
2554
|
+
try {
|
|
2555
|
+
rawResult = await returned;
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
return errorResult2(error, chargeContext);
|
|
2558
|
+
}
|
|
2559
|
+
const response = rawResult instanceof Response ? rawResult : import_server6.NextResponse.json(rawResult);
|
|
2560
|
+
return { kind: "request", response, rawResult };
|
|
2561
|
+
}
|
|
2562
|
+
function errorResult2(error, chargeContext) {
|
|
2563
|
+
const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
|
|
2564
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
2565
|
+
void chargeContext;
|
|
2566
|
+
return {
|
|
2567
|
+
kind: "request",
|
|
2568
|
+
response: import_server6.NextResponse.json({ success: false, error: message }, { status }),
|
|
2569
|
+
rawResult: void 0,
|
|
2570
|
+
handlerError: error
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
function isAsyncIterable2(value) {
|
|
2574
|
+
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
2575
|
+
}
|
|
2576
|
+
function isThenable2(value) {
|
|
2577
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// src/pipeline/flows/dynamic/dynamic-preflight.ts
|
|
2581
|
+
function resolveDynamicPreflight(strategy, request, routeEntry) {
|
|
2582
|
+
const outcome = strategy.preflight?.(request, routeEntry) ?? null;
|
|
2583
|
+
return {
|
|
2584
|
+
skipBody: outcome?.skipBody ?? false,
|
|
2585
|
+
skipHandler: outcome?.skipHandler ?? false
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/pipeline/flows/dynamic/dynamic-request.ts
|
|
2590
|
+
async function runDynamicRequestFlow(args) {
|
|
2591
|
+
const { ctx, strategy, verifyOutcome, account, body, result } = args;
|
|
2592
|
+
const { deps, routeEntry } = ctx;
|
|
2593
|
+
const settleScope = {
|
|
2594
|
+
wallet: verifyOutcome.wallet,
|
|
2595
|
+
account,
|
|
2596
|
+
body,
|
|
2597
|
+
payment: verifyOutcome.payment,
|
|
2598
|
+
response: result.response,
|
|
2599
|
+
rawResult: result.rawResult,
|
|
2600
|
+
handlerError: result.handlerError
|
|
2601
|
+
};
|
|
2602
|
+
if (result.response.status >= 400) {
|
|
2603
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2604
|
+
}
|
|
2605
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2606
|
+
if (beforeErr) return beforeErr;
|
|
2607
|
+
const billedAmount = routeEntry.tickCost;
|
|
2608
|
+
return settleAndFinalizeRequest({
|
|
2609
|
+
ctx,
|
|
2610
|
+
strategy,
|
|
2611
|
+
verifyOutcome,
|
|
2612
|
+
scope: settleScope,
|
|
2613
|
+
rawResult: result.rawResult,
|
|
2614
|
+
body,
|
|
2615
|
+
billedAmount,
|
|
2616
|
+
onSettleError: async (error, failMessage) => {
|
|
2617
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2618
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2619
|
+
level: "critical",
|
|
2620
|
+
message: `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
|
|
2621
|
+
route: routeEntry.key
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// src/pipeline/flows/dynamic/dynamic-stream.ts
|
|
2628
|
+
async function runDynamicStreamFlow(args) {
|
|
2629
|
+
const { ctx, strategy, verifyOutcome, account, body, result } = args;
|
|
2630
|
+
return settleAndFinalizeStream({
|
|
2631
|
+
ctx,
|
|
2632
|
+
strategy,
|
|
2633
|
+
verifyOutcome,
|
|
2634
|
+
source: result.source,
|
|
2635
|
+
account,
|
|
2636
|
+
body,
|
|
2637
|
+
bindChannelCharge: result.chargeContext.bindChannelCharge
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// src/pipeline/flows/dynamic/dynamic-paid.ts
|
|
2642
|
+
async function runDynamicPaidFlow(ctx) {
|
|
2643
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2644
|
+
const apiKeyGate = await runApiKeyGate(ctx);
|
|
2645
|
+
if (!apiKeyGate.ok) return apiKeyGate.response;
|
|
2646
|
+
const { account } = apiKeyGate;
|
|
1987
2647
|
const pricing = selectPricing(routeEntry.pricing, {
|
|
1988
|
-
alert:
|
|
2648
|
+
alert: report,
|
|
1989
2649
|
maxPrice: routeEntry.maxPrice,
|
|
1990
2650
|
minPrice: routeEntry.minPrice,
|
|
1991
2651
|
route: routeEntry.key
|
|
1992
2652
|
});
|
|
1993
2653
|
const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
|
|
1994
|
-
|
|
1995
|
-
if (
|
|
1996
|
-
|
|
1997
|
-
const earlyResult = await parseBody(earlyClone, routeEntry);
|
|
1998
|
-
if (earlyResult.ok) {
|
|
1999
|
-
earlyBody = earlyResult.data;
|
|
2000
|
-
const validateErr2 = await runValidate(ctx, earlyBody);
|
|
2001
|
-
if (validateErr2) return validateErr2;
|
|
2002
|
-
} else {
|
|
2003
|
-
firePluginResponse(ctx, earlyResult.response);
|
|
2004
|
-
return earlyResult.response;
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2654
|
+
const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
|
|
2655
|
+
if (!earlyResolution.ok) return earlyResolution.response;
|
|
2656
|
+
const { earlyBody } = earlyResolution;
|
|
2007
2657
|
const siwxFastPath = await trySiwxFastPath(ctx, account);
|
|
2008
2658
|
if (siwxFastPath) return siwxFastPath;
|
|
2009
2659
|
if (!incomingStrategy) {
|
|
@@ -2011,39 +2661,32 @@ async function runPaidFlow(ctx) {
|
|
|
2011
2661
|
if (initError) return fail(ctx, 500, initError);
|
|
2012
2662
|
return build402(ctx, pricing, earlyBody);
|
|
2013
2663
|
}
|
|
2014
|
-
const
|
|
2015
|
-
if (
|
|
2016
|
-
|
|
2017
|
-
return body.response;
|
|
2018
|
-
}
|
|
2019
|
-
const validateErr = await runValidate(ctx, body.data);
|
|
2020
|
-
if (validateErr) return validateErr;
|
|
2021
|
-
if (!pricing) {
|
|
2022
|
-
return fail(ctx, 500, "Pricing not configured", body.data);
|
|
2023
|
-
}
|
|
2024
|
-
let price;
|
|
2025
|
-
try {
|
|
2026
|
-
price = await pricing.quote(body.data);
|
|
2027
|
-
} catch (err) {
|
|
2028
|
-
return fail(
|
|
2664
|
+
const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
|
|
2665
|
+
if (skipHandler) {
|
|
2666
|
+
return runDynamicChannelMgmtFlow({
|
|
2029
2667
|
ctx,
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2668
|
+
strategy: incomingStrategy,
|
|
2669
|
+
account,
|
|
2670
|
+
pricing,
|
|
2671
|
+
skipBody
|
|
2672
|
+
});
|
|
2034
2673
|
}
|
|
2674
|
+
const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
|
|
2675
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2676
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
2035
2677
|
const verifyOutcome = await incomingStrategy.verify({
|
|
2036
2678
|
request,
|
|
2037
|
-
body:
|
|
2679
|
+
body: parsedBody,
|
|
2038
2680
|
price,
|
|
2039
2681
|
routeEntry,
|
|
2040
|
-
deps
|
|
2682
|
+
deps,
|
|
2683
|
+
report
|
|
2041
2684
|
});
|
|
2042
2685
|
if (verifyOutcome.ok === false) {
|
|
2043
2686
|
if (verifyOutcome.kind === "config") {
|
|
2044
|
-
return fail(ctx, 500, verifyOutcome.message,
|
|
2687
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
2045
2688
|
}
|
|
2046
|
-
return build402(ctx, pricing,
|
|
2689
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
2047
2690
|
}
|
|
2048
2691
|
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2049
2692
|
firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
|
|
@@ -2052,11 +2695,71 @@ async function runPaidFlow(ctx) {
|
|
|
2052
2695
|
amount: price,
|
|
2053
2696
|
network: verifyOutcome.payment.network
|
|
2054
2697
|
});
|
|
2055
|
-
const result = await
|
|
2698
|
+
const result = await invokeDynamic(
|
|
2699
|
+
ctx,
|
|
2700
|
+
verifyOutcome.wallet,
|
|
2701
|
+
account,
|
|
2702
|
+
parsedBody,
|
|
2703
|
+
verifyOutcome.payment
|
|
2704
|
+
);
|
|
2705
|
+
switch (result.kind) {
|
|
2706
|
+
case "stream":
|
|
2707
|
+
return runDynamicStreamFlow({
|
|
2708
|
+
ctx,
|
|
2709
|
+
strategy: incomingStrategy,
|
|
2710
|
+
verifyOutcome,
|
|
2711
|
+
account,
|
|
2712
|
+
body: parsedBody,
|
|
2713
|
+
result
|
|
2714
|
+
});
|
|
2715
|
+
case "request":
|
|
2716
|
+
return runDynamicRequestFlow({
|
|
2717
|
+
ctx,
|
|
2718
|
+
strategy: incomingStrategy,
|
|
2719
|
+
verifyOutcome,
|
|
2720
|
+
account,
|
|
2721
|
+
body: parsedBody,
|
|
2722
|
+
result
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// src/pipeline/flows/static/static-body-and-price.ts
|
|
2728
|
+
async function resolveStaticBodyAndPrice(args) {
|
|
2729
|
+
const { ctx, pricing } = args;
|
|
2730
|
+
const body = await parseBody(ctx);
|
|
2731
|
+
if (!body.ok) return { ok: false, response: body.response };
|
|
2732
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
2733
|
+
if (validateErr) {
|
|
2734
|
+
return { ok: false, response: validateErr };
|
|
2735
|
+
}
|
|
2736
|
+
if (!pricing) {
|
|
2737
|
+
return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
|
|
2738
|
+
}
|
|
2739
|
+
try {
|
|
2740
|
+
const price = await pricing.quote(body.data);
|
|
2741
|
+
return { ok: true, parsedBody: body.data, price };
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
return {
|
|
2744
|
+
ok: false,
|
|
2745
|
+
response: fail(
|
|
2746
|
+
ctx,
|
|
2747
|
+
errorStatus(err, 500),
|
|
2748
|
+
errorMessage(err, "Price calculation failed"),
|
|
2749
|
+
body.data
|
|
2750
|
+
)
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// src/pipeline/flows/static/static-request.ts
|
|
2756
|
+
async function runStaticRequestFlow(args) {
|
|
2757
|
+
const { ctx, strategy, verifyOutcome, account, body, price, result } = args;
|
|
2758
|
+
const { deps, routeEntry } = ctx;
|
|
2056
2759
|
const settleScope = {
|
|
2057
2760
|
wallet: verifyOutcome.wallet,
|
|
2058
2761
|
account,
|
|
2059
|
-
body
|
|
2762
|
+
body,
|
|
2060
2763
|
payment: verifyOutcome.payment,
|
|
2061
2764
|
response: result.response,
|
|
2062
2765
|
rawResult: result.rawResult,
|
|
@@ -2066,44 +2769,198 @@ async function runPaidFlow(ctx) {
|
|
|
2066
2769
|
if (result.response.status >= 400) {
|
|
2067
2770
|
const settledScope = settleScope;
|
|
2068
2771
|
await runSettledHandlerError(ctx, settledScope);
|
|
2069
|
-
return finalize(ctx, result.response, result.rawResult, body
|
|
2772
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2070
2773
|
}
|
|
2071
|
-
return
|
|
2774
|
+
return settleAndFinalizeRequest({
|
|
2072
2775
|
ctx,
|
|
2073
|
-
strategy
|
|
2776
|
+
strategy,
|
|
2074
2777
|
verifyOutcome,
|
|
2075
2778
|
scope: settleScope,
|
|
2076
2779
|
rawResult: result.rawResult,
|
|
2077
|
-
body
|
|
2780
|
+
body,
|
|
2781
|
+
billedAmount: price
|
|
2078
2782
|
});
|
|
2079
2783
|
}
|
|
2080
2784
|
if (result.response.status >= 400) {
|
|
2081
|
-
return finalize(ctx, result.response, result.rawResult, body
|
|
2785
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2082
2786
|
}
|
|
2083
2787
|
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2084
2788
|
if (beforeErr) return beforeErr;
|
|
2085
|
-
return
|
|
2789
|
+
return settleAndFinalizeRequest({
|
|
2086
2790
|
ctx,
|
|
2087
|
-
strategy
|
|
2791
|
+
strategy,
|
|
2088
2792
|
verifyOutcome,
|
|
2089
2793
|
scope: settleScope,
|
|
2090
2794
|
rawResult: result.rawResult,
|
|
2091
|
-
body
|
|
2795
|
+
body,
|
|
2796
|
+
billedAmount: price,
|
|
2092
2797
|
onSettleError: async (error, failMessage) => {
|
|
2093
2798
|
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2094
2799
|
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2095
2800
|
level: "critical",
|
|
2096
|
-
message: `${
|
|
2801
|
+
message: `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
|
|
2097
2802
|
route: routeEntry.key
|
|
2098
2803
|
});
|
|
2099
2804
|
}
|
|
2100
2805
|
});
|
|
2101
2806
|
}
|
|
2102
2807
|
|
|
2808
|
+
// src/pipeline/flows/static/static-paid.ts
|
|
2809
|
+
async function runStaticPaidFlow(ctx) {
|
|
2810
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2811
|
+
const apiKeyGate = await runApiKeyGate(ctx);
|
|
2812
|
+
if (!apiKeyGate.ok) return apiKeyGate.response;
|
|
2813
|
+
const { account } = apiKeyGate;
|
|
2814
|
+
const pricing = selectPricing(routeEntry.pricing, {
|
|
2815
|
+
alert: report,
|
|
2816
|
+
maxPrice: routeEntry.maxPrice,
|
|
2817
|
+
minPrice: routeEntry.minPrice,
|
|
2818
|
+
route: routeEntry.key
|
|
2819
|
+
});
|
|
2820
|
+
const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
|
|
2821
|
+
const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
|
|
2822
|
+
if (!earlyResolution.ok) return earlyResolution.response;
|
|
2823
|
+
const { earlyBody } = earlyResolution;
|
|
2824
|
+
const siwxFastPath = await trySiwxFastPath(ctx, account);
|
|
2825
|
+
if (siwxFastPath) return siwxFastPath;
|
|
2826
|
+
if (!incomingStrategy) {
|
|
2827
|
+
const initError = protocolInitError(routeEntry, deps);
|
|
2828
|
+
if (initError) return fail(ctx, 500, initError);
|
|
2829
|
+
return build402(ctx, pricing, earlyBody);
|
|
2830
|
+
}
|
|
2831
|
+
const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
|
|
2832
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2833
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
2834
|
+
const verifyOutcome = await incomingStrategy.verify({
|
|
2835
|
+
request,
|
|
2836
|
+
body: parsedBody,
|
|
2837
|
+
price,
|
|
2838
|
+
routeEntry,
|
|
2839
|
+
deps,
|
|
2840
|
+
report
|
|
2841
|
+
});
|
|
2842
|
+
if (verifyOutcome.ok === false) {
|
|
2843
|
+
if (verifyOutcome.kind === "config") {
|
|
2844
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
2845
|
+
}
|
|
2846
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
2847
|
+
}
|
|
2848
|
+
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2849
|
+
firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
|
|
2850
|
+
protocol: incomingStrategy.protocol,
|
|
2851
|
+
payer: verifyOutcome.wallet,
|
|
2852
|
+
amount: price,
|
|
2853
|
+
network: verifyOutcome.payment.network
|
|
2854
|
+
});
|
|
2855
|
+
const result = await invokePaidStatic(
|
|
2856
|
+
ctx,
|
|
2857
|
+
verifyOutcome.wallet,
|
|
2858
|
+
account,
|
|
2859
|
+
parsedBody,
|
|
2860
|
+
verifyOutcome.payment
|
|
2861
|
+
);
|
|
2862
|
+
return runStaticRequestFlow({
|
|
2863
|
+
ctx,
|
|
2864
|
+
strategy: incomingStrategy,
|
|
2865
|
+
verifyOutcome,
|
|
2866
|
+
account,
|
|
2867
|
+
body: parsedBody,
|
|
2868
|
+
price,
|
|
2869
|
+
result
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/pipeline/flows/paid.ts
|
|
2874
|
+
async function runPaidFlow(ctx) {
|
|
2875
|
+
const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
|
|
2876
|
+
switch (dynamicPrice) {
|
|
2877
|
+
case true:
|
|
2878
|
+
return runDynamicPaidFlow(ctx);
|
|
2879
|
+
case false:
|
|
2880
|
+
return runStaticPaidFlow(ctx);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2103
2884
|
// src/pipeline/flows/siwx-only.ts
|
|
2104
|
-
var
|
|
2885
|
+
var import_server7 = require("next/server");
|
|
2886
|
+
|
|
2887
|
+
// src/kv-store/client.ts
|
|
2888
|
+
function restKvStore(url, token) {
|
|
2889
|
+
const base = url.replace(/\/+$/, "");
|
|
2890
|
+
const authHeader = { Authorization: `Bearer ${token}` };
|
|
2891
|
+
const jsonHeaders = { ...authHeader, "Content-Type": "application/json" };
|
|
2892
|
+
async function exec(command) {
|
|
2893
|
+
const res = await fetch(base, {
|
|
2894
|
+
method: "POST",
|
|
2895
|
+
headers: jsonHeaders,
|
|
2896
|
+
body: JSON.stringify(command)
|
|
2897
|
+
});
|
|
2898
|
+
if (!res.ok) {
|
|
2899
|
+
throw new Error(`[kv-store] ${command[0]} ${command[1] ?? ""}: ${res.status}`);
|
|
2900
|
+
}
|
|
2901
|
+
const body = await res.json();
|
|
2902
|
+
if (body.error) throw new Error(`[kv-store] ${command[0]}: ${body.error}`);
|
|
2903
|
+
return body.result ?? null;
|
|
2904
|
+
}
|
|
2905
|
+
async function get(key) {
|
|
2906
|
+
const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
|
|
2907
|
+
if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
|
|
2908
|
+
const { result } = await res.json();
|
|
2909
|
+
return result ?? null;
|
|
2910
|
+
}
|
|
2911
|
+
async function set(key, value) {
|
|
2912
|
+
await exec(["SET", key, JSON.stringify(value)]);
|
|
2913
|
+
}
|
|
2914
|
+
async function del(key) {
|
|
2915
|
+
await exec(["DEL", key]);
|
|
2916
|
+
}
|
|
2917
|
+
async function setNxEx(key, value, ttlSeconds) {
|
|
2918
|
+
const result = await exec(["SET", key, JSON.stringify(value), "EX", ttlSeconds, "NX"]);
|
|
2919
|
+
return result === "OK";
|
|
2920
|
+
}
|
|
2921
|
+
async function sadd(key, member) {
|
|
2922
|
+
await exec(["SADD", key, member]);
|
|
2923
|
+
}
|
|
2924
|
+
async function sismember(key, member) {
|
|
2925
|
+
const result = await exec(["SISMEMBER", key, member]);
|
|
2926
|
+
return result === 1;
|
|
2927
|
+
}
|
|
2928
|
+
async function update(key, fn) {
|
|
2929
|
+
const current = await get(key);
|
|
2930
|
+
const change = fn(current);
|
|
2931
|
+
if (change.op === "set") await set(key, change.value);
|
|
2932
|
+
if (change.op === "delete") await del(key);
|
|
2933
|
+
return change.result;
|
|
2934
|
+
}
|
|
2935
|
+
return { get, set, del, setNxEx, sadd, sismember, update };
|
|
2936
|
+
}
|
|
2937
|
+
function isRestConfig(input) {
|
|
2938
|
+
return typeof input === "object" && input !== null && typeof input.url === "string" && typeof input.token === "string" && typeof input.get !== "function";
|
|
2939
|
+
}
|
|
2940
|
+
function resolveKvStore(input, env = process.env) {
|
|
2941
|
+
if (input) {
|
|
2942
|
+
if (isRestConfig(input)) return restKvStore(input.url, input.token);
|
|
2943
|
+
return input;
|
|
2944
|
+
}
|
|
2945
|
+
const url = env.KV_REST_API_URL;
|
|
2946
|
+
const token = env.KV_REST_API_TOKEN;
|
|
2947
|
+
if (url && token) return restKvStore(url, token);
|
|
2948
|
+
return void 0;
|
|
2949
|
+
}
|
|
2950
|
+
function withPrefix(kv, prefix) {
|
|
2951
|
+
const k = (key) => `${prefix}${key}`;
|
|
2952
|
+
return {
|
|
2953
|
+
get: (key) => kv.get(k(key)),
|
|
2954
|
+
set: (key, value) => kv.set(k(key), value),
|
|
2955
|
+
del: (key) => kv.del(k(key)),
|
|
2956
|
+
setNxEx: (key, value, ttl) => kv.setNxEx(k(key), value, ttl),
|
|
2957
|
+
sadd: (key, member) => kv.sadd(k(key), member),
|
|
2958
|
+
sismember: (key, member) => kv.sismember(k(key), member),
|
|
2959
|
+
update: (key, fn) => kv.update(k(key), fn)
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2105
2962
|
|
|
2106
|
-
// src/
|
|
2963
|
+
// src/kv-store/nonce.ts
|
|
2107
2964
|
var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
|
|
2108
2965
|
var MemoryNonceStore = class {
|
|
2109
2966
|
seen = /* @__PURE__ */ new Map();
|
|
@@ -2120,48 +2977,59 @@ var MemoryNonceStore = class {
|
|
|
2120
2977
|
}
|
|
2121
2978
|
}
|
|
2122
2979
|
};
|
|
2123
|
-
function
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
)
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2980
|
+
function createKvNonceStore(kv, options) {
|
|
2981
|
+
const prefix = options?.prefix ?? "siwx:nonce:";
|
|
2982
|
+
const ttlSeconds = Math.ceil((options?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
2983
|
+
return {
|
|
2984
|
+
async check(nonce) {
|
|
2985
|
+
return kv.setNxEx(`${prefix}${nonce}`, 1, ttlSeconds);
|
|
2986
|
+
}
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// src/kv-store/entitlement.ts
|
|
2991
|
+
var MemoryEntitlementStore = class {
|
|
2992
|
+
routeToWallets = /* @__PURE__ */ new Map();
|
|
2993
|
+
async has(route, wallet) {
|
|
2994
|
+
const wallets = this.routeToWallets.get(route);
|
|
2995
|
+
if (!wallets) return false;
|
|
2996
|
+
return wallets.has(normalizeWalletAddress(wallet));
|
|
2135
2997
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2998
|
+
async grant(route, wallet) {
|
|
2999
|
+
const normalized = normalizeWalletAddress(wallet);
|
|
3000
|
+
let wallets = this.routeToWallets.get(route);
|
|
3001
|
+
if (!wallets) {
|
|
3002
|
+
wallets = /* @__PURE__ */ new Set();
|
|
3003
|
+
this.routeToWallets.set(route, wallets);
|
|
3004
|
+
}
|
|
3005
|
+
wallets.add(normalized);
|
|
2138
3006
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
}
|
|
2143
|
-
function createRedisNonceStore(client, opts) {
|
|
2144
|
-
const prefix = opts?.prefix ?? "siwx:nonce:";
|
|
2145
|
-
const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
2146
|
-
const clientType = detectRedisClientType(client);
|
|
3007
|
+
};
|
|
3008
|
+
function createKvEntitlementStore(kv, options) {
|
|
3009
|
+
const prefix = options?.prefix ?? "siwx:ent:";
|
|
2147
3010
|
return {
|
|
2148
|
-
async
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
return result !== null;
|
|
2154
|
-
}
|
|
2155
|
-
if (clientType === "ioredis") {
|
|
2156
|
-
const redis = client;
|
|
2157
|
-
const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
|
|
2158
|
-
return result === "OK";
|
|
2159
|
-
}
|
|
2160
|
-
throw new Error("Unknown Redis client type");
|
|
3011
|
+
async has(route, wallet) {
|
|
3012
|
+
return kv.sismember(`${prefix}${route}`, normalizeWalletAddress(wallet));
|
|
3013
|
+
},
|
|
3014
|
+
async grant(route, wallet) {
|
|
3015
|
+
await kv.sadd(`${prefix}${route}`, normalizeWalletAddress(wallet));
|
|
2161
3016
|
}
|
|
2162
3017
|
};
|
|
2163
3018
|
}
|
|
2164
3019
|
|
|
3020
|
+
// src/kv-store/mpp.ts
|
|
3021
|
+
async function createKvMppStore(kv, options) {
|
|
3022
|
+
const prefix = options?.prefix ?? "mpp:";
|
|
3023
|
+
const namespaced = withPrefix(kv, prefix);
|
|
3024
|
+
const { Store: StoreNs } = await import("mppx");
|
|
3025
|
+
return StoreNs.upstash({
|
|
3026
|
+
get: (key) => namespaced.get(key),
|
|
3027
|
+
set: (key, value) => namespaced.set(key, value),
|
|
3028
|
+
del: (key) => namespaced.del(key),
|
|
3029
|
+
update: (key, fn) => namespaced.update(key, fn)
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
|
|
2165
3033
|
// src/protocols/mpp/siwx-mode.ts
|
|
2166
3034
|
var import_mppx3 = require("mppx");
|
|
2167
3035
|
async function verifyMppSiwx(request, mppx) {
|
|
@@ -2180,12 +3048,11 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2180
3048
|
const { request, routeEntry, deps } = ctx;
|
|
2181
3049
|
if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
|
|
2182
3050
|
const earlyClone = request.clone();
|
|
2183
|
-
const earlyBody = await parseBody(
|
|
3051
|
+
const earlyBody = await parseBody(ctx, earlyClone);
|
|
2184
3052
|
if (earlyBody.ok) {
|
|
2185
3053
|
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
2186
3054
|
if (validateErr) return validateErr;
|
|
2187
3055
|
} else {
|
|
2188
|
-
firePluginResponse(ctx, earlyBody.response);
|
|
2189
3056
|
return earlyBody.response;
|
|
2190
3057
|
}
|
|
2191
3058
|
}
|
|
@@ -2197,11 +3064,7 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2197
3064
|
mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
|
|
2198
3065
|
} catch (err) {
|
|
2199
3066
|
const message = err instanceof Error ? err.message : String(err);
|
|
2200
|
-
|
|
2201
|
-
level: "critical",
|
|
2202
|
-
message: `MPP SIWX verification failed: ${message}`,
|
|
2203
|
-
route: routeEntry.key
|
|
2204
|
-
});
|
|
3067
|
+
ctx.report("critical", `MPP SIWX verification failed: ${message}`);
|
|
2205
3068
|
return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
|
|
2206
3069
|
}
|
|
2207
3070
|
if (mppSiwxResult.valid) {
|
|
@@ -2223,7 +3086,7 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2223
3086
|
}
|
|
2224
3087
|
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
2225
3088
|
if (!siwx.valid) {
|
|
2226
|
-
const response =
|
|
3089
|
+
const response = import_server7.NextResponse.json(
|
|
2227
3090
|
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
2228
3091
|
{ status: 402 }
|
|
2229
3092
|
);
|
|
@@ -2273,7 +3136,6 @@ async function buildSiwxChallenge(ctx) {
|
|
|
2273
3136
|
extensions: {
|
|
2274
3137
|
"sign-in-with-x": {
|
|
2275
3138
|
info: siwxInfo,
|
|
2276
|
-
// Required by MCP tools at the top level for chain detection.
|
|
2277
3139
|
supportedChains,
|
|
2278
3140
|
...siwxSchema ? { schema: siwxSchema } : {}
|
|
2279
3141
|
}
|
|
@@ -2284,13 +3146,12 @@ async function buildSiwxChallenge(ctx) {
|
|
|
2284
3146
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
2285
3147
|
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
2286
3148
|
} catch (err) {
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
});
|
|
3149
|
+
ctx.report(
|
|
3150
|
+
"warn",
|
|
3151
|
+
`SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3152
|
+
);
|
|
2292
3153
|
}
|
|
2293
|
-
const response = new
|
|
3154
|
+
const response = new import_server7.NextResponse(JSON.stringify(paymentRequired), {
|
|
2294
3155
|
status: 402,
|
|
2295
3156
|
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
2296
3157
|
});
|
|
@@ -2396,6 +3257,12 @@ var RouteBuilder = class {
|
|
|
2396
3257
|
/** @internal */
|
|
2397
3258
|
_minPrice;
|
|
2398
3259
|
/** @internal */
|
|
3260
|
+
_dynamicPrice = false;
|
|
3261
|
+
/** @internal */
|
|
3262
|
+
_tickCost;
|
|
3263
|
+
/** @internal */
|
|
3264
|
+
_unitType;
|
|
3265
|
+
/** @internal */
|
|
2399
3266
|
_payTo;
|
|
2400
3267
|
/** @internal */
|
|
2401
3268
|
_bodySchema;
|
|
@@ -2440,7 +3307,8 @@ var RouteBuilder = class {
|
|
|
2440
3307
|
next._protocols = [...this._protocols];
|
|
2441
3308
|
return next;
|
|
2442
3309
|
}
|
|
2443
|
-
paid(
|
|
3310
|
+
paid(pricingOrOptions, options) {
|
|
3311
|
+
const { pricing, resolvedOptions } = resolvePaidArgs(this._key, pricingOrOptions, options);
|
|
2444
3312
|
if (this._authMode === "unprotected") {
|
|
2445
3313
|
throw new Error(
|
|
2446
3314
|
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
@@ -2454,16 +3322,24 @@ var RouteBuilder = class {
|
|
|
2454
3322
|
const next = this.fork();
|
|
2455
3323
|
next._authMode = "paid";
|
|
2456
3324
|
next._pricing = pricing;
|
|
2457
|
-
if (
|
|
2458
|
-
next._protocols = [...
|
|
3325
|
+
if (resolvedOptions?.protocols) {
|
|
3326
|
+
next._protocols = [...resolvedOptions.protocols];
|
|
2459
3327
|
} else if (next._protocols.length === 0) {
|
|
2460
3328
|
next._protocols = ["x402"];
|
|
2461
3329
|
}
|
|
2462
|
-
if (
|
|
2463
|
-
if (
|
|
2464
|
-
if (
|
|
2465
|
-
if (
|
|
3330
|
+
if (resolvedOptions?.maxPrice) next._maxPrice = resolvedOptions.maxPrice;
|
|
3331
|
+
if (resolvedOptions?.minPrice) next._minPrice = resolvedOptions.minPrice;
|
|
3332
|
+
if (resolvedOptions?.payTo) next._payTo = resolvedOptions.payTo;
|
|
3333
|
+
if (resolvedOptions?.mpp) next._mppInfo = resolvedOptions.mpp;
|
|
3334
|
+
if (resolvedOptions?.dynamic) next._dynamicPrice = true;
|
|
3335
|
+
if (resolvedOptions?.tickCost) next._tickCost = resolvedOptions.tickCost;
|
|
3336
|
+
if (resolvedOptions?.unitType) next._unitType = resolvedOptions.unitType;
|
|
2466
3337
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
3338
|
+
if (next._dynamicPrice) {
|
|
3339
|
+
throw new Error(
|
|
3340
|
+
`route '${this._key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
2467
3343
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
2468
3344
|
if (!tierKey) {
|
|
2469
3345
|
throw new Error(`route '${this._key}': tier key cannot be empty`);
|
|
@@ -2476,16 +3352,40 @@ var RouteBuilder = class {
|
|
|
2476
3352
|
}
|
|
2477
3353
|
}
|
|
2478
3354
|
}
|
|
2479
|
-
if (
|
|
2480
|
-
const parsed = parseFloat(
|
|
3355
|
+
if (resolvedOptions?.maxPrice !== void 0) {
|
|
3356
|
+
const parsed = parseFloat(resolvedOptions.maxPrice);
|
|
2481
3357
|
if (isNaN(parsed) || parsed <= 0) {
|
|
2482
3358
|
throw new Error(
|
|
2483
|
-
`route '${this._key}': maxPrice '${
|
|
3359
|
+
`route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
|
|
2484
3360
|
);
|
|
2485
3361
|
}
|
|
2486
3362
|
}
|
|
3363
|
+
if (resolvedOptions?.tickCost !== void 0) {
|
|
3364
|
+
const parsed = parseFloat(resolvedOptions.tickCost);
|
|
3365
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
3366
|
+
throw new Error(
|
|
3367
|
+
`route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
if (next._dynamicPrice && !next._maxPrice) {
|
|
3372
|
+
throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires maxPrice`);
|
|
3373
|
+
}
|
|
3374
|
+
if (next._dynamicPrice && !next._tickCost) {
|
|
3375
|
+
throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires tickCost`);
|
|
3376
|
+
}
|
|
2487
3377
|
return next;
|
|
2488
3378
|
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
3381
|
+
* control of a wallet via a signed challenge. Combine with `.paid()` to gate
|
|
3382
|
+
* a paid route on a verified wallet identity.
|
|
3383
|
+
*
|
|
3384
|
+
* @example
|
|
3385
|
+
* ```ts
|
|
3386
|
+
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
3387
|
+
* ```
|
|
3388
|
+
*/
|
|
2489
3389
|
siwx() {
|
|
2490
3390
|
if (this._authMode === "unprotected") {
|
|
2491
3391
|
throw new Error(
|
|
@@ -2508,6 +3408,19 @@ var RouteBuilder = class {
|
|
|
2508
3408
|
next._protocols = [];
|
|
2509
3409
|
return next;
|
|
2510
3410
|
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Require an `X-API-Key` header (or `Authorization: Bearer <key>`); the
|
|
3413
|
+
* resolver returns the account record, or `null` for 401. Composes with
|
|
3414
|
+
* `.paid()` — key is checked first, payment second.
|
|
3415
|
+
*
|
|
3416
|
+
* @example
|
|
3417
|
+
* ```ts
|
|
3418
|
+
* router
|
|
3419
|
+
* .route('admin/users')
|
|
3420
|
+
* .apiKey(async (key) => db.admin.findByKey(key))
|
|
3421
|
+
* .handler(async ({ account }) => db.user.list(account.orgId));
|
|
3422
|
+
* ```
|
|
3423
|
+
*/
|
|
2511
3424
|
apiKey(resolver) {
|
|
2512
3425
|
if (this._siwxEnabled) {
|
|
2513
3426
|
throw new Error(
|
|
@@ -2519,6 +3432,15 @@ var RouteBuilder = class {
|
|
|
2519
3432
|
next._apiKeyResolver = resolver;
|
|
2520
3433
|
return next;
|
|
2521
3434
|
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Mark the route as public — no auth, no payment, no SIWX. The handler
|
|
3437
|
+
* receives `null` for `wallet`, `payment`, and `account`.
|
|
3438
|
+
*
|
|
3439
|
+
* @example
|
|
3440
|
+
* ```ts
|
|
3441
|
+
* router.route('health').unprotected().handler(async () => ({ status: 'ok' }));
|
|
3442
|
+
* ```
|
|
3443
|
+
*/
|
|
2522
3444
|
unprotected() {
|
|
2523
3445
|
if (this._authMode && this._authMode !== "unprotected") {
|
|
2524
3446
|
throw new Error(
|
|
@@ -2535,60 +3457,82 @@ var RouteBuilder = class {
|
|
|
2535
3457
|
next._protocols = [];
|
|
2536
3458
|
return next;
|
|
2537
3459
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
3460
|
+
/**
|
|
3461
|
+
* Tag the route with an upstream provider for discovery and provider-side
|
|
3462
|
+
* monitoring. The provider name and config surface in `well-known` and
|
|
3463
|
+
* OpenAPI output.
|
|
3464
|
+
*
|
|
3465
|
+
* @example
|
|
3466
|
+
* ```ts
|
|
3467
|
+
* router
|
|
3468
|
+
* .route('search')
|
|
3469
|
+
* .paid('0.01')
|
|
3470
|
+
* .provider('exa', { quotaPerMonth: 1000 })
|
|
3471
|
+
* .handler(handler);
|
|
3472
|
+
* ```
|
|
3473
|
+
*/
|
|
2541
3474
|
provider(name, config) {
|
|
2542
3475
|
const next = this.fork();
|
|
2543
3476
|
next._providerName = name;
|
|
2544
3477
|
next._providerConfig = config ?? {};
|
|
2545
3478
|
return next;
|
|
2546
3479
|
}
|
|
2547
|
-
|
|
3480
|
+
/**
|
|
3481
|
+
* Declare the request body's Zod schema. Parsed body is typed as `ctx.body`
|
|
3482
|
+
* in the handler. Use `.inputExample()` to attach a discovery example.
|
|
3483
|
+
*
|
|
3484
|
+
* @example
|
|
3485
|
+
* ```ts
|
|
3486
|
+
* .body(z.object({ query: z.string() }))
|
|
3487
|
+
* .handler(async ({ body }) => search(body.query));
|
|
3488
|
+
* ```
|
|
3489
|
+
*/
|
|
3490
|
+
body(schema) {
|
|
2548
3491
|
const next = this.fork();
|
|
2549
3492
|
next._bodySchema = schema;
|
|
2550
|
-
if (example !== void 0) {
|
|
2551
|
-
next._inputExample = example;
|
|
2552
|
-
next._hasInputExample = true;
|
|
2553
|
-
}
|
|
2554
3493
|
return next;
|
|
2555
3494
|
}
|
|
2556
|
-
|
|
3495
|
+
/**
|
|
3496
|
+
* Declare a query-string Zod schema and switch the route to `GET`. Parsed
|
|
3497
|
+
* query is typed as `ctx.query` in the handler. Use `.inputExample()` to
|
|
3498
|
+
* attach a discovery example.
|
|
3499
|
+
*
|
|
3500
|
+
* @example
|
|
3501
|
+
* ```ts
|
|
3502
|
+
* .query(z.object({ id: z.string() }))
|
|
3503
|
+
* .handler(async ({ query }) => getById(query.id));
|
|
3504
|
+
* ```
|
|
3505
|
+
*/
|
|
3506
|
+
query(schema) {
|
|
2557
3507
|
const next = this.fork();
|
|
2558
3508
|
next._querySchema = schema;
|
|
2559
|
-
if (example !== void 0) {
|
|
2560
|
-
next._inputExample = example;
|
|
2561
|
-
next._hasInputExample = true;
|
|
2562
|
-
}
|
|
2563
3509
|
next._method = "GET";
|
|
2564
3510
|
return next;
|
|
2565
3511
|
}
|
|
2566
|
-
|
|
3512
|
+
/**
|
|
3513
|
+
* Declare the response output's Zod schema for OpenAPI generation. The
|
|
3514
|
+
* runtime does not validate handler return values — use Zod's `.parse()`
|
|
3515
|
+
* inside the handler if strict output validation is required. Use
|
|
3516
|
+
* `.outputExample()` to attach a discovery example.
|
|
3517
|
+
*
|
|
3518
|
+
* @example
|
|
3519
|
+
* ```ts
|
|
3520
|
+
* .output(z.object({ result: z.string() }))
|
|
3521
|
+
* .handler(async () => ({ result: 'ok' }));
|
|
3522
|
+
* ```
|
|
3523
|
+
*/
|
|
3524
|
+
output(schema) {
|
|
2567
3525
|
const next = this.fork();
|
|
2568
3526
|
next._outputSchema = schema;
|
|
2569
|
-
if (example !== void 0) {
|
|
2570
|
-
next._outputExample = example;
|
|
2571
|
-
next._hasOutputExample = true;
|
|
2572
|
-
}
|
|
2573
3527
|
return next;
|
|
2574
3528
|
}
|
|
2575
3529
|
/**
|
|
2576
|
-
*
|
|
2577
|
-
*
|
|
2578
|
-
* Optional. When provided, the example is validated against the request schema
|
|
2579
|
-
* at route registration and embedded in the bazaar discovery extension so
|
|
2580
|
-
* indexers can advertise a working sample call.
|
|
2581
|
-
*
|
|
2582
|
-
* For the common case, pass the example directly to `.body(schema, example)` or
|
|
2583
|
-
* `.query(schema, example)` instead.
|
|
3530
|
+
* Attach an example of the request body or query for discovery output,
|
|
3531
|
+
* validated against the registered schema at registration.
|
|
2584
3532
|
*
|
|
2585
3533
|
* @example
|
|
2586
3534
|
* ```ts
|
|
2587
|
-
*
|
|
2588
|
-
* .paid('0.01')
|
|
2589
|
-
* .body(z.object({ q: z.string() }))
|
|
2590
|
-
* .inputExample({ q: 'hello world' })
|
|
2591
|
-
* .handler(async ({ body }) => { ... });
|
|
3535
|
+
* .body(searchSchema).inputExample({ query: 'cats' });
|
|
2592
3536
|
* ```
|
|
2593
3537
|
*/
|
|
2594
3538
|
inputExample(example) {
|
|
@@ -2598,32 +3542,12 @@ var RouteBuilder = class {
|
|
|
2598
3542
|
return next;
|
|
2599
3543
|
}
|
|
2600
3544
|
/**
|
|
2601
|
-
*
|
|
2602
|
-
*
|
|
2603
|
-
* Optional. When provided, the example is validated against the output schema
|
|
2604
|
-
* at route registration and embedded in the bazaar discovery extension so
|
|
2605
|
-
* indexers can advertise the response shape.
|
|
2606
|
-
*
|
|
2607
|
-
* For the common case, pass the example directly to `.output(schema, example)` instead.
|
|
2608
|
-
*
|
|
2609
|
-
* Accepts any JSON value (objects, arrays, or primitives) — top-level array
|
|
2610
|
-
* or primitive responses (e.g. `z.array(...)`) are supported alongside the
|
|
2611
|
-
* common object case.
|
|
3545
|
+
* Attach an example response for discovery output, validated against the
|
|
3546
|
+
* registered output schema at registration.
|
|
2612
3547
|
*
|
|
2613
3548
|
* @example
|
|
2614
3549
|
* ```ts
|
|
2615
|
-
*
|
|
2616
|
-
* .paid('0.01')
|
|
2617
|
-
* .output(z.object({ results: z.array(z.string()) }))
|
|
2618
|
-
* .outputExample({ results: ['a', 'b'] })
|
|
2619
|
-
* .handler(async () => { ... });
|
|
2620
|
-
*
|
|
2621
|
-
* // Top-level array response
|
|
2622
|
-
* router.route('chains')
|
|
2623
|
-
* .paid('0.01')
|
|
2624
|
-
* .output(z.array(z.object({ name: z.string() })))
|
|
2625
|
-
* .outputExample([{ name: 'Ethereum' }])
|
|
2626
|
-
* .handler(async () => { ... });
|
|
3550
|
+
* .output(resultSchema).outputExample({ result: 'ok' });
|
|
2627
3551
|
* ```
|
|
2628
3552
|
*/
|
|
2629
3553
|
outputExample(example) {
|
|
@@ -2632,43 +3556,60 @@ var RouteBuilder = class {
|
|
|
2632
3556
|
next._hasOutputExample = true;
|
|
2633
3557
|
return next;
|
|
2634
3558
|
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Set a human-readable summary of the route. Surfaces in OpenAPI,
|
|
3561
|
+
* `well-known`, and `llms.txt` discovery output.
|
|
3562
|
+
*
|
|
3563
|
+
* @example
|
|
3564
|
+
* ```ts
|
|
3565
|
+
* .description('Search indexed web pages by full-text query');
|
|
3566
|
+
* ```
|
|
3567
|
+
*/
|
|
2635
3568
|
description(text) {
|
|
2636
3569
|
const next = this.fork();
|
|
2637
3570
|
next._description = text;
|
|
2638
3571
|
return next;
|
|
2639
3572
|
}
|
|
3573
|
+
/**
|
|
3574
|
+
* Override the URL path advertised in discovery output. Defaults to the
|
|
3575
|
+
* registry key passed to `.route()`.
|
|
3576
|
+
*
|
|
3577
|
+
* @example
|
|
3578
|
+
* ```ts
|
|
3579
|
+
* router.route('search').path('/v2/search').handler(handler);
|
|
3580
|
+
* ```
|
|
3581
|
+
*/
|
|
2640
3582
|
path(p) {
|
|
2641
3583
|
const next = this.fork();
|
|
2642
3584
|
next._path = p;
|
|
2643
3585
|
return next;
|
|
2644
3586
|
}
|
|
3587
|
+
/**
|
|
3588
|
+
* Override the HTTP method advertised in discovery. Defaults to `POST`, or
|
|
3589
|
+
* `GET` when `.query()` has been called.
|
|
3590
|
+
*
|
|
3591
|
+
* @example
|
|
3592
|
+
* ```ts
|
|
3593
|
+
* router.route('items/delete').method('DELETE').handler(handler);
|
|
3594
|
+
* ```
|
|
3595
|
+
*/
|
|
2645
3596
|
method(m) {
|
|
2646
3597
|
const next = this.fork();
|
|
2647
3598
|
next._method = m;
|
|
2648
3599
|
return next;
|
|
2649
3600
|
}
|
|
2650
|
-
// -------------------------------------------------------------------------
|
|
2651
|
-
// Pre-payment validation
|
|
2652
|
-
// -------------------------------------------------------------------------
|
|
2653
3601
|
/**
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2658
|
-
* Requires `.body()` — call `.body()` before `.validate()` for type inference.
|
|
3602
|
+
* Run validation against the parsed body before the 402 challenge. Throw
|
|
3603
|
+
* `Object.assign(new Error('...'), { status })` to reject with a custom
|
|
3604
|
+
* status code; defaults to 400. Requires `.body()` to be called first.
|
|
2659
3605
|
*
|
|
2660
3606
|
* @example
|
|
2661
|
-
* ```
|
|
2662
|
-
*
|
|
2663
|
-
* .
|
|
2664
|
-
*
|
|
2665
|
-
*
|
|
2666
|
-
*
|
|
2667
|
-
* if (await isDomainTaken(body.domain)) {
|
|
2668
|
-
* throw Object.assign(new Error('Domain taken'), { status: 409 });
|
|
2669
|
-
* }
|
|
2670
|
-
* })
|
|
2671
|
-
* .handler(async ({ body }) => { ... });
|
|
3607
|
+
* ```ts
|
|
3608
|
+
* .body(RegisterSchema).validate(async (body) => {
|
|
3609
|
+
* if (await isTaken(body.name)) {
|
|
3610
|
+
* throw Object.assign(new Error('taken'), { status: 409 });
|
|
3611
|
+
* }
|
|
3612
|
+
* });
|
|
2672
3613
|
* ```
|
|
2673
3614
|
*/
|
|
2674
3615
|
validate(fn) {
|
|
@@ -2676,27 +3617,64 @@ var RouteBuilder = class {
|
|
|
2676
3617
|
next._validateFn = fn;
|
|
2677
3618
|
return next;
|
|
2678
3619
|
}
|
|
2679
|
-
// -------------------------------------------------------------------------
|
|
2680
|
-
// Settlement lifecycle
|
|
2681
|
-
// -------------------------------------------------------------------------
|
|
2682
3620
|
/**
|
|
2683
|
-
*
|
|
3621
|
+
* Hook into the settlement lifecycle. `beforeSettle` runs after the handler
|
|
3622
|
+
* succeeds but before on-chain settlement and can cancel the charge;
|
|
3623
|
+
* `afterSettle` runs after settlement completes (success or failure).
|
|
2684
3624
|
*
|
|
2685
|
-
*
|
|
2686
|
-
*
|
|
2687
|
-
*
|
|
2688
|
-
*
|
|
3625
|
+
* @example
|
|
3626
|
+
* ```ts
|
|
3627
|
+
* .settlement({
|
|
3628
|
+
* beforeSettle: ({ result }) => (result.refund ? 'skip' : 'continue'),
|
|
3629
|
+
* afterSettle: ({ tx }) => analytics.track('settled', { tx }),
|
|
3630
|
+
* });
|
|
3631
|
+
* ```
|
|
2689
3632
|
*/
|
|
2690
3633
|
settlement(lifecycle) {
|
|
2691
3634
|
const next = this.fork();
|
|
2692
3635
|
next._settlement = lifecycle;
|
|
2693
3636
|
return next;
|
|
2694
3637
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
3638
|
+
/**
|
|
3639
|
+
* Register the request handler and return the Next.js route function. The
|
|
3640
|
+
* handler receives a typed context and may return a value (serialized to
|
|
3641
|
+
* JSON), a raw `Response`, or throw an `HttpError` for a non-2xx status.
|
|
3642
|
+
*
|
|
3643
|
+
* @example
|
|
3644
|
+
* ```ts
|
|
3645
|
+
* export const POST = router
|
|
3646
|
+
* .route('search')
|
|
3647
|
+
* .paid('0.01')
|
|
3648
|
+
* .body(schema)
|
|
3649
|
+
* .handler(async ({ body, wallet }) => searchService(body, wallet));
|
|
3650
|
+
* ```
|
|
3651
|
+
*/
|
|
2698
3652
|
handler(fn) {
|
|
2699
|
-
|
|
3653
|
+
return this.register(fn, false);
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Register a streaming handler (`async function*`) and return the Next.js
|
|
3657
|
+
* route function. Each `charge()` call bills one tick (`tickCost` USDC) up
|
|
3658
|
+
* to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
|
|
3659
|
+
*
|
|
3660
|
+
* @example
|
|
3661
|
+
* ```ts
|
|
3662
|
+
* export const POST = router
|
|
3663
|
+
* .route('llm/stream')
|
|
3664
|
+
* .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
|
|
3665
|
+
* .body(schema)
|
|
3666
|
+
* .stream(async function* ({ body, charge }) {
|
|
3667
|
+
* for await (const token of streamLLM(body.prompt)) {
|
|
3668
|
+
* await charge();
|
|
3669
|
+
* yield token;
|
|
3670
|
+
* }
|
|
3671
|
+
* });
|
|
3672
|
+
* ```
|
|
3673
|
+
*/
|
|
3674
|
+
stream(fn) {
|
|
3675
|
+
return this.register(fn, true);
|
|
3676
|
+
}
|
|
3677
|
+
register(handlerFn, streaming) {
|
|
2700
3678
|
if (!this._authMode) {
|
|
2701
3679
|
throw new Error(
|
|
2702
3680
|
`route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
@@ -2710,6 +3688,26 @@ var RouteBuilder = class {
|
|
|
2710
3688
|
if (this._settlement && !this._pricing) {
|
|
2711
3689
|
throw new Error(`route '${this._key}': .settlement() requires a paid route`);
|
|
2712
3690
|
}
|
|
3691
|
+
if (this._dynamicPrice && this._protocols.includes("x402")) {
|
|
3692
|
+
const hasUpto = this._deps.x402Accepts.some((accept) => accept.scheme === "upto");
|
|
3693
|
+
if (!hasUpto) {
|
|
3694
|
+
throw new Error(
|
|
3695
|
+
`route '${this._key}': .paid({ dynamic: true }) on an x402 route requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
if (this._dynamicPrice && this._protocols.includes("mpp")) {
|
|
3700
|
+
if (!this._deps.mppSessionConfig) {
|
|
3701
|
+
throw new Error(
|
|
3702
|
+
`route '${this._key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
|
|
3703
|
+
);
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
if (streaming && !this._dynamicPrice) {
|
|
3707
|
+
throw new Error(
|
|
3708
|
+
`route '${this._key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
2713
3711
|
validateExamples(
|
|
2714
3712
|
this._key,
|
|
2715
3713
|
this._bodySchema,
|
|
@@ -2725,6 +3723,8 @@ var RouteBuilder = class {
|
|
|
2725
3723
|
authMode: this._authMode,
|
|
2726
3724
|
siwxEnabled: this._siwxEnabled,
|
|
2727
3725
|
pricing: this._pricing,
|
|
3726
|
+
dynamicPrice: this._dynamicPrice ? true : void 0,
|
|
3727
|
+
streaming: streaming ? true : void 0,
|
|
2728
3728
|
protocols: this._protocols,
|
|
2729
3729
|
bodySchema: this._bodySchema,
|
|
2730
3730
|
querySchema: this._querySchema,
|
|
@@ -2742,81 +3742,28 @@ var RouteBuilder = class {
|
|
|
2742
3742
|
providerConfig: this._providerConfig,
|
|
2743
3743
|
validateFn: this._validateFn,
|
|
2744
3744
|
settlement: this._settlement,
|
|
2745
|
-
mppInfo: this._mppInfo
|
|
3745
|
+
mppInfo: this._mppInfo,
|
|
3746
|
+
tickCost: this._tickCost,
|
|
3747
|
+
unitType: this._unitType
|
|
2746
3748
|
};
|
|
2747
3749
|
this._registry.register(entry);
|
|
2748
|
-
return createRequestHandler(
|
|
2749
|
-
entry,
|
|
2750
|
-
handlerFn,
|
|
2751
|
-
this._deps
|
|
2752
|
-
);
|
|
3750
|
+
return createRequestHandler(entry, handlerFn, this._deps);
|
|
2753
3751
|
}
|
|
2754
3752
|
};
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
if (!wallets) return false;
|
|
2762
|
-
return wallets.has(normalizeWalletAddress(wallet));
|
|
2763
|
-
}
|
|
2764
|
-
async grant(route, wallet) {
|
|
2765
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2766
|
-
let wallets = this.routeToWallets.get(route);
|
|
2767
|
-
if (!wallets) {
|
|
2768
|
-
wallets = /* @__PURE__ */ new Set();
|
|
2769
|
-
this.routeToWallets.set(route, wallets);
|
|
3753
|
+
function resolvePaidArgs(routeKey, pricingOrOptions, options) {
|
|
3754
|
+
const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
|
|
3755
|
+
if (isHandlerDynamicShape) {
|
|
3756
|
+
const opts = pricingOrOptions;
|
|
3757
|
+
if (!opts.maxPrice) {
|
|
3758
|
+
throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
|
|
2770
3759
|
}
|
|
2771
|
-
|
|
3760
|
+
return { pricing: opts.maxPrice, resolvedOptions: opts };
|
|
2772
3761
|
}
|
|
2773
|
-
};
|
|
2774
|
-
function detectRedisClientType2(client) {
|
|
2775
|
-
if (!client || typeof client !== "object") {
|
|
2776
|
-
throw new Error(
|
|
2777
|
-
"createRedisEntitlementStore requires a Redis client. Supported: @upstash/redis, ioredis."
|
|
2778
|
-
);
|
|
2779
|
-
}
|
|
2780
|
-
if ("options" in client && "status" in client) return "ioredis";
|
|
2781
|
-
const constructor = client.constructor?.name;
|
|
2782
|
-
if (constructor === "Redis" && "url" in client) return "upstash";
|
|
2783
|
-
if (typeof client.sadd === "function" && typeof client.sismember === "function") {
|
|
2784
|
-
return "upstash";
|
|
2785
|
-
}
|
|
2786
|
-
throw new Error("Unrecognized Redis client for entitlement store.");
|
|
2787
|
-
}
|
|
2788
|
-
function createRedisEntitlementStore(client, options) {
|
|
2789
|
-
const clientType = detectRedisClientType2(client);
|
|
2790
|
-
const prefix = options?.prefix ?? "siwx:entitlement:";
|
|
2791
|
-
return {
|
|
2792
|
-
async has(route, wallet) {
|
|
2793
|
-
const key = `${prefix}${route}`;
|
|
2794
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2795
|
-
if (clientType === "upstash") {
|
|
2796
|
-
const redis2 = client;
|
|
2797
|
-
const result2 = await redis2.sismember(key, normalized);
|
|
2798
|
-
return result2 === 1 || result2 === true;
|
|
2799
|
-
}
|
|
2800
|
-
const redis = client;
|
|
2801
|
-
const result = await redis.sismember(key, normalized);
|
|
2802
|
-
return result === 1;
|
|
2803
|
-
},
|
|
2804
|
-
async grant(route, wallet) {
|
|
2805
|
-
const key = `${prefix}${route}`;
|
|
2806
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2807
|
-
if (clientType === "upstash") {
|
|
2808
|
-
const redis2 = client;
|
|
2809
|
-
await redis2.sadd(key, normalized);
|
|
2810
|
-
return;
|
|
2811
|
-
}
|
|
2812
|
-
const redis = client;
|
|
2813
|
-
await redis.sadd(key, normalized);
|
|
2814
|
-
}
|
|
2815
|
-
};
|
|
3762
|
+
return { pricing: pricingOrOptions, resolvedOptions: options };
|
|
2816
3763
|
}
|
|
2817
3764
|
|
|
2818
3765
|
// src/discovery/well-known.ts
|
|
2819
|
-
var
|
|
3766
|
+
var import_server8 = require("next/server");
|
|
2820
3767
|
|
|
2821
3768
|
// src/discovery/utils/guidance.ts
|
|
2822
3769
|
async function resolveGuidance(discovery) {
|
|
@@ -2860,7 +3807,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2860
3807
|
if (instructions) {
|
|
2861
3808
|
body.instructions = instructions;
|
|
2862
3809
|
}
|
|
2863
|
-
return
|
|
3810
|
+
return import_server8.NextResponse.json(body, {
|
|
2864
3811
|
headers: {
|
|
2865
3812
|
"Access-Control-Allow-Origin": "*",
|
|
2866
3813
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -2877,14 +3824,14 @@ function toDiscoveryResource(method, url, mode) {
|
|
|
2877
3824
|
}
|
|
2878
3825
|
|
|
2879
3826
|
// src/discovery/openapi.ts
|
|
2880
|
-
var
|
|
3827
|
+
var import_server9 = require("next/server");
|
|
2881
3828
|
init_constants();
|
|
2882
3829
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
2883
3830
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
2884
3831
|
let cached = null;
|
|
2885
3832
|
let validated = false;
|
|
2886
3833
|
return async (_request) => {
|
|
2887
|
-
if (cached) return
|
|
3834
|
+
if (cached) return import_server9.NextResponse.json(cached);
|
|
2888
3835
|
if (!validated && pricesKeys) {
|
|
2889
3836
|
registry.validate(pricesKeys);
|
|
2890
3837
|
validated = true;
|
|
@@ -2947,7 +3894,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2947
3894
|
paths
|
|
2948
3895
|
};
|
|
2949
3896
|
cached = createDocument(openApiDocument);
|
|
2950
|
-
return
|
|
3897
|
+
return import_server9.NextResponse.json(cached);
|
|
2951
3898
|
};
|
|
2952
3899
|
}
|
|
2953
3900
|
function deriveTag(routeKey) {
|
|
@@ -3009,97 +3956,336 @@ function buildOperation(routeKey, entry, tag) {
|
|
|
3009
3956
|
};
|
|
3010
3957
|
}
|
|
3011
3958
|
return {
|
|
3012
|
-
operation,
|
|
3013
|
-
requiresSiwxScheme,
|
|
3014
|
-
requiresApiKeyScheme
|
|
3959
|
+
operation,
|
|
3960
|
+
requiresSiwxScheme,
|
|
3961
|
+
requiresApiKeyScheme
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
function toProtocolObject(protocol, mppInfo) {
|
|
3965
|
+
if (protocol === "mpp") {
|
|
3966
|
+
return {
|
|
3967
|
+
mpp: {
|
|
3968
|
+
method: mppInfo?.method ?? "tempo",
|
|
3969
|
+
intent: mppInfo?.intent ?? "charge",
|
|
3970
|
+
currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
|
|
3971
|
+
}
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
return { [protocol]: {} };
|
|
3975
|
+
}
|
|
3976
|
+
function buildPricingInfo(entry) {
|
|
3977
|
+
if (!entry.pricing) return void 0;
|
|
3978
|
+
if (typeof entry.pricing === "string") {
|
|
3979
|
+
return {
|
|
3980
|
+
price: { mode: "fixed", currency: "USD", amount: entry.pricing }
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
if (typeof entry.pricing === "function") {
|
|
3984
|
+
return {
|
|
3985
|
+
price: {
|
|
3986
|
+
mode: "dynamic",
|
|
3987
|
+
currency: "USD",
|
|
3988
|
+
min: entry.minPrice ?? "0",
|
|
3989
|
+
max: entry.maxPrice ?? "0"
|
|
3990
|
+
}
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
if ("tiers" in entry.pricing) {
|
|
3994
|
+
const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
|
|
3995
|
+
const min = Math.min(...tierPrices);
|
|
3996
|
+
const max = Math.max(...tierPrices);
|
|
3997
|
+
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
3998
|
+
if (min === max) {
|
|
3999
|
+
return {
|
|
4000
|
+
price: { mode: "fixed", currency: "USD", amount: String(min) }
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
return {
|
|
4004
|
+
price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
|
|
4005
|
+
};
|
|
4006
|
+
}
|
|
4007
|
+
return {
|
|
4008
|
+
price: {
|
|
4009
|
+
mode: "dynamic",
|
|
4010
|
+
currency: "USD",
|
|
4011
|
+
min: "0",
|
|
4012
|
+
max: entry.maxPrice ?? "0"
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
4016
|
+
return void 0;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
// src/discovery/llms-txt.ts
|
|
4020
|
+
var import_server10 = require("next/server");
|
|
4021
|
+
function createLlmsTxtHandler(discovery) {
|
|
4022
|
+
return async (_request) => {
|
|
4023
|
+
const guidance = await resolveGuidance(discovery) ?? "";
|
|
4024
|
+
return new import_server10.NextResponse(guidance, {
|
|
4025
|
+
headers: {
|
|
4026
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
4027
|
+
"Access-Control-Allow-Origin": "*"
|
|
4028
|
+
}
|
|
4029
|
+
});
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
// src/index.ts
|
|
4034
|
+
init_accepts();
|
|
4035
|
+
init_constants();
|
|
4036
|
+
|
|
4037
|
+
// src/config/error.ts
|
|
4038
|
+
var RouterConfigError = class extends Error {
|
|
4039
|
+
issues;
|
|
4040
|
+
constructor(issues) {
|
|
4041
|
+
super(formatRouterConfigIssues(issues));
|
|
4042
|
+
this.name = "RouterConfigError";
|
|
4043
|
+
this.issues = issues;
|
|
4044
|
+
}
|
|
4045
|
+
};
|
|
4046
|
+
function formatRouterConfigIssues(issues) {
|
|
4047
|
+
return issues.map((issue) => issue.message).join("\n");
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
// src/config/validators/x402.ts
|
|
4051
|
+
init_accepts();
|
|
4052
|
+
|
|
4053
|
+
// src/config/validators/shared.ts
|
|
4054
|
+
init_evm();
|
|
4055
|
+
init_solana();
|
|
4056
|
+
init_accepts();
|
|
4057
|
+
function isEvmAddress(value) {
|
|
4058
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
|
4059
|
+
}
|
|
4060
|
+
function isEvmPrivateKey(value) {
|
|
4061
|
+
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
4062
|
+
}
|
|
4063
|
+
function isSupportedX402Network(network) {
|
|
4064
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
4065
|
+
}
|
|
4066
|
+
function findPlaceholderPayee(values) {
|
|
4067
|
+
return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
|
|
4068
|
+
}
|
|
4069
|
+
function usesDefaultEvmFacilitator(config) {
|
|
4070
|
+
return getConfiguredX402Networks(config).some(
|
|
4071
|
+
(network) => typeof network === "string" && isEvmNetwork(network)
|
|
4072
|
+
) && config.x402?.facilitators?.evm === void 0;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
// src/config/validators/x402.ts
|
|
4076
|
+
var CHECKS = [
|
|
4077
|
+
checkAcceptNetworkPresent,
|
|
4078
|
+
checkAcceptNetworkSupported,
|
|
4079
|
+
checkNonExactRequiresAsset,
|
|
4080
|
+
checkDecimalsAreValid,
|
|
4081
|
+
checkPayee,
|
|
4082
|
+
checkPlaceholderPayeeAddress,
|
|
4083
|
+
checkCdpKeys
|
|
4084
|
+
];
|
|
4085
|
+
function validateX402Config(config, env, options) {
|
|
4086
|
+
const accepts = getConfiguredX402Accepts(config);
|
|
4087
|
+
if (accepts.length === 0) {
|
|
4088
|
+
return [
|
|
4089
|
+
{
|
|
4090
|
+
code: "missing_x402_accepts",
|
|
4091
|
+
protocol: "x402",
|
|
4092
|
+
message: "x402 requires at least one accept configuration."
|
|
4093
|
+
}
|
|
4094
|
+
];
|
|
4095
|
+
}
|
|
4096
|
+
const args = { config, accepts, env, options };
|
|
4097
|
+
return CHECKS.map((check) => check(args)).filter(
|
|
4098
|
+
(issue) => issue !== null
|
|
4099
|
+
);
|
|
4100
|
+
}
|
|
4101
|
+
function checkAcceptNetworkPresent({ accepts }) {
|
|
4102
|
+
return accepts.some((accept) => !accept.network) ? { code: "missing_x402_network", protocol: "x402", message: "x402 accepts require a network." } : null;
|
|
4103
|
+
}
|
|
4104
|
+
function checkAcceptNetworkSupported({ accepts }) {
|
|
4105
|
+
const unsupported = accepts.find(
|
|
4106
|
+
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
4107
|
+
);
|
|
4108
|
+
if (!unsupported) return null;
|
|
4109
|
+
return {
|
|
4110
|
+
code: "unsupported_x402_network",
|
|
4111
|
+
protocol: "x402",
|
|
4112
|
+
message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
function checkNonExactRequiresAsset({ accepts }) {
|
|
4116
|
+
return accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset) ? {
|
|
4117
|
+
code: "missing_x402_asset",
|
|
4118
|
+
protocol: "x402",
|
|
4119
|
+
message: "non-exact x402 accepts require an asset."
|
|
4120
|
+
} : null;
|
|
4121
|
+
}
|
|
4122
|
+
function checkDecimalsAreValid({ accepts }) {
|
|
4123
|
+
const invalid = accepts.find(
|
|
4124
|
+
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
4125
|
+
);
|
|
4126
|
+
if (!invalid) return null;
|
|
4127
|
+
return {
|
|
4128
|
+
code: "invalid_x402_decimals",
|
|
4129
|
+
protocol: "x402",
|
|
4130
|
+
message: "x402 accept decimals must be a non-negative integer."
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
function checkPayee({ config, accepts }) {
|
|
4134
|
+
if (config.payeeAddress) return null;
|
|
4135
|
+
return accepts.some((accept) => !accept.payTo) ? {
|
|
4136
|
+
code: "missing_x402_payee",
|
|
4137
|
+
protocol: "x402",
|
|
4138
|
+
message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
4139
|
+
} : null;
|
|
4140
|
+
}
|
|
4141
|
+
function checkPlaceholderPayeeAddress({
|
|
4142
|
+
config,
|
|
4143
|
+
accepts
|
|
4144
|
+
}) {
|
|
4145
|
+
const placeholder = findPlaceholderPayee([
|
|
4146
|
+
config.payeeAddress,
|
|
4147
|
+
...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
|
|
4148
|
+
]);
|
|
4149
|
+
if (!placeholder) return null;
|
|
4150
|
+
return {
|
|
4151
|
+
code: "placeholder_payee",
|
|
4152
|
+
protocol: "x402",
|
|
4153
|
+
message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
function checkCdpKeys({ config, env, options }) {
|
|
4157
|
+
if (options.requireCdpKeys === false) return null;
|
|
4158
|
+
if (!usesDefaultEvmFacilitator(config)) return null;
|
|
4159
|
+
const missing = [
|
|
4160
|
+
env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
|
|
4161
|
+
env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
|
|
4162
|
+
].filter(Boolean);
|
|
4163
|
+
if (missing.length === 0) return null;
|
|
4164
|
+
return {
|
|
4165
|
+
code: "missing_cdp_keys",
|
|
4166
|
+
protocol: "x402",
|
|
4167
|
+
message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
// src/config/validators/mpp.ts
|
|
4172
|
+
var import_accounts = require("viem/accounts");
|
|
4173
|
+
var CHECKS2 = [
|
|
4174
|
+
checkSecretKey,
|
|
4175
|
+
checkCurrency,
|
|
4176
|
+
checkRecipient,
|
|
4177
|
+
checkPlaceholderRecipient,
|
|
4178
|
+
checkRpcUrl,
|
|
4179
|
+
checkFeePayerKey,
|
|
4180
|
+
checkOperatorKey,
|
|
4181
|
+
checkOperatorMatchesFeePayer
|
|
4182
|
+
];
|
|
4183
|
+
function validateMppConfig(config, env) {
|
|
4184
|
+
const mpp = config.mpp;
|
|
4185
|
+
if (!mpp) {
|
|
4186
|
+
return [
|
|
4187
|
+
{
|
|
4188
|
+
code: "missing_mpp_config",
|
|
4189
|
+
protocol: "mpp",
|
|
4190
|
+
message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
4191
|
+
}
|
|
4192
|
+
];
|
|
4193
|
+
}
|
|
4194
|
+
const args = { config, mpp, env };
|
|
4195
|
+
return CHECKS2.map((check) => check(args)).filter(
|
|
4196
|
+
(issue) => issue !== null
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
function checkSecretKey({ mpp }) {
|
|
4200
|
+
if (mpp.secretKey) return null;
|
|
4201
|
+
return {
|
|
4202
|
+
code: "missing_mpp_secret_key",
|
|
4203
|
+
protocol: "mpp",
|
|
4204
|
+
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
3015
4205
|
};
|
|
3016
4206
|
}
|
|
3017
|
-
function
|
|
3018
|
-
if (
|
|
4207
|
+
function checkCurrency({ mpp }) {
|
|
4208
|
+
if (!mpp.currency) {
|
|
3019
4209
|
return {
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
|
|
3024
|
-
}
|
|
4210
|
+
code: "missing_mpp_currency",
|
|
4211
|
+
protocol: "mpp",
|
|
4212
|
+
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
3025
4213
|
};
|
|
3026
4214
|
}
|
|
3027
|
-
|
|
3028
|
-
}
|
|
3029
|
-
function buildPricingInfo(entry) {
|
|
3030
|
-
if (!entry.pricing) return void 0;
|
|
3031
|
-
if (typeof entry.pricing === "string") {
|
|
4215
|
+
if (!isEvmAddress(mpp.currency)) {
|
|
3032
4216
|
return {
|
|
3033
|
-
|
|
4217
|
+
code: "invalid_mpp_currency",
|
|
4218
|
+
protocol: "mpp",
|
|
4219
|
+
message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
|
|
3034
4220
|
};
|
|
3035
4221
|
}
|
|
3036
|
-
|
|
4222
|
+
return null;
|
|
4223
|
+
}
|
|
4224
|
+
function checkRecipient({ config, mpp }) {
|
|
4225
|
+
const recipient = mpp.recipient ?? config.payeeAddress;
|
|
4226
|
+
if (!recipient) {
|
|
3037
4227
|
return {
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
min: entry.minPrice ?? "0",
|
|
3042
|
-
max: entry.maxPrice ?? "0"
|
|
3043
|
-
}
|
|
4228
|
+
code: "missing_mpp_recipient",
|
|
4229
|
+
protocol: "mpp",
|
|
4230
|
+
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
3044
4231
|
};
|
|
3045
4232
|
}
|
|
3046
|
-
if (
|
|
3047
|
-
const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
|
|
3048
|
-
const min = Math.min(...tierPrices);
|
|
3049
|
-
const max = Math.max(...tierPrices);
|
|
3050
|
-
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
3051
|
-
if (min === max) {
|
|
3052
|
-
return {
|
|
3053
|
-
price: { mode: "fixed", currency: "USD", amount: String(min) }
|
|
3054
|
-
};
|
|
3055
|
-
}
|
|
3056
|
-
return {
|
|
3057
|
-
price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
|
|
3058
|
-
};
|
|
3059
|
-
}
|
|
4233
|
+
if (!isEvmAddress(recipient)) {
|
|
3060
4234
|
return {
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
min: "0",
|
|
3065
|
-
max: entry.maxPrice ?? "0"
|
|
3066
|
-
}
|
|
4235
|
+
code: "invalid_mpp_recipient",
|
|
4236
|
+
protocol: "mpp",
|
|
4237
|
+
message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
3067
4238
|
};
|
|
3068
4239
|
}
|
|
3069
|
-
return
|
|
4240
|
+
return null;
|
|
3070
4241
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
4242
|
+
function checkPlaceholderRecipient({ config, mpp }) {
|
|
4243
|
+
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
4244
|
+
if (!placeholder) return null;
|
|
4245
|
+
return {
|
|
4246
|
+
code: "placeholder_payee",
|
|
4247
|
+
protocol: "mpp",
|
|
4248
|
+
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4249
|
+
};
|
|
4250
|
+
}
|
|
4251
|
+
function checkRpcUrl({ mpp, env }) {
|
|
4252
|
+
if (mpp.rpcUrl ?? env.TEMPO_RPC_URL) return null;
|
|
4253
|
+
return {
|
|
4254
|
+
code: "missing_mpp_rpc_url",
|
|
4255
|
+
protocol: "mpp",
|
|
4256
|
+
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
function checkFeePayerKey({ mpp }) {
|
|
4260
|
+
if (!mpp.feePayerKey || isEvmPrivateKey(mpp.feePayerKey)) return null;
|
|
4261
|
+
return {
|
|
4262
|
+
code: "invalid_mpp_fee_payer_key",
|
|
4263
|
+
protocol: "mpp",
|
|
4264
|
+
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4265
|
+
};
|
|
4266
|
+
}
|
|
4267
|
+
function checkOperatorKey({ mpp }) {
|
|
4268
|
+
if (!mpp.operatorKey || isEvmPrivateKey(mpp.operatorKey)) return null;
|
|
4269
|
+
return {
|
|
4270
|
+
code: "invalid_mpp_operator_key",
|
|
4271
|
+
protocol: "mpp",
|
|
4272
|
+
message: "MPP operatorKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4273
|
+
};
|
|
4274
|
+
}
|
|
4275
|
+
function checkOperatorMatchesFeePayer({ mpp }) {
|
|
4276
|
+
if (!mpp.operatorKey || !mpp.feePayerKey) return null;
|
|
4277
|
+
if (!isEvmPrivateKey(mpp.operatorKey) || !isEvmPrivateKey(mpp.feePayerKey)) return null;
|
|
4278
|
+
const opAddr = (0, import_accounts.privateKeyToAccount)(mpp.operatorKey).address.toLowerCase();
|
|
4279
|
+
const fpAddr = (0, import_accounts.privateKeyToAccount)(mpp.feePayerKey).address.toLowerCase();
|
|
4280
|
+
if (opAddr !== fpAddr) return null;
|
|
4281
|
+
return {
|
|
4282
|
+
code: "mpp_operator_equals_fee_payer",
|
|
4283
|
+
protocol: "mpp",
|
|
4284
|
+
message: `MPP operatorKey and feePayerKey resolve to the same address (${opAddr}). Tempo rejects fee-delegated txs with sender === feePayer, so channel close/settle would fail at runtime. Either use two distinct wallets, or omit feePayerKey to disable gas sponsorship (clients then pay their own gas).`
|
|
3083
4285
|
};
|
|
3084
4286
|
}
|
|
3085
4287
|
|
|
3086
|
-
// src/
|
|
3087
|
-
init_accepts();
|
|
3088
|
-
init_constants();
|
|
3089
|
-
|
|
3090
|
-
// src/config.ts
|
|
3091
|
-
init_constants();
|
|
3092
|
-
init_evm();
|
|
3093
|
-
init_solana();
|
|
3094
|
-
init_accepts();
|
|
3095
|
-
var RouterConfigError = class extends Error {
|
|
3096
|
-
issues;
|
|
3097
|
-
constructor(issues) {
|
|
3098
|
-
super(formatRouterConfigIssues(issues));
|
|
3099
|
-
this.name = "RouterConfigError";
|
|
3100
|
-
this.issues = issues;
|
|
3101
|
-
}
|
|
3102
|
-
};
|
|
4288
|
+
// src/config/validate.ts
|
|
3103
4289
|
function validateRouterConfig(config, options = {}) {
|
|
3104
4290
|
const issues = getRouterConfigIssues(config, options);
|
|
3105
4291
|
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
@@ -3128,9 +4314,9 @@ function getRouterConfigIssues(config, options = {}) {
|
|
|
3128
4314
|
}
|
|
3129
4315
|
return issues;
|
|
3130
4316
|
}
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
4317
|
+
|
|
4318
|
+
// src/config/env.ts
|
|
4319
|
+
init_constants();
|
|
3134
4320
|
function mppFromEnv(env, options = {}) {
|
|
3135
4321
|
const secretKey = env.MPP_SECRET_KEY;
|
|
3136
4322
|
const currency = env.MPP_CURRENCY;
|
|
@@ -3161,8 +4347,7 @@ function mppFromEnv(env, options = {}) {
|
|
|
3161
4347
|
currency,
|
|
3162
4348
|
rpcUrl,
|
|
3163
4349
|
...options.recipient ? { recipient: options.recipient } : {},
|
|
3164
|
-
...feePayerKey ? { feePayerKey } : {}
|
|
3165
|
-
...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
|
|
4350
|
+
...feePayerKey ? { feePayerKey } : {}
|
|
3166
4351
|
};
|
|
3167
4352
|
}
|
|
3168
4353
|
function x402AcceptsFromEnv(env, options = {}) {
|
|
@@ -3192,189 +4377,145 @@ function x402AcceptsFromEnv(env, options = {}) {
|
|
|
3192
4377
|
function paidOptionsForProtocols(protocols) {
|
|
3193
4378
|
return { protocols: [...protocols] };
|
|
3194
4379
|
}
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
if (
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
code: "missing_x402_network",
|
|
3210
|
-
protocol: "x402",
|
|
3211
|
-
message: "x402 accepts require a network."
|
|
3212
|
-
});
|
|
3213
|
-
}
|
|
3214
|
-
const unsupported = accepts.find(
|
|
3215
|
-
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
3216
|
-
);
|
|
3217
|
-
if (unsupported) {
|
|
3218
|
-
issues.push({
|
|
3219
|
-
code: "unsupported_x402_network",
|
|
3220
|
-
protocol: "x402",
|
|
3221
|
-
message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
3222
|
-
});
|
|
3223
|
-
}
|
|
3224
|
-
const missingAsset = accepts.find(
|
|
3225
|
-
(accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
|
|
3226
|
-
);
|
|
3227
|
-
if (missingAsset) {
|
|
3228
|
-
issues.push({
|
|
3229
|
-
code: "missing_x402_asset",
|
|
3230
|
-
protocol: "x402",
|
|
3231
|
-
message: "non-exact x402 accepts require an asset."
|
|
3232
|
-
});
|
|
3233
|
-
}
|
|
3234
|
-
const invalidDecimals = accepts.find(
|
|
3235
|
-
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
3236
|
-
);
|
|
3237
|
-
if (invalidDecimals) {
|
|
3238
|
-
issues.push({
|
|
3239
|
-
code: "invalid_x402_decimals",
|
|
3240
|
-
protocol: "x402",
|
|
3241
|
-
message: "x402 accept decimals must be a non-negative integer."
|
|
3242
|
-
});
|
|
3243
|
-
}
|
|
3244
|
-
if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
3245
|
-
issues.push({
|
|
3246
|
-
code: "missing_x402_payee",
|
|
3247
|
-
protocol: "x402",
|
|
3248
|
-
message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
3249
|
-
});
|
|
3250
|
-
}
|
|
3251
|
-
const placeholder = findPlaceholderPayee([
|
|
3252
|
-
config.payeeAddress,
|
|
3253
|
-
...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
|
|
3254
|
-
]);
|
|
3255
|
-
if (placeholder) {
|
|
3256
|
-
issues.push({
|
|
3257
|
-
code: "placeholder_payee",
|
|
3258
|
-
protocol: "x402",
|
|
3259
|
-
message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
3260
|
-
});
|
|
4380
|
+
|
|
4381
|
+
// src/init/x402.ts
|
|
4382
|
+
async function initX402(config, configError) {
|
|
4383
|
+
if (configError) return { initError: configError };
|
|
4384
|
+
try {
|
|
4385
|
+
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
4386
|
+
const result = await createX402Server2(config);
|
|
4387
|
+
await result.initPromise;
|
|
4388
|
+
return {
|
|
4389
|
+
server: result.server,
|
|
4390
|
+
facilitatorsByNetwork: result.facilitatorsByNetwork
|
|
4391
|
+
};
|
|
4392
|
+
} catch (err) {
|
|
4393
|
+
return { initError: err instanceof Error ? err.message : String(err) };
|
|
3261
4394
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
// src/mppx-init.ts
|
|
4398
|
+
function getMppxRequestContext(args) {
|
|
4399
|
+
const {
|
|
4400
|
+
Mppx,
|
|
4401
|
+
tempo,
|
|
4402
|
+
mppConfig,
|
|
4403
|
+
payeeAddress,
|
|
4404
|
+
getClient,
|
|
4405
|
+
feePayerAccount,
|
|
4406
|
+
resolvedStore,
|
|
4407
|
+
sessionEnabled,
|
|
4408
|
+
sharedSessionParams,
|
|
4409
|
+
realm
|
|
4410
|
+
} = args;
|
|
4411
|
+
const instance = Mppx.create({
|
|
4412
|
+
methods: [
|
|
4413
|
+
tempo.charge({
|
|
4414
|
+
currency: mppConfig.currency,
|
|
4415
|
+
recipient: mppConfig.recipient ?? payeeAddress,
|
|
4416
|
+
getClient,
|
|
4417
|
+
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
4418
|
+
...resolvedStore ? { store: resolvedStore } : {}
|
|
4419
|
+
}),
|
|
4420
|
+
...sessionEnabled ? [
|
|
4421
|
+
tempo.session({
|
|
4422
|
+
...sharedSessionParams,
|
|
4423
|
+
sse: false
|
|
4424
|
+
})
|
|
4425
|
+
] : []
|
|
4426
|
+
],
|
|
4427
|
+
secretKey: mppConfig.secretKey,
|
|
4428
|
+
realm
|
|
4429
|
+
});
|
|
4430
|
+
return instance;
|
|
4431
|
+
}
|
|
4432
|
+
function getMppxStreamingContext(args) {
|
|
4433
|
+
if (!args.sessionEnabled) return null;
|
|
4434
|
+
const { Mppx, tempo, mppConfig, sharedSessionParams, realm } = args;
|
|
4435
|
+
const instance = Mppx.create({
|
|
4436
|
+
methods: [
|
|
4437
|
+
tempo.session({
|
|
4438
|
+
...sharedSessionParams,
|
|
4439
|
+
sse: true
|
|
4440
|
+
})
|
|
4441
|
+
],
|
|
4442
|
+
secretKey: mppConfig.secretKey,
|
|
4443
|
+
realm
|
|
4444
|
+
});
|
|
4445
|
+
return instance;
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
// src/init/mpp.ts
|
|
4449
|
+
async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
|
|
4450
|
+
if (configError) return { initError: configError };
|
|
4451
|
+
if (!config.mpp) return {};
|
|
4452
|
+
try {
|
|
4453
|
+
const { Mppx, tempo } = await import("mppx/server");
|
|
4454
|
+
const { createClient, http } = await import("viem");
|
|
4455
|
+
const { tempo: tempoChain } = await import("viem/chains");
|
|
4456
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
4457
|
+
const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
4458
|
+
const tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
|
|
4459
|
+
const getClient = async () => tempoClient;
|
|
4460
|
+
const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
|
|
4461
|
+
const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
|
|
4462
|
+
if (config.mpp.session && operatorAccount) {
|
|
4463
|
+
assertOperatorMatchesRecipient(config, operatorAccount.address);
|
|
3273
4464
|
}
|
|
4465
|
+
const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
|
|
4466
|
+
const realm = new URL(resolvedBaseUrl).host;
|
|
4467
|
+
const mppConfig = config.mpp;
|
|
4468
|
+
const sessionEnabled = !!(mppConfig.session && operatorAccount);
|
|
4469
|
+
const sharedSessionParams = {
|
|
4470
|
+
currency: mppConfig.currency,
|
|
4471
|
+
decimals: 6,
|
|
4472
|
+
recipient: mppConfig.recipient ?? config.payeeAddress,
|
|
4473
|
+
getClient,
|
|
4474
|
+
...operatorAccount ? { account: operatorAccount } : {},
|
|
4475
|
+
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
4476
|
+
...resolvedStore ? { store: resolvedStore } : {}
|
|
4477
|
+
};
|
|
4478
|
+
const mppxArgs = {
|
|
4479
|
+
Mppx,
|
|
4480
|
+
tempo,
|
|
4481
|
+
mppConfig,
|
|
4482
|
+
payeeAddress: config.payeeAddress ?? "",
|
|
4483
|
+
getClient,
|
|
4484
|
+
feePayerAccount,
|
|
4485
|
+
resolvedStore,
|
|
4486
|
+
sessionEnabled,
|
|
4487
|
+
sharedSessionParams,
|
|
4488
|
+
realm
|
|
4489
|
+
};
|
|
4490
|
+
const primary = getMppxRequestContext(mppxArgs);
|
|
4491
|
+
const streaming = getMppxStreamingContext(mppxArgs);
|
|
4492
|
+
const mppx = {
|
|
4493
|
+
charge: primary.charge,
|
|
4494
|
+
...primary.session ? { sessionRequest: primary.session } : {},
|
|
4495
|
+
...streaming?.session ? { sessionStream: streaming.session } : {}
|
|
4496
|
+
};
|
|
4497
|
+
return { mppx, tempoClient };
|
|
4498
|
+
} catch (err) {
|
|
4499
|
+
return { initError: err instanceof Error ? err.message : String(err) };
|
|
3274
4500
|
}
|
|
3275
|
-
return issues;
|
|
3276
4501
|
}
|
|
3277
|
-
function
|
|
3278
|
-
const
|
|
3279
|
-
const
|
|
3280
|
-
if (
|
|
3281
|
-
|
|
3282
|
-
{
|
|
3283
|
-
|
|
3284
|
-
protocol: "mpp",
|
|
3285
|
-
message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
3286
|
-
}
|
|
3287
|
-
];
|
|
3288
|
-
}
|
|
3289
|
-
if (!mpp.secretKey) {
|
|
3290
|
-
issues.push({
|
|
3291
|
-
code: "missing_mpp_secret_key",
|
|
3292
|
-
protocol: "mpp",
|
|
3293
|
-
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
3294
|
-
});
|
|
3295
|
-
}
|
|
3296
|
-
if (!mpp.currency) {
|
|
3297
|
-
issues.push({
|
|
3298
|
-
code: "missing_mpp_currency",
|
|
3299
|
-
protocol: "mpp",
|
|
3300
|
-
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
3301
|
-
});
|
|
3302
|
-
} else if (!isEvmAddress(mpp.currency)) {
|
|
3303
|
-
issues.push({
|
|
3304
|
-
code: "invalid_mpp_currency",
|
|
3305
|
-
protocol: "mpp",
|
|
3306
|
-
message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
|
|
3307
|
-
});
|
|
3308
|
-
}
|
|
3309
|
-
const mppRecipient = mpp.recipient ?? config.payeeAddress;
|
|
3310
|
-
if (!mppRecipient) {
|
|
3311
|
-
issues.push({
|
|
3312
|
-
code: "missing_mpp_recipient",
|
|
3313
|
-
protocol: "mpp",
|
|
3314
|
-
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
3315
|
-
});
|
|
3316
|
-
} else if (!isEvmAddress(mppRecipient)) {
|
|
3317
|
-
issues.push({
|
|
3318
|
-
code: "invalid_mpp_recipient",
|
|
3319
|
-
protocol: "mpp",
|
|
3320
|
-
message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
3321
|
-
});
|
|
3322
|
-
}
|
|
3323
|
-
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
3324
|
-
if (placeholder) {
|
|
3325
|
-
issues.push({
|
|
3326
|
-
code: "placeholder_payee",
|
|
3327
|
-
protocol: "mpp",
|
|
3328
|
-
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
3329
|
-
});
|
|
3330
|
-
}
|
|
3331
|
-
if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
|
|
3332
|
-
issues.push({
|
|
3333
|
-
code: "missing_mpp_rpc_url",
|
|
3334
|
-
protocol: "mpp",
|
|
3335
|
-
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
3336
|
-
});
|
|
3337
|
-
}
|
|
3338
|
-
if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
|
|
3339
|
-
issues.push({
|
|
3340
|
-
code: "invalid_mpp_fee_payer_key",
|
|
3341
|
-
protocol: "mpp",
|
|
3342
|
-
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
3343
|
-
});
|
|
3344
|
-
}
|
|
3345
|
-
if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
|
|
3346
|
-
issues.push({
|
|
3347
|
-
code: "missing_mpp_default_store_env",
|
|
3348
|
-
protocol: "mpp",
|
|
3349
|
-
message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
3350
|
-
});
|
|
4502
|
+
function assertOperatorMatchesRecipient(config, operatorAddress) {
|
|
4503
|
+
const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
|
|
4504
|
+
const opAddr = operatorAddress.toLowerCase();
|
|
4505
|
+
if (recipient && opAddr !== recipient) {
|
|
4506
|
+
throw new Error(
|
|
4507
|
+
`MPP session config mismatch: operator address ${operatorAddress} must equal recipient/payee ${recipient}. mppx's channel-close handler asserts sender === payee. Set mpp.operatorKey to the private key for ${recipient}, or set mpp.recipient/payeeAddress to ${operatorAddress}.`
|
|
4508
|
+
);
|
|
3351
4509
|
}
|
|
3352
|
-
return issues;
|
|
3353
|
-
}
|
|
3354
|
-
function usesDefaultEvmFacilitator(config) {
|
|
3355
|
-
return getConfiguredX402Networks(config).some(
|
|
3356
|
-
(network) => typeof network === "string" && isEvmNetwork(network)
|
|
3357
|
-
) && config.x402?.facilitators?.evm === void 0;
|
|
3358
|
-
}
|
|
3359
|
-
function isSupportedX402Network(network) {
|
|
3360
|
-
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
3361
|
-
}
|
|
3362
|
-
function isEvmAddress(value) {
|
|
3363
|
-
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
|
3364
|
-
}
|
|
3365
|
-
function isEvmPrivateKey(value) {
|
|
3366
|
-
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
3367
|
-
}
|
|
3368
|
-
function findPlaceholderPayee(values) {
|
|
3369
|
-
return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
|
|
3370
4510
|
}
|
|
3371
4511
|
|
|
3372
4512
|
// src/index.ts
|
|
3373
4513
|
init_constants();
|
|
3374
4514
|
function createRouter(config) {
|
|
3375
4515
|
const registry = new RouteRegistry();
|
|
3376
|
-
const
|
|
3377
|
-
const
|
|
4516
|
+
const kvStore = resolveKvStore(config.kvStore);
|
|
4517
|
+
const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
|
|
4518
|
+
const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
|
|
3378
4519
|
const network = config.network ?? BASE_NETWORK;
|
|
3379
4520
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
3380
4521
|
const configIssues = getRouterConfigIssues(config, {
|
|
@@ -3420,69 +4561,20 @@ function createRouter(config) {
|
|
|
3420
4561
|
x402FacilitatorsByNetwork: void 0,
|
|
3421
4562
|
x402Accepts,
|
|
3422
4563
|
mppx: null,
|
|
3423
|
-
tempoClient: null
|
|
4564
|
+
tempoClient: null,
|
|
4565
|
+
mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
|
|
3424
4566
|
};
|
|
3425
4567
|
deps.initPromise = (async () => {
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
deps.x402Server = null;
|
|
3437
|
-
deps.x402InitError = err instanceof Error ? err.message : String(err);
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
|
-
if (mppConfigError) {
|
|
3441
|
-
deps.mppInitError = mppConfigError;
|
|
3442
|
-
} else if (config.mpp) {
|
|
3443
|
-
try {
|
|
3444
|
-
const { Mppx, tempo } = await import("mppx/server");
|
|
3445
|
-
const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
3446
|
-
const { createClient, http } = await import("viem");
|
|
3447
|
-
const { tempo: tempoChain } = await import("viem/chains");
|
|
3448
|
-
deps.tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
|
|
3449
|
-
const getClient = async () => deps.tempoClient;
|
|
3450
|
-
let feePayerAccount;
|
|
3451
|
-
if (config.mpp.feePayerKey) {
|
|
3452
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
3453
|
-
feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
|
|
3454
|
-
}
|
|
3455
|
-
let resolvedStore = config.mpp.store;
|
|
3456
|
-
if (!resolvedStore && config.mpp.useDefaultStore) {
|
|
3457
|
-
const kvUrl = process.env.KV_REST_API_URL;
|
|
3458
|
-
const kvToken = process.env.KV_REST_API_TOKEN;
|
|
3459
|
-
if (!kvUrl || !kvToken) {
|
|
3460
|
-
throw new Error(
|
|
3461
|
-
"mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
3462
|
-
);
|
|
3463
|
-
}
|
|
3464
|
-
const { Store } = await import("mppx");
|
|
3465
|
-
const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
|
|
3466
|
-
resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
|
|
3467
|
-
}
|
|
3468
|
-
deps.mppx = Mppx.create({
|
|
3469
|
-
methods: [
|
|
3470
|
-
tempo.charge({
|
|
3471
|
-
currency: config.mpp.currency,
|
|
3472
|
-
recipient: config.mpp.recipient ?? config.payeeAddress,
|
|
3473
|
-
getClient,
|
|
3474
|
-
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
3475
|
-
...resolvedStore ? { store: resolvedStore } : {}
|
|
3476
|
-
})
|
|
3477
|
-
],
|
|
3478
|
-
secretKey: config.mpp.secretKey,
|
|
3479
|
-
realm: new URL(resolvedBaseUrl).host
|
|
3480
|
-
});
|
|
3481
|
-
} catch (err) {
|
|
3482
|
-
deps.mppx = null;
|
|
3483
|
-
deps.mppInitError = err instanceof Error ? err.message : String(err);
|
|
3484
|
-
console.error(`[router] MPP initialization failed: ${deps.mppInitError}`);
|
|
3485
|
-
}
|
|
4568
|
+
const x402Result = await initX402(config, x402ConfigError);
|
|
4569
|
+
deps.x402Server = x402Result.server ?? null;
|
|
4570
|
+
deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
|
|
4571
|
+
if (x402Result.initError) deps.x402InitError = x402Result.initError;
|
|
4572
|
+
const mppResult = await initMpp(config, resolvedBaseUrl, kvStore, mppConfigError);
|
|
4573
|
+
deps.mppx = mppResult.mppx ?? null;
|
|
4574
|
+
deps.tempoClient = mppResult.tempoClient ?? null;
|
|
4575
|
+
if (mppResult.initError) {
|
|
4576
|
+
deps.mppInitError = mppResult.initError;
|
|
4577
|
+
console.error(`[router] MPP initialization failed: ${mppResult.initError}`);
|
|
3486
4578
|
}
|
|
3487
4579
|
})();
|
|
3488
4580
|
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
|
@@ -3564,13 +4656,15 @@ function normalizePath(path) {
|
|
|
3564
4656
|
TEMPO_USDC_CURRENCY,
|
|
3565
4657
|
ZERO_EVM_ADDRESS,
|
|
3566
4658
|
consolePlugin,
|
|
3567
|
-
|
|
3568
|
-
|
|
4659
|
+
createKvEntitlementStore,
|
|
4660
|
+
createKvMppStore,
|
|
4661
|
+
createKvNonceStore,
|
|
3569
4662
|
createRouter,
|
|
3570
4663
|
formatRouterConfigIssues,
|
|
3571
4664
|
getRouterConfigIssues,
|
|
3572
4665
|
mppFromEnv,
|
|
3573
4666
|
paidOptionsForProtocols,
|
|
3574
4667
|
validateRouterConfig,
|
|
4668
|
+
withPrefix,
|
|
3575
4669
|
x402AcceptsFromEnv
|
|
3576
4670
|
});
|