@agentcash/router 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/index.cjs +1177 -0
- package/dist/index.d.cts +273 -0
- package/dist/index.d.ts +273 -0
- package/dist/index.js +1159 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
7
|
+
}) : x)(function(x) {
|
|
8
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
|
+
});
|
|
11
|
+
var __esm = (fn, res) => function __init() {
|
|
12
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
|
+
};
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
|
|
28
|
+
// src/server.ts
|
|
29
|
+
var server_exports = {};
|
|
30
|
+
__export(server_exports, {
|
|
31
|
+
createX402Server: () => createX402Server
|
|
32
|
+
});
|
|
33
|
+
function createX402Server(config) {
|
|
34
|
+
const { x402ResourceServer, HTTPFacilitatorClient } = __require("@x402/core/server");
|
|
35
|
+
const { registerExactEvmScheme } = __require("@x402/evm/exact/server");
|
|
36
|
+
const { bazaarResourceServerExtension } = __require("@x402/extensions/bazaar");
|
|
37
|
+
const { siwxResourceServerExtension } = __require("@x402/extensions/sign-in-with-x");
|
|
38
|
+
const { facilitator: defaultFacilitator } = __require("@coinbase/x402");
|
|
39
|
+
const facilitatorUrl = config.facilitatorUrl ?? defaultFacilitator;
|
|
40
|
+
const client = new HTTPFacilitatorClient(facilitatorUrl);
|
|
41
|
+
const server = new x402ResourceServer(client);
|
|
42
|
+
registerExactEvmScheme(server);
|
|
43
|
+
server.registerExtension(bazaarResourceServerExtension);
|
|
44
|
+
server.registerExtension(siwxResourceServerExtension);
|
|
45
|
+
const initPromise = retryInit(server);
|
|
46
|
+
return { server, initPromise };
|
|
47
|
+
}
|
|
48
|
+
async function retryInit(server, maxAttempts = 3, backoff = [1e3, 2e3, 4e3]) {
|
|
49
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
50
|
+
try {
|
|
51
|
+
await server.init();
|
|
52
|
+
return;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const is429 = err instanceof Error && (err.message.includes("429") || err.message.includes("rate limit"));
|
|
55
|
+
if (!is429 || attempt === maxAttempts - 1) throw err;
|
|
56
|
+
await new Promise((r) => setTimeout(r, backoff[attempt] ?? 4e3));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var init_server = __esm({
|
|
61
|
+
"src/server.ts"() {
|
|
62
|
+
"use strict";
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/registry.ts
|
|
67
|
+
var RouteRegistry = class {
|
|
68
|
+
routes = /* @__PURE__ */ new Map();
|
|
69
|
+
register(entry) {
|
|
70
|
+
if (this.routes.has(entry.key)) {
|
|
71
|
+
throw new Error(`route '${entry.key}': already registered (duplicate route key)`);
|
|
72
|
+
}
|
|
73
|
+
this.routes.set(entry.key, entry);
|
|
74
|
+
}
|
|
75
|
+
get(key) {
|
|
76
|
+
return this.routes.get(key);
|
|
77
|
+
}
|
|
78
|
+
entries() {
|
|
79
|
+
return this.routes.entries();
|
|
80
|
+
}
|
|
81
|
+
has(key) {
|
|
82
|
+
return this.routes.has(key);
|
|
83
|
+
}
|
|
84
|
+
get size() {
|
|
85
|
+
return this.routes.size;
|
|
86
|
+
}
|
|
87
|
+
validate(expectedKeys) {
|
|
88
|
+
if (!expectedKeys) return;
|
|
89
|
+
const missing = expectedKeys.filter((k) => !this.routes.has(k));
|
|
90
|
+
if (missing.length > 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`route${missing.length > 1 ? "s" : ""} ${missing.map((k) => `'${k}'`).join(", ")} in prices map but not registered \u2014 add to barrel imports`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/orchestrate.ts
|
|
99
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
100
|
+
|
|
101
|
+
// src/plugin.ts
|
|
102
|
+
function createDefaultContext(meta) {
|
|
103
|
+
const ctx = {
|
|
104
|
+
requestId: meta.requestId,
|
|
105
|
+
route: meta.route,
|
|
106
|
+
walletAddress: meta.walletAddress,
|
|
107
|
+
clientId: meta.clientId,
|
|
108
|
+
sessionId: meta.sessionId,
|
|
109
|
+
verifiedWallet: null,
|
|
110
|
+
setVerifiedWallet(address) {
|
|
111
|
+
ctx.verifiedWallet = address;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return ctx;
|
|
115
|
+
}
|
|
116
|
+
function firePluginHook(plugin, method, ...args) {
|
|
117
|
+
if (!plugin) return void 0;
|
|
118
|
+
const fn = plugin[method];
|
|
119
|
+
if (typeof fn !== "function") return void 0;
|
|
120
|
+
try {
|
|
121
|
+
const result = fn.apply(plugin, args);
|
|
122
|
+
if (result && typeof result.catch === "function") {
|
|
123
|
+
result.catch(() => {
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
} catch {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function consolePlugin() {
|
|
132
|
+
return {
|
|
133
|
+
onRequest(meta) {
|
|
134
|
+
const ctx = createDefaultContext(meta);
|
|
135
|
+
return ctx;
|
|
136
|
+
},
|
|
137
|
+
onPaymentVerified(_ctx, payment) {
|
|
138
|
+
console.log(`[router] VERIFIED ${payment.protocol} ${payment.payer} ${payment.amount}`);
|
|
139
|
+
},
|
|
140
|
+
onPaymentSettled(_ctx, settlement) {
|
|
141
|
+
console.log(`[router] SETTLED ${settlement.protocol} tx=${settlement.transaction}`);
|
|
142
|
+
},
|
|
143
|
+
onResponse(ctx, response) {
|
|
144
|
+
const wallet = ctx.verifiedWallet ? ` wallet=${ctx.verifiedWallet}` : "";
|
|
145
|
+
console.log(
|
|
146
|
+
`[router] ${ctx.route} \u2192 ${response.statusCode} (${response.duration}ms)${wallet}`
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
onError(_ctx, error) {
|
|
150
|
+
console.error(`[router] ERROR ${error.status}: ${error.message}`);
|
|
151
|
+
},
|
|
152
|
+
onAlert(_ctx, alert) {
|
|
153
|
+
const logFn = alert.level === "critical" || alert.level === "error" ? console.error : alert.level === "warn" ? console.warn : console.log;
|
|
154
|
+
logFn(
|
|
155
|
+
`[router] ${alert.level.toUpperCase()} ${alert.route}: ${alert.message}`,
|
|
156
|
+
alert.meta ?? ""
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
onProviderQuota(_ctx, event) {
|
|
160
|
+
const logFn = event.level === "critical" ? console.error : event.level === "warn" ? console.warn : console.log;
|
|
161
|
+
logFn(`[router] QUOTA ${event.level.toUpperCase()} ${event.provider}: ${event.message}`);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/protocols/detect.ts
|
|
167
|
+
function detectProtocol(request) {
|
|
168
|
+
if (request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT")) {
|
|
169
|
+
return "x402";
|
|
170
|
+
}
|
|
171
|
+
const auth = request.headers.get("Authorization");
|
|
172
|
+
if (auth && auth.startsWith("Payment ")) {
|
|
173
|
+
return "mpp";
|
|
174
|
+
}
|
|
175
|
+
if (request.headers.get("SIGN-IN-WITH-X")) {
|
|
176
|
+
return "siwx";
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/handler.ts
|
|
182
|
+
import { NextResponse } from "next/server";
|
|
183
|
+
|
|
184
|
+
// src/types.ts
|
|
185
|
+
var HttpError = class extends Error {
|
|
186
|
+
constructor(message, status) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.status = status;
|
|
189
|
+
this.name = "HttpError";
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/handler.ts
|
|
194
|
+
async function safeCallHandler(handler, ctx) {
|
|
195
|
+
try {
|
|
196
|
+
const result = await handler(ctx);
|
|
197
|
+
if (result instanceof Response) return result;
|
|
198
|
+
return NextResponse.json(result);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const status = error instanceof HttpError ? error.status : 500;
|
|
201
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
202
|
+
return NextResponse.json({ success: false, error: message }, { status });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/body.ts
|
|
207
|
+
async function bufferBody(request) {
|
|
208
|
+
const text = await request.text();
|
|
209
|
+
if (!text) return void 0;
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(text);
|
|
212
|
+
} catch {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function validateBody(parsed, schema) {
|
|
217
|
+
const result = schema.safeParse(parsed);
|
|
218
|
+
if (result.success) {
|
|
219
|
+
return { success: true, data: result.data };
|
|
220
|
+
}
|
|
221
|
+
const err = result.error;
|
|
222
|
+
const issues = "issues" in err ? err.issues : [{ message: String(err.message ?? "Validation failed") }];
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
error: "Validation failed",
|
|
226
|
+
issues
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/pricing.ts
|
|
231
|
+
async function resolvePrice(pricing, body) {
|
|
232
|
+
if (typeof pricing === "string") {
|
|
233
|
+
return pricing;
|
|
234
|
+
}
|
|
235
|
+
if (typeof pricing === "function") {
|
|
236
|
+
return pricing(body);
|
|
237
|
+
}
|
|
238
|
+
const { field, tiers, default: defaultTier } = pricing;
|
|
239
|
+
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
240
|
+
if (tierKey && tiers[tierKey]) {
|
|
241
|
+
return tiers[tierKey].price;
|
|
242
|
+
}
|
|
243
|
+
if (defaultTier && tiers[defaultTier]) {
|
|
244
|
+
return tiers[defaultTier].price;
|
|
245
|
+
}
|
|
246
|
+
if (!tierKey) {
|
|
247
|
+
throw Object.assign(new Error(`Missing required field '${field}' for tier pricing`), {
|
|
248
|
+
status: 400
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
throw Object.assign(
|
|
252
|
+
new Error(
|
|
253
|
+
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
254
|
+
),
|
|
255
|
+
{ status: 400 }
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
function resolveMaxPrice(pricing) {
|
|
259
|
+
if (typeof pricing === "string") return pricing;
|
|
260
|
+
if (typeof pricing === "function") {
|
|
261
|
+
throw new Error("Dynamic pricing requires maxPrice \u2014 this should be caught at registration");
|
|
262
|
+
}
|
|
263
|
+
const { tiers } = pricing;
|
|
264
|
+
let max = "0";
|
|
265
|
+
for (const tier of Object.values(tiers)) {
|
|
266
|
+
if (parseFloat(tier.price) > parseFloat(max)) {
|
|
267
|
+
max = tier.price;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return max;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/protocols/x402.ts
|
|
274
|
+
function buildX402Challenge(server, routeEntry, request, price, payeeAddress, network, extensions) {
|
|
275
|
+
const { encodePaymentRequiredHeader } = __require("@x402/core/http");
|
|
276
|
+
const options = {
|
|
277
|
+
scheme: "exact",
|
|
278
|
+
network,
|
|
279
|
+
price,
|
|
280
|
+
payTo: payeeAddress
|
|
281
|
+
};
|
|
282
|
+
const resource = {
|
|
283
|
+
url: request.url,
|
|
284
|
+
method: routeEntry.method,
|
|
285
|
+
description: routeEntry.description
|
|
286
|
+
};
|
|
287
|
+
const requirements = server.buildPaymentRequirementsFromOptions(options, {
|
|
288
|
+
request
|
|
289
|
+
});
|
|
290
|
+
const paymentRequired = server.createPaymentRequiredResponse(
|
|
291
|
+
requirements,
|
|
292
|
+
resource,
|
|
293
|
+
null,
|
|
294
|
+
extensions
|
|
295
|
+
);
|
|
296
|
+
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
297
|
+
return { encoded, requirements };
|
|
298
|
+
}
|
|
299
|
+
async function verifyX402Payment(server, request, routeEntry, price, payeeAddress, network) {
|
|
300
|
+
const { decodePaymentSignatureHeader } = __require("@x402/core/http");
|
|
301
|
+
const paymentHeader = request.headers.get("PAYMENT-SIGNATURE") ?? request.headers.get("X-PAYMENT");
|
|
302
|
+
if (!paymentHeader) return null;
|
|
303
|
+
const payload = decodePaymentSignatureHeader(paymentHeader);
|
|
304
|
+
const options = {
|
|
305
|
+
scheme: "exact",
|
|
306
|
+
network,
|
|
307
|
+
price,
|
|
308
|
+
payTo: payeeAddress
|
|
309
|
+
};
|
|
310
|
+
const requirements = server.buildPaymentRequirementsFromOptions(options, {
|
|
311
|
+
request
|
|
312
|
+
});
|
|
313
|
+
const matching = server.findMatchingRequirements(requirements, payload);
|
|
314
|
+
const verify = await server.verifyPayment(payload, matching);
|
|
315
|
+
if (!verify.isValid) {
|
|
316
|
+
return { valid: false, payload: null, requirements: null, payer: null };
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
valid: true,
|
|
320
|
+
payer: verify.payer,
|
|
321
|
+
payload,
|
|
322
|
+
requirements: matching
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
async function settleX402Payment(server, payload, requirements) {
|
|
326
|
+
const { encodePaymentResponseHeader } = __require("@x402/core/http");
|
|
327
|
+
const result = await server.settlePayment(payload, requirements);
|
|
328
|
+
const encoded = encodePaymentResponseHeader(result);
|
|
329
|
+
return { encoded, result };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/protocols/mpp.ts
|
|
333
|
+
var mpayLoaded = false;
|
|
334
|
+
var Challenge;
|
|
335
|
+
var Credential;
|
|
336
|
+
var Receipt;
|
|
337
|
+
var tempo;
|
|
338
|
+
function ensureMpay() {
|
|
339
|
+
if (mpayLoaded) return;
|
|
340
|
+
try {
|
|
341
|
+
const mpay = __require("mpay");
|
|
342
|
+
Challenge = mpay.Challenge;
|
|
343
|
+
Credential = mpay.Credential;
|
|
344
|
+
Receipt = mpay.Receipt;
|
|
345
|
+
const mpayServer = __require("mpay/server");
|
|
346
|
+
tempo = mpayServer.tempo;
|
|
347
|
+
mpayLoaded = true;
|
|
348
|
+
} catch {
|
|
349
|
+
throw new Error("mpay package is required for MPP protocol support. Install it: pnpm add mpay");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
353
|
+
ensureMpay();
|
|
354
|
+
const intent = {
|
|
355
|
+
amount: price,
|
|
356
|
+
currency: mppConfig.currency,
|
|
357
|
+
recipient: mppConfig.recipient ?? ""
|
|
358
|
+
};
|
|
359
|
+
const challenge = Challenge.fromIntent(intent, {
|
|
360
|
+
secretKey: mppConfig.secretKey,
|
|
361
|
+
realm: new URL(request.url).origin,
|
|
362
|
+
request
|
|
363
|
+
});
|
|
364
|
+
return Challenge.serialize(challenge);
|
|
365
|
+
}
|
|
366
|
+
async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
|
|
367
|
+
ensureMpay();
|
|
368
|
+
const credential = Credential.fromRequest(request);
|
|
369
|
+
if (!credential) return null;
|
|
370
|
+
const isValid = Challenge.verify(credential, { secretKey: mppConfig.secretKey });
|
|
371
|
+
if (!isValid) {
|
|
372
|
+
return { valid: false, payer: null };
|
|
373
|
+
}
|
|
374
|
+
const chargeConfig = {
|
|
375
|
+
amount: price,
|
|
376
|
+
currency: mppConfig.currency,
|
|
377
|
+
recipient: mppConfig.recipient ?? ""
|
|
378
|
+
};
|
|
379
|
+
const verifyResult = await tempo.charge(chargeConfig).verify(credential);
|
|
380
|
+
if (!verifyResult?.valid) {
|
|
381
|
+
return { valid: false, payer: null };
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
valid: true,
|
|
385
|
+
payer: verifyResult.payer
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function buildMPPReceipt(reference) {
|
|
389
|
+
ensureMpay();
|
|
390
|
+
const receipt = Receipt.from({
|
|
391
|
+
method: "tempo",
|
|
392
|
+
status: "success",
|
|
393
|
+
reference,
|
|
394
|
+
timestamp: Date.now()
|
|
395
|
+
});
|
|
396
|
+
return Receipt.serialize(receipt);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/auth/siwx.ts
|
|
400
|
+
async function verifySIWX(request, _routeEntry, nonceStore) {
|
|
401
|
+
const {
|
|
402
|
+
parseSIWxHeader,
|
|
403
|
+
validateSIWxMessage,
|
|
404
|
+
verifySIWxSignature
|
|
405
|
+
} = __require("@x402/extensions/sign-in-with-x");
|
|
406
|
+
const header = request.headers.get("SIGN-IN-WITH-X");
|
|
407
|
+
if (!header) return { valid: false, wallet: null };
|
|
408
|
+
const payload = parseSIWxHeader(header);
|
|
409
|
+
const uri = request.url;
|
|
410
|
+
const validation = await validateSIWxMessage(payload, uri, {
|
|
411
|
+
checkNonce: (nonce) => nonceStore.check(nonce)
|
|
412
|
+
});
|
|
413
|
+
if (!validation.isValid) {
|
|
414
|
+
return { valid: false, wallet: null };
|
|
415
|
+
}
|
|
416
|
+
const verified = await verifySIWxSignature(payload);
|
|
417
|
+
if (!verified?.isValid) {
|
|
418
|
+
return { valid: false, wallet: null };
|
|
419
|
+
}
|
|
420
|
+
return { valid: true, wallet: verified.address };
|
|
421
|
+
}
|
|
422
|
+
function buildSIWXExtension() {
|
|
423
|
+
const { declareSIWxExtension } = __require("@x402/extensions/sign-in-with-x");
|
|
424
|
+
return declareSIWxExtension();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/auth/api-key.ts
|
|
428
|
+
async function verifyApiKey(request, resolver) {
|
|
429
|
+
const apiKey = request.headers.get("X-API-Key") ?? extractBearerToken(request.headers.get("Authorization"));
|
|
430
|
+
if (!apiKey) {
|
|
431
|
+
return { valid: false, account: null };
|
|
432
|
+
}
|
|
433
|
+
const account = await resolver(apiKey);
|
|
434
|
+
if (account == null) {
|
|
435
|
+
return { valid: false, account: null };
|
|
436
|
+
}
|
|
437
|
+
return { valid: true, account };
|
|
438
|
+
}
|
|
439
|
+
function extractBearerToken(header) {
|
|
440
|
+
if (!header) return null;
|
|
441
|
+
if (header.startsWith("Bearer ")) return header.slice(7);
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/orchestrate.ts
|
|
446
|
+
function createRequestHandler(routeEntry, handler, deps) {
|
|
447
|
+
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
|
|
448
|
+
const ctx = {
|
|
449
|
+
body: parsedBody,
|
|
450
|
+
query: parseQuery(request, routeEntry),
|
|
451
|
+
request,
|
|
452
|
+
requestId: meta.requestId,
|
|
453
|
+
route: routeEntry.key,
|
|
454
|
+
wallet,
|
|
455
|
+
account,
|
|
456
|
+
alert(level, message, alertMeta) {
|
|
457
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
458
|
+
level,
|
|
459
|
+
message,
|
|
460
|
+
route: routeEntry.key,
|
|
461
|
+
meta: alertMeta
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
|
|
465
|
+
};
|
|
466
|
+
let rawResult;
|
|
467
|
+
const response = await safeCallHandler(async (c) => {
|
|
468
|
+
rawResult = await handler(c);
|
|
469
|
+
return rawResult;
|
|
470
|
+
}, ctx);
|
|
471
|
+
return { response, rawResult };
|
|
472
|
+
}
|
|
473
|
+
function finalize(response, rawResult, meta, pluginCtx) {
|
|
474
|
+
fireProviderQuota(routeEntry, response, rawResult, deps, pluginCtx);
|
|
475
|
+
firePluginResponse(deps, pluginCtx, meta, response);
|
|
476
|
+
}
|
|
477
|
+
function fail(status, message, meta, pluginCtx) {
|
|
478
|
+
const response = NextResponse2.json({ success: false, error: message }, { status });
|
|
479
|
+
firePluginResponse(deps, pluginCtx, meta, response);
|
|
480
|
+
return response;
|
|
481
|
+
}
|
|
482
|
+
return async (request) => {
|
|
483
|
+
await deps.initPromise;
|
|
484
|
+
const meta = buildMeta(request, routeEntry);
|
|
485
|
+
const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
|
|
486
|
+
async function handleAuth(wallet, account2) {
|
|
487
|
+
const body2 = await parseBody(request, routeEntry);
|
|
488
|
+
if (!body2.ok) {
|
|
489
|
+
firePluginResponse(deps, pluginCtx, meta, body2.response);
|
|
490
|
+
return body2.response;
|
|
491
|
+
}
|
|
492
|
+
const { response, rawResult } = await invoke(
|
|
493
|
+
request,
|
|
494
|
+
meta,
|
|
495
|
+
pluginCtx,
|
|
496
|
+
wallet,
|
|
497
|
+
account2,
|
|
498
|
+
body2.data
|
|
499
|
+
);
|
|
500
|
+
finalize(response, rawResult, meta, pluginCtx);
|
|
501
|
+
return response;
|
|
502
|
+
}
|
|
503
|
+
if (routeEntry.authMode === "unprotected") {
|
|
504
|
+
return handleAuth(null, void 0);
|
|
505
|
+
}
|
|
506
|
+
let account = void 0;
|
|
507
|
+
if (routeEntry.authMode === "apiKey" || routeEntry.apiKeyResolver) {
|
|
508
|
+
if (!routeEntry.apiKeyResolver) {
|
|
509
|
+
return fail(401, "API key resolver not configured", meta, pluginCtx);
|
|
510
|
+
}
|
|
511
|
+
const keyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
512
|
+
if (!keyResult.valid) {
|
|
513
|
+
return fail(401, "Invalid or missing API key", meta, pluginCtx);
|
|
514
|
+
}
|
|
515
|
+
account = keyResult.account;
|
|
516
|
+
if (routeEntry.authMode === "apiKey" && !routeEntry.pricing) {
|
|
517
|
+
return handleAuth(null, account);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const protocol = detectProtocol(request);
|
|
521
|
+
if (routeEntry.authMode === "siwx") {
|
|
522
|
+
if (!request.headers.get("SIGN-IN-WITH-X")) {
|
|
523
|
+
const response = new NextResponse2(null, { status: 402 });
|
|
524
|
+
try {
|
|
525
|
+
if (buildSIWXExtension()) response.headers.set("X-SIWX-REQUIRED", "true");
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
firePluginResponse(deps, pluginCtx, meta, response);
|
|
529
|
+
return response;
|
|
530
|
+
}
|
|
531
|
+
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
532
|
+
if (!siwx.valid) {
|
|
533
|
+
return fail(
|
|
534
|
+
402,
|
|
535
|
+
"SIWX verification failed \u2014 signature invalid, message expired, or nonce already used",
|
|
536
|
+
meta,
|
|
537
|
+
pluginCtx
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
pluginCtx.setVerifiedWallet(siwx.wallet);
|
|
541
|
+
return handleAuth(siwx.wallet, void 0);
|
|
542
|
+
}
|
|
543
|
+
if (!protocol || protocol === "siwx") {
|
|
544
|
+
return build402(request, routeEntry, deps, meta, pluginCtx);
|
|
545
|
+
}
|
|
546
|
+
const body = await parseBody(request, routeEntry);
|
|
547
|
+
if (!body.ok) {
|
|
548
|
+
firePluginResponse(deps, pluginCtx, meta, body.response);
|
|
549
|
+
return body.response;
|
|
550
|
+
}
|
|
551
|
+
let price;
|
|
552
|
+
try {
|
|
553
|
+
price = await resolvePrice(routeEntry.pricing, body.data);
|
|
554
|
+
} catch (err) {
|
|
555
|
+
return fail(
|
|
556
|
+
err.status ?? 500,
|
|
557
|
+
err instanceof Error ? err.message : "Price resolution failed",
|
|
558
|
+
meta,
|
|
559
|
+
pluginCtx
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (protocol === "x402") {
|
|
563
|
+
if (!deps.x402Server) {
|
|
564
|
+
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";
|
|
565
|
+
return fail(500, reason, meta, pluginCtx);
|
|
566
|
+
}
|
|
567
|
+
const verify = await verifyX402Payment(
|
|
568
|
+
deps.x402Server,
|
|
569
|
+
request,
|
|
570
|
+
routeEntry,
|
|
571
|
+
price,
|
|
572
|
+
deps.payeeAddress,
|
|
573
|
+
deps.network
|
|
574
|
+
);
|
|
575
|
+
if (!verify?.valid) return build402(request, routeEntry, deps, meta, pluginCtx);
|
|
576
|
+
pluginCtx.setVerifiedWallet(verify.payer);
|
|
577
|
+
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
578
|
+
protocol: "x402",
|
|
579
|
+
payer: verify.payer,
|
|
580
|
+
amount: price,
|
|
581
|
+
network: deps.network
|
|
582
|
+
});
|
|
583
|
+
const { response, rawResult } = await invoke(
|
|
584
|
+
request,
|
|
585
|
+
meta,
|
|
586
|
+
pluginCtx,
|
|
587
|
+
verify.payer,
|
|
588
|
+
account,
|
|
589
|
+
body.data
|
|
590
|
+
);
|
|
591
|
+
if (response.status < 400) {
|
|
592
|
+
try {
|
|
593
|
+
const settle = await settleX402Payment(
|
|
594
|
+
deps.x402Server,
|
|
595
|
+
verify.payload,
|
|
596
|
+
verify.requirements
|
|
597
|
+
);
|
|
598
|
+
response.headers.set("PAYMENT-RESPONSE", settle.encoded);
|
|
599
|
+
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
600
|
+
protocol: "x402",
|
|
601
|
+
payer: verify.payer,
|
|
602
|
+
transaction: String(settle.result?.transaction ?? ""),
|
|
603
|
+
network: deps.network
|
|
604
|
+
});
|
|
605
|
+
} catch (err) {
|
|
606
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
607
|
+
level: "critical",
|
|
608
|
+
message: `Settlement failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
609
|
+
route: routeEntry.key
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
finalize(response, rawResult, meta, pluginCtx);
|
|
614
|
+
return response;
|
|
615
|
+
}
|
|
616
|
+
if (protocol === "mpp") {
|
|
617
|
+
if (!deps.mppConfig) return build402(request, routeEntry, deps, meta, pluginCtx);
|
|
618
|
+
const verify = await verifyMPPCredential(request, routeEntry, deps.mppConfig, price);
|
|
619
|
+
if (!verify?.valid) return build402(request, routeEntry, deps, meta, pluginCtx);
|
|
620
|
+
const wallet = verify.payer;
|
|
621
|
+
pluginCtx.setVerifiedWallet(wallet);
|
|
622
|
+
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
623
|
+
protocol: "mpp",
|
|
624
|
+
payer: wallet,
|
|
625
|
+
amount: price,
|
|
626
|
+
network: "tempo:42431"
|
|
627
|
+
});
|
|
628
|
+
const { response, rawResult } = await invoke(
|
|
629
|
+
request,
|
|
630
|
+
meta,
|
|
631
|
+
pluginCtx,
|
|
632
|
+
wallet,
|
|
633
|
+
account,
|
|
634
|
+
body.data
|
|
635
|
+
);
|
|
636
|
+
if (response.status < 400) {
|
|
637
|
+
try {
|
|
638
|
+
response.headers.set("Payment-Receipt", buildMPPReceipt(crypto.randomUUID()));
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
finalize(response, rawResult, meta, pluginCtx);
|
|
643
|
+
return response;
|
|
644
|
+
}
|
|
645
|
+
return build402(request, routeEntry, deps, meta, pluginCtx);
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async function parseBody(request, routeEntry) {
|
|
649
|
+
if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
650
|
+
const raw = await bufferBody(request);
|
|
651
|
+
const result = validateBody(raw, routeEntry.bodySchema);
|
|
652
|
+
if (result.success) return { ok: true, data: result.data };
|
|
653
|
+
return {
|
|
654
|
+
ok: false,
|
|
655
|
+
response: NextResponse2.json(
|
|
656
|
+
{ success: false, error: result.error, issues: result.issues },
|
|
657
|
+
{ status: 400 }
|
|
658
|
+
)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function buildMeta(request, routeEntry) {
|
|
662
|
+
return {
|
|
663
|
+
requestId: crypto.randomUUID(),
|
|
664
|
+
method: request.method,
|
|
665
|
+
route: routeEntry.key,
|
|
666
|
+
origin: request.headers.get("origin") ?? new URL(request.url).origin,
|
|
667
|
+
referer: request.headers.get("referer"),
|
|
668
|
+
walletAddress: request.headers.get("X-Wallet-Address"),
|
|
669
|
+
clientId: request.headers.get("X-Client-ID"),
|
|
670
|
+
sessionId: request.headers.get("X-Session-ID"),
|
|
671
|
+
contentType: request.headers.get("content-type"),
|
|
672
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
673
|
+
startTime: Date.now()
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function parseQuery(request, routeEntry) {
|
|
677
|
+
if (!routeEntry.querySchema) return void 0;
|
|
678
|
+
const params = Object.fromEntries(request.nextUrl.searchParams.entries());
|
|
679
|
+
const result = routeEntry.querySchema.safeParse(params);
|
|
680
|
+
return result.success ? result.data : params;
|
|
681
|
+
}
|
|
682
|
+
function build402(request, routeEntry, deps, meta, pluginCtx) {
|
|
683
|
+
const response = new NextResponse2(null, { status: 402 });
|
|
684
|
+
let challengePrice;
|
|
685
|
+
if (routeEntry.maxPrice) {
|
|
686
|
+
challengePrice = routeEntry.maxPrice;
|
|
687
|
+
} else if (routeEntry.pricing) {
|
|
688
|
+
try {
|
|
689
|
+
challengePrice = resolveMaxPrice(routeEntry.pricing);
|
|
690
|
+
} catch {
|
|
691
|
+
challengePrice = "0";
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
challengePrice = "0";
|
|
695
|
+
}
|
|
696
|
+
let extensions;
|
|
697
|
+
try {
|
|
698
|
+
const { z } = __require("zod");
|
|
699
|
+
const { declareDiscoveryExtension } = __require("@x402/extensions/bazaar");
|
|
700
|
+
const inputSchema = routeEntry.bodySchema ? z.toJSONSchema(routeEntry.bodySchema, { target: "draft-2020-12" }) : routeEntry.querySchema ? z.toJSONSchema(routeEntry.querySchema, { target: "draft-2020-12" }) : void 0;
|
|
701
|
+
const outputSchema = routeEntry.outputSchema ? z.toJSONSchema(routeEntry.outputSchema, { target: "draft-2020-12" }) : void 0;
|
|
702
|
+
if (inputSchema) {
|
|
703
|
+
const config = {
|
|
704
|
+
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
705
|
+
inputSchema
|
|
706
|
+
};
|
|
707
|
+
if (outputSchema) config.output = { schema: outputSchema, example: {} };
|
|
708
|
+
extensions = declareDiscoveryExtension(config);
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
if (routeEntry.protocols.includes("x402") && deps.x402Server) {
|
|
713
|
+
try {
|
|
714
|
+
const { encoded } = buildX402Challenge(
|
|
715
|
+
deps.x402Server,
|
|
716
|
+
routeEntry,
|
|
717
|
+
request,
|
|
718
|
+
challengePrice,
|
|
719
|
+
deps.payeeAddress,
|
|
720
|
+
deps.network,
|
|
721
|
+
extensions
|
|
722
|
+
);
|
|
723
|
+
response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
724
|
+
} catch {
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (routeEntry.protocols.includes("mpp") && deps.mppConfig) {
|
|
728
|
+
try {
|
|
729
|
+
response.headers.set(
|
|
730
|
+
"WWW-Authenticate",
|
|
731
|
+
buildMPPChallenge(routeEntry, request, deps.mppConfig, challengePrice)
|
|
732
|
+
);
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
firePluginResponse(deps, pluginCtx, meta, response);
|
|
737
|
+
return response;
|
|
738
|
+
}
|
|
739
|
+
function firePluginResponse(deps, pluginCtx, meta, response) {
|
|
740
|
+
firePluginHook(deps.plugin, "onResponse", pluginCtx, {
|
|
741
|
+
statusCode: response.status,
|
|
742
|
+
statusText: response.statusText,
|
|
743
|
+
duration: Date.now() - meta.startTime,
|
|
744
|
+
contentType: response.headers.get("content-type"),
|
|
745
|
+
headers: Object.fromEntries(response.headers.entries())
|
|
746
|
+
});
|
|
747
|
+
if (response.status >= 400 && response.status !== 402) {
|
|
748
|
+
firePluginHook(deps.plugin, "onError", pluginCtx, {
|
|
749
|
+
status: response.status,
|
|
750
|
+
message: response.statusText || `HTTP ${response.status}`,
|
|
751
|
+
settled: false
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function computeQuotaLevel(remaining, warn, critical) {
|
|
756
|
+
if (remaining === null) return "healthy";
|
|
757
|
+
if (critical !== void 0 && remaining <= critical) return "critical";
|
|
758
|
+
if (warn !== void 0 && remaining <= warn) return "warn";
|
|
759
|
+
return "healthy";
|
|
760
|
+
}
|
|
761
|
+
function fireProviderQuota(routeEntry, response, handlerResult, deps, pluginCtx) {
|
|
762
|
+
const { providerName, providerConfig } = routeEntry;
|
|
763
|
+
if (!providerName || !providerConfig?.extractQuota) return;
|
|
764
|
+
if (response.status >= 400) return;
|
|
765
|
+
try {
|
|
766
|
+
const quota = providerConfig.extractQuota(handlerResult, response.headers);
|
|
767
|
+
if (!quota) return;
|
|
768
|
+
const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
|
|
769
|
+
const overage = providerConfig.overage ?? "same-rate";
|
|
770
|
+
const event = {
|
|
771
|
+
provider: providerName,
|
|
772
|
+
route: routeEntry.key,
|
|
773
|
+
remaining: quota.remaining,
|
|
774
|
+
limit: quota.limit,
|
|
775
|
+
spend: quota.spend,
|
|
776
|
+
level,
|
|
777
|
+
overage,
|
|
778
|
+
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
779
|
+
};
|
|
780
|
+
firePluginHook(deps.plugin, "onProviderQuota", pluginCtx, event);
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/builder.ts
|
|
786
|
+
var RouteBuilder = class {
|
|
787
|
+
/** @internal */
|
|
788
|
+
_key;
|
|
789
|
+
/** @internal */
|
|
790
|
+
_registry;
|
|
791
|
+
/** @internal */
|
|
792
|
+
_deps;
|
|
793
|
+
/** @internal */
|
|
794
|
+
_authMode = null;
|
|
795
|
+
/** @internal */
|
|
796
|
+
_pricing;
|
|
797
|
+
/** @internal */
|
|
798
|
+
_protocols = ["x402"];
|
|
799
|
+
/** @internal */
|
|
800
|
+
_maxPrice;
|
|
801
|
+
/** @internal */
|
|
802
|
+
_bodySchema;
|
|
803
|
+
/** @internal */
|
|
804
|
+
_querySchema;
|
|
805
|
+
/** @internal */
|
|
806
|
+
_outputSchema;
|
|
807
|
+
/** @internal */
|
|
808
|
+
_description;
|
|
809
|
+
/** @internal */
|
|
810
|
+
_path;
|
|
811
|
+
/** @internal */
|
|
812
|
+
_method = "POST";
|
|
813
|
+
/** @internal */
|
|
814
|
+
_apiKeyResolver;
|
|
815
|
+
/** @internal */
|
|
816
|
+
_providerName;
|
|
817
|
+
/** @internal */
|
|
818
|
+
_providerConfig;
|
|
819
|
+
constructor(key, registry, deps) {
|
|
820
|
+
this._key = key;
|
|
821
|
+
this._registry = registry;
|
|
822
|
+
this._deps = deps;
|
|
823
|
+
}
|
|
824
|
+
fork() {
|
|
825
|
+
const next = Object.create(Object.getPrototypeOf(this));
|
|
826
|
+
Object.assign(next, this);
|
|
827
|
+
next._protocols = [...this._protocols];
|
|
828
|
+
return next;
|
|
829
|
+
}
|
|
830
|
+
paid(pricing, options) {
|
|
831
|
+
const next = this.fork();
|
|
832
|
+
next._authMode = "paid";
|
|
833
|
+
next._pricing = pricing;
|
|
834
|
+
if (options?.protocols) next._protocols = options.protocols;
|
|
835
|
+
if (options?.maxPrice) next._maxPrice = options.maxPrice;
|
|
836
|
+
if (typeof pricing === "function" && !options?.maxPrice) {
|
|
837
|
+
throw new Error(`route '${this._key}': dynamic pricing requires maxPrice option`);
|
|
838
|
+
}
|
|
839
|
+
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
840
|
+
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
841
|
+
if (!tierKey) {
|
|
842
|
+
throw new Error(`route '${this._key}': tier key cannot be empty`);
|
|
843
|
+
}
|
|
844
|
+
const tierPrice = parseFloat(tierConfig.price);
|
|
845
|
+
if (isNaN(tierPrice) || tierPrice <= 0) {
|
|
846
|
+
throw new Error(
|
|
847
|
+
`route '${this._key}': tier '${tierKey}' price '${tierConfig.price}' must be a positive decimal string`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (options?.maxPrice !== void 0) {
|
|
853
|
+
const parsed = parseFloat(options.maxPrice);
|
|
854
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
`route '${this._key}': maxPrice '${options.maxPrice}' must be a positive decimal string`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return next;
|
|
861
|
+
}
|
|
862
|
+
siwx() {
|
|
863
|
+
const next = this.fork();
|
|
864
|
+
next._authMode = "siwx";
|
|
865
|
+
next._protocols = [];
|
|
866
|
+
return next;
|
|
867
|
+
}
|
|
868
|
+
apiKey(resolver) {
|
|
869
|
+
const next = this.fork();
|
|
870
|
+
next._authMode = "apiKey";
|
|
871
|
+
next._apiKeyResolver = resolver;
|
|
872
|
+
return next;
|
|
873
|
+
}
|
|
874
|
+
unprotected() {
|
|
875
|
+
const next = this.fork();
|
|
876
|
+
next._authMode = "unprotected";
|
|
877
|
+
next._protocols = [];
|
|
878
|
+
return next;
|
|
879
|
+
}
|
|
880
|
+
// -------------------------------------------------------------------------
|
|
881
|
+
// Provider monitoring
|
|
882
|
+
// -------------------------------------------------------------------------
|
|
883
|
+
provider(name, config) {
|
|
884
|
+
const next = this.fork();
|
|
885
|
+
next._providerName = name;
|
|
886
|
+
next._providerConfig = config ?? {};
|
|
887
|
+
return next;
|
|
888
|
+
}
|
|
889
|
+
// -------------------------------------------------------------------------
|
|
890
|
+
// Schema methods
|
|
891
|
+
// -------------------------------------------------------------------------
|
|
892
|
+
body(schema) {
|
|
893
|
+
const next = this.fork();
|
|
894
|
+
next._bodySchema = schema;
|
|
895
|
+
return next;
|
|
896
|
+
}
|
|
897
|
+
query(schema) {
|
|
898
|
+
const next = this.fork();
|
|
899
|
+
next._querySchema = schema;
|
|
900
|
+
next._method = "GET";
|
|
901
|
+
return next;
|
|
902
|
+
}
|
|
903
|
+
output(schema) {
|
|
904
|
+
const next = this.fork();
|
|
905
|
+
next._outputSchema = schema;
|
|
906
|
+
return next;
|
|
907
|
+
}
|
|
908
|
+
description(text) {
|
|
909
|
+
const next = this.fork();
|
|
910
|
+
next._description = text;
|
|
911
|
+
return next;
|
|
912
|
+
}
|
|
913
|
+
path(p) {
|
|
914
|
+
const next = this.fork();
|
|
915
|
+
next._path = p;
|
|
916
|
+
return next;
|
|
917
|
+
}
|
|
918
|
+
handler(fn) {
|
|
919
|
+
const entry = {
|
|
920
|
+
key: this._key,
|
|
921
|
+
authMode: this._authMode,
|
|
922
|
+
pricing: this._pricing,
|
|
923
|
+
protocols: this._protocols,
|
|
924
|
+
bodySchema: this._bodySchema,
|
|
925
|
+
querySchema: this._querySchema,
|
|
926
|
+
outputSchema: this._outputSchema,
|
|
927
|
+
description: this._description,
|
|
928
|
+
path: this._path,
|
|
929
|
+
method: this._method,
|
|
930
|
+
maxPrice: this._maxPrice,
|
|
931
|
+
apiKeyResolver: this._apiKeyResolver,
|
|
932
|
+
providerName: this._providerName,
|
|
933
|
+
providerConfig: this._providerConfig
|
|
934
|
+
};
|
|
935
|
+
this._registry.register(entry);
|
|
936
|
+
return createRequestHandler(entry, fn, this._deps);
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
// src/auth/nonce.ts
|
|
941
|
+
var MemoryNonceStore = class {
|
|
942
|
+
seen = /* @__PURE__ */ new Map();
|
|
943
|
+
async check(nonce) {
|
|
944
|
+
this.evict();
|
|
945
|
+
if (this.seen.has(nonce)) return false;
|
|
946
|
+
this.seen.set(nonce, Date.now() + 5 * 60 * 1e3);
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
evict() {
|
|
950
|
+
const now = Date.now();
|
|
951
|
+
for (const [n, exp] of this.seen) {
|
|
952
|
+
if (exp < now) this.seen.delete(n);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/discovery/well-known.ts
|
|
958
|
+
import { NextResponse as NextResponse3 } from "next/server";
|
|
959
|
+
function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
960
|
+
let validated = false;
|
|
961
|
+
return async (_request) => {
|
|
962
|
+
if (!validated && pricesKeys) {
|
|
963
|
+
registry.validate(pricesKeys);
|
|
964
|
+
validated = true;
|
|
965
|
+
}
|
|
966
|
+
const x402Resources = [];
|
|
967
|
+
const mppResources = [];
|
|
968
|
+
for (const [key, entry] of registry.entries()) {
|
|
969
|
+
const url = `${baseUrl}/api/${entry.path ?? key}`;
|
|
970
|
+
if (entry.protocols.includes("x402")) x402Resources.push(url);
|
|
971
|
+
if (entry.protocols.includes("mpp")) mppResources.push(url);
|
|
972
|
+
}
|
|
973
|
+
let instructions;
|
|
974
|
+
if (typeof options.instructions === "function") {
|
|
975
|
+
instructions = await options.instructions();
|
|
976
|
+
} else if (typeof options.instructions === "string") {
|
|
977
|
+
instructions = options.instructions;
|
|
978
|
+
}
|
|
979
|
+
const body = {
|
|
980
|
+
version: 1,
|
|
981
|
+
resources: x402Resources
|
|
982
|
+
};
|
|
983
|
+
if (mppResources.length > 0) {
|
|
984
|
+
body.mppResources = mppResources;
|
|
985
|
+
}
|
|
986
|
+
if (options.ownershipProofs) {
|
|
987
|
+
body.ownershipProofs = options.ownershipProofs;
|
|
988
|
+
}
|
|
989
|
+
if (instructions) {
|
|
990
|
+
body.instructions = instructions;
|
|
991
|
+
}
|
|
992
|
+
return NextResponse3.json(body);
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/discovery/openapi.ts
|
|
997
|
+
import { NextResponse as NextResponse4 } from "next/server";
|
|
998
|
+
function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
999
|
+
let cached = null;
|
|
1000
|
+
let validated = false;
|
|
1001
|
+
return async (_request) => {
|
|
1002
|
+
if (cached) return NextResponse4.json(cached);
|
|
1003
|
+
if (!validated && pricesKeys) {
|
|
1004
|
+
registry.validate(pricesKeys);
|
|
1005
|
+
validated = true;
|
|
1006
|
+
}
|
|
1007
|
+
const { createDocument } = await import("zod-openapi");
|
|
1008
|
+
const paths = {};
|
|
1009
|
+
const tagSet = /* @__PURE__ */ new Set();
|
|
1010
|
+
for (const [key, entry] of registry.entries()) {
|
|
1011
|
+
const apiPath = `/api/${entry.path ?? key}`;
|
|
1012
|
+
const method = entry.method.toLowerCase();
|
|
1013
|
+
const tag = deriveTag(key);
|
|
1014
|
+
tagSet.add(tag);
|
|
1015
|
+
paths[apiPath] = {
|
|
1016
|
+
[method]: buildOperation(key, entry, tag)
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
cached = createDocument({
|
|
1020
|
+
openapi: "3.1.0",
|
|
1021
|
+
info: {
|
|
1022
|
+
title: options.title,
|
|
1023
|
+
description: options.description,
|
|
1024
|
+
version: options.version,
|
|
1025
|
+
...options.contact && { contact: options.contact }
|
|
1026
|
+
},
|
|
1027
|
+
servers: [{ url: options.baseUrl ?? baseUrl }],
|
|
1028
|
+
tags: Array.from(tagSet).sort().map((name) => ({ name })),
|
|
1029
|
+
paths
|
|
1030
|
+
});
|
|
1031
|
+
return NextResponse4.json(cached);
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function deriveTag(routeKey) {
|
|
1035
|
+
return routeKey.split("/")[0].split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
1036
|
+
}
|
|
1037
|
+
function buildOperation(routeKey, entry, tag) {
|
|
1038
|
+
const protocols = entry.protocols.length > 0 ? entry.protocols : void 0;
|
|
1039
|
+
let price;
|
|
1040
|
+
if (typeof entry.pricing === "string") {
|
|
1041
|
+
price = parseFloat(entry.pricing);
|
|
1042
|
+
} else if (entry.maxPrice) {
|
|
1043
|
+
price = parseFloat(entry.maxPrice);
|
|
1044
|
+
}
|
|
1045
|
+
const operation = {
|
|
1046
|
+
operationId: routeKey.replace(/\//g, "_"),
|
|
1047
|
+
summary: entry.description ?? routeKey,
|
|
1048
|
+
tags: [tag],
|
|
1049
|
+
responses: {
|
|
1050
|
+
"200": {
|
|
1051
|
+
description: "Successful response",
|
|
1052
|
+
...entry.outputSchema && {
|
|
1053
|
+
content: {
|
|
1054
|
+
"application/json": { schema: entry.outputSchema }
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
...entry.authMode === "paid" && {
|
|
1059
|
+
"402": {
|
|
1060
|
+
description: "Payment Required"
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
if (price !== void 0 || protocols) {
|
|
1066
|
+
operation["x-payment-info"] = {
|
|
1067
|
+
...price !== void 0 && { price },
|
|
1068
|
+
...protocols && { protocols }
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
if (entry.bodySchema) {
|
|
1072
|
+
operation.requestBody = {
|
|
1073
|
+
required: true,
|
|
1074
|
+
content: {
|
|
1075
|
+
"application/json": { schema: entry.bodySchema }
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
if (entry.querySchema) {
|
|
1080
|
+
operation.requestParams = {
|
|
1081
|
+
query: entry.querySchema
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
return operation;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/index.ts
|
|
1088
|
+
function createRouter(config) {
|
|
1089
|
+
const registry = new RouteRegistry();
|
|
1090
|
+
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1091
|
+
const network = config.network ?? "eip155:8453";
|
|
1092
|
+
const baseUrl = typeof globalThis.process !== "undefined" ? process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000" : "http://localhost:3000";
|
|
1093
|
+
if (config.plugin?.init) {
|
|
1094
|
+
try {
|
|
1095
|
+
config.plugin.init({ origin: baseUrl });
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const deps = {
|
|
1100
|
+
x402Server: null,
|
|
1101
|
+
initPromise: Promise.resolve(),
|
|
1102
|
+
plugin: config.plugin,
|
|
1103
|
+
nonceStore,
|
|
1104
|
+
payeeAddress: config.payeeAddress,
|
|
1105
|
+
network,
|
|
1106
|
+
mppConfig: config.mpp
|
|
1107
|
+
};
|
|
1108
|
+
try {
|
|
1109
|
+
const { createX402Server: createX402Server2 } = (init_server(), __toCommonJS(server_exports));
|
|
1110
|
+
const result = createX402Server2(config);
|
|
1111
|
+
deps.x402Server = result.server;
|
|
1112
|
+
deps.initPromise = result.initPromise.catch((err) => {
|
|
1113
|
+
deps.x402Server = null;
|
|
1114
|
+
deps.x402InitError = err instanceof Error ? err.message : String(err);
|
|
1115
|
+
});
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
|
1119
|
+
return {
|
|
1120
|
+
route(key) {
|
|
1121
|
+
const builder = new RouteBuilder(key, registry, deps);
|
|
1122
|
+
if (config.prices && key in config.prices) {
|
|
1123
|
+
return builder.paid(config.prices[key]);
|
|
1124
|
+
}
|
|
1125
|
+
return builder;
|
|
1126
|
+
},
|
|
1127
|
+
wellKnown(options) {
|
|
1128
|
+
return createWellKnownHandler(registry, baseUrl, pricesKeys, options);
|
|
1129
|
+
},
|
|
1130
|
+
openapi(options) {
|
|
1131
|
+
return createOpenAPIHandler(registry, baseUrl, pricesKeys, options);
|
|
1132
|
+
},
|
|
1133
|
+
monitors() {
|
|
1134
|
+
const result = [];
|
|
1135
|
+
for (const [, entry] of registry.entries()) {
|
|
1136
|
+
if (entry.providerName && entry.providerConfig?.monitor) {
|
|
1137
|
+
result.push({
|
|
1138
|
+
provider: entry.providerName,
|
|
1139
|
+
route: entry.key,
|
|
1140
|
+
monitor: entry.providerConfig.monitor,
|
|
1141
|
+
overage: entry.providerConfig.overage ?? "same-rate",
|
|
1142
|
+
warn: entry.providerConfig.warn,
|
|
1143
|
+
critical: entry.providerConfig.critical
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
},
|
|
1149
|
+
registry
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
export {
|
|
1153
|
+
HttpError,
|
|
1154
|
+
MemoryNonceStore,
|
|
1155
|
+
RouteBuilder,
|
|
1156
|
+
RouteRegistry,
|
|
1157
|
+
consolePlugin,
|
|
1158
|
+
createRouter
|
|
1159
|
+
};
|