@agentcash/router 1.9.2 → 1.9.4
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 -3
- package/dist/index.cjs +210 -183
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +208 -181
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
11
|
<strong>The fastest way to ship an API on x402 and MPP.</strong><br/>
|
|
12
|
-
x402 and MPP payments, compatible discovery, and minimal boilerplate. With @agentcash/router, agents on <a href="https://agentcash.dev">AgentCash</a> and across the agentic commerce ecosystem
|
|
12
|
+
x402 and MPP payments, compatible discovery, and minimal boilerplate. With @agentcash/router, agents on <a href="https://agentcash.dev">AgentCash</a> and across the agentic commerce ecosystem can call your endpoints from day one.
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
@@ -27,8 +27,6 @@ pnpm add @agentcash/router
|
|
|
27
27
|
pnpm add next zod # peer dependencies
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
`next` and `zod` are peer dependencies — the router shares your app's copy. Everything else (the x402 packages, `mppx`, `viem`, `zod-openapi`) is bundled as a regular dependency and installed automatically.
|
|
31
|
-
|
|
32
30
|
## Environment
|
|
33
31
|
|
|
34
32
|
The recommended entry point reads its config from `process.env`. A copy-paste `.env.example` lives at the repo root.
|
|
@@ -165,6 +163,20 @@ router.route({ path: 'gated' })
|
|
|
165
163
|
.handler(fn);
|
|
166
164
|
```
|
|
167
165
|
|
|
166
|
+
### `.siwx()` on paid routes. Pay once, identity-gated replays
|
|
167
|
+
|
|
168
|
+
`.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.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
router.route({ path: 'inbox' })
|
|
172
|
+
.paid('0.01').siwx() // first call pays $0.01, later calls present a SIWX sig instead
|
|
173
|
+
.handler(async ({ wallet }) => getInbox(wallet));
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`.metered()` is mutually exclusive with `.siwx()` — per-tick MPP billing has no entitlement model — and the builder throws at registration if you combine them.
|
|
177
|
+
|
|
178
|
+
> **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.
|
|
179
|
+
|
|
168
180
|
## Pricing
|
|
169
181
|
|
|
170
182
|
`.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,180 @@ function getAllowedStrategies(allowed) {
|
|
|
2347
2177
|
return allowed.map((name) => STRATEGIES[name]);
|
|
2348
2178
|
}
|
|
2349
2179
|
|
|
2180
|
+
// src/pipeline/flows/api-key-only.ts
|
|
2181
|
+
async function runApiKeyOnlyFlow(ctx) {
|
|
2182
|
+
if (!ctx.routeEntry.apiKeyResolver) {
|
|
2183
|
+
return fail(ctx, 401, "API key resolver not configured");
|
|
2184
|
+
}
|
|
2185
|
+
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
2186
|
+
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
2187
|
+
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
|
|
2188
|
+
return runHandlerOnly(ctx, null, result.account);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/pricing/dynamic.ts
|
|
2192
|
+
var DynamicPricing = class {
|
|
2193
|
+
constructor(opts) {
|
|
2194
|
+
this.opts = opts;
|
|
2195
|
+
}
|
|
2196
|
+
needsBody = true;
|
|
2197
|
+
async quote(body) {
|
|
2198
|
+
let priced;
|
|
2199
|
+
try {
|
|
2200
|
+
const raw = await this.opts.fn(body);
|
|
2201
|
+
priced = this.cap(raw, body);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
if (err instanceof HttpError) throw err;
|
|
2204
|
+
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
2205
|
+
error: err instanceof Error ? err.stack : String(err),
|
|
2206
|
+
body
|
|
2207
|
+
});
|
|
2208
|
+
if (this.opts.maxPrice) {
|
|
2209
|
+
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
2210
|
+
return this.opts.maxPrice;
|
|
2211
|
+
}
|
|
2212
|
+
throw err;
|
|
2213
|
+
}
|
|
2214
|
+
if (!isPositiveDecimal(priced)) {
|
|
2215
|
+
throw new HttpError(
|
|
2216
|
+
`route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
|
|
2217
|
+
500
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
return priced;
|
|
2221
|
+
}
|
|
2222
|
+
challengeQuote(body) {
|
|
2223
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
|
|
2224
|
+
return this.quote(body);
|
|
2225
|
+
}
|
|
2226
|
+
describe() {
|
|
2227
|
+
return {
|
|
2228
|
+
mode: "dynamic",
|
|
2229
|
+
min: this.opts.minPrice ?? "0",
|
|
2230
|
+
max: this.opts.maxPrice ?? "0"
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
cap(raw, body) {
|
|
2234
|
+
if (!this.opts.maxPrice) return raw;
|
|
2235
|
+
let overCap;
|
|
2236
|
+
try {
|
|
2237
|
+
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
2238
|
+
} catch {
|
|
2239
|
+
overCap = true;
|
|
2240
|
+
}
|
|
2241
|
+
if (overCap) {
|
|
2242
|
+
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
2243
|
+
calculated: raw,
|
|
2244
|
+
maxPrice: this.opts.maxPrice,
|
|
2245
|
+
body
|
|
2246
|
+
});
|
|
2247
|
+
return this.opts.maxPrice;
|
|
2248
|
+
}
|
|
2249
|
+
return raw;
|
|
2250
|
+
}
|
|
2251
|
+
alert(level, message, meta) {
|
|
2252
|
+
this.opts.alert?.(level, message, meta);
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
function msg(err) {
|
|
2256
|
+
return err instanceof Error ? err.message : String(err);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// src/pricing/fixed.ts
|
|
2260
|
+
var FixedPricing = class {
|
|
2261
|
+
constructor(price) {
|
|
2262
|
+
this.price = price;
|
|
2263
|
+
}
|
|
2264
|
+
needsBody = false;
|
|
2265
|
+
quote() {
|
|
2266
|
+
return Promise.resolve(this.price);
|
|
2267
|
+
}
|
|
2268
|
+
challengeQuote() {
|
|
2269
|
+
return Promise.resolve(this.price);
|
|
2270
|
+
}
|
|
2271
|
+
describe() {
|
|
2272
|
+
return { mode: "fixed", amount: this.price };
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2276
|
+
// src/pricing/tiered.ts
|
|
2277
|
+
var TieredPricing = class {
|
|
2278
|
+
constructor(opts) {
|
|
2279
|
+
this.opts = opts;
|
|
2280
|
+
}
|
|
2281
|
+
needsBody = true;
|
|
2282
|
+
async quote(body) {
|
|
2283
|
+
const { field, tiers, default: defaultTier } = this.opts;
|
|
2284
|
+
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
2285
|
+
if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
|
|
2286
|
+
if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
|
|
2287
|
+
if (!tierKey) {
|
|
2288
|
+
throw httpError(400, `Missing required field '${field}' for tier pricing`);
|
|
2289
|
+
}
|
|
2290
|
+
throw httpError(
|
|
2291
|
+
400,
|
|
2292
|
+
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
async challengeQuote(body) {
|
|
2296
|
+
if (body !== void 0) {
|
|
2297
|
+
try {
|
|
2298
|
+
return await this.quote(body);
|
|
2299
|
+
} catch {
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
return this.maxTierPrice();
|
|
2303
|
+
}
|
|
2304
|
+
describe() {
|
|
2305
|
+
return {
|
|
2306
|
+
mode: "tiered",
|
|
2307
|
+
tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
|
|
2308
|
+
key,
|
|
2309
|
+
price: tier.price,
|
|
2310
|
+
...tier.label !== void 0 ? { label: tier.label } : {}
|
|
2311
|
+
})),
|
|
2312
|
+
...this.opts.default !== void 0 ? { default: this.opts.default } : {}
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
maxTierPrice() {
|
|
2316
|
+
let max = "0";
|
|
2317
|
+
for (const tier of Object.values(this.opts.tiers)) {
|
|
2318
|
+
if (compareDecimals(tier.price, max) > 0) max = tier.price;
|
|
2319
|
+
}
|
|
2320
|
+
return max;
|
|
2321
|
+
}
|
|
2322
|
+
};
|
|
2323
|
+
function httpError(status, message) {
|
|
2324
|
+
return Object.assign(new Error(message), { status });
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// src/pricing/index.ts
|
|
2328
|
+
function selectPricing(raw, deps = {}) {
|
|
2329
|
+
if (raw == null) return null;
|
|
2330
|
+
if (typeof raw === "string") {
|
|
2331
|
+
return new FixedPricing(raw);
|
|
2332
|
+
}
|
|
2333
|
+
if (typeof raw === "function") {
|
|
2334
|
+
return new DynamicPricing({
|
|
2335
|
+
fn: raw,
|
|
2336
|
+
maxPrice: deps.maxPrice,
|
|
2337
|
+
minPrice: deps.minPrice,
|
|
2338
|
+
route: deps.route,
|
|
2339
|
+
alert: deps.alert
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
if (typeof raw === "object" && "tiers" in raw) {
|
|
2343
|
+
return new TieredPricing({
|
|
2344
|
+
field: raw.field,
|
|
2345
|
+
tiers: raw.tiers,
|
|
2346
|
+
default: raw.default,
|
|
2347
|
+
maxPrice: deps.maxPrice,
|
|
2348
|
+
minPrice: deps.minPrice
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2350
2354
|
// src/pipeline/flows/challenge-response.ts
|
|
2351
2355
|
var import_server5 = require("next/server");
|
|
2352
2356
|
|
|
@@ -3381,13 +3385,20 @@ async function runUnprotectedFlow(ctx) {
|
|
|
3381
3385
|
}
|
|
3382
3386
|
|
|
3383
3387
|
// src/pipeline/orchestrate.ts
|
|
3388
|
+
function shouldSkipQueryValidation(routeEntry, request) {
|
|
3389
|
+
const isPaidRoute = !!routeEntry.pricing || routeEntry.authMode === "paid";
|
|
3390
|
+
if (!isPaidRoute) return false;
|
|
3391
|
+
return selectIncomingStrategy(request, routeEntry.protocols) === null;
|
|
3392
|
+
}
|
|
3384
3393
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
3385
3394
|
return async (request) => {
|
|
3386
3395
|
await deps.initPromise;
|
|
3387
3396
|
const ctx = preflight(routeEntry, handler, deps, request);
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3397
|
+
if (!shouldSkipQueryValidation(routeEntry, request)) {
|
|
3398
|
+
const query = validateQuery(ctx);
|
|
3399
|
+
if (!query.ok) return query.response;
|
|
3400
|
+
ctx.query = query.data;
|
|
3401
|
+
}
|
|
3391
3402
|
if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
|
|
3392
3403
|
if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
|
|
3393
3404
|
if (routeEntry.pricing) return runPaidFlow(ctx);
|
|
@@ -3519,6 +3530,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3519
3530
|
`route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
|
|
3520
3531
|
);
|
|
3521
3532
|
}
|
|
3533
|
+
if (this.#s.siwxEnabled && billing === "metered") {
|
|
3534
|
+
throw new Error(
|
|
3535
|
+
`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.`
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3522
3538
|
const next = this.fork();
|
|
3523
3539
|
next.#s.authMode = "paid";
|
|
3524
3540
|
next.#s.pricing = pricing;
|
|
@@ -3585,12 +3601,16 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3585
3601
|
}
|
|
3586
3602
|
/**
|
|
3587
3603
|
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
3588
|
-
* control of a wallet via a signed challenge.
|
|
3589
|
-
*
|
|
3604
|
+
* control of a wallet via a signed challenge. Composes with `.paid()` and
|
|
3605
|
+
* `.upTo()` for pay-once-then-replay: the first request settles normally,
|
|
3606
|
+
* subsequent requests with a valid SIWX signature for the same wallet skip
|
|
3607
|
+
* payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
|
|
3608
|
+
* Mutually exclusive with `.metered()`.
|
|
3590
3609
|
*
|
|
3591
3610
|
* @example
|
|
3592
3611
|
* ```ts
|
|
3593
3612
|
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
3613
|
+
* router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
|
|
3594
3614
|
* ```
|
|
3595
3615
|
*/
|
|
3596
3616
|
siwx() {
|
|
@@ -3604,6 +3624,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3604
3624
|
`route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
|
|
3605
3625
|
);
|
|
3606
3626
|
}
|
|
3627
|
+
if (this.#s.billing === "metered") {
|
|
3628
|
+
throw new Error(
|
|
3629
|
+
`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.`
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3607
3632
|
const next = this.fork();
|
|
3608
3633
|
next.#s.siwxEnabled = true;
|
|
3609
3634
|
if (next.#s.authMode === "paid" || next.#s.pricing) {
|
|
@@ -4201,6 +4226,8 @@ function buildOperation(routeKey, entry, tag) {
|
|
|
4201
4226
|
operation.security = [{ siwx: [] }];
|
|
4202
4227
|
} else if (requiresApiKeyScheme) {
|
|
4203
4228
|
operation.security = [{ apiKey: [] }];
|
|
4229
|
+
} else if (entry.authMode === "unprotected") {
|
|
4230
|
+
operation.security = [];
|
|
4204
4231
|
}
|
|
4205
4232
|
if (entry.bodySchema) {
|
|
4206
4233
|
operation.requestBody = {
|
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,180 @@ function getAllowedStrategies(allowed) {
|
|
|
2306
2136
|
return allowed.map((name) => STRATEGIES[name]);
|
|
2307
2137
|
}
|
|
2308
2138
|
|
|
2139
|
+
// src/pipeline/flows/api-key-only.ts
|
|
2140
|
+
async function runApiKeyOnlyFlow(ctx) {
|
|
2141
|
+
if (!ctx.routeEntry.apiKeyResolver) {
|
|
2142
|
+
return fail(ctx, 401, "API key resolver not configured");
|
|
2143
|
+
}
|
|
2144
|
+
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
2145
|
+
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
2146
|
+
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
|
|
2147
|
+
return runHandlerOnly(ctx, null, result.account);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// src/pricing/dynamic.ts
|
|
2151
|
+
var DynamicPricing = class {
|
|
2152
|
+
constructor(opts) {
|
|
2153
|
+
this.opts = opts;
|
|
2154
|
+
}
|
|
2155
|
+
needsBody = true;
|
|
2156
|
+
async quote(body) {
|
|
2157
|
+
let priced;
|
|
2158
|
+
try {
|
|
2159
|
+
const raw = await this.opts.fn(body);
|
|
2160
|
+
priced = this.cap(raw, body);
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
if (err instanceof HttpError) throw err;
|
|
2163
|
+
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
2164
|
+
error: err instanceof Error ? err.stack : String(err),
|
|
2165
|
+
body
|
|
2166
|
+
});
|
|
2167
|
+
if (this.opts.maxPrice) {
|
|
2168
|
+
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
2169
|
+
return this.opts.maxPrice;
|
|
2170
|
+
}
|
|
2171
|
+
throw err;
|
|
2172
|
+
}
|
|
2173
|
+
if (!isPositiveDecimal(priced)) {
|
|
2174
|
+
throw new HttpError(
|
|
2175
|
+
`route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
|
|
2176
|
+
500
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
return priced;
|
|
2180
|
+
}
|
|
2181
|
+
challengeQuote(body) {
|
|
2182
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
|
|
2183
|
+
return this.quote(body);
|
|
2184
|
+
}
|
|
2185
|
+
describe() {
|
|
2186
|
+
return {
|
|
2187
|
+
mode: "dynamic",
|
|
2188
|
+
min: this.opts.minPrice ?? "0",
|
|
2189
|
+
max: this.opts.maxPrice ?? "0"
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
cap(raw, body) {
|
|
2193
|
+
if (!this.opts.maxPrice) return raw;
|
|
2194
|
+
let overCap;
|
|
2195
|
+
try {
|
|
2196
|
+
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
2197
|
+
} catch {
|
|
2198
|
+
overCap = true;
|
|
2199
|
+
}
|
|
2200
|
+
if (overCap) {
|
|
2201
|
+
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
2202
|
+
calculated: raw,
|
|
2203
|
+
maxPrice: this.opts.maxPrice,
|
|
2204
|
+
body
|
|
2205
|
+
});
|
|
2206
|
+
return this.opts.maxPrice;
|
|
2207
|
+
}
|
|
2208
|
+
return raw;
|
|
2209
|
+
}
|
|
2210
|
+
alert(level, message, meta) {
|
|
2211
|
+
this.opts.alert?.(level, message, meta);
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
function msg(err) {
|
|
2215
|
+
return err instanceof Error ? err.message : String(err);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// src/pricing/fixed.ts
|
|
2219
|
+
var FixedPricing = class {
|
|
2220
|
+
constructor(price) {
|
|
2221
|
+
this.price = price;
|
|
2222
|
+
}
|
|
2223
|
+
needsBody = false;
|
|
2224
|
+
quote() {
|
|
2225
|
+
return Promise.resolve(this.price);
|
|
2226
|
+
}
|
|
2227
|
+
challengeQuote() {
|
|
2228
|
+
return Promise.resolve(this.price);
|
|
2229
|
+
}
|
|
2230
|
+
describe() {
|
|
2231
|
+
return { mode: "fixed", amount: this.price };
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
// src/pricing/tiered.ts
|
|
2236
|
+
var TieredPricing = class {
|
|
2237
|
+
constructor(opts) {
|
|
2238
|
+
this.opts = opts;
|
|
2239
|
+
}
|
|
2240
|
+
needsBody = true;
|
|
2241
|
+
async quote(body) {
|
|
2242
|
+
const { field, tiers, default: defaultTier } = this.opts;
|
|
2243
|
+
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
2244
|
+
if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
|
|
2245
|
+
if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
|
|
2246
|
+
if (!tierKey) {
|
|
2247
|
+
throw httpError(400, `Missing required field '${field}' for tier pricing`);
|
|
2248
|
+
}
|
|
2249
|
+
throw httpError(
|
|
2250
|
+
400,
|
|
2251
|
+
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
async challengeQuote(body) {
|
|
2255
|
+
if (body !== void 0) {
|
|
2256
|
+
try {
|
|
2257
|
+
return await this.quote(body);
|
|
2258
|
+
} catch {
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
return this.maxTierPrice();
|
|
2262
|
+
}
|
|
2263
|
+
describe() {
|
|
2264
|
+
return {
|
|
2265
|
+
mode: "tiered",
|
|
2266
|
+
tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
|
|
2267
|
+
key,
|
|
2268
|
+
price: tier.price,
|
|
2269
|
+
...tier.label !== void 0 ? { label: tier.label } : {}
|
|
2270
|
+
})),
|
|
2271
|
+
...this.opts.default !== void 0 ? { default: this.opts.default } : {}
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
maxTierPrice() {
|
|
2275
|
+
let max = "0";
|
|
2276
|
+
for (const tier of Object.values(this.opts.tiers)) {
|
|
2277
|
+
if (compareDecimals(tier.price, max) > 0) max = tier.price;
|
|
2278
|
+
}
|
|
2279
|
+
return max;
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
function httpError(status, message) {
|
|
2283
|
+
return Object.assign(new Error(message), { status });
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// src/pricing/index.ts
|
|
2287
|
+
function selectPricing(raw, deps = {}) {
|
|
2288
|
+
if (raw == null) return null;
|
|
2289
|
+
if (typeof raw === "string") {
|
|
2290
|
+
return new FixedPricing(raw);
|
|
2291
|
+
}
|
|
2292
|
+
if (typeof raw === "function") {
|
|
2293
|
+
return new DynamicPricing({
|
|
2294
|
+
fn: raw,
|
|
2295
|
+
maxPrice: deps.maxPrice,
|
|
2296
|
+
minPrice: deps.minPrice,
|
|
2297
|
+
route: deps.route,
|
|
2298
|
+
alert: deps.alert
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
if (typeof raw === "object" && "tiers" in raw) {
|
|
2302
|
+
return new TieredPricing({
|
|
2303
|
+
field: raw.field,
|
|
2304
|
+
tiers: raw.tiers,
|
|
2305
|
+
default: raw.default,
|
|
2306
|
+
maxPrice: deps.maxPrice,
|
|
2307
|
+
minPrice: deps.minPrice
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2309
2313
|
// src/pipeline/flows/challenge-response.ts
|
|
2310
2314
|
import { NextResponse as NextResponse5 } from "next/server";
|
|
2311
2315
|
|
|
@@ -3340,13 +3344,20 @@ async function runUnprotectedFlow(ctx) {
|
|
|
3340
3344
|
}
|
|
3341
3345
|
|
|
3342
3346
|
// src/pipeline/orchestrate.ts
|
|
3347
|
+
function shouldSkipQueryValidation(routeEntry, request) {
|
|
3348
|
+
const isPaidRoute = !!routeEntry.pricing || routeEntry.authMode === "paid";
|
|
3349
|
+
if (!isPaidRoute) return false;
|
|
3350
|
+
return selectIncomingStrategy(request, routeEntry.protocols) === null;
|
|
3351
|
+
}
|
|
3343
3352
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
3344
3353
|
return async (request) => {
|
|
3345
3354
|
await deps.initPromise;
|
|
3346
3355
|
const ctx = preflight(routeEntry, handler, deps, request);
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3356
|
+
if (!shouldSkipQueryValidation(routeEntry, request)) {
|
|
3357
|
+
const query = validateQuery(ctx);
|
|
3358
|
+
if (!query.ok) return query.response;
|
|
3359
|
+
ctx.query = query.data;
|
|
3360
|
+
}
|
|
3350
3361
|
if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
|
|
3351
3362
|
if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
|
|
3352
3363
|
if (routeEntry.pricing) return runPaidFlow(ctx);
|
|
@@ -3478,6 +3489,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3478
3489
|
`route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
|
|
3479
3490
|
);
|
|
3480
3491
|
}
|
|
3492
|
+
if (this.#s.siwxEnabled && billing === "metered") {
|
|
3493
|
+
throw new Error(
|
|
3494
|
+
`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.`
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3481
3497
|
const next = this.fork();
|
|
3482
3498
|
next.#s.authMode = "paid";
|
|
3483
3499
|
next.#s.pricing = pricing;
|
|
@@ -3544,12 +3560,16 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3544
3560
|
}
|
|
3545
3561
|
/**
|
|
3546
3562
|
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
3547
|
-
* control of a wallet via a signed challenge.
|
|
3548
|
-
*
|
|
3563
|
+
* control of a wallet via a signed challenge. Composes with `.paid()` and
|
|
3564
|
+
* `.upTo()` for pay-once-then-replay: the first request settles normally,
|
|
3565
|
+
* subsequent requests with a valid SIWX signature for the same wallet skip
|
|
3566
|
+
* payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
|
|
3567
|
+
* Mutually exclusive with `.metered()`.
|
|
3549
3568
|
*
|
|
3550
3569
|
* @example
|
|
3551
3570
|
* ```ts
|
|
3552
3571
|
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
3572
|
+
* router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
|
|
3553
3573
|
* ```
|
|
3554
3574
|
*/
|
|
3555
3575
|
siwx() {
|
|
@@ -3563,6 +3583,11 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3563
3583
|
`route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
|
|
3564
3584
|
);
|
|
3565
3585
|
}
|
|
3586
|
+
if (this.#s.billing === "metered") {
|
|
3587
|
+
throw new Error(
|
|
3588
|
+
`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.`
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3566
3591
|
const next = this.fork();
|
|
3567
3592
|
next.#s.siwxEnabled = true;
|
|
3568
3593
|
if (next.#s.authMode === "paid" || next.#s.pricing) {
|
|
@@ -4160,6 +4185,8 @@ function buildOperation(routeKey, entry, tag) {
|
|
|
4160
4185
|
operation.security = [{ siwx: [] }];
|
|
4161
4186
|
} else if (requiresApiKeyScheme) {
|
|
4162
4187
|
operation.security = [{ apiKey: [] }];
|
|
4188
|
+
} else if (entry.authMode === "unprotected") {
|
|
4189
|
+
operation.security = [];
|
|
4163
4190
|
}
|
|
4164
4191
|
if (entry.bodySchema) {
|
|
4165
4192
|
operation.requestBody = {
|