@agentcash/router 1.10.0 → 1.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/index.cjs +45 -8
- package/dist/index.js +45 -8
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
<a href="#install"><img alt="next.js" src="https://img.shields.io/badge/Next.js-App%20Router-111"></a>
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FMerit-Systems%2Fagentcash-router%2Ftree%2Fmain%2Fexamples%2Fvercel-deploy&project-name=agentcash-fortune-demo&repository-name=agentcash-fortune-demo&demo-title=AgentCash%20Router%20Fortune%20Demo&demo-description=Pay-per-call%20fortune%20API%20on%20x402%20and%20MPP&demo-url=https%3A%2F%2Fagentcash.dev&env=EVM_PAYEE_ADDRESS%2CCDP_API_KEY_ID%2CCDP_API_KEY_SECRET&envDescription=Wallet%20that%20receives%20payments%20%2B%20Coinbase%20Developer%20Platform%20API%20keys%20for%20the%20default%20x402%20facilitator.%20Create%20keys%20in%20the%20generous%20CDP%20free%20tier%3A%20https%3A%2F%2Fportal.cdp.coinbase.com%2Fprojects%2Fapi-keys&envLink=https%3A%2F%2Fgithub.com%2FMerit-Systems%2Fagentcash-router%2Fblob%2Fmain%2Fexamples%2Fvercel-deploy%2FREADME.md%23environment-variables">
|
|
23
|
+
<img alt="Deploy with Vercel" src="https://vercel.com/button">
|
|
24
|
+
</a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
21
27
|
---
|
|
22
28
|
|
|
23
29
|
## Install
|
|
@@ -36,7 +42,7 @@ The recommended entry point reads its config from `process.env`. A copy-paste `.
|
|
|
36
42
|
| Var | Required | Purpose |
|
|
37
43
|
|-----|----------|---------|
|
|
38
44
|
| `EVM_PAYEE_ADDRESS` | yes | EVM address that receives x402 and MPP payments (`0x…`, 20 bytes). Canonicalized to lowercase. The zero address is rejected. |
|
|
39
|
-
| `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET` | yes (EVM) | Coinbase Developer Platform credentials for the default EVM facilitator. Create
|
|
45
|
+
| `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET` | yes (EVM) | Coinbase Developer Platform credentials for the default EVM facilitator. Create API keys in the generous CDP free tier at https://portal.cdp.coinbase.com/projects/api-keys. T3 / `@t3-oss/env-nextjs` users must declare these in their env schema. |
|
|
40
46
|
|
|
41
47
|
### Solana
|
|
42
48
|
|
|
@@ -59,7 +65,8 @@ The recommended entry point reads its config from `process.env`. A copy-paste `.
|
|
|
59
65
|
|
|
60
66
|
| Var | Required | Purpose |
|
|
61
67
|
|-----|----------|---------|
|
|
62
|
-
| `BASE_URL` | yes | Origin URL (`https://api.example.com`). Load-bearing: used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain. |
|
|
68
|
+
| `BASE_URL` | yes outside Vercel | Origin URL (`https://api.example.com`). Load-bearing: used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain. On Vercel, `createRouterFromEnv` auto-derives this from `VERCEL_PROJECT_PRODUCTION_URL`, then `VERCEL_URL`. |
|
|
69
|
+
| `VERCEL_PROJECT_PRODUCTION_URL`, `VERCEL_URL` | no | Vercel system env vars used as fallbacks when `BASE_URL` is unset. Do not set these manually; Vercel provides them during builds and runtime. |
|
|
63
70
|
| `KV_REST_API_URL`, `KV_REST_API_TOKEN` | no | Upstash / Vercel KV. Backs SIWX nonce, SIWX entitlement, and MPP replay. In-memory fallback is unsafe in serverless production. Providing a Kv Store is highly recommended. |
|
|
64
71
|
|
|
65
72
|
## Quick start
|
|
@@ -306,4 +313,3 @@ const loggingPlugin: RouterPlugin = {
|
|
|
306
313
|
},
|
|
307
314
|
};
|
|
308
315
|
```
|
|
309
|
-
|
package/dist/index.cjs
CHANGED
|
@@ -412,7 +412,7 @@ async function createX402Server(config, kvStore) {
|
|
|
412
412
|
const { x402ResourceServer, HTTPFacilitatorClient } = await import("@x402/core/server");
|
|
413
413
|
const { registerExactEvmScheme } = await import("@x402/evm/exact/server");
|
|
414
414
|
const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
|
|
415
|
-
const {
|
|
415
|
+
const { createSIWxResourceServerExtension, InMemorySIWxStorage } = await import("@x402/extensions/sign-in-with-x");
|
|
416
416
|
const { facilitator: defaultFacilitator } = await import("@coinbase/x402");
|
|
417
417
|
const configuredNetworks = getConfiguredX402Networks(config);
|
|
418
418
|
const facilitatorsByNetwork = getResolvedX402Facilitators(
|
|
@@ -442,7 +442,9 @@ async function createX402Server(config, kvStore) {
|
|
|
442
442
|
registerExactSvmScheme(server, { networks: svmNetworks });
|
|
443
443
|
}
|
|
444
444
|
server.registerExtension(bazaarResourceServerExtension);
|
|
445
|
-
server.registerExtension(
|
|
445
|
+
server.registerExtension(
|
|
446
|
+
createSIWxResourceServerExtension({ storage: new InMemorySIWxStorage() })
|
|
447
|
+
);
|
|
446
448
|
const initPromise = server.initialize();
|
|
447
449
|
return {
|
|
448
450
|
server,
|
|
@@ -1938,8 +1940,15 @@ function tagBareDecimalAsDollars(amount) {
|
|
|
1938
1940
|
var import_types2 = require("@x402/core/types");
|
|
1939
1941
|
async function verifyX402Payment(opts) {
|
|
1940
1942
|
const { server, request, price, accepts, report } = opts;
|
|
1941
|
-
const
|
|
1942
|
-
if (
|
|
1943
|
+
const payment = await readPaymentPayload(request);
|
|
1944
|
+
if (payment.kind === "none") return null;
|
|
1945
|
+
if (payment.kind === "malformed") {
|
|
1946
|
+
return invalidPaymentVerification({
|
|
1947
|
+
reason: "malformed_payment_header",
|
|
1948
|
+
message: `X-PAYMENT header could not be decoded: ${payment.message}`
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
const payload = payment.payload;
|
|
1943
1952
|
const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
|
|
1944
1953
|
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
1945
1954
|
const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
|
|
@@ -2000,9 +2009,16 @@ function matchesStableFields(requirement, accepted) {
|
|
|
2000
2009
|
}
|
|
2001
2010
|
async function readPaymentPayload(request) {
|
|
2002
2011
|
const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
|
|
2003
|
-
if (!paymentHeader) return
|
|
2012
|
+
if (!paymentHeader) return { kind: "none" };
|
|
2004
2013
|
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
2005
|
-
|
|
2014
|
+
try {
|
|
2015
|
+
return { kind: "ok", payload: decodePaymentSignatureHeader(paymentHeader) };
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
return {
|
|
2018
|
+
kind: "malformed",
|
|
2019
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2006
2022
|
}
|
|
2007
2023
|
function invalidPaymentVerification(failure) {
|
|
2008
2024
|
return {
|
|
@@ -3445,6 +3461,7 @@ ${issues}`
|
|
|
3445
3461
|
}
|
|
3446
3462
|
|
|
3447
3463
|
// src/builder.ts
|
|
3464
|
+
var MAX_X402_DESCRIPTION_LENGTH = 400;
|
|
3448
3465
|
var RouteBuilder = class _RouteBuilder {
|
|
3449
3466
|
#s;
|
|
3450
3467
|
constructor(key, registry, deps, defaults) {
|
|
@@ -3961,6 +3978,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3961
3978
|
`route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
|
|
3962
3979
|
);
|
|
3963
3980
|
}
|
|
3981
|
+
if (this.#s.description !== void 0 && this.#s.description.length > MAX_X402_DESCRIPTION_LENGTH && this.#s.pricing !== void 0 && this.#s.protocols.includes("x402")) {
|
|
3982
|
+
throw new Error(
|
|
3983
|
+
`route '${this.#s.key}': .description() is ${this.#s.description.length} chars; must be \u2264 ${MAX_X402_DESCRIPTION_LENGTH} chars \u2014 the CDP x402 facilitator rejects payments whose 402 challenge resource.description exceeds ~500 chars.`
|
|
3984
|
+
);
|
|
3985
|
+
}
|
|
3964
3986
|
validateExamples(
|
|
3965
3987
|
this.#s.key,
|
|
3966
3988
|
this.#s.bodySchema,
|
|
@@ -4416,6 +4438,8 @@ var envShape = {
|
|
|
4416
4438
|
params: { code: "invalid_base_url" },
|
|
4417
4439
|
message: "BASE_URL must be a valid URL \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain."
|
|
4418
4440
|
}).optional(),
|
|
4441
|
+
VERCEL_PROJECT_PRODUCTION_URL: import_zod.z.string().optional(),
|
|
4442
|
+
VERCEL_URL: import_zod.z.string().optional(),
|
|
4419
4443
|
EVM_PAYEE_ADDRESS: import_zod.z.string().refine(isEvmAddress, {
|
|
4420
4444
|
params: { code: "invalid_x402_payee", ...x402 },
|
|
4421
4445
|
message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
|
|
@@ -4460,7 +4484,7 @@ var EnvInputSchema = import_zod.z.object(envShape).passthrough().superRefine((en
|
|
|
4460
4484
|
addIssue(
|
|
4461
4485
|
ctx,
|
|
4462
4486
|
{ code: "missing_base_url" },
|
|
4463
|
-
"BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain.",
|
|
4487
|
+
"BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain. On Vercel, the router auto-derives it from VERCEL_PROJECT_PRODUCTION_URL or VERCEL_URL.",
|
|
4464
4488
|
["BASE_URL"]
|
|
4465
4489
|
);
|
|
4466
4490
|
}
|
|
@@ -4541,6 +4565,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
|
|
|
4541
4565
|
}
|
|
4542
4566
|
return [];
|
|
4543
4567
|
}
|
|
4568
|
+
function withHttps(host) {
|
|
4569
|
+
if (!host) return void 0;
|
|
4570
|
+
return host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
|
|
4571
|
+
}
|
|
4572
|
+
function deriveBaseUrlEnv(env) {
|
|
4573
|
+
if (env.BASE_URL) return env;
|
|
4574
|
+
const vercelBaseUrl = withHttps(env.VERCEL_PROJECT_PRODUCTION_URL) ?? withHttps(env.VERCEL_URL);
|
|
4575
|
+
if (!vercelBaseUrl) return env;
|
|
4576
|
+
return {
|
|
4577
|
+
...env,
|
|
4578
|
+
BASE_URL: vercelBaseUrl
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4544
4581
|
function getConfiguredX402Accepts2(config) {
|
|
4545
4582
|
if (config.x402?.accepts?.length) return [...config.x402.accepts];
|
|
4546
4583
|
return [
|
|
@@ -4709,7 +4746,7 @@ function translateZodIssues(error) {
|
|
|
4709
4746
|
}
|
|
4710
4747
|
function routerConfigFromEnv(options) {
|
|
4711
4748
|
const rawEnv = options.env ?? process.env;
|
|
4712
|
-
const env = trimAll(rawEnv);
|
|
4749
|
+
const env = deriveBaseUrlEnv(trimAll(rawEnv));
|
|
4713
4750
|
const optionIssues = [];
|
|
4714
4751
|
if (!options.title?.trim()) {
|
|
4715
4752
|
optionIssues.push({
|
package/dist/index.js
CHANGED
|
@@ -390,7 +390,7 @@ async function createX402Server(config, kvStore) {
|
|
|
390
390
|
const { x402ResourceServer, HTTPFacilitatorClient } = await import("@x402/core/server");
|
|
391
391
|
const { registerExactEvmScheme } = await import("@x402/evm/exact/server");
|
|
392
392
|
const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
|
|
393
|
-
const {
|
|
393
|
+
const { createSIWxResourceServerExtension, InMemorySIWxStorage } = await import("@x402/extensions/sign-in-with-x");
|
|
394
394
|
const { facilitator: defaultFacilitator } = await import("@coinbase/x402");
|
|
395
395
|
const configuredNetworks = getConfiguredX402Networks(config);
|
|
396
396
|
const facilitatorsByNetwork = getResolvedX402Facilitators(
|
|
@@ -420,7 +420,9 @@ async function createX402Server(config, kvStore) {
|
|
|
420
420
|
registerExactSvmScheme(server, { networks: svmNetworks });
|
|
421
421
|
}
|
|
422
422
|
server.registerExtension(bazaarResourceServerExtension);
|
|
423
|
-
server.registerExtension(
|
|
423
|
+
server.registerExtension(
|
|
424
|
+
createSIWxResourceServerExtension({ storage: new InMemorySIWxStorage() })
|
|
425
|
+
);
|
|
424
426
|
const initPromise = server.initialize();
|
|
425
427
|
return {
|
|
426
428
|
server,
|
|
@@ -1897,8 +1899,15 @@ function tagBareDecimalAsDollars(amount) {
|
|
|
1897
1899
|
import { VerifyError } from "@x402/core/types";
|
|
1898
1900
|
async function verifyX402Payment(opts) {
|
|
1899
1901
|
const { server, request, price, accepts, report } = opts;
|
|
1900
|
-
const
|
|
1901
|
-
if (
|
|
1902
|
+
const payment = await readPaymentPayload(request);
|
|
1903
|
+
if (payment.kind === "none") return null;
|
|
1904
|
+
if (payment.kind === "malformed") {
|
|
1905
|
+
return invalidPaymentVerification({
|
|
1906
|
+
reason: "malformed_payment_header",
|
|
1907
|
+
message: `X-PAYMENT header could not be decoded: ${payment.message}`
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
const payload = payment.payload;
|
|
1902
1911
|
const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
|
|
1903
1912
|
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
1904
1913
|
const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
|
|
@@ -1959,9 +1968,16 @@ function matchesStableFields(requirement, accepted) {
|
|
|
1959
1968
|
}
|
|
1960
1969
|
async function readPaymentPayload(request) {
|
|
1961
1970
|
const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
|
|
1962
|
-
if (!paymentHeader) return
|
|
1971
|
+
if (!paymentHeader) return { kind: "none" };
|
|
1963
1972
|
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
1964
|
-
|
|
1973
|
+
try {
|
|
1974
|
+
return { kind: "ok", payload: decodePaymentSignatureHeader(paymentHeader) };
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
return {
|
|
1977
|
+
kind: "malformed",
|
|
1978
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1965
1981
|
}
|
|
1966
1982
|
function invalidPaymentVerification(failure) {
|
|
1967
1983
|
return {
|
|
@@ -3404,6 +3420,7 @@ ${issues}`
|
|
|
3404
3420
|
}
|
|
3405
3421
|
|
|
3406
3422
|
// src/builder.ts
|
|
3423
|
+
var MAX_X402_DESCRIPTION_LENGTH = 400;
|
|
3407
3424
|
var RouteBuilder = class _RouteBuilder {
|
|
3408
3425
|
#s;
|
|
3409
3426
|
constructor(key, registry, deps, defaults) {
|
|
@@ -3920,6 +3937,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3920
3937
|
`route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
|
|
3921
3938
|
);
|
|
3922
3939
|
}
|
|
3940
|
+
if (this.#s.description !== void 0 && this.#s.description.length > MAX_X402_DESCRIPTION_LENGTH && this.#s.pricing !== void 0 && this.#s.protocols.includes("x402")) {
|
|
3941
|
+
throw new Error(
|
|
3942
|
+
`route '${this.#s.key}': .description() is ${this.#s.description.length} chars; must be \u2264 ${MAX_X402_DESCRIPTION_LENGTH} chars \u2014 the CDP x402 facilitator rejects payments whose 402 challenge resource.description exceeds ~500 chars.`
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3923
3945
|
validateExamples(
|
|
3924
3946
|
this.#s.key,
|
|
3925
3947
|
this.#s.bodySchema,
|
|
@@ -4375,6 +4397,8 @@ var envShape = {
|
|
|
4375
4397
|
params: { code: "invalid_base_url" },
|
|
4376
4398
|
message: "BASE_URL must be a valid URL \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain."
|
|
4377
4399
|
}).optional(),
|
|
4400
|
+
VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(),
|
|
4401
|
+
VERCEL_URL: z.string().optional(),
|
|
4378
4402
|
EVM_PAYEE_ADDRESS: z.string().refine(isEvmAddress, {
|
|
4379
4403
|
params: { code: "invalid_x402_payee", ...x402 },
|
|
4380
4404
|
message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
|
|
@@ -4419,7 +4443,7 @@ var EnvInputSchema = z.object(envShape).passthrough().superRefine((env, ctx) =>
|
|
|
4419
4443
|
addIssue(
|
|
4420
4444
|
ctx,
|
|
4421
4445
|
{ code: "missing_base_url" },
|
|
4422
|
-
"BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain.",
|
|
4446
|
+
"BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain. On Vercel, the router auto-derives it from VERCEL_PROJECT_PRODUCTION_URL or VERCEL_URL.",
|
|
4423
4447
|
["BASE_URL"]
|
|
4424
4448
|
);
|
|
4425
4449
|
}
|
|
@@ -4500,6 +4524,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
|
|
|
4500
4524
|
}
|
|
4501
4525
|
return [];
|
|
4502
4526
|
}
|
|
4527
|
+
function withHttps(host) {
|
|
4528
|
+
if (!host) return void 0;
|
|
4529
|
+
return host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
|
|
4530
|
+
}
|
|
4531
|
+
function deriveBaseUrlEnv(env) {
|
|
4532
|
+
if (env.BASE_URL) return env;
|
|
4533
|
+
const vercelBaseUrl = withHttps(env.VERCEL_PROJECT_PRODUCTION_URL) ?? withHttps(env.VERCEL_URL);
|
|
4534
|
+
if (!vercelBaseUrl) return env;
|
|
4535
|
+
return {
|
|
4536
|
+
...env,
|
|
4537
|
+
BASE_URL: vercelBaseUrl
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4503
4540
|
function getConfiguredX402Accepts2(config) {
|
|
4504
4541
|
if (config.x402?.accepts?.length) return [...config.x402.accepts];
|
|
4505
4542
|
return [
|
|
@@ -4668,7 +4705,7 @@ function translateZodIssues(error) {
|
|
|
4668
4705
|
}
|
|
4669
4706
|
function routerConfigFromEnv(options) {
|
|
4670
4707
|
const rawEnv = options.env ?? process.env;
|
|
4671
|
-
const env = trimAll(rawEnv);
|
|
4708
|
+
const env = deriveBaseUrlEnv(trimAll(rawEnv));
|
|
4672
4709
|
const optionIssues = [];
|
|
4673
4710
|
if (!options.title?.trim()) {
|
|
4674
4711
|
optionIssues.push({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
4
4
|
"description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@coinbase/x402": "^2.1.0",
|
|
31
|
-
"@x402/core": "^2.
|
|
32
|
-
"@x402/evm": "^2.
|
|
33
|
-
"@x402/extensions": "^2.
|
|
34
|
-
"@x402/svm": "^2.
|
|
31
|
+
"@x402/core": "^2.13.0",
|
|
32
|
+
"@x402/evm": "^2.13.0",
|
|
33
|
+
"@x402/extensions": "^2.13.0",
|
|
34
|
+
"@x402/svm": "^2.13.0",
|
|
35
35
|
"mppx": "^0.6.16",
|
|
36
36
|
"viem": "^2.47.6",
|
|
37
37
|
"zod-openapi": "^5.0.0"
|