@agentcash/router 1.9.4 → 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 +10 -3
- package/dist/index.cjs +80 -34
- package/dist/index.js +80 -34
- package/package.json +1 -1
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
|
|
@@ -119,6 +126,7 @@ export const POST = router.route({ path: 'search' })
|
|
|
119
126
|
// app/api/inbox/status/route.ts
|
|
120
127
|
export const GET = router.route({ path: 'inbox/status' })
|
|
121
128
|
.siwx()
|
|
129
|
+
.method('GET')
|
|
122
130
|
.handler(async ({ wallet }) => getStatus(wallet));
|
|
123
131
|
```
|
|
124
132
|
|
|
@@ -305,4 +313,3 @@ const loggingPlugin: RouterPlugin = {
|
|
|
305
313
|
},
|
|
306
314
|
};
|
|
307
315
|
```
|
|
308
|
-
|
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
|
|
1942
|
-
if (
|
|
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
|
|
2010
|
+
if (!paymentHeader) return { kind: "none" };
|
|
2004
2011
|
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
2005
|
-
|
|
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 {
|
|
@@ -2177,6 +2191,21 @@ function getAllowedStrategies(allowed) {
|
|
|
2177
2191
|
return allowed.map((name) => STRATEGIES[name]);
|
|
2178
2192
|
}
|
|
2179
2193
|
|
|
2194
|
+
// src/protocols/detect.ts
|
|
2195
|
+
function detectProtocol(request) {
|
|
2196
|
+
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
2197
|
+
return "x402";
|
|
2198
|
+
}
|
|
2199
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
2200
|
+
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
2201
|
+
return "mpp";
|
|
2202
|
+
}
|
|
2203
|
+
if (request.headers.get(HEADERS.SIWX)) {
|
|
2204
|
+
return "siwx";
|
|
2205
|
+
}
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2180
2209
|
// src/pipeline/flows/api-key-only.ts
|
|
2181
2210
|
async function runApiKeyOnlyFlow(ctx) {
|
|
2182
2211
|
if (!ctx.routeEntry.apiKeyResolver) {
|
|
@@ -2192,6 +2221,11 @@ async function runApiKeyOnlyFlow(ctx) {
|
|
|
2192
2221
|
var DynamicPricing = class {
|
|
2193
2222
|
constructor(opts) {
|
|
2194
2223
|
this.opts = opts;
|
|
2224
|
+
if (!isPositiveDecimal(opts.maxPrice)) {
|
|
2225
|
+
throw new Error(
|
|
2226
|
+
`route '${opts.route ?? "unknown"}': dynamic pricing requires a positive maxPrice, got '${opts.maxPrice}'`
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2195
2229
|
}
|
|
2196
2230
|
needsBody = true;
|
|
2197
2231
|
async quote(body) {
|
|
@@ -2205,11 +2239,8 @@ var DynamicPricing = class {
|
|
|
2205
2239
|
error: err instanceof Error ? err.stack : String(err),
|
|
2206
2240
|
body
|
|
2207
2241
|
});
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
return this.opts.maxPrice;
|
|
2211
|
-
}
|
|
2212
|
-
throw err;
|
|
2242
|
+
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
2243
|
+
return this.opts.maxPrice;
|
|
2213
2244
|
}
|
|
2214
2245
|
if (!isPositiveDecimal(priced)) {
|
|
2215
2246
|
throw new HttpError(
|
|
@@ -2220,18 +2251,17 @@ var DynamicPricing = class {
|
|
|
2220
2251
|
return priced;
|
|
2221
2252
|
}
|
|
2222
2253
|
challengeQuote(body) {
|
|
2223
|
-
if (body === void 0) return Promise.resolve(this.opts.maxPrice
|
|
2254
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice);
|
|
2224
2255
|
return this.quote(body);
|
|
2225
2256
|
}
|
|
2226
2257
|
describe() {
|
|
2227
2258
|
return {
|
|
2228
2259
|
mode: "dynamic",
|
|
2229
2260
|
min: this.opts.minPrice ?? "0",
|
|
2230
|
-
max: this.opts.maxPrice
|
|
2261
|
+
max: this.opts.maxPrice
|
|
2231
2262
|
};
|
|
2232
2263
|
}
|
|
2233
2264
|
cap(raw, body) {
|
|
2234
|
-
if (!this.opts.maxPrice) return raw;
|
|
2235
2265
|
let overCap;
|
|
2236
2266
|
try {
|
|
2237
2267
|
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
@@ -2331,6 +2361,9 @@ function selectPricing(raw, deps = {}) {
|
|
|
2331
2361
|
return new FixedPricing(raw);
|
|
2332
2362
|
}
|
|
2333
2363
|
if (typeof raw === "function") {
|
|
2364
|
+
if (!deps.maxPrice) {
|
|
2365
|
+
throw new Error(`route '${deps.route ?? "unknown"}': dynamic pricing requires maxPrice`);
|
|
2366
|
+
}
|
|
2334
2367
|
return new DynamicPricing({
|
|
2335
2368
|
fn: raw,
|
|
2336
2369
|
maxPrice: deps.maxPrice,
|
|
@@ -3215,21 +3248,6 @@ async function createKvMppStore(kv, options) {
|
|
|
3215
3248
|
});
|
|
3216
3249
|
}
|
|
3217
3250
|
|
|
3218
|
-
// src/protocols/detect.ts
|
|
3219
|
-
function detectProtocol(request) {
|
|
3220
|
-
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
3221
|
-
return "x402";
|
|
3222
|
-
}
|
|
3223
|
-
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
3224
|
-
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
3225
|
-
return "mpp";
|
|
3226
|
-
}
|
|
3227
|
-
if (request.headers.get(HEADERS.SIWX)) {
|
|
3228
|
-
return "siwx";
|
|
3229
|
-
}
|
|
3230
|
-
return null;
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
3251
|
// src/protocols/mpp/siwx-mode.ts
|
|
3234
3252
|
var import_mppx3 = require("mppx");
|
|
3235
3253
|
async function verifyMppSiwx(request, mppx) {
|
|
@@ -3252,8 +3270,6 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
3252
3270
|
if (earlyBody.ok) {
|
|
3253
3271
|
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
3254
3272
|
if (validateErr) return validateErr;
|
|
3255
|
-
} else {
|
|
3256
|
-
return earlyBody.response;
|
|
3257
3273
|
}
|
|
3258
3274
|
}
|
|
3259
3275
|
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
@@ -3386,9 +3402,13 @@ async function runUnprotectedFlow(ctx) {
|
|
|
3386
3402
|
|
|
3387
3403
|
// src/pipeline/orchestrate.ts
|
|
3388
3404
|
function shouldSkipQueryValidation(routeEntry, request) {
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3405
|
+
if (routeEntry.pricing || routeEntry.authMode === "paid") {
|
|
3406
|
+
return selectIncomingStrategy(request, routeEntry.protocols) === null;
|
|
3407
|
+
}
|
|
3408
|
+
if (routeEntry.authMode === "siwx") {
|
|
3409
|
+
return !request.headers.get(HEADERS.SIWX) && detectProtocol(request) !== "mpp";
|
|
3410
|
+
}
|
|
3411
|
+
return false;
|
|
3392
3412
|
}
|
|
3393
3413
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
3394
3414
|
return async (request) => {
|
|
@@ -3439,6 +3459,7 @@ ${issues}`
|
|
|
3439
3459
|
}
|
|
3440
3460
|
|
|
3441
3461
|
// src/builder.ts
|
|
3462
|
+
var MAX_X402_DESCRIPTION_LENGTH = 400;
|
|
3442
3463
|
var RouteBuilder = class _RouteBuilder {
|
|
3443
3464
|
#s;
|
|
3444
3465
|
constructor(key, registry, deps, defaults) {
|
|
@@ -3582,6 +3603,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3582
3603
|
`route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
|
|
3583
3604
|
);
|
|
3584
3605
|
}
|
|
3606
|
+
if (typeof pricing === "function" && next.#s.maxPrice === void 0) {
|
|
3607
|
+
throw new Error(
|
|
3608
|
+
`route '${this.#s.key}': dynamic pricing requires maxPrice \u2014 without it, bare probes would advertise a $0 challenge`
|
|
3609
|
+
);
|
|
3610
|
+
}
|
|
3585
3611
|
if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
|
|
3586
3612
|
throw new Error(
|
|
3587
3613
|
`route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
|
|
@@ -3950,6 +3976,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3950
3976
|
`route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
|
|
3951
3977
|
);
|
|
3952
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
|
+
}
|
|
3953
3984
|
validateExamples(
|
|
3954
3985
|
this.#s.key,
|
|
3955
3986
|
this.#s.bodySchema,
|
|
@@ -4405,6 +4436,8 @@ var envShape = {
|
|
|
4405
4436
|
params: { code: "invalid_base_url" },
|
|
4406
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."
|
|
4407
4438
|
}).optional(),
|
|
4439
|
+
VERCEL_PROJECT_PRODUCTION_URL: import_zod.z.string().optional(),
|
|
4440
|
+
VERCEL_URL: import_zod.z.string().optional(),
|
|
4408
4441
|
EVM_PAYEE_ADDRESS: import_zod.z.string().refine(isEvmAddress, {
|
|
4409
4442
|
params: { code: "invalid_x402_payee", ...x402 },
|
|
4410
4443
|
message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
|
|
@@ -4449,7 +4482,7 @@ var EnvInputSchema = import_zod.z.object(envShape).passthrough().superRefine((en
|
|
|
4449
4482
|
addIssue(
|
|
4450
4483
|
ctx,
|
|
4451
4484
|
{ code: "missing_base_url" },
|
|
4452
|
-
"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.",
|
|
4453
4486
|
["BASE_URL"]
|
|
4454
4487
|
);
|
|
4455
4488
|
}
|
|
@@ -4530,6 +4563,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
|
|
|
4530
4563
|
}
|
|
4531
4564
|
return [];
|
|
4532
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
|
+
}
|
|
4533
4579
|
function getConfiguredX402Accepts2(config) {
|
|
4534
4580
|
if (config.x402?.accepts?.length) return [...config.x402.accepts];
|
|
4535
4581
|
return [
|
|
@@ -4698,7 +4744,7 @@ function translateZodIssues(error) {
|
|
|
4698
4744
|
}
|
|
4699
4745
|
function routerConfigFromEnv(options) {
|
|
4700
4746
|
const rawEnv = options.env ?? process.env;
|
|
4701
|
-
const env = trimAll(rawEnv);
|
|
4747
|
+
const env = deriveBaseUrlEnv(trimAll(rawEnv));
|
|
4702
4748
|
const optionIssues = [];
|
|
4703
4749
|
if (!options.title?.trim()) {
|
|
4704
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
|
|
1901
|
-
if (
|
|
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
|
|
1969
|
+
if (!paymentHeader) return { kind: "none" };
|
|
1963
1970
|
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
1964
|
-
|
|
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 {
|
|
@@ -2136,6 +2150,21 @@ function getAllowedStrategies(allowed) {
|
|
|
2136
2150
|
return allowed.map((name) => STRATEGIES[name]);
|
|
2137
2151
|
}
|
|
2138
2152
|
|
|
2153
|
+
// src/protocols/detect.ts
|
|
2154
|
+
function detectProtocol(request) {
|
|
2155
|
+
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
2156
|
+
return "x402";
|
|
2157
|
+
}
|
|
2158
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
2159
|
+
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
2160
|
+
return "mpp";
|
|
2161
|
+
}
|
|
2162
|
+
if (request.headers.get(HEADERS.SIWX)) {
|
|
2163
|
+
return "siwx";
|
|
2164
|
+
}
|
|
2165
|
+
return null;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2139
2168
|
// src/pipeline/flows/api-key-only.ts
|
|
2140
2169
|
async function runApiKeyOnlyFlow(ctx) {
|
|
2141
2170
|
if (!ctx.routeEntry.apiKeyResolver) {
|
|
@@ -2151,6 +2180,11 @@ async function runApiKeyOnlyFlow(ctx) {
|
|
|
2151
2180
|
var DynamicPricing = class {
|
|
2152
2181
|
constructor(opts) {
|
|
2153
2182
|
this.opts = opts;
|
|
2183
|
+
if (!isPositiveDecimal(opts.maxPrice)) {
|
|
2184
|
+
throw new Error(
|
|
2185
|
+
`route '${opts.route ?? "unknown"}': dynamic pricing requires a positive maxPrice, got '${opts.maxPrice}'`
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2154
2188
|
}
|
|
2155
2189
|
needsBody = true;
|
|
2156
2190
|
async quote(body) {
|
|
@@ -2164,11 +2198,8 @@ var DynamicPricing = class {
|
|
|
2164
2198
|
error: err instanceof Error ? err.stack : String(err),
|
|
2165
2199
|
body
|
|
2166
2200
|
});
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
return this.opts.maxPrice;
|
|
2170
|
-
}
|
|
2171
|
-
throw err;
|
|
2201
|
+
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
2202
|
+
return this.opts.maxPrice;
|
|
2172
2203
|
}
|
|
2173
2204
|
if (!isPositiveDecimal(priced)) {
|
|
2174
2205
|
throw new HttpError(
|
|
@@ -2179,18 +2210,17 @@ var DynamicPricing = class {
|
|
|
2179
2210
|
return priced;
|
|
2180
2211
|
}
|
|
2181
2212
|
challengeQuote(body) {
|
|
2182
|
-
if (body === void 0) return Promise.resolve(this.opts.maxPrice
|
|
2213
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice);
|
|
2183
2214
|
return this.quote(body);
|
|
2184
2215
|
}
|
|
2185
2216
|
describe() {
|
|
2186
2217
|
return {
|
|
2187
2218
|
mode: "dynamic",
|
|
2188
2219
|
min: this.opts.minPrice ?? "0",
|
|
2189
|
-
max: this.opts.maxPrice
|
|
2220
|
+
max: this.opts.maxPrice
|
|
2190
2221
|
};
|
|
2191
2222
|
}
|
|
2192
2223
|
cap(raw, body) {
|
|
2193
|
-
if (!this.opts.maxPrice) return raw;
|
|
2194
2224
|
let overCap;
|
|
2195
2225
|
try {
|
|
2196
2226
|
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
@@ -2290,6 +2320,9 @@ function selectPricing(raw, deps = {}) {
|
|
|
2290
2320
|
return new FixedPricing(raw);
|
|
2291
2321
|
}
|
|
2292
2322
|
if (typeof raw === "function") {
|
|
2323
|
+
if (!deps.maxPrice) {
|
|
2324
|
+
throw new Error(`route '${deps.route ?? "unknown"}': dynamic pricing requires maxPrice`);
|
|
2325
|
+
}
|
|
2293
2326
|
return new DynamicPricing({
|
|
2294
2327
|
fn: raw,
|
|
2295
2328
|
maxPrice: deps.maxPrice,
|
|
@@ -3174,21 +3207,6 @@ async function createKvMppStore(kv, options) {
|
|
|
3174
3207
|
});
|
|
3175
3208
|
}
|
|
3176
3209
|
|
|
3177
|
-
// src/protocols/detect.ts
|
|
3178
|
-
function detectProtocol(request) {
|
|
3179
|
-
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
3180
|
-
return "x402";
|
|
3181
|
-
}
|
|
3182
|
-
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
3183
|
-
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
3184
|
-
return "mpp";
|
|
3185
|
-
}
|
|
3186
|
-
if (request.headers.get(HEADERS.SIWX)) {
|
|
3187
|
-
return "siwx";
|
|
3188
|
-
}
|
|
3189
|
-
return null;
|
|
3190
|
-
}
|
|
3191
|
-
|
|
3192
3210
|
// src/protocols/mpp/siwx-mode.ts
|
|
3193
3211
|
import { Credential as Credential2 } from "mppx";
|
|
3194
3212
|
async function verifyMppSiwx(request, mppx) {
|
|
@@ -3211,8 +3229,6 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
3211
3229
|
if (earlyBody.ok) {
|
|
3212
3230
|
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
3213
3231
|
if (validateErr) return validateErr;
|
|
3214
|
-
} else {
|
|
3215
|
-
return earlyBody.response;
|
|
3216
3232
|
}
|
|
3217
3233
|
}
|
|
3218
3234
|
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
@@ -3345,9 +3361,13 @@ async function runUnprotectedFlow(ctx) {
|
|
|
3345
3361
|
|
|
3346
3362
|
// src/pipeline/orchestrate.ts
|
|
3347
3363
|
function shouldSkipQueryValidation(routeEntry, request) {
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3364
|
+
if (routeEntry.pricing || routeEntry.authMode === "paid") {
|
|
3365
|
+
return selectIncomingStrategy(request, routeEntry.protocols) === null;
|
|
3366
|
+
}
|
|
3367
|
+
if (routeEntry.authMode === "siwx") {
|
|
3368
|
+
return !request.headers.get(HEADERS.SIWX) && detectProtocol(request) !== "mpp";
|
|
3369
|
+
}
|
|
3370
|
+
return false;
|
|
3351
3371
|
}
|
|
3352
3372
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
3353
3373
|
return async (request) => {
|
|
@@ -3398,6 +3418,7 @@ ${issues}`
|
|
|
3398
3418
|
}
|
|
3399
3419
|
|
|
3400
3420
|
// src/builder.ts
|
|
3421
|
+
var MAX_X402_DESCRIPTION_LENGTH = 400;
|
|
3401
3422
|
var RouteBuilder = class _RouteBuilder {
|
|
3402
3423
|
#s;
|
|
3403
3424
|
constructor(key, registry, deps, defaults) {
|
|
@@ -3541,6 +3562,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3541
3562
|
`route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
|
|
3542
3563
|
);
|
|
3543
3564
|
}
|
|
3565
|
+
if (typeof pricing === "function" && next.#s.maxPrice === void 0) {
|
|
3566
|
+
throw new Error(
|
|
3567
|
+
`route '${this.#s.key}': dynamic pricing requires maxPrice \u2014 without it, bare probes would advertise a $0 challenge`
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3544
3570
|
if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
|
|
3545
3571
|
throw new Error(
|
|
3546
3572
|
`route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
|
|
@@ -3909,6 +3935,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3909
3935
|
`route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
|
|
3910
3936
|
);
|
|
3911
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
|
+
}
|
|
3912
3943
|
validateExamples(
|
|
3913
3944
|
this.#s.key,
|
|
3914
3945
|
this.#s.bodySchema,
|
|
@@ -4364,6 +4395,8 @@ var envShape = {
|
|
|
4364
4395
|
params: { code: "invalid_base_url" },
|
|
4365
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."
|
|
4366
4397
|
}).optional(),
|
|
4398
|
+
VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(),
|
|
4399
|
+
VERCEL_URL: z.string().optional(),
|
|
4367
4400
|
EVM_PAYEE_ADDRESS: z.string().refine(isEvmAddress, {
|
|
4368
4401
|
params: { code: "invalid_x402_payee", ...x402 },
|
|
4369
4402
|
message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
|
|
@@ -4408,7 +4441,7 @@ var EnvInputSchema = z.object(envShape).passthrough().superRefine((env, ctx) =>
|
|
|
4408
4441
|
addIssue(
|
|
4409
4442
|
ctx,
|
|
4410
4443
|
{ code: "missing_base_url" },
|
|
4411
|
-
"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.",
|
|
4412
4445
|
["BASE_URL"]
|
|
4413
4446
|
);
|
|
4414
4447
|
}
|
|
@@ -4489,6 +4522,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
|
|
|
4489
4522
|
}
|
|
4490
4523
|
return [];
|
|
4491
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
|
+
}
|
|
4492
4538
|
function getConfiguredX402Accepts2(config) {
|
|
4493
4539
|
if (config.x402?.accepts?.length) return [...config.x402.accepts];
|
|
4494
4540
|
return [
|
|
@@ -4657,7 +4703,7 @@ function translateZodIssues(error) {
|
|
|
4657
4703
|
}
|
|
4658
4704
|
function routerConfigFromEnv(options) {
|
|
4659
4705
|
const rawEnv = options.env ?? process.env;
|
|
4660
|
-
const env = trimAll(rawEnv);
|
|
4706
|
+
const env = deriveBaseUrlEnv(trimAll(rawEnv));
|
|
4661
4707
|
const optionIssues = [];
|
|
4662
4708
|
if (!options.title?.trim()) {
|
|
4663
4709
|
optionIssues.push({
|