@agentcash/router 0.4.8 → 0.4.10
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/CLAUDE.md +2 -39
- package/.claude/skills/router-guide/SKILL.md +16 -15
- package/dist/index.cjs +148 -151
- package/dist/index.d.cts +17 -9
- package/dist/index.d.ts +17 -9
- package/dist/index.js +136 -139
- package/package.json +3 -3
package/.claude/CLAUDE.md
CHANGED
|
@@ -10,7 +10,7 @@ Protocol-agnostic route framework for Next.js App Router APIs with x402 payment,
|
|
|
10
10
|
4. **Observability is pluggable via RouterPlugin.** Zero boilerplate.
|
|
11
11
|
5. **The package owns the x402 server lifecycle.** Init, verify, settle.
|
|
12
12
|
6. **Convention over configuration.** Sane defaults for Base, USDC, exact scheme.
|
|
13
|
-
7. **Compose, don't reimplement.** Zero payment/auth protocol logic — delegates to `@x402
|
|
13
|
+
7. **Compose, don't reimplement.** Zero payment/auth protocol logic — delegates to `@x402/*`, `@coinbase/x402`, and `mppx`.
|
|
14
14
|
|
|
15
15
|
## Architecture
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ Protocol-agnostic route framework for Next.js App Router APIs with x402 payment,
|
|
|
25
25
|
- `src/plugin.ts` — Plugin hook system
|
|
26
26
|
- `src/server.ts` — x402 server initialization
|
|
27
27
|
- `src/auth/` — Auth modules (siwx.ts, api-key.ts, nonce.ts)
|
|
28
|
-
- `src/protocols/` — Protocol handlers (x402.ts,
|
|
28
|
+
- `src/protocols/` — Protocol handlers (x402.ts, detect.ts). MPP is handled via `mppx` high-level API (`Mppx.create` in index.ts)
|
|
29
29
|
- `src/discovery/` — Auto-generated endpoints (well-known.ts, openapi.ts)
|
|
30
30
|
|
|
31
31
|
## Auth Modes
|
|
@@ -187,43 +187,6 @@ pnpm typecheck # tsc --noEmit
|
|
|
187
187
|
pnpm check # format + lint + typecheck + build + test
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
## Releasing
|
|
191
|
-
|
|
192
|
-
**Release flow:** PR with version bump → merge → create GitHub Release → auto-publish to npm
|
|
193
|
-
|
|
194
|
-
### When doing work that should be released:
|
|
195
|
-
|
|
196
|
-
1. **Update `CHANGELOG.md`** — Add entry under new version heading with changes
|
|
197
|
-
2. **Bump version in `package.json`** — Match the changelog version
|
|
198
|
-
3. **Commit both** — e.g., `chore: bump to v0.6.0`
|
|
199
|
-
4. **Merge PR to main**
|
|
200
|
-
|
|
201
|
-
### To publish (human step):
|
|
202
|
-
|
|
203
|
-
1. Go to [GitHub Releases](https://github.com/Merit-Systems/agentcash-router/releases)
|
|
204
|
-
2. Click **Draft a new release**
|
|
205
|
-
3. Create tag: `v0.6.0` (must match package.json version)
|
|
206
|
-
4. Title: `v0.6.0`
|
|
207
|
-
5. Description: Copy from CHANGELOG.md or click "Generate release notes"
|
|
208
|
-
6. Click **Publish release**
|
|
209
|
-
|
|
210
|
-
The `publish.yml` workflow will:
|
|
211
|
-
- Run full test suite (`pnpm check`)
|
|
212
|
-
- Verify package.json version matches tag
|
|
213
|
-
- Publish to npm with `--access public`
|
|
214
|
-
|
|
215
|
-
### Version format
|
|
216
|
-
|
|
217
|
-
- **Patch** (`0.5.1`): Bug fixes, docs, internal changes
|
|
218
|
-
- **Minor** (`0.6.0`): New features, non-breaking additions
|
|
219
|
-
- **Major** (`1.0.0`): Breaking changes (holding until API stabilizes)
|
|
220
|
-
|
|
221
|
-
### Troubleshooting
|
|
222
|
-
|
|
223
|
-
- **Version mismatch error**: package.json version must exactly match the release tag (without `v` prefix)
|
|
224
|
-
- **Publish fails**: Check `NPM_TOKEN` secret is set and has write access to `@agentcash` scope
|
|
225
|
-
- **Tests fail**: Fix in a new PR, then re-create the release
|
|
226
|
-
|
|
227
190
|
## Development Record
|
|
228
191
|
|
|
229
192
|
The `.claude/` directory contains design docs, decision records, and bug analyses that document the reasoning behind the router's architecture. See `.claude/INDEX.md` for a table of contents.
|
|
@@ -31,7 +31,7 @@ Every paid API route in a Merit Systems service shared the same ~80-150 lines of
|
|
|
31
31
|
|
|
32
32
|
7. **Self-registering routes + validated barrel (Approach B).** Routes self-register via `.handler()` at import time. A barrel file imports all route modules. Discovery endpoints (`.wellKnown()`, `.openapi()`) validate that the barrel is complete by comparing registered routes against the `prices` map. For routes in separate handler files (e.g., Next.js `route.ts` files), use **discovery stubs** — lightweight registrations that provide metadata for discovery without the real handler. Guard stubs with `registry.has()` to avoid unnecessary overwrites.
|
|
33
33
|
|
|
34
|
-
8. **Both x402 and MPP ship from day one.** Dual-protocol support is not an afterthought. Routes declare `protocols: ['x402', 'mpp']` and the orchestration layer routes to the correct handler based on the request header. MPP uses
|
|
34
|
+
8. **Both x402 and MPP ship from day one.** Dual-protocol support is not an afterthought. Routes declare `protocols: ['x402', 'mpp']` and the orchestration layer routes to the correct handler based on the request header. MPP uses `mppx`'s high-level `Mppx.create()` API — the router creates an instance at init time and calls `mppx.charge({ amount })(request)` at request time. This returns either a 402 challenge or a 200 with `withReceipt()` for attaching the payment receipt header.
|
|
35
35
|
|
|
36
36
|
9. **`zod-openapi` for OpenAPI 3.1.** Zod schemas are the single source of truth for request/response types. OpenAPI docs are auto-generated from them. No manual spec maintenance.
|
|
37
37
|
|
|
@@ -80,7 +80,7 @@ Request in
|
|
|
80
80
|
→ protocol verify (x402 or MPP)
|
|
81
81
|
→ plugin.onPaymentVerified()
|
|
82
82
|
→ handler(ctx) → result
|
|
83
|
-
→ if status < 400: settle payment (x402) or
|
|
83
|
+
→ if status < 400: settle payment (x402) or withReceipt (MPP)
|
|
84
84
|
→ if provider configured: fireProviderQuota()
|
|
85
85
|
→ plugin.onResponse()
|
|
86
86
|
Response out
|
|
@@ -101,7 +101,6 @@ Response out
|
|
|
101
101
|
| `src/registry.ts` | `RouteRegistry` with barrel validation |
|
|
102
102
|
| `src/protocols/detect.ts` | Header-based protocol detection |
|
|
103
103
|
| `src/protocols/x402.ts` | x402 challenge/verify/settle wrappers |
|
|
104
|
-
| `src/protocols/mpp.ts` | MPP challenge/verify/receipt wrappers (uses mppx low-level primitives) |
|
|
105
104
|
| `src/server.ts` | x402 server initialization with retry |
|
|
106
105
|
| `src/auth/siwx.ts` | SIWX verification |
|
|
107
106
|
| `src/auth/api-key.ts` | API key verification |
|
|
@@ -159,7 +158,7 @@ TEMPO_RPC_URL=https://user:pass@rpc.mainnet.tempo.xyz # Authenticated Tempo RPC
|
|
|
159
158
|
|
|
160
159
|
**Tempo RPC requires authentication.** The default `rpc.tempo.xyz` returns 401. Get credentials from the Tempo team. The `rpcUrl` can be set in config or via `TEMPO_RPC_URL` env var (config takes precedence).
|
|
161
160
|
|
|
162
|
-
**Peer
|
|
161
|
+
**Peer dependency for MPP:** `mppx` is an optional peer dep. Required only when `protocols` includes `'mpp'`.
|
|
163
162
|
|
|
164
163
|
## Creating Routes
|
|
165
164
|
|
|
@@ -415,21 +414,26 @@ The type system (generic parameters `HasAuth`, `NeedsBody`, `HasBody`) prevents
|
|
|
415
414
|
- `.siwx()` is mutually exclusive with `.paid()`
|
|
416
415
|
- `.apiKey()` CAN compose with `.paid()`
|
|
417
416
|
|
|
418
|
-
## MPP Internals
|
|
417
|
+
## MPP Internals
|
|
419
418
|
|
|
420
|
-
The router uses mppx's
|
|
419
|
+
The router uses `mppx`'s high-level `Mppx.create()` API, which encapsulates the entire challenge-credential-receipt lifecycle.
|
|
421
420
|
|
|
422
|
-
|
|
421
|
+
### How it works
|
|
423
422
|
|
|
424
|
-
|
|
423
|
+
1. **Init time** (`src/index.ts`): `Mppx.create({ methods: [tempo.charge({ currency, recipient, rpcUrl })], secretKey })` creates a server instance. This is done inside the async `initPromise` IIFE alongside x402 server init.
|
|
425
424
|
|
|
426
|
-
|
|
425
|
+
2. **Challenge** (`build402` in orchestrate.ts): `deps.mppx.charge({ amount })(request)` returns `{ status: 402, challenge: Response }`. The `WWW-Authenticate` header is extracted from the challenge `Response` and set on the router's 402 response.
|
|
427
426
|
|
|
428
|
-
|
|
427
|
+
3. **Verify + Receipt** (MPP section in orchestrate.ts): `deps.mppx.charge({ amount })(request)` returns `{ status: 200, withReceipt }` when the credential is valid. `withReceipt(response)` returns a new `Response` with the `Payment-Receipt` header attached.
|
|
429
428
|
|
|
430
|
-
|
|
429
|
+
4. **Wallet extraction**: `Credential.fromRequest(request)` (from `mppx` core) extracts the credential, and `credential.source` contains the payer's DID.
|
|
431
430
|
|
|
432
|
-
|
|
431
|
+
### Key details
|
|
432
|
+
|
|
433
|
+
- **`withReceipt()` creates a new Response** — it does not mutate the original. The returned value must be used (cast to `NextResponse`).
|
|
434
|
+
- **`Credential.fromRequest()` is the only low-level mppx import** — used solely for wallet extraction after mppx has already verified the payment.
|
|
435
|
+
- **`mppx` is an optional peer dep** — routes using `protocols: ['mpp']` require it. The router's `OrchestrateDeps.mppx` is `null` when not configured.
|
|
436
|
+
- **Tempo RPC requires authentication.** The default `rpc.tempo.xyz` returns 401. Always provide `rpcUrl` or set `TEMPO_RPC_URL` env var with authenticated credentials.
|
|
433
437
|
|
|
434
438
|
## Registration-Time Validation
|
|
435
439
|
|
|
@@ -497,9 +501,6 @@ Barrel validation catches mismatches: keys in `prices` but not registered → er
|
|
|
497
501
|
| Route not in discovery docs | Missing barrel import | Import the route file in your barrel |
|
|
498
502
|
| Settlement not happening | Handler returned status >= 400 | Settlement is gated on success responses |
|
|
499
503
|
| MPP 401 `unauthorized: authentication required` | Using default unauthenticated Tempo RPC | Set `TEMPO_RPC_URL` env var or `mpp.rpcUrl` config with authenticated URL |
|
|
500
|
-
| MPP `Credential.fromRequest()` returns undefined | NextRequest header handling incompatibility | Router handles this via `toStandardRequest()` — if you see this, the router dist is stale |
|
|
501
|
-
| MPP verify returns `status: 'success'` but route returns 402 | Code checking `.valid` instead of `.status` | Verify returns a receipt `{ status, reference }`, not `{ valid, payer }` |
|
|
502
|
-
| MPP using wrong RPC URL after rebuild | Next.js webpack cache or stale pnpm link | Delete `.next/`, run `pnpm install` in the app to pick up new router dist |
|
|
503
504
|
| `route 'X' in prices map but not registered` | Discovery endpoint hit before route module loaded | Add barrel import to discovery route files |
|
|
504
505
|
| `mppx package is required` | mppx not installed | `pnpm add mppx` — it's an optional peer dep |
|
|
505
506
|
|
package/dist/index.cjs
CHANGED
|
@@ -123,7 +123,7 @@ var RouteRegistry = class {
|
|
|
123
123
|
};
|
|
124
124
|
|
|
125
125
|
// src/orchestrate.ts
|
|
126
|
-
var
|
|
126
|
+
var import_server2 = require("next/server");
|
|
127
127
|
|
|
128
128
|
// src/plugin.ts
|
|
129
129
|
function createDefaultContext(meta) {
|
|
@@ -366,6 +366,9 @@ function resolveMaxPrice(pricing) {
|
|
|
366
366
|
return max;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
// src/orchestrate.ts
|
|
370
|
+
var import_mppx = require("mppx");
|
|
371
|
+
|
|
369
372
|
// src/protocols/x402.ts
|
|
370
373
|
async function buildX402Challenge(server, routeEntry, request, price, payeeAddress, network, extensions) {
|
|
371
374
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
@@ -431,107 +434,6 @@ async function settleX402Payment(server, payload, requirements) {
|
|
|
431
434
|
return { encoded, result };
|
|
432
435
|
}
|
|
433
436
|
|
|
434
|
-
// src/protocols/mpp.ts
|
|
435
|
-
var import_mppx = require("mppx");
|
|
436
|
-
var import_server2 = require("mppx/server");
|
|
437
|
-
var import_tempo = require("mppx/tempo");
|
|
438
|
-
var import_viem = require("viem");
|
|
439
|
-
var import_chains = require("viem/chains");
|
|
440
|
-
function buildGetClient(rpcUrl) {
|
|
441
|
-
const url = rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
442
|
-
if (!url) return {};
|
|
443
|
-
return {
|
|
444
|
-
getClient: () => (0, import_viem.createClient)({ chain: import_chains.tempo, transport: (0, import_viem.http)(url) })
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
function toStandardRequest(request) {
|
|
448
|
-
if (request.constructor.name === "Request") {
|
|
449
|
-
return request;
|
|
450
|
-
}
|
|
451
|
-
return new Request(request.url, {
|
|
452
|
-
method: request.method,
|
|
453
|
-
headers: request.headers
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
var DEFAULT_DECIMALS = 6;
|
|
457
|
-
async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
458
|
-
const standardRequest = toStandardRequest(request);
|
|
459
|
-
const currency = mppConfig.currency;
|
|
460
|
-
const recipient = mppConfig.recipient ?? "";
|
|
461
|
-
const challenge = import_mppx.Challenge.fromMethod(import_tempo.Methods.charge, {
|
|
462
|
-
secretKey: mppConfig.secretKey,
|
|
463
|
-
realm: new URL(standardRequest.url).origin,
|
|
464
|
-
request: {
|
|
465
|
-
amount: price,
|
|
466
|
-
currency,
|
|
467
|
-
recipient,
|
|
468
|
-
decimals: DEFAULT_DECIMALS
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
return import_mppx.Challenge.serialize(challenge);
|
|
472
|
-
}
|
|
473
|
-
async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
|
|
474
|
-
const standardRequest = toStandardRequest(request);
|
|
475
|
-
const currency = mppConfig.currency;
|
|
476
|
-
const recipient = mppConfig.recipient ?? "";
|
|
477
|
-
try {
|
|
478
|
-
const authHeader = standardRequest.headers.get("Authorization");
|
|
479
|
-
if (!authHeader) {
|
|
480
|
-
console.error("[MPP] No Authorization header found");
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
const credential = import_mppx.Credential.fromRequest(standardRequest);
|
|
484
|
-
if (!credential?.challenge) {
|
|
485
|
-
console.error("[MPP] Invalid credential structure");
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
const isValid = import_mppx.Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
|
|
489
|
-
if (!isValid) {
|
|
490
|
-
console.error("[MPP] Challenge HMAC verification failed");
|
|
491
|
-
return { valid: false, payer: null };
|
|
492
|
-
}
|
|
493
|
-
const methodIntent = import_server2.tempo.charge({
|
|
494
|
-
...buildGetClient(mppConfig.rpcUrl)
|
|
495
|
-
});
|
|
496
|
-
const paymentRequest = {
|
|
497
|
-
amount: price,
|
|
498
|
-
currency,
|
|
499
|
-
recipient,
|
|
500
|
-
decimals: DEFAULT_DECIMALS
|
|
501
|
-
};
|
|
502
|
-
const receipt = await methodIntent.verify({
|
|
503
|
-
credential,
|
|
504
|
-
request: paymentRequest
|
|
505
|
-
});
|
|
506
|
-
if (!receipt || receipt.status !== "success") {
|
|
507
|
-
console.error("[MPP] Tempo verification failed:", receipt);
|
|
508
|
-
return { valid: false, payer: null };
|
|
509
|
-
}
|
|
510
|
-
const payer = receipt.reference ?? "";
|
|
511
|
-
return {
|
|
512
|
-
valid: true,
|
|
513
|
-
payer,
|
|
514
|
-
txHash: receipt.reference
|
|
515
|
-
};
|
|
516
|
-
} catch (error) {
|
|
517
|
-
console.error("[MPP] Credential verification error:", {
|
|
518
|
-
message: error instanceof Error ? error.message : String(error),
|
|
519
|
-
stack: error instanceof Error ? error.stack : void 0,
|
|
520
|
-
errorType: error?.constructor?.name
|
|
521
|
-
});
|
|
522
|
-
return null;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
function buildMPPReceipt(reference) {
|
|
526
|
-
const receipt = import_mppx.Receipt.from({
|
|
527
|
-
method: "tempo",
|
|
528
|
-
status: "success",
|
|
529
|
-
reference,
|
|
530
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
531
|
-
});
|
|
532
|
-
return import_mppx.Receipt.serialize(receipt);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
437
|
// src/auth/siwx.ts
|
|
536
438
|
var SIWX_ERROR_MESSAGES = {
|
|
537
439
|
siwx_missing_header: "Missing SIGN-IN-WITH-X header",
|
|
@@ -633,7 +535,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
633
535
|
firePluginResponse(deps, pluginCtx, meta, response);
|
|
634
536
|
}
|
|
635
537
|
function fail(status, message, meta, pluginCtx) {
|
|
636
|
-
const response =
|
|
538
|
+
const response = import_server2.NextResponse.json({ success: false, error: message }, { status });
|
|
637
539
|
firePluginResponse(deps, pluginCtx, meta, response);
|
|
638
540
|
return response;
|
|
639
541
|
}
|
|
@@ -692,7 +594,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
692
594
|
}
|
|
693
595
|
const protocol = detectProtocol(request);
|
|
694
596
|
let earlyBodyData;
|
|
695
|
-
const
|
|
597
|
+
const pricingNeedsBody = routeEntry.pricing != null && typeof routeEntry.pricing !== "string";
|
|
598
|
+
const needsEarlyParse = !protocol && routeEntry.bodySchema && (pricingNeedsBody || routeEntry.validateFn);
|
|
696
599
|
if (needsEarlyParse) {
|
|
697
600
|
const requestForPricing = request.clone();
|
|
698
601
|
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
@@ -775,7 +678,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
775
678
|
route: routeEntry.key
|
|
776
679
|
});
|
|
777
680
|
}
|
|
778
|
-
const response = new
|
|
681
|
+
const response = new import_server2.NextResponse(JSON.stringify(paymentRequired), {
|
|
779
682
|
status: 402,
|
|
780
683
|
headers: { "Content-Type": "application/json" }
|
|
781
684
|
});
|
|
@@ -785,7 +688,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
785
688
|
}
|
|
786
689
|
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
787
690
|
if (!siwx.valid) {
|
|
788
|
-
const response =
|
|
691
|
+
const response = import_server2.NextResponse.json(
|
|
789
692
|
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
790
693
|
{ status: 402 }
|
|
791
694
|
);
|
|
@@ -802,6 +705,21 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
802
705
|
return handleAuth(wallet, void 0);
|
|
803
706
|
}
|
|
804
707
|
if (!protocol || protocol === "siwx") {
|
|
708
|
+
if (routeEntry.pricing) {
|
|
709
|
+
const initErrors = routeEntry.protocols.map((p) => {
|
|
710
|
+
if (p === "x402" && deps.x402InitError) return `x402: ${deps.x402InitError}`;
|
|
711
|
+
if (p === "mpp" && deps.mppInitError) return `mpp: ${deps.mppInitError}`;
|
|
712
|
+
return null;
|
|
713
|
+
}).filter(Boolean);
|
|
714
|
+
if (initErrors.length > 0) {
|
|
715
|
+
return fail(
|
|
716
|
+
500,
|
|
717
|
+
`Payment protocol initialization failed. ${initErrors.join("; ")}`,
|
|
718
|
+
meta,
|
|
719
|
+
pluginCtx
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
805
723
|
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
806
724
|
}
|
|
807
725
|
const body = await parseBody(request, routeEntry);
|
|
@@ -829,9 +747,22 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
829
747
|
pluginCtx
|
|
830
748
|
);
|
|
831
749
|
}
|
|
750
|
+
if (!routeEntry.protocols.includes(protocol)) {
|
|
751
|
+
const accepted = routeEntry.protocols.join(", ") || "none";
|
|
752
|
+
console.warn(
|
|
753
|
+
`[router] ${routeEntry.key}: received ${protocol} payment but route accepts [${accepted}]`
|
|
754
|
+
);
|
|
755
|
+
return fail(
|
|
756
|
+
400,
|
|
757
|
+
`This route does not accept ${protocol} payments. Accepted protocols: ${accepted}`,
|
|
758
|
+
meta,
|
|
759
|
+
pluginCtx
|
|
760
|
+
);
|
|
761
|
+
}
|
|
832
762
|
if (protocol === "x402") {
|
|
833
763
|
if (!deps.x402Server) {
|
|
834
764
|
const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
|
|
765
|
+
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
835
766
|
return fail(500, reason, meta, pluginCtx);
|
|
836
767
|
}
|
|
837
768
|
const verify = await verifyX402Payment(
|
|
@@ -904,10 +835,37 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
904
835
|
return response;
|
|
905
836
|
}
|
|
906
837
|
if (protocol === "mpp") {
|
|
907
|
-
if (!deps.
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
838
|
+
if (!deps.mppx) {
|
|
839
|
+
const reason = deps.mppInitError ? `MPP initialization failed: ${deps.mppInitError}` : "MPP not initialized \u2014 ensure mppx is installed and mpp config (secretKey, currency, recipient) is correct";
|
|
840
|
+
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
841
|
+
return fail(500, reason, meta, pluginCtx);
|
|
842
|
+
}
|
|
843
|
+
let mppResult;
|
|
844
|
+
try {
|
|
845
|
+
mppResult = await deps.mppx.charge({ amount: price })(request);
|
|
846
|
+
} catch (err) {
|
|
847
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
848
|
+
console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
|
|
849
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
850
|
+
level: "critical",
|
|
851
|
+
message: `MPP charge failed: ${message}`,
|
|
852
|
+
route: routeEntry.key
|
|
853
|
+
});
|
|
854
|
+
return fail(500, `MPP payment processing failed: ${message}`, meta, pluginCtx);
|
|
855
|
+
}
|
|
856
|
+
if (mppResult.status === 402) {
|
|
857
|
+
console.warn(
|
|
858
|
+
`[router] ${routeEntry.key}: MPP credential present but charge() returned 402 \u2014 credential may be invalid, or check TEMPO_RPC_URL configuration`
|
|
859
|
+
);
|
|
860
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
861
|
+
level: "warn",
|
|
862
|
+
message: "MPP payment rejected despite credential present \u2014 possible config issue (TEMPO_RPC_URL)",
|
|
863
|
+
route: routeEntry.key
|
|
864
|
+
});
|
|
865
|
+
return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
866
|
+
}
|
|
867
|
+
const credential = import_mppx.Credential.fromRequest(request);
|
|
868
|
+
const wallet = (credential?.source ?? "").toLowerCase();
|
|
911
869
|
pluginCtx.setVerifiedWallet(wallet);
|
|
912
870
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
913
871
|
protocol: "mpp",
|
|
@@ -924,10 +882,9 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
924
882
|
body.data
|
|
925
883
|
);
|
|
926
884
|
if (response.status < 400) {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
885
|
+
const receiptResponse = mppResult.withReceipt(response);
|
|
886
|
+
finalize(receiptResponse, rawResult, meta, pluginCtx);
|
|
887
|
+
return receiptResponse;
|
|
931
888
|
}
|
|
932
889
|
finalize(response, rawResult, meta, pluginCtx);
|
|
933
890
|
return response;
|
|
@@ -942,7 +899,7 @@ async function parseBody(request, routeEntry) {
|
|
|
942
899
|
if (result.success) return { ok: true, data: result.data };
|
|
943
900
|
return {
|
|
944
901
|
ok: false,
|
|
945
|
-
response:
|
|
902
|
+
response: import_server2.NextResponse.json(
|
|
946
903
|
{ success: false, error: result.error, issues: result.issues },
|
|
947
904
|
{ status: 400 }
|
|
948
905
|
)
|
|
@@ -1001,7 +958,7 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
|
|
|
1001
958
|
});
|
|
1002
959
|
return { price: routeEntry.maxPrice };
|
|
1003
960
|
} else {
|
|
1004
|
-
const errorResponse =
|
|
961
|
+
const errorResponse = import_server2.NextResponse.json(
|
|
1005
962
|
{ success: false, error: "Price calculation failed" },
|
|
1006
963
|
{ status: 500 }
|
|
1007
964
|
);
|
|
@@ -1011,14 +968,14 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
|
|
|
1011
968
|
}
|
|
1012
969
|
}
|
|
1013
970
|
async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
1014
|
-
const response = new
|
|
971
|
+
const response = new import_server2.NextResponse(null, {
|
|
1015
972
|
status: 402,
|
|
1016
973
|
headers: {
|
|
1017
974
|
"Content-Type": "application/json"
|
|
1018
975
|
}
|
|
1019
976
|
});
|
|
1020
977
|
let challengePrice;
|
|
1021
|
-
if (bodyData !== void 0 && typeof routeEntry.pricing
|
|
978
|
+
if (bodyData !== void 0 && typeof routeEntry.pricing !== "string" && routeEntry.pricing != null) {
|
|
1022
979
|
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
1023
980
|
if ("error" in result) return result.error;
|
|
1024
981
|
challengePrice = result.price;
|
|
@@ -1069,12 +1026,13 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
|
1069
1026
|
});
|
|
1070
1027
|
}
|
|
1071
1028
|
}
|
|
1072
|
-
if (routeEntry.protocols.includes("mpp") && deps.
|
|
1029
|
+
if (routeEntry.protocols.includes("mpp") && deps.mppx) {
|
|
1073
1030
|
try {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1031
|
+
const result = await deps.mppx.charge({ amount: challengePrice })(request);
|
|
1032
|
+
if (result.status === 402) {
|
|
1033
|
+
const wwwAuth = result.challenge.headers.get("WWW-Authenticate");
|
|
1034
|
+
if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
|
|
1035
|
+
}
|
|
1078
1036
|
} catch (err) {
|
|
1079
1037
|
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1080
1038
|
level: "critical",
|
|
@@ -1341,7 +1299,7 @@ var RouteBuilder = class {
|
|
|
1341
1299
|
};
|
|
1342
1300
|
|
|
1343
1301
|
// src/discovery/well-known.ts
|
|
1344
|
-
var
|
|
1302
|
+
var import_server3 = require("next/server");
|
|
1345
1303
|
function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
1346
1304
|
let validated = false;
|
|
1347
1305
|
return async (_request) => {
|
|
@@ -1379,7 +1337,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1379
1337
|
if (instructions) {
|
|
1380
1338
|
body.instructions = instructions;
|
|
1381
1339
|
}
|
|
1382
|
-
return
|
|
1340
|
+
return import_server3.NextResponse.json(body, {
|
|
1383
1341
|
headers: {
|
|
1384
1342
|
"Access-Control-Allow-Origin": "*",
|
|
1385
1343
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -1390,12 +1348,12 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1390
1348
|
}
|
|
1391
1349
|
|
|
1392
1350
|
// src/discovery/openapi.ts
|
|
1393
|
-
var
|
|
1351
|
+
var import_server4 = require("next/server");
|
|
1394
1352
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
1395
1353
|
let cached = null;
|
|
1396
1354
|
let validated = false;
|
|
1397
1355
|
return async (_request) => {
|
|
1398
|
-
if (cached) return
|
|
1356
|
+
if (cached) return import_server4.NextResponse.json(cached);
|
|
1399
1357
|
if (!validated && pricesKeys) {
|
|
1400
1358
|
registry.validate(pricesKeys);
|
|
1401
1359
|
validated = true;
|
|
@@ -1422,7 +1380,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
|
1422
1380
|
tags: Array.from(tagSet).sort().map((name) => ({ name })),
|
|
1423
1381
|
paths
|
|
1424
1382
|
});
|
|
1425
|
-
return
|
|
1383
|
+
return import_server4.NextResponse.json(cached);
|
|
1426
1384
|
};
|
|
1427
1385
|
}
|
|
1428
1386
|
function deriveTag(routeKey) {
|
|
@@ -1491,21 +1449,30 @@ function createRouter(config) {
|
|
|
1491
1449
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1492
1450
|
const network = config.network ?? "eip155:8453";
|
|
1493
1451
|
const baseUrl = typeof globalThis.process !== "undefined" ? process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000" : "http://localhost:3000";
|
|
1494
|
-
if (config.protocols) {
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1452
|
+
if (config.protocols && config.protocols.length === 0) {
|
|
1453
|
+
throw new Error(
|
|
1454
|
+
"RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
let x402ConfigError;
|
|
1458
|
+
let mppConfigError;
|
|
1459
|
+
if ((!config.protocols || config.protocols.includes("x402")) && !config.payeeAddress) {
|
|
1460
|
+
x402ConfigError = "x402 requires payeeAddress in router config.";
|
|
1461
|
+
}
|
|
1462
|
+
if (config.protocols?.includes("mpp")) {
|
|
1463
|
+
if (!config.mpp) {
|
|
1464
|
+
mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
|
|
1465
|
+
} else if (!config.mpp.recipient && !config.payeeAddress) {
|
|
1466
|
+
mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
|
|
1467
|
+
} else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
|
|
1468
|
+
mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
|
|
1504
1469
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1470
|
+
}
|
|
1471
|
+
const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
|
|
1472
|
+
if (allConfigErrors.length > 0) {
|
|
1473
|
+
for (const err of allConfigErrors) console.error(`[router] ${err}`);
|
|
1474
|
+
if (process.env.NODE_ENV === "production") {
|
|
1475
|
+
throw new Error(allConfigErrors.join("\n"));
|
|
1509
1476
|
}
|
|
1510
1477
|
}
|
|
1511
1478
|
if (config.plugin?.init) {
|
|
@@ -1525,17 +1492,47 @@ function createRouter(config) {
|
|
|
1525
1492
|
nonceStore,
|
|
1526
1493
|
payeeAddress: config.payeeAddress,
|
|
1527
1494
|
network,
|
|
1528
|
-
|
|
1495
|
+
mppx: null
|
|
1529
1496
|
};
|
|
1530
1497
|
deps.initPromise = (async () => {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1498
|
+
if (x402ConfigError) {
|
|
1499
|
+
deps.x402InitError = x402ConfigError;
|
|
1500
|
+
} else {
|
|
1501
|
+
try {
|
|
1502
|
+
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
1503
|
+
const result = await createX402Server2(config);
|
|
1504
|
+
deps.x402Server = result.server;
|
|
1505
|
+
await result.initPromise;
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
deps.x402Server = null;
|
|
1508
|
+
deps.x402InitError = err instanceof Error ? err.message : String(err);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (mppConfigError) {
|
|
1512
|
+
deps.mppInitError = mppConfigError;
|
|
1513
|
+
} else if (config.mpp) {
|
|
1514
|
+
try {
|
|
1515
|
+
const { Mppx, tempo } = await import("mppx/server");
|
|
1516
|
+
const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
1517
|
+
deps.mppx = Mppx.create({
|
|
1518
|
+
methods: [
|
|
1519
|
+
tempo.charge({
|
|
1520
|
+
currency: config.mpp.currency,
|
|
1521
|
+
recipient: config.mpp.recipient ?? config.payeeAddress,
|
|
1522
|
+
getClient: async () => {
|
|
1523
|
+
const { createClient, http } = await import("viem");
|
|
1524
|
+
const { tempo: tempoChain } = await import("viem/chains");
|
|
1525
|
+
return createClient({ chain: tempoChain, transport: http(rpcUrl) });
|
|
1526
|
+
}
|
|
1527
|
+
})
|
|
1528
|
+
],
|
|
1529
|
+
secretKey: config.mpp.secretKey
|
|
1530
|
+
});
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
deps.mppx = null;
|
|
1533
|
+
deps.mppInitError = err instanceof Error ? err.message : String(err);
|
|
1534
|
+
console.error(`[router] MPP initialization failed: ${deps.mppInitError}`);
|
|
1535
|
+
}
|
|
1539
1536
|
}
|
|
1540
1537
|
})();
|
|
1541
1538
|
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
package/dist/index.d.cts
CHANGED
|
@@ -288,16 +288,22 @@ interface OrchestrateDeps {
|
|
|
288
288
|
x402Server: X402Server | null;
|
|
289
289
|
initPromise: Promise<void>;
|
|
290
290
|
x402InitError?: string;
|
|
291
|
+
mppInitError?: string;
|
|
291
292
|
plugin?: RouterPlugin;
|
|
292
293
|
nonceStore: NonceStore;
|
|
293
294
|
payeeAddress: string;
|
|
294
295
|
network: string;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
mppx?: {
|
|
297
|
+
charge: (options: {
|
|
298
|
+
amount: string;
|
|
299
|
+
}) => (input: Request) => Promise<{
|
|
300
|
+
status: 402;
|
|
301
|
+
challenge: Response;
|
|
302
|
+
} | {
|
|
303
|
+
status: 200;
|
|
304
|
+
withReceipt: (response: Response) => Response;
|
|
305
|
+
}>;
|
|
306
|
+
} | null;
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
type True = true;
|
|
@@ -381,13 +387,15 @@ interface MonitorEntry {
|
|
|
381
387
|
warn?: number;
|
|
382
388
|
critical?: number;
|
|
383
389
|
}
|
|
384
|
-
interface ServiceRouter {
|
|
385
|
-
route(key:
|
|
390
|
+
interface ServiceRouter<TPriceKeys extends string = never> {
|
|
391
|
+
route<K extends string>(key: K): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
|
|
386
392
|
wellKnown(options?: WellKnownOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
387
393
|
openapi(options: OpenAPIOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
388
394
|
monitors(): MonitorEntry[];
|
|
389
395
|
registry: RouteRegistry;
|
|
390
396
|
}
|
|
391
|
-
declare function createRouter(config: RouterConfig
|
|
397
|
+
declare function createRouter<const P extends Record<string, string> = Record<string, never>>(config: RouterConfig & {
|
|
398
|
+
prices?: P;
|
|
399
|
+
}): ServiceRouter<Extract<keyof P, string>>;
|
|
392
400
|
|
|
393
401
|
export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type ErrorEvent, type HandlerContext, HttpError, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisNonceStore, createRouter };
|
package/dist/index.d.ts
CHANGED
|
@@ -288,16 +288,22 @@ interface OrchestrateDeps {
|
|
|
288
288
|
x402Server: X402Server | null;
|
|
289
289
|
initPromise: Promise<void>;
|
|
290
290
|
x402InitError?: string;
|
|
291
|
+
mppInitError?: string;
|
|
291
292
|
plugin?: RouterPlugin;
|
|
292
293
|
nonceStore: NonceStore;
|
|
293
294
|
payeeAddress: string;
|
|
294
295
|
network: string;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
mppx?: {
|
|
297
|
+
charge: (options: {
|
|
298
|
+
amount: string;
|
|
299
|
+
}) => (input: Request) => Promise<{
|
|
300
|
+
status: 402;
|
|
301
|
+
challenge: Response;
|
|
302
|
+
} | {
|
|
303
|
+
status: 200;
|
|
304
|
+
withReceipt: (response: Response) => Response;
|
|
305
|
+
}>;
|
|
306
|
+
} | null;
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
type True = true;
|
|
@@ -381,13 +387,15 @@ interface MonitorEntry {
|
|
|
381
387
|
warn?: number;
|
|
382
388
|
critical?: number;
|
|
383
389
|
}
|
|
384
|
-
interface ServiceRouter {
|
|
385
|
-
route(key:
|
|
390
|
+
interface ServiceRouter<TPriceKeys extends string = never> {
|
|
391
|
+
route<K extends string>(key: K): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
|
|
386
392
|
wellKnown(options?: WellKnownOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
387
393
|
openapi(options: OpenAPIOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
388
394
|
monitors(): MonitorEntry[];
|
|
389
395
|
registry: RouteRegistry;
|
|
390
396
|
}
|
|
391
|
-
declare function createRouter(config: RouterConfig
|
|
397
|
+
declare function createRouter<const P extends Record<string, string> = Record<string, never>>(config: RouterConfig & {
|
|
398
|
+
prices?: P;
|
|
399
|
+
}): ServiceRouter<Extract<keyof P, string>>;
|
|
392
400
|
|
|
393
401
|
export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type ErrorEvent, type HandlerContext, HttpError, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisNonceStore, createRouter };
|
package/dist/index.js
CHANGED
|
@@ -329,6 +329,9 @@ function resolveMaxPrice(pricing) {
|
|
|
329
329
|
return max;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
// src/orchestrate.ts
|
|
333
|
+
import { Credential } from "mppx";
|
|
334
|
+
|
|
332
335
|
// src/protocols/x402.ts
|
|
333
336
|
async function buildX402Challenge(server, routeEntry, request, price, payeeAddress, network, extensions) {
|
|
334
337
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
@@ -394,107 +397,6 @@ async function settleX402Payment(server, payload, requirements) {
|
|
|
394
397
|
return { encoded, result };
|
|
395
398
|
}
|
|
396
399
|
|
|
397
|
-
// src/protocols/mpp.ts
|
|
398
|
-
import { Challenge, Credential, Receipt } from "mppx";
|
|
399
|
-
import { tempo } from "mppx/server";
|
|
400
|
-
import { Methods } from "mppx/tempo";
|
|
401
|
-
import { createClient, http } from "viem";
|
|
402
|
-
import { tempo as tempoChain } from "viem/chains";
|
|
403
|
-
function buildGetClient(rpcUrl) {
|
|
404
|
-
const url = rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
405
|
-
if (!url) return {};
|
|
406
|
-
return {
|
|
407
|
-
getClient: () => createClient({ chain: tempoChain, transport: http(url) })
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
function toStandardRequest(request) {
|
|
411
|
-
if (request.constructor.name === "Request") {
|
|
412
|
-
return request;
|
|
413
|
-
}
|
|
414
|
-
return new Request(request.url, {
|
|
415
|
-
method: request.method,
|
|
416
|
-
headers: request.headers
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
var DEFAULT_DECIMALS = 6;
|
|
420
|
-
async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
421
|
-
const standardRequest = toStandardRequest(request);
|
|
422
|
-
const currency = mppConfig.currency;
|
|
423
|
-
const recipient = mppConfig.recipient ?? "";
|
|
424
|
-
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
425
|
-
secretKey: mppConfig.secretKey,
|
|
426
|
-
realm: new URL(standardRequest.url).origin,
|
|
427
|
-
request: {
|
|
428
|
-
amount: price,
|
|
429
|
-
currency,
|
|
430
|
-
recipient,
|
|
431
|
-
decimals: DEFAULT_DECIMALS
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
return Challenge.serialize(challenge);
|
|
435
|
-
}
|
|
436
|
-
async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
|
|
437
|
-
const standardRequest = toStandardRequest(request);
|
|
438
|
-
const currency = mppConfig.currency;
|
|
439
|
-
const recipient = mppConfig.recipient ?? "";
|
|
440
|
-
try {
|
|
441
|
-
const authHeader = standardRequest.headers.get("Authorization");
|
|
442
|
-
if (!authHeader) {
|
|
443
|
-
console.error("[MPP] No Authorization header found");
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
446
|
-
const credential = Credential.fromRequest(standardRequest);
|
|
447
|
-
if (!credential?.challenge) {
|
|
448
|
-
console.error("[MPP] Invalid credential structure");
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
const isValid = Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
|
|
452
|
-
if (!isValid) {
|
|
453
|
-
console.error("[MPP] Challenge HMAC verification failed");
|
|
454
|
-
return { valid: false, payer: null };
|
|
455
|
-
}
|
|
456
|
-
const methodIntent = tempo.charge({
|
|
457
|
-
...buildGetClient(mppConfig.rpcUrl)
|
|
458
|
-
});
|
|
459
|
-
const paymentRequest = {
|
|
460
|
-
amount: price,
|
|
461
|
-
currency,
|
|
462
|
-
recipient,
|
|
463
|
-
decimals: DEFAULT_DECIMALS
|
|
464
|
-
};
|
|
465
|
-
const receipt = await methodIntent.verify({
|
|
466
|
-
credential,
|
|
467
|
-
request: paymentRequest
|
|
468
|
-
});
|
|
469
|
-
if (!receipt || receipt.status !== "success") {
|
|
470
|
-
console.error("[MPP] Tempo verification failed:", receipt);
|
|
471
|
-
return { valid: false, payer: null };
|
|
472
|
-
}
|
|
473
|
-
const payer = receipt.reference ?? "";
|
|
474
|
-
return {
|
|
475
|
-
valid: true,
|
|
476
|
-
payer,
|
|
477
|
-
txHash: receipt.reference
|
|
478
|
-
};
|
|
479
|
-
} catch (error) {
|
|
480
|
-
console.error("[MPP] Credential verification error:", {
|
|
481
|
-
message: error instanceof Error ? error.message : String(error),
|
|
482
|
-
stack: error instanceof Error ? error.stack : void 0,
|
|
483
|
-
errorType: error?.constructor?.name
|
|
484
|
-
});
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function buildMPPReceipt(reference) {
|
|
489
|
-
const receipt = Receipt.from({
|
|
490
|
-
method: "tempo",
|
|
491
|
-
status: "success",
|
|
492
|
-
reference,
|
|
493
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
494
|
-
});
|
|
495
|
-
return Receipt.serialize(receipt);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
400
|
// src/auth/siwx.ts
|
|
499
401
|
var SIWX_ERROR_MESSAGES = {
|
|
500
402
|
siwx_missing_header: "Missing SIGN-IN-WITH-X header",
|
|
@@ -655,7 +557,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
655
557
|
}
|
|
656
558
|
const protocol = detectProtocol(request);
|
|
657
559
|
let earlyBodyData;
|
|
658
|
-
const
|
|
560
|
+
const pricingNeedsBody = routeEntry.pricing != null && typeof routeEntry.pricing !== "string";
|
|
561
|
+
const needsEarlyParse = !protocol && routeEntry.bodySchema && (pricingNeedsBody || routeEntry.validateFn);
|
|
659
562
|
if (needsEarlyParse) {
|
|
660
563
|
const requestForPricing = request.clone();
|
|
661
564
|
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
@@ -765,6 +668,21 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
765
668
|
return handleAuth(wallet, void 0);
|
|
766
669
|
}
|
|
767
670
|
if (!protocol || protocol === "siwx") {
|
|
671
|
+
if (routeEntry.pricing) {
|
|
672
|
+
const initErrors = routeEntry.protocols.map((p) => {
|
|
673
|
+
if (p === "x402" && deps.x402InitError) return `x402: ${deps.x402InitError}`;
|
|
674
|
+
if (p === "mpp" && deps.mppInitError) return `mpp: ${deps.mppInitError}`;
|
|
675
|
+
return null;
|
|
676
|
+
}).filter(Boolean);
|
|
677
|
+
if (initErrors.length > 0) {
|
|
678
|
+
return fail(
|
|
679
|
+
500,
|
|
680
|
+
`Payment protocol initialization failed. ${initErrors.join("; ")}`,
|
|
681
|
+
meta,
|
|
682
|
+
pluginCtx
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
768
686
|
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
769
687
|
}
|
|
770
688
|
const body = await parseBody(request, routeEntry);
|
|
@@ -792,9 +710,22 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
792
710
|
pluginCtx
|
|
793
711
|
);
|
|
794
712
|
}
|
|
713
|
+
if (!routeEntry.protocols.includes(protocol)) {
|
|
714
|
+
const accepted = routeEntry.protocols.join(", ") || "none";
|
|
715
|
+
console.warn(
|
|
716
|
+
`[router] ${routeEntry.key}: received ${protocol} payment but route accepts [${accepted}]`
|
|
717
|
+
);
|
|
718
|
+
return fail(
|
|
719
|
+
400,
|
|
720
|
+
`This route does not accept ${protocol} payments. Accepted protocols: ${accepted}`,
|
|
721
|
+
meta,
|
|
722
|
+
pluginCtx
|
|
723
|
+
);
|
|
724
|
+
}
|
|
795
725
|
if (protocol === "x402") {
|
|
796
726
|
if (!deps.x402Server) {
|
|
797
727
|
const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
|
|
728
|
+
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
798
729
|
return fail(500, reason, meta, pluginCtx);
|
|
799
730
|
}
|
|
800
731
|
const verify = await verifyX402Payment(
|
|
@@ -867,10 +798,37 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
867
798
|
return response;
|
|
868
799
|
}
|
|
869
800
|
if (protocol === "mpp") {
|
|
870
|
-
if (!deps.
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
801
|
+
if (!deps.mppx) {
|
|
802
|
+
const reason = deps.mppInitError ? `MPP initialization failed: ${deps.mppInitError}` : "MPP not initialized \u2014 ensure mppx is installed and mpp config (secretKey, currency, recipient) is correct";
|
|
803
|
+
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
804
|
+
return fail(500, reason, meta, pluginCtx);
|
|
805
|
+
}
|
|
806
|
+
let mppResult;
|
|
807
|
+
try {
|
|
808
|
+
mppResult = await deps.mppx.charge({ amount: price })(request);
|
|
809
|
+
} catch (err) {
|
|
810
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
811
|
+
console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
|
|
812
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
813
|
+
level: "critical",
|
|
814
|
+
message: `MPP charge failed: ${message}`,
|
|
815
|
+
route: routeEntry.key
|
|
816
|
+
});
|
|
817
|
+
return fail(500, `MPP payment processing failed: ${message}`, meta, pluginCtx);
|
|
818
|
+
}
|
|
819
|
+
if (mppResult.status === 402) {
|
|
820
|
+
console.warn(
|
|
821
|
+
`[router] ${routeEntry.key}: MPP credential present but charge() returned 402 \u2014 credential may be invalid, or check TEMPO_RPC_URL configuration`
|
|
822
|
+
);
|
|
823
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
824
|
+
level: "warn",
|
|
825
|
+
message: "MPP payment rejected despite credential present \u2014 possible config issue (TEMPO_RPC_URL)",
|
|
826
|
+
route: routeEntry.key
|
|
827
|
+
});
|
|
828
|
+
return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
829
|
+
}
|
|
830
|
+
const credential = Credential.fromRequest(request);
|
|
831
|
+
const wallet = (credential?.source ?? "").toLowerCase();
|
|
874
832
|
pluginCtx.setVerifiedWallet(wallet);
|
|
875
833
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
876
834
|
protocol: "mpp",
|
|
@@ -887,10 +845,9 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
887
845
|
body.data
|
|
888
846
|
);
|
|
889
847
|
if (response.status < 400) {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
848
|
+
const receiptResponse = mppResult.withReceipt(response);
|
|
849
|
+
finalize(receiptResponse, rawResult, meta, pluginCtx);
|
|
850
|
+
return receiptResponse;
|
|
894
851
|
}
|
|
895
852
|
finalize(response, rawResult, meta, pluginCtx);
|
|
896
853
|
return response;
|
|
@@ -981,7 +938,7 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
|
981
938
|
}
|
|
982
939
|
});
|
|
983
940
|
let challengePrice;
|
|
984
|
-
if (bodyData !== void 0 && typeof routeEntry.pricing
|
|
941
|
+
if (bodyData !== void 0 && typeof routeEntry.pricing !== "string" && routeEntry.pricing != null) {
|
|
985
942
|
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
986
943
|
if ("error" in result) return result.error;
|
|
987
944
|
challengePrice = result.price;
|
|
@@ -1032,12 +989,13 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
|
1032
989
|
});
|
|
1033
990
|
}
|
|
1034
991
|
}
|
|
1035
|
-
if (routeEntry.protocols.includes("mpp") && deps.
|
|
992
|
+
if (routeEntry.protocols.includes("mpp") && deps.mppx) {
|
|
1036
993
|
try {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
994
|
+
const result = await deps.mppx.charge({ amount: challengePrice })(request);
|
|
995
|
+
if (result.status === 402) {
|
|
996
|
+
const wwwAuth = result.challenge.headers.get("WWW-Authenticate");
|
|
997
|
+
if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
|
|
998
|
+
}
|
|
1041
999
|
} catch (err) {
|
|
1042
1000
|
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1043
1001
|
level: "critical",
|
|
@@ -1454,21 +1412,30 @@ function createRouter(config) {
|
|
|
1454
1412
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1455
1413
|
const network = config.network ?? "eip155:8453";
|
|
1456
1414
|
const baseUrl = typeof globalThis.process !== "undefined" ? process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000" : "http://localhost:3000";
|
|
1457
|
-
if (config.protocols) {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1415
|
+
if (config.protocols && config.protocols.length === 0) {
|
|
1416
|
+
throw new Error(
|
|
1417
|
+
"RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
let x402ConfigError;
|
|
1421
|
+
let mppConfigError;
|
|
1422
|
+
if ((!config.protocols || config.protocols.includes("x402")) && !config.payeeAddress) {
|
|
1423
|
+
x402ConfigError = "x402 requires payeeAddress in router config.";
|
|
1424
|
+
}
|
|
1425
|
+
if (config.protocols?.includes("mpp")) {
|
|
1426
|
+
if (!config.mpp) {
|
|
1427
|
+
mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
|
|
1428
|
+
} else if (!config.mpp.recipient && !config.payeeAddress) {
|
|
1429
|
+
mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
|
|
1430
|
+
} else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
|
|
1431
|
+
mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
|
|
1467
1432
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1433
|
+
}
|
|
1434
|
+
const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
|
|
1435
|
+
if (allConfigErrors.length > 0) {
|
|
1436
|
+
for (const err of allConfigErrors) console.error(`[router] ${err}`);
|
|
1437
|
+
if (process.env.NODE_ENV === "production") {
|
|
1438
|
+
throw new Error(allConfigErrors.join("\n"));
|
|
1472
1439
|
}
|
|
1473
1440
|
}
|
|
1474
1441
|
if (config.plugin?.init) {
|
|
@@ -1488,17 +1455,47 @@ function createRouter(config) {
|
|
|
1488
1455
|
nonceStore,
|
|
1489
1456
|
payeeAddress: config.payeeAddress,
|
|
1490
1457
|
network,
|
|
1491
|
-
|
|
1458
|
+
mppx: null
|
|
1492
1459
|
};
|
|
1493
1460
|
deps.initPromise = (async () => {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1461
|
+
if (x402ConfigError) {
|
|
1462
|
+
deps.x402InitError = x402ConfigError;
|
|
1463
|
+
} else {
|
|
1464
|
+
try {
|
|
1465
|
+
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
1466
|
+
const result = await createX402Server2(config);
|
|
1467
|
+
deps.x402Server = result.server;
|
|
1468
|
+
await result.initPromise;
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
deps.x402Server = null;
|
|
1471
|
+
deps.x402InitError = err instanceof Error ? err.message : String(err);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (mppConfigError) {
|
|
1475
|
+
deps.mppInitError = mppConfigError;
|
|
1476
|
+
} else if (config.mpp) {
|
|
1477
|
+
try {
|
|
1478
|
+
const { Mppx, tempo } = await import("mppx/server");
|
|
1479
|
+
const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
1480
|
+
deps.mppx = Mppx.create({
|
|
1481
|
+
methods: [
|
|
1482
|
+
tempo.charge({
|
|
1483
|
+
currency: config.mpp.currency,
|
|
1484
|
+
recipient: config.mpp.recipient ?? config.payeeAddress,
|
|
1485
|
+
getClient: async () => {
|
|
1486
|
+
const { createClient, http } = await import("viem");
|
|
1487
|
+
const { tempo: tempoChain } = await import("viem/chains");
|
|
1488
|
+
return createClient({ chain: tempoChain, transport: http(rpcUrl) });
|
|
1489
|
+
}
|
|
1490
|
+
})
|
|
1491
|
+
],
|
|
1492
|
+
secretKey: config.mpp.secretKey
|
|
1493
|
+
});
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
deps.mppx = null;
|
|
1496
|
+
deps.mppInitError = err instanceof Error ? err.message : String(err);
|
|
1497
|
+
console.error(`[router] MPP initialization failed: ${deps.mppInitError}`);
|
|
1498
|
+
}
|
|
1502
1499
|
}
|
|
1503
1500
|
})();
|
|
1504
1501
|
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
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": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@x402/core": "^2.3.0",
|
|
29
29
|
"@x402/evm": "^2.3.0",
|
|
30
30
|
"@x402/extensions": "^2.3.0",
|
|
31
|
-
"mppx": "^0.2.
|
|
31
|
+
"mppx": "^0.2.4",
|
|
32
32
|
"next": ">=15.0.0",
|
|
33
33
|
"zod": "^4.0.0",
|
|
34
34
|
"zod-openapi": "^5.0.0"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@x402/evm": "^2.3.0",
|
|
48
48
|
"@x402/extensions": "^2.3.0",
|
|
49
49
|
"eslint": "^10.0.0",
|
|
50
|
+
"mppx": "^0.2.5",
|
|
50
51
|
"next": "^15.0.0",
|
|
51
|
-
"mppx": "^0.2.2",
|
|
52
52
|
"prettier": "^3.8.1",
|
|
53
53
|
"react": "^19.0.0",
|
|
54
54
|
"tsup": "^8.0.0",
|