@agentcash/router 1.10.0 → 1.10.1

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 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 an API key at https://portal.cdp.coinbase.com. T3 / `@t3-oss/env-nextjs` users must declare these in their env schema. |
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
@@ -1938,8 +1938,15 @@ function tagBareDecimalAsDollars(amount) {
1938
1938
  var import_types2 = require("@x402/core/types");
1939
1939
  async function verifyX402Payment(opts) {
1940
1940
  const { server, request, price, accepts, report } = opts;
1941
- const payload = await readPaymentPayload(request);
1942
- if (!payload) return null;
1941
+ const payment = await readPaymentPayload(request);
1942
+ if (payment.kind === "none") return null;
1943
+ if (payment.kind === "malformed") {
1944
+ return invalidPaymentVerification({
1945
+ reason: "malformed_payment_header",
1946
+ message: `X-PAYMENT header could not be decoded: ${payment.message}`
1947
+ });
1948
+ }
1949
+ const payload = payment.payload;
1943
1950
  const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1944
1951
  const matching = findVerifiableRequirements(server, requirements, payload);
1945
1952
  const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
@@ -2000,9 +2007,16 @@ function matchesStableFields(requirement, accepted) {
2000
2007
  }
2001
2008
  async function readPaymentPayload(request) {
2002
2009
  const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
2003
- if (!paymentHeader) return null;
2010
+ if (!paymentHeader) return { kind: "none" };
2004
2011
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
2005
- return decodePaymentSignatureHeader(paymentHeader);
2012
+ try {
2013
+ return { kind: "ok", payload: decodePaymentSignatureHeader(paymentHeader) };
2014
+ } catch (err) {
2015
+ return {
2016
+ kind: "malformed",
2017
+ message: err instanceof Error ? err.message : String(err)
2018
+ };
2019
+ }
2006
2020
  }
2007
2021
  function invalidPaymentVerification(failure) {
2008
2022
  return {
@@ -3445,6 +3459,7 @@ ${issues}`
3445
3459
  }
3446
3460
 
3447
3461
  // src/builder.ts
3462
+ var MAX_X402_DESCRIPTION_LENGTH = 400;
3448
3463
  var RouteBuilder = class _RouteBuilder {
3449
3464
  #s;
3450
3465
  constructor(key, registry, deps, defaults) {
@@ -3961,6 +3976,11 @@ var RouteBuilder = class _RouteBuilder {
3961
3976
  `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3962
3977
  );
3963
3978
  }
3979
+ if (this.#s.description !== void 0 && this.#s.description.length > MAX_X402_DESCRIPTION_LENGTH && this.#s.pricing !== void 0 && this.#s.protocols.includes("x402")) {
3980
+ throw new Error(
3981
+ `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.`
3982
+ );
3983
+ }
3964
3984
  validateExamples(
3965
3985
  this.#s.key,
3966
3986
  this.#s.bodySchema,
@@ -4416,6 +4436,8 @@ var envShape = {
4416
4436
  params: { code: "invalid_base_url" },
4417
4437
  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
4438
  }).optional(),
4439
+ VERCEL_PROJECT_PRODUCTION_URL: import_zod.z.string().optional(),
4440
+ VERCEL_URL: import_zod.z.string().optional(),
4419
4441
  EVM_PAYEE_ADDRESS: import_zod.z.string().refine(isEvmAddress, {
4420
4442
  params: { code: "invalid_x402_payee", ...x402 },
4421
4443
  message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
@@ -4460,7 +4482,7 @@ var EnvInputSchema = import_zod.z.object(envShape).passthrough().superRefine((en
4460
4482
  addIssue(
4461
4483
  ctx,
4462
4484
  { 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.",
4485
+ "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
4486
  ["BASE_URL"]
4465
4487
  );
4466
4488
  }
@@ -4541,6 +4563,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
4541
4563
  }
4542
4564
  return [];
4543
4565
  }
4566
+ function withHttps(host) {
4567
+ if (!host) return void 0;
4568
+ return host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
4569
+ }
4570
+ function deriveBaseUrlEnv(env) {
4571
+ if (env.BASE_URL) return env;
4572
+ const vercelBaseUrl = withHttps(env.VERCEL_PROJECT_PRODUCTION_URL) ?? withHttps(env.VERCEL_URL);
4573
+ if (!vercelBaseUrl) return env;
4574
+ return {
4575
+ ...env,
4576
+ BASE_URL: vercelBaseUrl
4577
+ };
4578
+ }
4544
4579
  function getConfiguredX402Accepts2(config) {
4545
4580
  if (config.x402?.accepts?.length) return [...config.x402.accepts];
4546
4581
  return [
@@ -4709,7 +4744,7 @@ function translateZodIssues(error) {
4709
4744
  }
4710
4745
  function routerConfigFromEnv(options) {
4711
4746
  const rawEnv = options.env ?? process.env;
4712
- const env = trimAll(rawEnv);
4747
+ const env = deriveBaseUrlEnv(trimAll(rawEnv));
4713
4748
  const optionIssues = [];
4714
4749
  if (!options.title?.trim()) {
4715
4750
  optionIssues.push({
package/dist/index.js CHANGED
@@ -1897,8 +1897,15 @@ function tagBareDecimalAsDollars(amount) {
1897
1897
  import { VerifyError } from "@x402/core/types";
1898
1898
  async function verifyX402Payment(opts) {
1899
1899
  const { server, request, price, accepts, report } = opts;
1900
- const payload = await readPaymentPayload(request);
1901
- if (!payload) return null;
1900
+ const payment = await readPaymentPayload(request);
1901
+ if (payment.kind === "none") return null;
1902
+ if (payment.kind === "malformed") {
1903
+ return invalidPaymentVerification({
1904
+ reason: "malformed_payment_header",
1905
+ message: `X-PAYMENT header could not be decoded: ${payment.message}`
1906
+ });
1907
+ }
1908
+ const payload = payment.payload;
1902
1909
  const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1903
1910
  const matching = findVerifiableRequirements(server, requirements, payload);
1904
1911
  const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
@@ -1959,9 +1966,16 @@ function matchesStableFields(requirement, accepted) {
1959
1966
  }
1960
1967
  async function readPaymentPayload(request) {
1961
1968
  const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
1962
- if (!paymentHeader) return null;
1969
+ if (!paymentHeader) return { kind: "none" };
1963
1970
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
1964
- return decodePaymentSignatureHeader(paymentHeader);
1971
+ try {
1972
+ return { kind: "ok", payload: decodePaymentSignatureHeader(paymentHeader) };
1973
+ } catch (err) {
1974
+ return {
1975
+ kind: "malformed",
1976
+ message: err instanceof Error ? err.message : String(err)
1977
+ };
1978
+ }
1965
1979
  }
1966
1980
  function invalidPaymentVerification(failure) {
1967
1981
  return {
@@ -3404,6 +3418,7 @@ ${issues}`
3404
3418
  }
3405
3419
 
3406
3420
  // src/builder.ts
3421
+ var MAX_X402_DESCRIPTION_LENGTH = 400;
3407
3422
  var RouteBuilder = class _RouteBuilder {
3408
3423
  #s;
3409
3424
  constructor(key, registry, deps, defaults) {
@@ -3920,6 +3935,11 @@ var RouteBuilder = class _RouteBuilder {
3920
3935
  `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3921
3936
  );
3922
3937
  }
3938
+ if (this.#s.description !== void 0 && this.#s.description.length > MAX_X402_DESCRIPTION_LENGTH && this.#s.pricing !== void 0 && this.#s.protocols.includes("x402")) {
3939
+ throw new Error(
3940
+ `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.`
3941
+ );
3942
+ }
3923
3943
  validateExamples(
3924
3944
  this.#s.key,
3925
3945
  this.#s.bodySchema,
@@ -4375,6 +4395,8 @@ var envShape = {
4375
4395
  params: { code: "invalid_base_url" },
4376
4396
  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
4397
  }).optional(),
4398
+ VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(),
4399
+ VERCEL_URL: z.string().optional(),
4378
4400
  EVM_PAYEE_ADDRESS: z.string().refine(isEvmAddress, {
4379
4401
  params: { code: "invalid_x402_payee", ...x402 },
4380
4402
  message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
@@ -4419,7 +4441,7 @@ var EnvInputSchema = z.object(envShape).passthrough().superRefine((env, ctx) =>
4419
4441
  addIssue(
4420
4442
  ctx,
4421
4443
  { 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.",
4444
+ "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
4445
  ["BASE_URL"]
4424
4446
  );
4425
4447
  }
@@ -4500,6 +4522,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
4500
4522
  }
4501
4523
  return [];
4502
4524
  }
4525
+ function withHttps(host) {
4526
+ if (!host) return void 0;
4527
+ return host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
4528
+ }
4529
+ function deriveBaseUrlEnv(env) {
4530
+ if (env.BASE_URL) return env;
4531
+ const vercelBaseUrl = withHttps(env.VERCEL_PROJECT_PRODUCTION_URL) ?? withHttps(env.VERCEL_URL);
4532
+ if (!vercelBaseUrl) return env;
4533
+ return {
4534
+ ...env,
4535
+ BASE_URL: vercelBaseUrl
4536
+ };
4537
+ }
4503
4538
  function getConfiguredX402Accepts2(config) {
4504
4539
  if (config.x402?.accepts?.length) return [...config.x402.accepts];
4505
4540
  return [
@@ -4668,7 +4703,7 @@ function translateZodIssues(error) {
4668
4703
  }
4669
4704
  function routerConfigFromEnv(options) {
4670
4705
  const rawEnv = options.env ?? process.env;
4671
- const env = trimAll(rawEnv);
4706
+ const env = deriveBaseUrlEnv(trimAll(rawEnv));
4672
4707
  const optionIssues = [];
4673
4708
  if (!options.title?.trim()) {
4674
4709
  optionIssues.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
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": {