@agentcash/router 0.4.7 → 0.4.9
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 +161 -153
- package/dist/index.d.cts +20 -9
- package/dist/index.d.ts +20 -9
- package/dist/index.js +149 -141
- 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_viem = require("viem");
|
|
438
|
-
var import_chains = require("viem/chains");
|
|
439
|
-
function buildGetClient(rpcUrl) {
|
|
440
|
-
const url = rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
441
|
-
if (!url) return {};
|
|
442
|
-
return {
|
|
443
|
-
getClient: () => (0, import_viem.createClient)({ chain: import_chains.tempo, transport: (0, import_viem.http)(url) })
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
function toStandardRequest(request) {
|
|
447
|
-
if (request.constructor.name === "Request") {
|
|
448
|
-
return request;
|
|
449
|
-
}
|
|
450
|
-
return new Request(request.url, {
|
|
451
|
-
method: request.method,
|
|
452
|
-
headers: request.headers
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
var DEFAULT_DECIMALS = 6;
|
|
456
|
-
async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
457
|
-
const standardRequest = toStandardRequest(request);
|
|
458
|
-
const currency = mppConfig.currency;
|
|
459
|
-
const recipient = mppConfig.recipient ?? "";
|
|
460
|
-
const methodIntent = import_server2.tempo.charge();
|
|
461
|
-
const challenge = import_mppx.Challenge.fromIntent(methodIntent, {
|
|
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",
|
|
@@ -1149,6 +1107,8 @@ var RouteBuilder = class {
|
|
|
1149
1107
|
/** @internal */
|
|
1150
1108
|
_maxPrice;
|
|
1151
1109
|
/** @internal */
|
|
1110
|
+
_minPrice;
|
|
1111
|
+
/** @internal */
|
|
1152
1112
|
_bodySchema;
|
|
1153
1113
|
/** @internal */
|
|
1154
1114
|
_querySchema;
|
|
@@ -1190,6 +1150,7 @@ var RouteBuilder = class {
|
|
|
1190
1150
|
next._pricing = pricing;
|
|
1191
1151
|
if (options?.protocols) next._protocols = options.protocols;
|
|
1192
1152
|
if (options?.maxPrice) next._maxPrice = options.maxPrice;
|
|
1153
|
+
if (options?.minPrice) next._minPrice = options.minPrice;
|
|
1193
1154
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
1194
1155
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
1195
1156
|
if (!tierKey) {
|
|
@@ -1326,6 +1287,7 @@ var RouteBuilder = class {
|
|
|
1326
1287
|
path: this._path,
|
|
1327
1288
|
method: this._method,
|
|
1328
1289
|
maxPrice: this._maxPrice,
|
|
1290
|
+
minPrice: this._minPrice,
|
|
1329
1291
|
apiKeyResolver: this._apiKeyResolver,
|
|
1330
1292
|
providerName: this._providerName,
|
|
1331
1293
|
providerConfig: this._providerConfig,
|
|
@@ -1337,7 +1299,7 @@ var RouteBuilder = class {
|
|
|
1337
1299
|
};
|
|
1338
1300
|
|
|
1339
1301
|
// src/discovery/well-known.ts
|
|
1340
|
-
var
|
|
1302
|
+
var import_server3 = require("next/server");
|
|
1341
1303
|
function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
1342
1304
|
let validated = false;
|
|
1343
1305
|
return async (_request) => {
|
|
@@ -1375,7 +1337,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1375
1337
|
if (instructions) {
|
|
1376
1338
|
body.instructions = instructions;
|
|
1377
1339
|
}
|
|
1378
|
-
return
|
|
1340
|
+
return import_server3.NextResponse.json(body, {
|
|
1379
1341
|
headers: {
|
|
1380
1342
|
"Access-Control-Allow-Origin": "*",
|
|
1381
1343
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -1386,12 +1348,12 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1386
1348
|
}
|
|
1387
1349
|
|
|
1388
1350
|
// src/discovery/openapi.ts
|
|
1389
|
-
var
|
|
1351
|
+
var import_server4 = require("next/server");
|
|
1390
1352
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
1391
1353
|
let cached = null;
|
|
1392
1354
|
let validated = false;
|
|
1393
1355
|
return async (_request) => {
|
|
1394
|
-
if (cached) return
|
|
1356
|
+
if (cached) return import_server4.NextResponse.json(cached);
|
|
1395
1357
|
if (!validated && pricesKeys) {
|
|
1396
1358
|
registry.validate(pricesKeys);
|
|
1397
1359
|
validated = true;
|
|
@@ -1418,7 +1380,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
|
1418
1380
|
tags: Array.from(tagSet).sort().map((name) => ({ name })),
|
|
1419
1381
|
paths
|
|
1420
1382
|
});
|
|
1421
|
-
return
|
|
1383
|
+
return import_server4.NextResponse.json(cached);
|
|
1422
1384
|
};
|
|
1423
1385
|
}
|
|
1424
1386
|
function deriveTag(routeKey) {
|
|
@@ -1428,9 +1390,16 @@ function buildOperation(routeKey, entry, tag) {
|
|
|
1428
1390
|
const protocols = entry.protocols.length > 0 ? entry.protocols : void 0;
|
|
1429
1391
|
let price;
|
|
1430
1392
|
if (typeof entry.pricing === "string") {
|
|
1431
|
-
price =
|
|
1393
|
+
price = entry.pricing;
|
|
1394
|
+
} else if (typeof entry.pricing === "object" && "tiers" in entry.pricing) {
|
|
1395
|
+
const tierPrices = Object.values(entry.pricing.tiers).map((t) => parseFloat(t.price));
|
|
1396
|
+
const min = Math.min(...tierPrices);
|
|
1397
|
+
const max = Math.max(...tierPrices);
|
|
1398
|
+
price = min === max ? String(min) : `${min}-${max}`;
|
|
1399
|
+
} else if (entry.minPrice && entry.maxPrice) {
|
|
1400
|
+
price = `${entry.minPrice}-${entry.maxPrice}`;
|
|
1432
1401
|
} else if (entry.maxPrice) {
|
|
1433
|
-
price =
|
|
1402
|
+
price = entry.maxPrice;
|
|
1434
1403
|
}
|
|
1435
1404
|
const operation = {
|
|
1436
1405
|
operationId: routeKey.replace(/\//g, "_"),
|
|
@@ -1480,21 +1449,30 @@ function createRouter(config) {
|
|
|
1480
1449
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1481
1450
|
const network = config.network ?? "eip155:8453";
|
|
1482
1451
|
const baseUrl = typeof globalThis.process !== "undefined" ? process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000" : "http://localhost:3000";
|
|
1483
|
-
if (config.protocols) {
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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.";
|
|
1493
1469
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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"));
|
|
1498
1476
|
}
|
|
1499
1477
|
}
|
|
1500
1478
|
if (config.plugin?.init) {
|
|
@@ -1514,17 +1492,47 @@ function createRouter(config) {
|
|
|
1514
1492
|
nonceStore,
|
|
1515
1493
|
payeeAddress: config.payeeAddress,
|
|
1516
1494
|
network,
|
|
1517
|
-
|
|
1495
|
+
mppx: null
|
|
1518
1496
|
};
|
|
1519
1497
|
deps.initPromise = (async () => {
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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
|
+
}
|
|
1528
1536
|
}
|
|
1529
1537
|
})();
|
|
1530
1538
|
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
package/dist/index.d.cts
CHANGED
|
@@ -170,6 +170,7 @@ type PricingConfig<TBody = unknown> = string | ((body: TBody) => string | Promis
|
|
|
170
170
|
interface PaidOptions {
|
|
171
171
|
protocols?: ProtocolType[];
|
|
172
172
|
maxPrice?: string;
|
|
173
|
+
minPrice?: string;
|
|
173
174
|
}
|
|
174
175
|
interface HandlerContext<TBody = undefined, TQuery = undefined> {
|
|
175
176
|
body: TBody;
|
|
@@ -218,6 +219,7 @@ interface RouteEntry {
|
|
|
218
219
|
path?: string;
|
|
219
220
|
method: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
|
|
220
221
|
maxPrice?: string;
|
|
222
|
+
minPrice?: string;
|
|
221
223
|
apiKeyResolver?: (key: string) => unknown | Promise<unknown>;
|
|
222
224
|
providerName?: string;
|
|
223
225
|
providerConfig?: ProviderConfig;
|
|
@@ -286,16 +288,22 @@ interface OrchestrateDeps {
|
|
|
286
288
|
x402Server: X402Server | null;
|
|
287
289
|
initPromise: Promise<void>;
|
|
288
290
|
x402InitError?: string;
|
|
291
|
+
mppInitError?: string;
|
|
289
292
|
plugin?: RouterPlugin;
|
|
290
293
|
nonceStore: NonceStore;
|
|
291
294
|
payeeAddress: string;
|
|
292
295
|
network: string;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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;
|
|
299
307
|
}
|
|
300
308
|
|
|
301
309
|
type True = true;
|
|
@@ -308,6 +316,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
308
316
|
/** @internal */ _pricing: PricingConfig | undefined;
|
|
309
317
|
/** @internal */ _protocols: ProtocolType[];
|
|
310
318
|
/** @internal */ _maxPrice: string | undefined;
|
|
319
|
+
/** @internal */ _minPrice: string | undefined;
|
|
311
320
|
/** @internal */ _bodySchema: ZodType | undefined;
|
|
312
321
|
/** @internal */ _querySchema: ZodType | undefined;
|
|
313
322
|
/** @internal */ _outputSchema: ZodType | undefined;
|
|
@@ -378,13 +387,15 @@ interface MonitorEntry {
|
|
|
378
387
|
warn?: number;
|
|
379
388
|
critical?: number;
|
|
380
389
|
}
|
|
381
|
-
interface ServiceRouter {
|
|
382
|
-
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>;
|
|
383
392
|
wellKnown(options?: WellKnownOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
384
393
|
openapi(options: OpenAPIOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
385
394
|
monitors(): MonitorEntry[];
|
|
386
395
|
registry: RouteRegistry;
|
|
387
396
|
}
|
|
388
|
-
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>>;
|
|
389
400
|
|
|
390
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
|
@@ -170,6 +170,7 @@ type PricingConfig<TBody = unknown> = string | ((body: TBody) => string | Promis
|
|
|
170
170
|
interface PaidOptions {
|
|
171
171
|
protocols?: ProtocolType[];
|
|
172
172
|
maxPrice?: string;
|
|
173
|
+
minPrice?: string;
|
|
173
174
|
}
|
|
174
175
|
interface HandlerContext<TBody = undefined, TQuery = undefined> {
|
|
175
176
|
body: TBody;
|
|
@@ -218,6 +219,7 @@ interface RouteEntry {
|
|
|
218
219
|
path?: string;
|
|
219
220
|
method: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
|
|
220
221
|
maxPrice?: string;
|
|
222
|
+
minPrice?: string;
|
|
221
223
|
apiKeyResolver?: (key: string) => unknown | Promise<unknown>;
|
|
222
224
|
providerName?: string;
|
|
223
225
|
providerConfig?: ProviderConfig;
|
|
@@ -286,16 +288,22 @@ interface OrchestrateDeps {
|
|
|
286
288
|
x402Server: X402Server | null;
|
|
287
289
|
initPromise: Promise<void>;
|
|
288
290
|
x402InitError?: string;
|
|
291
|
+
mppInitError?: string;
|
|
289
292
|
plugin?: RouterPlugin;
|
|
290
293
|
nonceStore: NonceStore;
|
|
291
294
|
payeeAddress: string;
|
|
292
295
|
network: string;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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;
|
|
299
307
|
}
|
|
300
308
|
|
|
301
309
|
type True = true;
|
|
@@ -308,6 +316,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
308
316
|
/** @internal */ _pricing: PricingConfig | undefined;
|
|
309
317
|
/** @internal */ _protocols: ProtocolType[];
|
|
310
318
|
/** @internal */ _maxPrice: string | undefined;
|
|
319
|
+
/** @internal */ _minPrice: string | undefined;
|
|
311
320
|
/** @internal */ _bodySchema: ZodType | undefined;
|
|
312
321
|
/** @internal */ _querySchema: ZodType | undefined;
|
|
313
322
|
/** @internal */ _outputSchema: ZodType | undefined;
|
|
@@ -378,13 +387,15 @@ interface MonitorEntry {
|
|
|
378
387
|
warn?: number;
|
|
379
388
|
critical?: number;
|
|
380
389
|
}
|
|
381
|
-
interface ServiceRouter {
|
|
382
|
-
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>;
|
|
383
392
|
wellKnown(options?: WellKnownOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
384
393
|
openapi(options: OpenAPIOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
385
394
|
monitors(): MonitorEntry[];
|
|
386
395
|
registry: RouteRegistry;
|
|
387
396
|
}
|
|
388
|
-
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>>;
|
|
389
400
|
|
|
390
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 { createClient, http } from "viem";
|
|
401
|
-
import { tempo as tempoChain } from "viem/chains";
|
|
402
|
-
function buildGetClient(rpcUrl) {
|
|
403
|
-
const url = rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
404
|
-
if (!url) return {};
|
|
405
|
-
return {
|
|
406
|
-
getClient: () => createClient({ chain: tempoChain, transport: http(url) })
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
function toStandardRequest(request) {
|
|
410
|
-
if (request.constructor.name === "Request") {
|
|
411
|
-
return request;
|
|
412
|
-
}
|
|
413
|
-
return new Request(request.url, {
|
|
414
|
-
method: request.method,
|
|
415
|
-
headers: request.headers
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
var DEFAULT_DECIMALS = 6;
|
|
419
|
-
async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
420
|
-
const standardRequest = toStandardRequest(request);
|
|
421
|
-
const currency = mppConfig.currency;
|
|
422
|
-
const recipient = mppConfig.recipient ?? "";
|
|
423
|
-
const methodIntent = tempo.charge();
|
|
424
|
-
const challenge = Challenge.fromIntent(methodIntent, {
|
|
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",
|
|
@@ -1112,6 +1070,8 @@ var RouteBuilder = class {
|
|
|
1112
1070
|
/** @internal */
|
|
1113
1071
|
_maxPrice;
|
|
1114
1072
|
/** @internal */
|
|
1073
|
+
_minPrice;
|
|
1074
|
+
/** @internal */
|
|
1115
1075
|
_bodySchema;
|
|
1116
1076
|
/** @internal */
|
|
1117
1077
|
_querySchema;
|
|
@@ -1153,6 +1113,7 @@ var RouteBuilder = class {
|
|
|
1153
1113
|
next._pricing = pricing;
|
|
1154
1114
|
if (options?.protocols) next._protocols = options.protocols;
|
|
1155
1115
|
if (options?.maxPrice) next._maxPrice = options.maxPrice;
|
|
1116
|
+
if (options?.minPrice) next._minPrice = options.minPrice;
|
|
1156
1117
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
1157
1118
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
1158
1119
|
if (!tierKey) {
|
|
@@ -1289,6 +1250,7 @@ var RouteBuilder = class {
|
|
|
1289
1250
|
path: this._path,
|
|
1290
1251
|
method: this._method,
|
|
1291
1252
|
maxPrice: this._maxPrice,
|
|
1253
|
+
minPrice: this._minPrice,
|
|
1292
1254
|
apiKeyResolver: this._apiKeyResolver,
|
|
1293
1255
|
providerName: this._providerName,
|
|
1294
1256
|
providerConfig: this._providerConfig,
|
|
@@ -1391,9 +1353,16 @@ function buildOperation(routeKey, entry, tag) {
|
|
|
1391
1353
|
const protocols = entry.protocols.length > 0 ? entry.protocols : void 0;
|
|
1392
1354
|
let price;
|
|
1393
1355
|
if (typeof entry.pricing === "string") {
|
|
1394
|
-
price =
|
|
1356
|
+
price = entry.pricing;
|
|
1357
|
+
} else if (typeof entry.pricing === "object" && "tiers" in entry.pricing) {
|
|
1358
|
+
const tierPrices = Object.values(entry.pricing.tiers).map((t) => parseFloat(t.price));
|
|
1359
|
+
const min = Math.min(...tierPrices);
|
|
1360
|
+
const max = Math.max(...tierPrices);
|
|
1361
|
+
price = min === max ? String(min) : `${min}-${max}`;
|
|
1362
|
+
} else if (entry.minPrice && entry.maxPrice) {
|
|
1363
|
+
price = `${entry.minPrice}-${entry.maxPrice}`;
|
|
1395
1364
|
} else if (entry.maxPrice) {
|
|
1396
|
-
price =
|
|
1365
|
+
price = entry.maxPrice;
|
|
1397
1366
|
}
|
|
1398
1367
|
const operation = {
|
|
1399
1368
|
operationId: routeKey.replace(/\//g, "_"),
|
|
@@ -1443,21 +1412,30 @@ function createRouter(config) {
|
|
|
1443
1412
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1444
1413
|
const network = config.network ?? "eip155:8453";
|
|
1445
1414
|
const baseUrl = typeof globalThis.process !== "undefined" ? process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000" : "http://localhost:3000";
|
|
1446
|
-
if (config.protocols) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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.";
|
|
1456
1432
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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"));
|
|
1461
1439
|
}
|
|
1462
1440
|
}
|
|
1463
1441
|
if (config.plugin?.init) {
|
|
@@ -1477,17 +1455,47 @@ function createRouter(config) {
|
|
|
1477
1455
|
nonceStore,
|
|
1478
1456
|
payeeAddress: config.payeeAddress,
|
|
1479
1457
|
network,
|
|
1480
|
-
|
|
1458
|
+
mppx: null
|
|
1481
1459
|
};
|
|
1482
1460
|
deps.initPromise = (async () => {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
+
}
|
|
1491
1499
|
}
|
|
1492
1500
|
})();
|
|
1493
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.9",
|
|
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.0",
|
|
51
50
|
"next": "^15.0.0",
|
|
51
|
+
"mppx": "^0.2.3",
|
|
52
52
|
"prettier": "^3.8.1",
|
|
53
53
|
"react": "^19.0.0",
|
|
54
54
|
"tsup": "^8.0.0",
|