@agentcash/router 1.2.3 → 1.2.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/.claude/skills/router-guide/SKILL.md +45 -0
- package/dist/index.cjs +58 -3
- package/dist/index.d.cts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +58 -3
- package/package.json +4 -4
|
@@ -104,6 +104,7 @@ Response out
|
|
|
104
104
|
| `src/server.ts` | x402 server initialization with retry |
|
|
105
105
|
| `src/auth/siwx.ts` | SIWX verification |
|
|
106
106
|
| `src/auth/api-key.ts` | API key verification |
|
|
107
|
+
| `src/upstash-rest.ts` | Minimal fetch-only Upstash REST client for `useDefaultStore` |
|
|
107
108
|
| `src/auth/nonce.ts` | `NonceStore` interface + `MemoryNonceStore` |
|
|
108
109
|
| `src/discovery/well-known.ts` | `.well-known/x402` generation |
|
|
109
110
|
| `src/discovery/openapi.ts` | OpenAPI 3.1 spec generation |
|
|
@@ -180,6 +181,7 @@ export const router = createRouter({
|
|
|
180
181
|
currency: '0x20c0000000000000000000000000000000000000', // PathUSD on Tempo
|
|
181
182
|
recipient: process.env.X402_PAYEE_ADDRESS!,
|
|
182
183
|
rpcUrl: process.env.TEMPO_RPC_URL, // falls back to TEMPO_RPC_URL env var
|
|
184
|
+
useDefaultStore: true, // auto-configures Upstash from KV_REST_API_URL + KV_REST_API_TOKEN
|
|
183
185
|
},
|
|
184
186
|
siwx: { nonceStore }, // custom nonce store
|
|
185
187
|
});
|
|
@@ -420,6 +422,48 @@ The type system (generic parameters `HasAuth`, `NeedsBody`, `HasBody`) prevents
|
|
|
420
422
|
- `.siwx()` is mutually exclusive with `.paid()`
|
|
421
423
|
- `.apiKey()` CAN compose with `.paid()`
|
|
422
424
|
|
|
425
|
+
## MPP Persistent Store
|
|
426
|
+
|
|
427
|
+
mppx uses a key-value store for transaction hash replay protection. Without a persistent store, `Store.memory()` is used — which is wiped on every cold start. This is unsafe on Vercel or any multi-instance deployment.
|
|
428
|
+
|
|
429
|
+
### Vercel (zero config)
|
|
430
|
+
|
|
431
|
+
Set `useDefaultStore: true` to auto-configure an Upstash-backed store from Vercel KV environment variables (`KV_REST_API_URL` + `KV_REST_API_TOKEN`). Uses raw `fetch` — no extra npm dependencies.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
createRouter({
|
|
435
|
+
mpp: {
|
|
436
|
+
secretKey: process.env.MPP_SECRET_KEY!,
|
|
437
|
+
currency: USDC,
|
|
438
|
+
useDefaultStore: true, // reads KV_REST_API_URL + KV_REST_API_TOKEN automatically
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Cloudflare / custom
|
|
444
|
+
|
|
445
|
+
Pass any `Store.Store` implementation directly via `mpp.store`:
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import { Store } from 'mppx'
|
|
449
|
+
|
|
450
|
+
createRouter({
|
|
451
|
+
mpp: {
|
|
452
|
+
secretKey: process.env.MPP_SECRET_KEY!,
|
|
453
|
+
currency: USDC,
|
|
454
|
+
store: Store.cloudflare(env.MY_KV_NAMESPACE),
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Available adapters from `mppx`: `Store.upstash(redis)`, `Store.cloudflare(kv)`, `Store.redis(client)`, `Store.memory()`, `Store.from(custom)`.
|
|
460
|
+
|
|
461
|
+
### Resolution order
|
|
462
|
+
|
|
463
|
+
1. Explicit `store` wins if provided
|
|
464
|
+
2. `useDefaultStore: true` creates an Upstash store from env vars
|
|
465
|
+
3. Neither → mppx defaults to `Store.memory()`
|
|
466
|
+
|
|
423
467
|
## MPP Internals
|
|
424
468
|
|
|
425
469
|
The router uses `mppx`'s high-level `Mppx.create()` API, which encapsulates the entire challenge-credential-receipt lifecycle.
|
|
@@ -509,6 +553,7 @@ Barrel validation catches mismatches: keys in `prices` but not registered → er
|
|
|
509
553
|
| MPP 401 `unauthorized: authentication required` | Using default unauthenticated Tempo RPC | Set `TEMPO_RPC_URL` env var or `mpp.rpcUrl` config with authenticated URL |
|
|
510
554
|
| `route 'X' in prices map but not registered` | Discovery endpoint hit before route module loaded | Add barrel import to discovery route files |
|
|
511
555
|
| `mppx package is required` | mppx not installed | `pnpm add mppx` — it's an optional peer dep |
|
|
556
|
+
| `useDefaultStore requires KV_REST_API_URL` | Vercel KV env vars not set | Add Vercel KV integration or set `KV_REST_API_URL` + `KV_REST_API_TOKEN` manually |
|
|
512
557
|
|
|
513
558
|
## Maintaining This Skill
|
|
514
559
|
|
package/dist/index.cjs
CHANGED
|
@@ -307,6 +307,47 @@ var init_server = __esm({
|
|
|
307
307
|
}
|
|
308
308
|
});
|
|
309
309
|
|
|
310
|
+
// src/upstash-rest.ts
|
|
311
|
+
var upstash_rest_exports = {};
|
|
312
|
+
__export(upstash_rest_exports, {
|
|
313
|
+
createUpstashRest: () => createUpstashRest
|
|
314
|
+
});
|
|
315
|
+
function createUpstashRest(url, token) {
|
|
316
|
+
const base = url.replace(/\/+$/, "");
|
|
317
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
318
|
+
return {
|
|
319
|
+
async get(key) {
|
|
320
|
+
const res = await fetch(`${base}/get/${key}`, { headers });
|
|
321
|
+
if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
|
|
322
|
+
const { result } = await res.json();
|
|
323
|
+
return result ?? null;
|
|
324
|
+
},
|
|
325
|
+
async set(key, value) {
|
|
326
|
+
const res = await fetch(`${base}`, {
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
329
|
+
body: JSON.stringify(["SET", key, JSON.stringify(value)])
|
|
330
|
+
});
|
|
331
|
+
if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
|
|
332
|
+
return await res.json();
|
|
333
|
+
},
|
|
334
|
+
async del(key) {
|
|
335
|
+
const res = await fetch(`${base}`, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
338
|
+
body: JSON.stringify(["DEL", key])
|
|
339
|
+
});
|
|
340
|
+
if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
|
|
341
|
+
return await res.json();
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
var init_upstash_rest = __esm({
|
|
346
|
+
"src/upstash-rest.ts"() {
|
|
347
|
+
"use strict";
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
310
351
|
// src/index.ts
|
|
311
352
|
var index_exports = {};
|
|
312
353
|
__export(index_exports, {
|
|
@@ -2474,8 +2515,21 @@ function createRouter(config) {
|
|
|
2474
2515
|
const getClient = async () => deps.tempoClient;
|
|
2475
2516
|
let feePayerAccount;
|
|
2476
2517
|
if (config.mpp.feePayerKey) {
|
|
2477
|
-
const {
|
|
2478
|
-
feePayerAccount =
|
|
2518
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
2519
|
+
feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
|
|
2520
|
+
}
|
|
2521
|
+
let resolvedStore = config.mpp.store;
|
|
2522
|
+
if (!resolvedStore && config.mpp.useDefaultStore) {
|
|
2523
|
+
const kvUrl = process.env.KV_REST_API_URL;
|
|
2524
|
+
const kvToken = process.env.KV_REST_API_TOKEN;
|
|
2525
|
+
if (!kvUrl || !kvToken) {
|
|
2526
|
+
throw new Error(
|
|
2527
|
+
"mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
const { Store } = await import("mppx");
|
|
2531
|
+
const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
|
|
2532
|
+
resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
|
|
2479
2533
|
}
|
|
2480
2534
|
deps.mppx = Mppx.create({
|
|
2481
2535
|
methods: [
|
|
@@ -2483,7 +2537,8 @@ function createRouter(config) {
|
|
|
2483
2537
|
currency: config.mpp.currency,
|
|
2484
2538
|
recipient: config.mpp.recipient ?? config.payeeAddress,
|
|
2485
2539
|
getClient,
|
|
2486
|
-
...feePayerAccount ? { feePayer: feePayerAccount } : {}
|
|
2540
|
+
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
2541
|
+
...resolvedStore ? { store: resolvedStore } : {}
|
|
2487
2542
|
})
|
|
2488
2543
|
],
|
|
2489
2544
|
secretKey: config.mpp.secretKey,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FacilitatorConfig } from '@x402/core/http';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
3
|
import { ZodType } from 'zod';
|
|
4
|
+
import { Store } from 'mppx';
|
|
4
5
|
import { PaymentRequirements, PaymentRequired, SettleResponse, Network } from '@x402/core/types';
|
|
5
6
|
import * as viem from 'viem';
|
|
6
7
|
export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from './siwx-BMlja_nt.cjs';
|
|
@@ -354,6 +355,36 @@ interface RouterConfig {
|
|
|
354
355
|
* Must be a hex-encoded private key (e.g. `0xabc123...`).
|
|
355
356
|
*/
|
|
356
357
|
feePayerKey?: string;
|
|
358
|
+
/**
|
|
359
|
+
* Persistent store for transaction hash replay protection.
|
|
360
|
+
*
|
|
361
|
+
* Without this, mppx defaults to `Store.memory()` which is wiped on every cold start —
|
|
362
|
+
* unsafe on Vercel or any multi-instance deployment. Pass `Store.upstash(redis)` or
|
|
363
|
+
* `Store.cloudflare(kv)` for a shared persistent store.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* import { Store } from 'mppx'
|
|
367
|
+
* store: Store.upstash({ get, set, del })
|
|
368
|
+
* store: Store.cloudflare(env.MY_KV_NAMESPACE)
|
|
369
|
+
*/
|
|
370
|
+
store?: Store.Store;
|
|
371
|
+
/**
|
|
372
|
+
* When `true`, auto-configures an Upstash-backed persistent store from Vercel KV
|
|
373
|
+
* environment variables (`KV_REST_API_URL` + `KV_REST_API_TOKEN`).
|
|
374
|
+
*
|
|
375
|
+
* Uses raw `fetch` against the Upstash REST API — no extra npm dependencies.
|
|
376
|
+
* Ignored when `store` is explicitly provided.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* createRouter({
|
|
380
|
+
* mpp: {
|
|
381
|
+
* secretKey: process.env.MPP_SECRET_KEY!,
|
|
382
|
+
* currency: USDC,
|
|
383
|
+
* useDefaultStore: true,
|
|
384
|
+
* }
|
|
385
|
+
* })
|
|
386
|
+
*/
|
|
387
|
+
useDefaultStore?: boolean;
|
|
357
388
|
};
|
|
358
389
|
/**
|
|
359
390
|
* Payment protocols to accept on auto-priced routes (those using the `prices` config).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FacilitatorConfig } from '@x402/core/http';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
3
|
import { ZodType } from 'zod';
|
|
4
|
+
import { Store } from 'mppx';
|
|
4
5
|
import { PaymentRequirements, PaymentRequired, SettleResponse, Network } from '@x402/core/types';
|
|
5
6
|
import * as viem from 'viem';
|
|
6
7
|
export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from './siwx-BMlja_nt.js';
|
|
@@ -354,6 +355,36 @@ interface RouterConfig {
|
|
|
354
355
|
* Must be a hex-encoded private key (e.g. `0xabc123...`).
|
|
355
356
|
*/
|
|
356
357
|
feePayerKey?: string;
|
|
358
|
+
/**
|
|
359
|
+
* Persistent store for transaction hash replay protection.
|
|
360
|
+
*
|
|
361
|
+
* Without this, mppx defaults to `Store.memory()` which is wiped on every cold start —
|
|
362
|
+
* unsafe on Vercel or any multi-instance deployment. Pass `Store.upstash(redis)` or
|
|
363
|
+
* `Store.cloudflare(kv)` for a shared persistent store.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* import { Store } from 'mppx'
|
|
367
|
+
* store: Store.upstash({ get, set, del })
|
|
368
|
+
* store: Store.cloudflare(env.MY_KV_NAMESPACE)
|
|
369
|
+
*/
|
|
370
|
+
store?: Store.Store;
|
|
371
|
+
/**
|
|
372
|
+
* When `true`, auto-configures an Upstash-backed persistent store from Vercel KV
|
|
373
|
+
* environment variables (`KV_REST_API_URL` + `KV_REST_API_TOKEN`).
|
|
374
|
+
*
|
|
375
|
+
* Uses raw `fetch` against the Upstash REST API — no extra npm dependencies.
|
|
376
|
+
* Ignored when `store` is explicitly provided.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* createRouter({
|
|
380
|
+
* mpp: {
|
|
381
|
+
* secretKey: process.env.MPP_SECRET_KEY!,
|
|
382
|
+
* currency: USDC,
|
|
383
|
+
* useDefaultStore: true,
|
|
384
|
+
* }
|
|
385
|
+
* })
|
|
386
|
+
*/
|
|
387
|
+
useDefaultStore?: boolean;
|
|
357
388
|
};
|
|
358
389
|
/**
|
|
359
390
|
* Payment protocols to accept on auto-priced routes (those using the `prices` config).
|
package/dist/index.js
CHANGED
|
@@ -285,6 +285,47 @@ var init_server = __esm({
|
|
|
285
285
|
}
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
// src/upstash-rest.ts
|
|
289
|
+
var upstash_rest_exports = {};
|
|
290
|
+
__export(upstash_rest_exports, {
|
|
291
|
+
createUpstashRest: () => createUpstashRest
|
|
292
|
+
});
|
|
293
|
+
function createUpstashRest(url, token) {
|
|
294
|
+
const base = url.replace(/\/+$/, "");
|
|
295
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
296
|
+
return {
|
|
297
|
+
async get(key) {
|
|
298
|
+
const res = await fetch(`${base}/get/${key}`, { headers });
|
|
299
|
+
if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
|
|
300
|
+
const { result } = await res.json();
|
|
301
|
+
return result ?? null;
|
|
302
|
+
},
|
|
303
|
+
async set(key, value) {
|
|
304
|
+
const res = await fetch(`${base}`, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
307
|
+
body: JSON.stringify(["SET", key, JSON.stringify(value)])
|
|
308
|
+
});
|
|
309
|
+
if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
|
|
310
|
+
return await res.json();
|
|
311
|
+
},
|
|
312
|
+
async del(key) {
|
|
313
|
+
const res = await fetch(`${base}`, {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
316
|
+
body: JSON.stringify(["DEL", key])
|
|
317
|
+
});
|
|
318
|
+
if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
|
|
319
|
+
return await res.json();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
var init_upstash_rest = __esm({
|
|
324
|
+
"src/upstash-rest.ts"() {
|
|
325
|
+
"use strict";
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
288
329
|
// src/registry.ts
|
|
289
330
|
var RouteRegistry = class {
|
|
290
331
|
routes = /* @__PURE__ */ new Map();
|
|
@@ -2435,8 +2476,21 @@ function createRouter(config) {
|
|
|
2435
2476
|
const getClient = async () => deps.tempoClient;
|
|
2436
2477
|
let feePayerAccount;
|
|
2437
2478
|
if (config.mpp.feePayerKey) {
|
|
2438
|
-
const {
|
|
2439
|
-
feePayerAccount =
|
|
2479
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
2480
|
+
feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
|
|
2481
|
+
}
|
|
2482
|
+
let resolvedStore = config.mpp.store;
|
|
2483
|
+
if (!resolvedStore && config.mpp.useDefaultStore) {
|
|
2484
|
+
const kvUrl = process.env.KV_REST_API_URL;
|
|
2485
|
+
const kvToken = process.env.KV_REST_API_TOKEN;
|
|
2486
|
+
if (!kvUrl || !kvToken) {
|
|
2487
|
+
throw new Error(
|
|
2488
|
+
"mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
const { Store } = await import("mppx");
|
|
2492
|
+
const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
|
|
2493
|
+
resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
|
|
2440
2494
|
}
|
|
2441
2495
|
deps.mppx = Mppx.create({
|
|
2442
2496
|
methods: [
|
|
@@ -2444,7 +2498,8 @@ function createRouter(config) {
|
|
|
2444
2498
|
currency: config.mpp.currency,
|
|
2445
2499
|
recipient: config.mpp.recipient ?? config.payeeAddress,
|
|
2446
2500
|
getClient,
|
|
2447
|
-
...feePayerAccount ? { feePayer: feePayerAccount } : {}
|
|
2501
|
+
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
2502
|
+
...resolvedStore ? { store: resolvedStore } : {}
|
|
2448
2503
|
})
|
|
2449
2504
|
],
|
|
2450
2505
|
secretKey: config.mpp.secretKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@x402/evm": "^2.3.0",
|
|
30
30
|
"@x402/extensions": "^2.3.0",
|
|
31
31
|
"@x402/svm": "2.3.0",
|
|
32
|
-
"mppx": "^0.4.
|
|
32
|
+
"mppx": "^0.4.10",
|
|
33
33
|
"next": ">=15.0.0",
|
|
34
34
|
"zod": "^4.0.0",
|
|
35
35
|
"zod-openapi": "^5.0.0"
|
|
@@ -61,14 +61,14 @@
|
|
|
61
61
|
"@x402/extensions": "^2.3.0",
|
|
62
62
|
"@x402/svm": "2.3.0",
|
|
63
63
|
"eslint": "^10.0.0",
|
|
64
|
-
"mppx": "^0.4.
|
|
64
|
+
"mppx": "^0.4.10",
|
|
65
65
|
"next": "^15.0.0",
|
|
66
66
|
"prettier": "^3.8.1",
|
|
67
67
|
"react": "^19.0.0",
|
|
68
68
|
"tsup": "^8.0.0",
|
|
69
69
|
"typescript": "^5.8.0",
|
|
70
70
|
"typescript-eslint": "^8.55.0",
|
|
71
|
-
"viem": "^2.47.
|
|
71
|
+
"viem": "^2.47.6",
|
|
72
72
|
"vitest": "^3.0.0",
|
|
73
73
|
"zod": "^4.0.0",
|
|
74
74
|
"zod-openapi": "^5.0.0"
|