@agentcash/router 1.9.3 → 1.10.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/README.md +15 -0
- package/dist/index.cjs +236 -200
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +234 -198
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,7 @@ export const POST = router.route({ path: 'search' })
|
|
|
119
119
|
// app/api/inbox/status/route.ts
|
|
120
120
|
export const GET = router.route({ path: 'inbox/status' })
|
|
121
121
|
.siwx()
|
|
122
|
+
.method('GET')
|
|
122
123
|
.handler(async ({ wallet }) => getStatus(wallet));
|
|
123
124
|
```
|
|
124
125
|
|
|
@@ -163,6 +164,20 @@ router.route({ path: 'gated' })
|
|
|
163
164
|
.handler(fn);
|
|
164
165
|
```
|
|
165
166
|
|
|
167
|
+
### `.siwx()` on paid routes. Pay once, identity-gated replays
|
|
168
|
+
|
|
169
|
+
`.paid()` and `.upTo()` compose with `.siwx()` for a pay-once-then-replay-for-free model. The first request settles normally (x402 payment); on success the wallet is recorded in the entitlement KV. Subsequent requests that present a valid SIWX signature for that wallet skip payment and run the handler directly. On `.upTo()` routes, `charge(amount)` becomes a no-op on the SIWX replay path. The handler can continue to call the route unconditionally.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
router.route({ path: 'inbox' })
|
|
173
|
+
.paid('0.01').siwx() // first call pays $0.01, later calls present a SIWX sig instead
|
|
174
|
+
.handler(async ({ wallet }) => getInbox(wallet));
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`.metered()` is mutually exclusive with `.siwx()` — per-tick MPP billing has no entitlement model — and the builder throws at registration if you combine them.
|
|
178
|
+
|
|
179
|
+
> **Gotcha:** serverless / multi-instance deployments must provide a real `kvStore` (Upstash / Vercel KV). Without one the entitlement is kept in a per-process `Map`, so a wallet that paid on instance A is treated as unpaid on instance B and the user gets charged again.
|
|
180
|
+
|
|
166
181
|
## Pricing
|
|
167
182
|
|
|
168
183
|
`.paid()`, `.upTo()`, and `.metered()` are mutually exclusive pricing modes: pick one per route.
|
package/dist/index.cjs
CHANGED
|
@@ -808,7 +808,11 @@ function invokePaidStatic(ctx, wallet, account, body, payment) {
|
|
|
808
808
|
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
|
|
809
809
|
}
|
|
810
810
|
function invokeUnauthed(ctx, wallet, account, body) {
|
|
811
|
-
|
|
811
|
+
const base = buildHandlerCtx(ctx, wallet, account, body, null);
|
|
812
|
+
if (ctx.routeEntry.billing !== "upto") return runHandler(ctx, base);
|
|
813
|
+
const uptoCtx = { ...base, charge: async () => {
|
|
814
|
+
} };
|
|
815
|
+
return runHandler(ctx, uptoCtx);
|
|
812
816
|
}
|
|
813
817
|
function buildHandlerCtx(ctx, wallet, account, body, payment) {
|
|
814
818
|
return {
|
|
@@ -1137,7 +1141,7 @@ async function resolveEarlyBody(args) {
|
|
|
1137
1141
|
}
|
|
1138
1142
|
const earlyClone = ctx.request.clone();
|
|
1139
1143
|
const earlyResult = await parseBody(ctx, earlyClone);
|
|
1140
|
-
if (!earlyResult.ok) return { ok:
|
|
1144
|
+
if (!earlyResult.ok) return { ok: true, earlyBody: void 0 };
|
|
1141
1145
|
const validateErr = await runValidate(ctx, earlyResult.data);
|
|
1142
1146
|
if (validateErr) return { ok: false, response: validateErr };
|
|
1143
1147
|
return { ok: true, earlyBody: earlyResult.data };
|
|
@@ -1189,17 +1193,6 @@ function protocolInitError(routeEntry, deps) {
|
|
|
1189
1193
|
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
1190
1194
|
}
|
|
1191
1195
|
|
|
1192
|
-
// src/pipeline/flows/api-key-only.ts
|
|
1193
|
-
async function runApiKeyOnlyFlow(ctx) {
|
|
1194
|
-
if (!ctx.routeEntry.apiKeyResolver) {
|
|
1195
|
-
return fail(ctx, 401, "API key resolver not configured");
|
|
1196
|
-
}
|
|
1197
|
-
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
1198
|
-
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
1199
|
-
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
|
|
1200
|
-
return runHandlerOnly(ctx, null, result.account);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
1196
|
// src/pricing/format.ts
|
|
1204
1197
|
var USDC_DECIMALS = 6;
|
|
1205
1198
|
var DECIMAL_RE = /^(\d+)(?:\.(\d+))?$/;
|
|
@@ -1255,169 +1248,6 @@ function multiplyDecimal(decimal, factor) {
|
|
|
1255
1248
|
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
1256
1249
|
}
|
|
1257
1250
|
|
|
1258
|
-
// src/pricing/dynamic.ts
|
|
1259
|
-
var DynamicPricing = class {
|
|
1260
|
-
constructor(opts) {
|
|
1261
|
-
this.opts = opts;
|
|
1262
|
-
}
|
|
1263
|
-
needsBody = true;
|
|
1264
|
-
async quote(body) {
|
|
1265
|
-
let priced;
|
|
1266
|
-
try {
|
|
1267
|
-
const raw = await this.opts.fn(body);
|
|
1268
|
-
priced = this.cap(raw, body);
|
|
1269
|
-
} catch (err) {
|
|
1270
|
-
if (err instanceof HttpError) throw err;
|
|
1271
|
-
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
1272
|
-
error: err instanceof Error ? err.stack : String(err),
|
|
1273
|
-
body
|
|
1274
|
-
});
|
|
1275
|
-
if (this.opts.maxPrice) {
|
|
1276
|
-
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
1277
|
-
return this.opts.maxPrice;
|
|
1278
|
-
}
|
|
1279
|
-
throw err;
|
|
1280
|
-
}
|
|
1281
|
-
if (!isPositiveDecimal(priced)) {
|
|
1282
|
-
throw new HttpError(
|
|
1283
|
-
`route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
|
|
1284
|
-
500
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1287
|
-
return priced;
|
|
1288
|
-
}
|
|
1289
|
-
challengeQuote(body) {
|
|
1290
|
-
if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
|
|
1291
|
-
return this.quote(body);
|
|
1292
|
-
}
|
|
1293
|
-
describe() {
|
|
1294
|
-
return {
|
|
1295
|
-
mode: "dynamic",
|
|
1296
|
-
min: this.opts.minPrice ?? "0",
|
|
1297
|
-
max: this.opts.maxPrice ?? "0"
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
cap(raw, body) {
|
|
1301
|
-
if (!this.opts.maxPrice) return raw;
|
|
1302
|
-
let overCap;
|
|
1303
|
-
try {
|
|
1304
|
-
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
1305
|
-
} catch {
|
|
1306
|
-
overCap = true;
|
|
1307
|
-
}
|
|
1308
|
-
if (overCap) {
|
|
1309
|
-
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
1310
|
-
calculated: raw,
|
|
1311
|
-
maxPrice: this.opts.maxPrice,
|
|
1312
|
-
body
|
|
1313
|
-
});
|
|
1314
|
-
return this.opts.maxPrice;
|
|
1315
|
-
}
|
|
1316
|
-
return raw;
|
|
1317
|
-
}
|
|
1318
|
-
alert(level, message, meta) {
|
|
1319
|
-
this.opts.alert?.(level, message, meta);
|
|
1320
|
-
}
|
|
1321
|
-
};
|
|
1322
|
-
function msg(err) {
|
|
1323
|
-
return err instanceof Error ? err.message : String(err);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
// src/pricing/fixed.ts
|
|
1327
|
-
var FixedPricing = class {
|
|
1328
|
-
constructor(price) {
|
|
1329
|
-
this.price = price;
|
|
1330
|
-
}
|
|
1331
|
-
needsBody = false;
|
|
1332
|
-
quote() {
|
|
1333
|
-
return Promise.resolve(this.price);
|
|
1334
|
-
}
|
|
1335
|
-
challengeQuote() {
|
|
1336
|
-
return Promise.resolve(this.price);
|
|
1337
|
-
}
|
|
1338
|
-
describe() {
|
|
1339
|
-
return { mode: "fixed", amount: this.price };
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1342
|
-
|
|
1343
|
-
// src/pricing/tiered.ts
|
|
1344
|
-
var TieredPricing = class {
|
|
1345
|
-
constructor(opts) {
|
|
1346
|
-
this.opts = opts;
|
|
1347
|
-
}
|
|
1348
|
-
needsBody = true;
|
|
1349
|
-
async quote(body) {
|
|
1350
|
-
const { field, tiers, default: defaultTier } = this.opts;
|
|
1351
|
-
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
1352
|
-
if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
|
|
1353
|
-
if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
|
|
1354
|
-
if (!tierKey) {
|
|
1355
|
-
throw httpError(400, `Missing required field '${field}' for tier pricing`);
|
|
1356
|
-
}
|
|
1357
|
-
throw httpError(
|
|
1358
|
-
400,
|
|
1359
|
-
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
async challengeQuote(body) {
|
|
1363
|
-
if (body !== void 0) {
|
|
1364
|
-
try {
|
|
1365
|
-
return await this.quote(body);
|
|
1366
|
-
} catch {
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
return this.maxTierPrice();
|
|
1370
|
-
}
|
|
1371
|
-
describe() {
|
|
1372
|
-
return {
|
|
1373
|
-
mode: "tiered",
|
|
1374
|
-
tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
|
|
1375
|
-
key,
|
|
1376
|
-
price: tier.price,
|
|
1377
|
-
...tier.label !== void 0 ? { label: tier.label } : {}
|
|
1378
|
-
})),
|
|
1379
|
-
...this.opts.default !== void 0 ? { default: this.opts.default } : {}
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
maxTierPrice() {
|
|
1383
|
-
let max = "0";
|
|
1384
|
-
for (const tier of Object.values(this.opts.tiers)) {
|
|
1385
|
-
if (compareDecimals(tier.price, max) > 0) max = tier.price;
|
|
1386
|
-
}
|
|
1387
|
-
return max;
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1390
|
-
function httpError(status, message) {
|
|
1391
|
-
return Object.assign(new Error(message), { status });
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
// src/pricing/index.ts
|
|
1395
|
-
function selectPricing(raw, deps = {}) {
|
|
1396
|
-
if (raw == null) return null;
|
|
1397
|
-
if (typeof raw === "string") {
|
|
1398
|
-
return new FixedPricing(raw);
|
|
1399
|
-
}
|
|
1400
|
-
if (typeof raw === "function") {
|
|
1401
|
-
return new DynamicPricing({
|
|
1402
|
-
fn: raw,
|
|
1403
|
-
maxPrice: deps.maxPrice,
|
|
1404
|
-
minPrice: deps.minPrice,
|
|
1405
|
-
route: deps.route,
|
|
1406
|
-
alert: deps.alert
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
if (typeof raw === "object" && "tiers" in raw) {
|
|
1410
|
-
return new TieredPricing({
|
|
1411
|
-
field: raw.field,
|
|
1412
|
-
tiers: raw.tiers,
|
|
1413
|
-
default: raw.default,
|
|
1414
|
-
maxPrice: deps.maxPrice,
|
|
1415
|
-
minPrice: deps.minPrice
|
|
1416
|
-
});
|
|
1417
|
-
}
|
|
1418
|
-
throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
1251
|
// src/protocols/mpp/credential.ts
|
|
1422
1252
|
var import_mppx = require("mppx");
|
|
1423
1253
|
var import_viem = require("viem");
|
|
@@ -2105,7 +1935,7 @@ function tagBareDecimalAsDollars(amount) {
|
|
|
2105
1935
|
}
|
|
2106
1936
|
|
|
2107
1937
|
// src/protocols/x402/verify.ts
|
|
2108
|
-
var
|
|
1938
|
+
var import_types2 = require("@x402/core/types");
|
|
2109
1939
|
async function verifyX402Payment(opts) {
|
|
2110
1940
|
const { server, request, price, accepts, report } = opts;
|
|
2111
1941
|
const payload = await readPaymentPayload(request);
|
|
@@ -2124,7 +1954,7 @@ async function verifyX402Payment(opts) {
|
|
|
2124
1954
|
try {
|
|
2125
1955
|
verify = await server.verifyPayment(payload, matching);
|
|
2126
1956
|
} catch (err) {
|
|
2127
|
-
if (err instanceof
|
|
1957
|
+
if (err instanceof import_types2.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
|
|
2128
1958
|
return invalidPaymentVerification({
|
|
2129
1959
|
reason: err.invalidReason ?? "verify_error",
|
|
2130
1960
|
...err.invalidMessage ? { message: err.invalidMessage } : {},
|
|
@@ -2347,6 +2177,199 @@ function getAllowedStrategies(allowed) {
|
|
|
2347
2177
|
return allowed.map((name) => STRATEGIES[name]);
|
|
2348
2178
|
}
|
|
2349
2179
|
|
|
2180
|
+
// src/protocols/detect.ts
|
|
2181
|
+
function detectProtocol(request) {
|
|
2182
|
+
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
2183
|
+
return "x402";
|
|
2184
|
+
}
|
|
2185
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
2186
|
+
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
2187
|
+
return "mpp";
|
|
2188
|
+
}
|
|
2189
|
+
if (request.headers.get(HEADERS.SIWX)) {
|
|
2190
|
+
return "siwx";
|
|
2191
|
+
}
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/pipeline/flows/api-key-only.ts
|
|
2196
|
+
async function runApiKeyOnlyFlow(ctx) {
|
|
2197
|
+
if (!ctx.routeEntry.apiKeyResolver) {
|
|
2198
|
+
return fail(ctx, 401, "API key resolver not configured");
|
|
2199
|
+
}
|
|
2200
|
+
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
2201
|
+
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
2202
|
+
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
|
|
2203
|
+
return runHandlerOnly(ctx, null, result.account);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// src/pricing/dynamic.ts
|
|
2207
|
+
var DynamicPricing = class {
|
|
2208
|
+
constructor(opts) {
|
|
2209
|
+
this.opts = opts;
|
|
2210
|
+
if (!isPositiveDecimal(opts.maxPrice)) {
|
|
2211
|
+
throw new Error(
|
|
2212
|
+
`route '${opts.route ?? "unknown"}': dynamic pricing requires a positive maxPrice, got '${opts.maxPrice}'`
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
needsBody = true;
|
|
2217
|
+
async quote(body) {
|
|
2218
|
+
let priced;
|
|
2219
|
+
try {
|
|
2220
|
+
const raw = await this.opts.fn(body);
|
|
2221
|
+
priced = this.cap(raw, body);
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
if (err instanceof HttpError) throw err;
|
|
2224
|
+
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
2225
|
+
error: err instanceof Error ? err.stack : String(err),
|
|
2226
|
+
body
|
|
2227
|
+
});
|
|
2228
|
+
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
2229
|
+
return this.opts.maxPrice;
|
|
2230
|
+
}
|
|
2231
|
+
if (!isPositiveDecimal(priced)) {
|
|
2232
|
+
throw new HttpError(
|
|
2233
|
+
`route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
|
|
2234
|
+
500
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
return priced;
|
|
2238
|
+
}
|
|
2239
|
+
challengeQuote(body) {
|
|
2240
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice);
|
|
2241
|
+
return this.quote(body);
|
|
2242
|
+
}
|
|
2243
|
+
describe() {
|
|
2244
|
+
return {
|
|
2245
|
+
mode: "dynamic",
|
|
2246
|
+
min: this.opts.minPrice ?? "0",
|
|
2247
|
+
max: this.opts.maxPrice
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
cap(raw, body) {
|
|
2251
|
+
let overCap;
|
|
2252
|
+
try {
|
|
2253
|
+
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
2254
|
+
} catch {
|
|
2255
|
+
overCap = true;
|
|
2256
|
+
}
|
|
2257
|
+
if (overCap) {
|
|
2258
|
+
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
2259
|
+
calculated: raw,
|
|
2260
|
+
maxPrice: this.opts.maxPrice,
|
|
2261
|
+
body
|
|
2262
|
+
});
|
|
2263
|
+
return this.opts.maxPrice;
|
|
2264
|
+
}
|
|
2265
|
+
return raw;
|
|
2266
|
+
}
|
|
2267
|
+
alert(level, message, meta) {
|
|
2268
|
+
this.opts.alert?.(level, message, meta);
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
function msg(err) {
|
|
2272
|
+
return err instanceof Error ? err.message : String(err);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/pricing/fixed.ts
|
|
2276
|
+
var FixedPricing = class {
|
|
2277
|
+
constructor(price) {
|
|
2278
|
+
this.price = price;
|
|
2279
|
+
}
|
|
2280
|
+
needsBody = false;
|
|
2281
|
+
quote() {
|
|
2282
|
+
return Promise.resolve(this.price);
|
|
2283
|
+
}
|
|
2284
|
+
challengeQuote() {
|
|
2285
|
+
return Promise.resolve(this.price);
|
|
2286
|
+
}
|
|
2287
|
+
describe() {
|
|
2288
|
+
return { mode: "fixed", amount: this.price };
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2292
|
+
// src/pricing/tiered.ts
|
|
2293
|
+
var TieredPricing = class {
|
|
2294
|
+
constructor(opts) {
|
|
2295
|
+
this.opts = opts;
|
|
2296
|
+
}
|
|
2297
|
+
needsBody = true;
|
|
2298
|
+
async quote(body) {
|
|
2299
|
+
const { field, tiers, default: defaultTier } = this.opts;
|
|
2300
|
+
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
2301
|
+
if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
|
|
2302
|
+
if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
|
|
2303
|
+
if (!tierKey) {
|
|
2304
|
+
throw httpError(400, `Missing required field '${field}' for tier pricing`);
|
|
2305
|
+
}
|
|
2306
|
+
throw httpError(
|
|
2307
|
+
400,
|
|
2308
|
+
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
async challengeQuote(body) {
|
|
2312
|
+
if (body !== void 0) {
|
|
2313
|
+
try {
|
|
2314
|
+
return await this.quote(body);
|
|
2315
|
+
} catch {
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return this.maxTierPrice();
|
|
2319
|
+
}
|
|
2320
|
+
describe() {
|
|
2321
|
+
return {
|
|
2322
|
+
mode: "tiered",
|
|
2323
|
+
tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
|
|
2324
|
+
key,
|
|
2325
|
+
price: tier.price,
|
|
2326
|
+
...tier.label !== void 0 ? { label: tier.label } : {}
|
|
2327
|
+
})),
|
|
2328
|
+
...this.opts.default !== void 0 ? { default: this.opts.default } : {}
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
maxTierPrice() {
|
|
2332
|
+
let max = "0";
|
|
2333
|
+
for (const tier of Object.values(this.opts.tiers)) {
|
|
2334
|
+
if (compareDecimals(tier.price, max) > 0) max = tier.price;
|
|
2335
|
+
}
|
|
2336
|
+
return max;
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
function httpError(status, message) {
|
|
2340
|
+
return Object.assign(new Error(message), { status });
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// src/pricing/index.ts
|
|
2344
|
+
function selectPricing(raw, deps = {}) {
|
|
2345
|
+
if (raw == null) return null;
|
|
2346
|
+
if (typeof raw === "string") {
|
|
2347
|
+
return new FixedPricing(raw);
|
|
2348
|
+
}
|
|
2349
|
+
if (typeof raw === "function") {
|
|
2350
|
+
if (!deps.maxPrice) {
|
|
2351
|
+
throw new Error(`route '${deps.route ?? "unknown"}': dynamic pricing requires maxPrice`);
|
|
2352
|
+
}
|
|
2353
|
+
return new DynamicPricing({
|
|
2354
|
+
fn: raw,
|
|
2355
|
+
maxPrice: deps.maxPrice,
|
|
2356
|
+
minPrice: deps.minPrice,
|
|
2357
|
+
route: deps.route,
|
|
2358
|
+
alert: deps.alert
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
if (typeof raw === "object" && "tiers" in raw) {
|
|
2362
|
+
return new TieredPricing({
|
|
2363
|
+
field: raw.field,
|
|
2364
|
+
tiers: raw.tiers,
|
|
2365
|
+
default: raw.default,
|
|
2366
|
+
maxPrice: deps.maxPrice,
|
|
2367
|
+
minPrice: deps.minPrice
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2350
2373
|
// src/pipeline/flows/challenge-response.ts
|
|
2351
2374
|
var import_server5 = require("next/server");
|
|
2352
2375
|
|
|
@@ -3211,21 +3234,6 @@ async function createKvMppStore(kv, options) {
|
|
|
3211
3234
|
});
|
|
3212
3235
|
}
|
|
3213
3236
|
|
|
3214
|
-
// src/protocols/detect.ts
|
|
3215
|
-
function detectProtocol(request) {
|
|
3216
|
-
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
3217
|
-
return "x402";
|
|
3218
|
-
}
|
|
3219
|
-
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
3220
|
-
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
3221
|
-
return "mpp";
|
|
3222
|
-
}
|
|
3223
|
-
if (request.headers.get(HEADERS.SIWX)) {
|
|
3224
|
-
return "siwx";
|
|
3225
|
-
}
|
|
3226
|
-
return null;
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
3237
|
// src/protocols/mpp/siwx-mode.ts
|
|
3230
3238
|
var import_mppx3 = require("mppx");
|
|
3231
3239
|
async function verifyMppSiwx(request, mppx) {
|
|
@@ -3248,8 +3256,6 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
3248
3256
|
if (earlyBody.ok) {
|
|
3249
3257
|
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
3250
3258
|
if (validateErr) return validateErr;
|
|
3251
|
-
} else {
|
|
3252
|
-
return earlyBody.response;
|
|
3253
3259
|
}
|
|
3254
3260
|
}
|
|
3255
3261
|
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
@@ -3381,13 +3387,24 @@ async function runUnprotectedFlow(ctx) {
|
|
|
3381
3387
|
}
|
|
3382
3388
|
|
|
3383
3389
|
// src/pipeline/orchestrate.ts
|
|
3390
|
+
function shouldSkipQueryValidation(routeEntry, request) {
|
|
3391
|
+
if (routeEntry.pricing || routeEntry.authMode === "paid") {
|
|
3392
|
+
return selectIncomingStrategy(request, routeEntry.protocols) === null;
|
|
3393
|
+
}
|
|
3394
|
+
if (routeEntry.authMode === "siwx") {
|
|
3395
|
+
return !request.headers.get(HEADERS.SIWX) && detectProtocol(request) !== "mpp";
|
|
3396
|
+
}
|
|
3397
|
+
return false;
|
|
3398
|
+
}
|
|
3384
3399
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
3385
3400
|
return async (request) => {
|
|
3386
3401
|
await deps.initPromise;
|
|
3387
3402
|
const ctx = preflight(routeEntry, handler, deps, request);
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3403
|
+
if (!shouldSkipQueryValidation(routeEntry, request)) {
|
|
3404
|
+
const query = validateQuery(ctx);
|
|
3405
|
+
if (!query.ok) return query.response;
|
|
3406
|
+
ctx.query = query.data;
|
|
3407
|
+
}
|
|
3391
3408
|
if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
|
|
3392
3409
|
if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
|
|
3393
3410
|
if (routeEntry.pricing) return runPaidFlow(ctx);
|
|
@@ -3519,6 +3536,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3519
3536
|
`route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
|
|
3520
3537
|
);
|
|
3521
3538
|
}
|
|
3539
|
+
if (this.#s.siwxEnabled && billing === "metered") {
|
|
3540
|
+
throw new Error(
|
|
3541
|
+
`route '${this.#s.key}': Cannot combine .siwx() and .metered() \u2014 per-tick MPP billing has no entitlement model. Use .paid() or .upTo() with .siwx(), or drop .siwx() for metered routes.`
|
|
3542
|
+
);
|
|
3543
|
+
}
|
|
3522
3544
|
const next = this.fork();
|
|
3523
3545
|
next.#s.authMode = "paid";
|
|
3524
3546
|
next.#s.pricing = pricing;
|
|
@@ -3566,6 +3588,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3566
3588
|
`route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
|
|
3567
3589
|
);
|
|
3568
3590
|
}
|
|
3591
|
+
if (typeof pricing === "function" && next.#s.maxPrice === void 0) {
|
|
3592
|
+
throw new Error(
|
|
3593
|
+
`route '${this.#s.key}': dynamic pricing requires maxPrice \u2014 without it, bare probes would advertise a $0 challenge`
|
|
3594
|
+
);
|
|
3595
|
+
}
|
|
3569
3596
|
if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
|
|
3570
3597
|
throw new Error(
|
|
3571
3598
|
`route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
|
|
@@ -3585,12 +3612,16 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3585
3612
|
}
|
|
3586
3613
|
/**
|
|
3587
3614
|
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
3588
|
-
* control of a wallet via a signed challenge.
|
|
3589
|
-
*
|
|
3615
|
+
* control of a wallet via a signed challenge. Composes with `.paid()` and
|
|
3616
|
+
* `.upTo()` for pay-once-then-replay: the first request settles normally,
|
|
3617
|
+
* subsequent requests with a valid SIWX signature for the same wallet skip
|
|
3618
|
+
* payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
|
|
3619
|
+
* Mutually exclusive with `.metered()`.
|
|
3590
3620
|
*
|
|
3591
3621
|
* @example
|
|
3592
3622
|
* ```ts
|
|
3593
3623
|
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
3624
|
+
* router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
|
|
3594
3625
|
* ```
|
|
3595
3626
|
*/
|
|
3596
3627
|
siwx() {
|
|
@@ -3604,6 +3635,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3604
3635
|
`route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
|
|
3605
3636
|
);
|
|
3606
3637
|
}
|
|
3638
|
+
if (this.#s.billing === "metered") {
|
|
3639
|
+
throw new Error(
|
|
3640
|
+
`route '${this.#s.key}': Cannot combine .metered() and .siwx() \u2014 per-tick MPP billing has no entitlement model. Use .paid() or .upTo() with .siwx(), or drop .siwx() for metered routes.`
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3607
3643
|
const next = this.fork();
|
|
3608
3644
|
next.#s.siwxEnabled = true;
|
|
3609
3645
|
if (next.#s.authMode === "paid" || next.#s.pricing) {
|
package/dist/index.d.cts
CHANGED
|
@@ -588,12 +588,16 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
|
|
|
588
588
|
private applyPaid;
|
|
589
589
|
/**
|
|
590
590
|
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
591
|
-
* control of a wallet via a signed challenge.
|
|
592
|
-
*
|
|
591
|
+
* control of a wallet via a signed challenge. Composes with `.paid()` and
|
|
592
|
+
* `.upTo()` for pay-once-then-replay: the first request settles normally,
|
|
593
|
+
* subsequent requests with a valid SIWX signature for the same wallet skip
|
|
594
|
+
* payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
|
|
595
|
+
* Mutually exclusive with `.metered()`.
|
|
593
596
|
*
|
|
594
597
|
* @example
|
|
595
598
|
* ```ts
|
|
596
599
|
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
600
|
+
* router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
|
|
597
601
|
* ```
|
|
598
602
|
*/
|
|
599
603
|
siwx(): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody, Bill>;
|
package/dist/index.d.ts
CHANGED
|
@@ -588,12 +588,16 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
|
|
|
588
588
|
private applyPaid;
|
|
589
589
|
/**
|
|
590
590
|
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
591
|
-
* control of a wallet via a signed challenge.
|
|
592
|
-
*
|
|
591
|
+
* control of a wallet via a signed challenge. Composes with `.paid()` and
|
|
592
|
+
* `.upTo()` for pay-once-then-replay: the first request settles normally,
|
|
593
|
+
* subsequent requests with a valid SIWX signature for the same wallet skip
|
|
594
|
+
* payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
|
|
595
|
+
* Mutually exclusive with `.metered()`.
|
|
593
596
|
*
|
|
594
597
|
* @example
|
|
595
598
|
* ```ts
|
|
596
599
|
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
600
|
+
* router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
|
|
597
601
|
* ```
|
|
598
602
|
*/
|
|
599
603
|
siwx(): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody, Bill>;
|
package/dist/index.js
CHANGED
|
@@ -767,7 +767,11 @@ function invokePaidStatic(ctx, wallet, account, body, payment) {
|
|
|
767
767
|
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
|
|
768
768
|
}
|
|
769
769
|
function invokeUnauthed(ctx, wallet, account, body) {
|
|
770
|
-
|
|
770
|
+
const base = buildHandlerCtx(ctx, wallet, account, body, null);
|
|
771
|
+
if (ctx.routeEntry.billing !== "upto") return runHandler(ctx, base);
|
|
772
|
+
const uptoCtx = { ...base, charge: async () => {
|
|
773
|
+
} };
|
|
774
|
+
return runHandler(ctx, uptoCtx);
|
|
771
775
|
}
|
|
772
776
|
function buildHandlerCtx(ctx, wallet, account, body, payment) {
|
|
773
777
|
return {
|
|
@@ -1096,7 +1100,7 @@ async function resolveEarlyBody(args) {
|
|
|
1096
1100
|
}
|
|
1097
1101
|
const earlyClone = ctx.request.clone();
|
|
1098
1102
|
const earlyResult = await parseBody(ctx, earlyClone);
|
|
1099
|
-
if (!earlyResult.ok) return { ok:
|
|
1103
|
+
if (!earlyResult.ok) return { ok: true, earlyBody: void 0 };
|
|
1100
1104
|
const validateErr = await runValidate(ctx, earlyResult.data);
|
|
1101
1105
|
if (validateErr) return { ok: false, response: validateErr };
|
|
1102
1106
|
return { ok: true, earlyBody: earlyResult.data };
|
|
@@ -1148,17 +1152,6 @@ function protocolInitError(routeEntry, deps) {
|
|
|
1148
1152
|
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
1149
1153
|
}
|
|
1150
1154
|
|
|
1151
|
-
// src/pipeline/flows/api-key-only.ts
|
|
1152
|
-
async function runApiKeyOnlyFlow(ctx) {
|
|
1153
|
-
if (!ctx.routeEntry.apiKeyResolver) {
|
|
1154
|
-
return fail(ctx, 401, "API key resolver not configured");
|
|
1155
|
-
}
|
|
1156
|
-
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
1157
|
-
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
1158
|
-
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
|
|
1159
|
-
return runHandlerOnly(ctx, null, result.account);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
1155
|
// src/pricing/format.ts
|
|
1163
1156
|
var USDC_DECIMALS = 6;
|
|
1164
1157
|
var DECIMAL_RE = /^(\d+)(?:\.(\d+))?$/;
|
|
@@ -1214,169 +1207,6 @@ function multiplyDecimal(decimal, factor) {
|
|
|
1214
1207
|
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
1215
1208
|
}
|
|
1216
1209
|
|
|
1217
|
-
// src/pricing/dynamic.ts
|
|
1218
|
-
var DynamicPricing = class {
|
|
1219
|
-
constructor(opts) {
|
|
1220
|
-
this.opts = opts;
|
|
1221
|
-
}
|
|
1222
|
-
needsBody = true;
|
|
1223
|
-
async quote(body) {
|
|
1224
|
-
let priced;
|
|
1225
|
-
try {
|
|
1226
|
-
const raw = await this.opts.fn(body);
|
|
1227
|
-
priced = this.cap(raw, body);
|
|
1228
|
-
} catch (err) {
|
|
1229
|
-
if (err instanceof HttpError) throw err;
|
|
1230
|
-
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
1231
|
-
error: err instanceof Error ? err.stack : String(err),
|
|
1232
|
-
body
|
|
1233
|
-
});
|
|
1234
|
-
if (this.opts.maxPrice) {
|
|
1235
|
-
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
1236
|
-
return this.opts.maxPrice;
|
|
1237
|
-
}
|
|
1238
|
-
throw err;
|
|
1239
|
-
}
|
|
1240
|
-
if (!isPositiveDecimal(priced)) {
|
|
1241
|
-
throw new HttpError(
|
|
1242
|
-
`route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
|
|
1243
|
-
500
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
return priced;
|
|
1247
|
-
}
|
|
1248
|
-
challengeQuote(body) {
|
|
1249
|
-
if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
|
|
1250
|
-
return this.quote(body);
|
|
1251
|
-
}
|
|
1252
|
-
describe() {
|
|
1253
|
-
return {
|
|
1254
|
-
mode: "dynamic",
|
|
1255
|
-
min: this.opts.minPrice ?? "0",
|
|
1256
|
-
max: this.opts.maxPrice ?? "0"
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
cap(raw, body) {
|
|
1260
|
-
if (!this.opts.maxPrice) return raw;
|
|
1261
|
-
let overCap;
|
|
1262
|
-
try {
|
|
1263
|
-
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
1264
|
-
} catch {
|
|
1265
|
-
overCap = true;
|
|
1266
|
-
}
|
|
1267
|
-
if (overCap) {
|
|
1268
|
-
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
1269
|
-
calculated: raw,
|
|
1270
|
-
maxPrice: this.opts.maxPrice,
|
|
1271
|
-
body
|
|
1272
|
-
});
|
|
1273
|
-
return this.opts.maxPrice;
|
|
1274
|
-
}
|
|
1275
|
-
return raw;
|
|
1276
|
-
}
|
|
1277
|
-
alert(level, message, meta) {
|
|
1278
|
-
this.opts.alert?.(level, message, meta);
|
|
1279
|
-
}
|
|
1280
|
-
};
|
|
1281
|
-
function msg(err) {
|
|
1282
|
-
return err instanceof Error ? err.message : String(err);
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
// src/pricing/fixed.ts
|
|
1286
|
-
var FixedPricing = class {
|
|
1287
|
-
constructor(price) {
|
|
1288
|
-
this.price = price;
|
|
1289
|
-
}
|
|
1290
|
-
needsBody = false;
|
|
1291
|
-
quote() {
|
|
1292
|
-
return Promise.resolve(this.price);
|
|
1293
|
-
}
|
|
1294
|
-
challengeQuote() {
|
|
1295
|
-
return Promise.resolve(this.price);
|
|
1296
|
-
}
|
|
1297
|
-
describe() {
|
|
1298
|
-
return { mode: "fixed", amount: this.price };
|
|
1299
|
-
}
|
|
1300
|
-
};
|
|
1301
|
-
|
|
1302
|
-
// src/pricing/tiered.ts
|
|
1303
|
-
var TieredPricing = class {
|
|
1304
|
-
constructor(opts) {
|
|
1305
|
-
this.opts = opts;
|
|
1306
|
-
}
|
|
1307
|
-
needsBody = true;
|
|
1308
|
-
async quote(body) {
|
|
1309
|
-
const { field, tiers, default: defaultTier } = this.opts;
|
|
1310
|
-
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
1311
|
-
if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
|
|
1312
|
-
if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
|
|
1313
|
-
if (!tierKey) {
|
|
1314
|
-
throw httpError(400, `Missing required field '${field}' for tier pricing`);
|
|
1315
|
-
}
|
|
1316
|
-
throw httpError(
|
|
1317
|
-
400,
|
|
1318
|
-
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
1321
|
-
async challengeQuote(body) {
|
|
1322
|
-
if (body !== void 0) {
|
|
1323
|
-
try {
|
|
1324
|
-
return await this.quote(body);
|
|
1325
|
-
} catch {
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
return this.maxTierPrice();
|
|
1329
|
-
}
|
|
1330
|
-
describe() {
|
|
1331
|
-
return {
|
|
1332
|
-
mode: "tiered",
|
|
1333
|
-
tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
|
|
1334
|
-
key,
|
|
1335
|
-
price: tier.price,
|
|
1336
|
-
...tier.label !== void 0 ? { label: tier.label } : {}
|
|
1337
|
-
})),
|
|
1338
|
-
...this.opts.default !== void 0 ? { default: this.opts.default } : {}
|
|
1339
|
-
};
|
|
1340
|
-
}
|
|
1341
|
-
maxTierPrice() {
|
|
1342
|
-
let max = "0";
|
|
1343
|
-
for (const tier of Object.values(this.opts.tiers)) {
|
|
1344
|
-
if (compareDecimals(tier.price, max) > 0) max = tier.price;
|
|
1345
|
-
}
|
|
1346
|
-
return max;
|
|
1347
|
-
}
|
|
1348
|
-
};
|
|
1349
|
-
function httpError(status, message) {
|
|
1350
|
-
return Object.assign(new Error(message), { status });
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// src/pricing/index.ts
|
|
1354
|
-
function selectPricing(raw, deps = {}) {
|
|
1355
|
-
if (raw == null) return null;
|
|
1356
|
-
if (typeof raw === "string") {
|
|
1357
|
-
return new FixedPricing(raw);
|
|
1358
|
-
}
|
|
1359
|
-
if (typeof raw === "function") {
|
|
1360
|
-
return new DynamicPricing({
|
|
1361
|
-
fn: raw,
|
|
1362
|
-
maxPrice: deps.maxPrice,
|
|
1363
|
-
minPrice: deps.minPrice,
|
|
1364
|
-
route: deps.route,
|
|
1365
|
-
alert: deps.alert
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
if (typeof raw === "object" && "tiers" in raw) {
|
|
1369
|
-
return new TieredPricing({
|
|
1370
|
-
field: raw.field,
|
|
1371
|
-
tiers: raw.tiers,
|
|
1372
|
-
default: raw.default,
|
|
1373
|
-
maxPrice: deps.maxPrice,
|
|
1374
|
-
minPrice: deps.minPrice
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
1210
|
// src/protocols/mpp/credential.ts
|
|
1381
1211
|
import { Credential } from "mppx";
|
|
1382
1212
|
import { getAddress, isAddress } from "viem";
|
|
@@ -2306,6 +2136,199 @@ function getAllowedStrategies(allowed) {
|
|
|
2306
2136
|
return allowed.map((name) => STRATEGIES[name]);
|
|
2307
2137
|
}
|
|
2308
2138
|
|
|
2139
|
+
// src/protocols/detect.ts
|
|
2140
|
+
function detectProtocol(request) {
|
|
2141
|
+
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
2142
|
+
return "x402";
|
|
2143
|
+
}
|
|
2144
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
2145
|
+
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
2146
|
+
return "mpp";
|
|
2147
|
+
}
|
|
2148
|
+
if (request.headers.get(HEADERS.SIWX)) {
|
|
2149
|
+
return "siwx";
|
|
2150
|
+
}
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// src/pipeline/flows/api-key-only.ts
|
|
2155
|
+
async function runApiKeyOnlyFlow(ctx) {
|
|
2156
|
+
if (!ctx.routeEntry.apiKeyResolver) {
|
|
2157
|
+
return fail(ctx, 401, "API key resolver not configured");
|
|
2158
|
+
}
|
|
2159
|
+
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
2160
|
+
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
2161
|
+
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
|
|
2162
|
+
return runHandlerOnly(ctx, null, result.account);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// src/pricing/dynamic.ts
|
|
2166
|
+
var DynamicPricing = class {
|
|
2167
|
+
constructor(opts) {
|
|
2168
|
+
this.opts = opts;
|
|
2169
|
+
if (!isPositiveDecimal(opts.maxPrice)) {
|
|
2170
|
+
throw new Error(
|
|
2171
|
+
`route '${opts.route ?? "unknown"}': dynamic pricing requires a positive maxPrice, got '${opts.maxPrice}'`
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
needsBody = true;
|
|
2176
|
+
async quote(body) {
|
|
2177
|
+
let priced;
|
|
2178
|
+
try {
|
|
2179
|
+
const raw = await this.opts.fn(body);
|
|
2180
|
+
priced = this.cap(raw, body);
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
if (err instanceof HttpError) throw err;
|
|
2183
|
+
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
2184
|
+
error: err instanceof Error ? err.stack : String(err),
|
|
2185
|
+
body
|
|
2186
|
+
});
|
|
2187
|
+
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
2188
|
+
return this.opts.maxPrice;
|
|
2189
|
+
}
|
|
2190
|
+
if (!isPositiveDecimal(priced)) {
|
|
2191
|
+
throw new HttpError(
|
|
2192
|
+
`route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
|
|
2193
|
+
500
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
return priced;
|
|
2197
|
+
}
|
|
2198
|
+
challengeQuote(body) {
|
|
2199
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice);
|
|
2200
|
+
return this.quote(body);
|
|
2201
|
+
}
|
|
2202
|
+
describe() {
|
|
2203
|
+
return {
|
|
2204
|
+
mode: "dynamic",
|
|
2205
|
+
min: this.opts.minPrice ?? "0",
|
|
2206
|
+
max: this.opts.maxPrice
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
cap(raw, body) {
|
|
2210
|
+
let overCap;
|
|
2211
|
+
try {
|
|
2212
|
+
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
2213
|
+
} catch {
|
|
2214
|
+
overCap = true;
|
|
2215
|
+
}
|
|
2216
|
+
if (overCap) {
|
|
2217
|
+
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
2218
|
+
calculated: raw,
|
|
2219
|
+
maxPrice: this.opts.maxPrice,
|
|
2220
|
+
body
|
|
2221
|
+
});
|
|
2222
|
+
return this.opts.maxPrice;
|
|
2223
|
+
}
|
|
2224
|
+
return raw;
|
|
2225
|
+
}
|
|
2226
|
+
alert(level, message, meta) {
|
|
2227
|
+
this.opts.alert?.(level, message, meta);
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
function msg(err) {
|
|
2231
|
+
return err instanceof Error ? err.message : String(err);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// src/pricing/fixed.ts
|
|
2235
|
+
var FixedPricing = class {
|
|
2236
|
+
constructor(price) {
|
|
2237
|
+
this.price = price;
|
|
2238
|
+
}
|
|
2239
|
+
needsBody = false;
|
|
2240
|
+
quote() {
|
|
2241
|
+
return Promise.resolve(this.price);
|
|
2242
|
+
}
|
|
2243
|
+
challengeQuote() {
|
|
2244
|
+
return Promise.resolve(this.price);
|
|
2245
|
+
}
|
|
2246
|
+
describe() {
|
|
2247
|
+
return { mode: "fixed", amount: this.price };
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
|
|
2251
|
+
// src/pricing/tiered.ts
|
|
2252
|
+
var TieredPricing = class {
|
|
2253
|
+
constructor(opts) {
|
|
2254
|
+
this.opts = opts;
|
|
2255
|
+
}
|
|
2256
|
+
needsBody = true;
|
|
2257
|
+
async quote(body) {
|
|
2258
|
+
const { field, tiers, default: defaultTier } = this.opts;
|
|
2259
|
+
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
2260
|
+
if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
|
|
2261
|
+
if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
|
|
2262
|
+
if (!tierKey) {
|
|
2263
|
+
throw httpError(400, `Missing required field '${field}' for tier pricing`);
|
|
2264
|
+
}
|
|
2265
|
+
throw httpError(
|
|
2266
|
+
400,
|
|
2267
|
+
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
async challengeQuote(body) {
|
|
2271
|
+
if (body !== void 0) {
|
|
2272
|
+
try {
|
|
2273
|
+
return await this.quote(body);
|
|
2274
|
+
} catch {
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return this.maxTierPrice();
|
|
2278
|
+
}
|
|
2279
|
+
describe() {
|
|
2280
|
+
return {
|
|
2281
|
+
mode: "tiered",
|
|
2282
|
+
tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
|
|
2283
|
+
key,
|
|
2284
|
+
price: tier.price,
|
|
2285
|
+
...tier.label !== void 0 ? { label: tier.label } : {}
|
|
2286
|
+
})),
|
|
2287
|
+
...this.opts.default !== void 0 ? { default: this.opts.default } : {}
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
maxTierPrice() {
|
|
2291
|
+
let max = "0";
|
|
2292
|
+
for (const tier of Object.values(this.opts.tiers)) {
|
|
2293
|
+
if (compareDecimals(tier.price, max) > 0) max = tier.price;
|
|
2294
|
+
}
|
|
2295
|
+
return max;
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
function httpError(status, message) {
|
|
2299
|
+
return Object.assign(new Error(message), { status });
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// src/pricing/index.ts
|
|
2303
|
+
function selectPricing(raw, deps = {}) {
|
|
2304
|
+
if (raw == null) return null;
|
|
2305
|
+
if (typeof raw === "string") {
|
|
2306
|
+
return new FixedPricing(raw);
|
|
2307
|
+
}
|
|
2308
|
+
if (typeof raw === "function") {
|
|
2309
|
+
if (!deps.maxPrice) {
|
|
2310
|
+
throw new Error(`route '${deps.route ?? "unknown"}': dynamic pricing requires maxPrice`);
|
|
2311
|
+
}
|
|
2312
|
+
return new DynamicPricing({
|
|
2313
|
+
fn: raw,
|
|
2314
|
+
maxPrice: deps.maxPrice,
|
|
2315
|
+
minPrice: deps.minPrice,
|
|
2316
|
+
route: deps.route,
|
|
2317
|
+
alert: deps.alert
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
if (typeof raw === "object" && "tiers" in raw) {
|
|
2321
|
+
return new TieredPricing({
|
|
2322
|
+
field: raw.field,
|
|
2323
|
+
tiers: raw.tiers,
|
|
2324
|
+
default: raw.default,
|
|
2325
|
+
maxPrice: deps.maxPrice,
|
|
2326
|
+
minPrice: deps.minPrice
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2309
2332
|
// src/pipeline/flows/challenge-response.ts
|
|
2310
2333
|
import { NextResponse as NextResponse5 } from "next/server";
|
|
2311
2334
|
|
|
@@ -3170,21 +3193,6 @@ async function createKvMppStore(kv, options) {
|
|
|
3170
3193
|
});
|
|
3171
3194
|
}
|
|
3172
3195
|
|
|
3173
|
-
// src/protocols/detect.ts
|
|
3174
|
-
function detectProtocol(request) {
|
|
3175
|
-
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
3176
|
-
return "x402";
|
|
3177
|
-
}
|
|
3178
|
-
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
3179
|
-
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
3180
|
-
return "mpp";
|
|
3181
|
-
}
|
|
3182
|
-
if (request.headers.get(HEADERS.SIWX)) {
|
|
3183
|
-
return "siwx";
|
|
3184
|
-
}
|
|
3185
|
-
return null;
|
|
3186
|
-
}
|
|
3187
|
-
|
|
3188
3196
|
// src/protocols/mpp/siwx-mode.ts
|
|
3189
3197
|
import { Credential as Credential2 } from "mppx";
|
|
3190
3198
|
async function verifyMppSiwx(request, mppx) {
|
|
@@ -3207,8 +3215,6 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
3207
3215
|
if (earlyBody.ok) {
|
|
3208
3216
|
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
3209
3217
|
if (validateErr) return validateErr;
|
|
3210
|
-
} else {
|
|
3211
|
-
return earlyBody.response;
|
|
3212
3218
|
}
|
|
3213
3219
|
}
|
|
3214
3220
|
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
@@ -3340,13 +3346,24 @@ async function runUnprotectedFlow(ctx) {
|
|
|
3340
3346
|
}
|
|
3341
3347
|
|
|
3342
3348
|
// src/pipeline/orchestrate.ts
|
|
3349
|
+
function shouldSkipQueryValidation(routeEntry, request) {
|
|
3350
|
+
if (routeEntry.pricing || routeEntry.authMode === "paid") {
|
|
3351
|
+
return selectIncomingStrategy(request, routeEntry.protocols) === null;
|
|
3352
|
+
}
|
|
3353
|
+
if (routeEntry.authMode === "siwx") {
|
|
3354
|
+
return !request.headers.get(HEADERS.SIWX) && detectProtocol(request) !== "mpp";
|
|
3355
|
+
}
|
|
3356
|
+
return false;
|
|
3357
|
+
}
|
|
3343
3358
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
3344
3359
|
return async (request) => {
|
|
3345
3360
|
await deps.initPromise;
|
|
3346
3361
|
const ctx = preflight(routeEntry, handler, deps, request);
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3362
|
+
if (!shouldSkipQueryValidation(routeEntry, request)) {
|
|
3363
|
+
const query = validateQuery(ctx);
|
|
3364
|
+
if (!query.ok) return query.response;
|
|
3365
|
+
ctx.query = query.data;
|
|
3366
|
+
}
|
|
3350
3367
|
if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
|
|
3351
3368
|
if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
|
|
3352
3369
|
if (routeEntry.pricing) return runPaidFlow(ctx);
|
|
@@ -3478,6 +3495,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3478
3495
|
`route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
|
|
3479
3496
|
);
|
|
3480
3497
|
}
|
|
3498
|
+
if (this.#s.siwxEnabled && billing === "metered") {
|
|
3499
|
+
throw new Error(
|
|
3500
|
+
`route '${this.#s.key}': Cannot combine .siwx() and .metered() \u2014 per-tick MPP billing has no entitlement model. Use .paid() or .upTo() with .siwx(), or drop .siwx() for metered routes.`
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3481
3503
|
const next = this.fork();
|
|
3482
3504
|
next.#s.authMode = "paid";
|
|
3483
3505
|
next.#s.pricing = pricing;
|
|
@@ -3525,6 +3547,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3525
3547
|
`route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
|
|
3526
3548
|
);
|
|
3527
3549
|
}
|
|
3550
|
+
if (typeof pricing === "function" && next.#s.maxPrice === void 0) {
|
|
3551
|
+
throw new Error(
|
|
3552
|
+
`route '${this.#s.key}': dynamic pricing requires maxPrice \u2014 without it, bare probes would advertise a $0 challenge`
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3528
3555
|
if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
|
|
3529
3556
|
throw new Error(
|
|
3530
3557
|
`route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
|
|
@@ -3544,12 +3571,16 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3544
3571
|
}
|
|
3545
3572
|
/**
|
|
3546
3573
|
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
3547
|
-
* control of a wallet via a signed challenge.
|
|
3548
|
-
*
|
|
3574
|
+
* control of a wallet via a signed challenge. Composes with `.paid()` and
|
|
3575
|
+
* `.upTo()` for pay-once-then-replay: the first request settles normally,
|
|
3576
|
+
* subsequent requests with a valid SIWX signature for the same wallet skip
|
|
3577
|
+
* payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
|
|
3578
|
+
* Mutually exclusive with `.metered()`.
|
|
3549
3579
|
*
|
|
3550
3580
|
* @example
|
|
3551
3581
|
* ```ts
|
|
3552
3582
|
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
3583
|
+
* router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
|
|
3553
3584
|
* ```
|
|
3554
3585
|
*/
|
|
3555
3586
|
siwx() {
|
|
@@ -3563,6 +3594,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3563
3594
|
`route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
|
|
3564
3595
|
);
|
|
3565
3596
|
}
|
|
3597
|
+
if (this.#s.billing === "metered") {
|
|
3598
|
+
throw new Error(
|
|
3599
|
+
`route '${this.#s.key}': Cannot combine .metered() and .siwx() \u2014 per-tick MPP billing has no entitlement model. Use .paid() or .upTo() with .siwx(), or drop .siwx() for metered routes.`
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3566
3602
|
const next = this.fork();
|
|
3567
3603
|
next.#s.siwxEnabled = true;
|
|
3568
3604
|
if (next.#s.authMode === "paid" || next.#s.pricing) {
|