@t2000/serve 10.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 +96 -0
- package/dist/index.cjs +581 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +228 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.js +568 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { isValidSuiAddress, normalizeSuiAddress } from '@mysten/sui/utils';
|
|
2
|
+
import { USDC_TESTNET, USDC, InMemoryDigestStore } from '@suimpp/mpp/server';
|
|
3
|
+
export { InMemoryDigestStore } from '@suimpp/mpp/server';
|
|
4
|
+
import { X402_PAYMENT_HEADER, isX402EscrowHeader, parseX402Header, verifyX402Payment, settleX402Payment, X402_PAYMENT_RESPONSE_HEADER, encodeX402Response, createX402Requirements, X402_VERSION } from '@suimpp/mpp/x402';
|
|
5
|
+
import { SuiGrpcClient } from '@mysten/sui/grpc';
|
|
6
|
+
|
|
7
|
+
// src/serve.ts
|
|
8
|
+
function buildOpenApiDocument(serve, origin) {
|
|
9
|
+
const base = serve.baseUrl ?? origin;
|
|
10
|
+
const paths = {};
|
|
11
|
+
for (const route of serve.routes.values()) {
|
|
12
|
+
const { path, priceUsdc, description, inputSchema } = route.meta;
|
|
13
|
+
const operation = {
|
|
14
|
+
operationId: path.replace(/[^a-zA-Z0-9]+/g, "_"),
|
|
15
|
+
summary: description ?? path,
|
|
16
|
+
responses: {
|
|
17
|
+
"200": { description: "Success" },
|
|
18
|
+
...priceUsdc ? {
|
|
19
|
+
"402": {
|
|
20
|
+
description: 'Payment required \u2014 the body carries an x402 accepts[] envelope (scheme "exact", network sui). Sign it and retry with the X-PAYMENT header.'
|
|
21
|
+
}
|
|
22
|
+
} : {},
|
|
23
|
+
"422": { description: "Invalid request body \u2014 never charged" }
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
if (priceUsdc) {
|
|
27
|
+
operation["x-payment-info"] = {
|
|
28
|
+
pricingMode: "fixed",
|
|
29
|
+
price: priceUsdc,
|
|
30
|
+
currency: "USDC",
|
|
31
|
+
protocols: ["mpp", "x402"],
|
|
32
|
+
x402: {
|
|
33
|
+
scheme: "exact",
|
|
34
|
+
network: `sui:${serve.network}`,
|
|
35
|
+
asset: (serve.network === "testnet" ? USDC_TESTNET : USDC).type,
|
|
36
|
+
payTo: serve.payTo
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (inputSchema) {
|
|
41
|
+
operation.requestBody = {
|
|
42
|
+
required: true,
|
|
43
|
+
content: { "application/json": { schema: inputSchema } }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
paths[`/${path}`] = { post: operation };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
openapi: "3.1.0",
|
|
50
|
+
info: {
|
|
51
|
+
title: serve.name ?? "Paid API",
|
|
52
|
+
version: "1.0.0",
|
|
53
|
+
description: serve.description ?? "Agent-payable API. Every paid endpoint answers 402 with an x402 accepts[] envelope; payment settles in USDC on Sui."
|
|
54
|
+
},
|
|
55
|
+
...base ? { servers: [{ url: base }] } : {},
|
|
56
|
+
paths
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildLlmsTxt(serve, origin) {
|
|
60
|
+
const base = serve.baseUrl ?? origin ?? "";
|
|
61
|
+
const lines = [];
|
|
62
|
+
lines.push(`# ${serve.name ?? "Paid API"}`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
if (serve.description) {
|
|
65
|
+
lines.push(serve.description);
|
|
66
|
+
lines.push("");
|
|
67
|
+
}
|
|
68
|
+
lines.push("## How to pay");
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push(
|
|
71
|
+
"Endpoints below are paid per call in USDC on Sui (x402). An unpaid request returns",
|
|
72
|
+
"HTTP 402 with an `accepts[]` envelope; sign it and retry with the `X-PAYMENT` header.",
|
|
73
|
+
"Invalid input returns 422 before any payment is taken. A failed call is never charged.",
|
|
74
|
+
"",
|
|
75
|
+
"Easiest client: `npm i -g @t2000/cli` then `t2 pay <url> --method POST --body '{...}'`,",
|
|
76
|
+
"or the t2000 MCP server (`t2000_pay`), or `@t2000/sdk` `pay()`.",
|
|
77
|
+
""
|
|
78
|
+
);
|
|
79
|
+
lines.push("## Endpoints");
|
|
80
|
+
lines.push("");
|
|
81
|
+
for (const route of serve.routes.values()) {
|
|
82
|
+
const { path, priceUsdc, description, inputSchema } = route.meta;
|
|
83
|
+
const price = priceUsdc ? `${priceUsdc} USDC per call` : "free";
|
|
84
|
+
lines.push(`### POST ${base}/${path} \u2014 ${price}`);
|
|
85
|
+
if (description) lines.push(description);
|
|
86
|
+
if (inputSchema) {
|
|
87
|
+
lines.push("Request body (JSON Schema):");
|
|
88
|
+
lines.push("```json");
|
|
89
|
+
lines.push(JSON.stringify(inputSchema, null, 2));
|
|
90
|
+
lines.push("```");
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
}
|
|
94
|
+
lines.push(`Discovery: ${base}/openapi.json`);
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
var FULLNODE_URLS = {
|
|
98
|
+
mainnet: "https://fullnode.mainnet.sui.io:443",
|
|
99
|
+
testnet: "https://fullnode.testnet.sui.io:443"
|
|
100
|
+
};
|
|
101
|
+
var EPOCH_TTL_MS = 10 * 60 * 1e3;
|
|
102
|
+
var state = {};
|
|
103
|
+
function getGrpcClient(network, rpcUrl) {
|
|
104
|
+
const s = state[network] ??= {};
|
|
105
|
+
if (!s.client) {
|
|
106
|
+
s.client = new SuiGrpcClient({
|
|
107
|
+
baseUrl: rpcUrl ?? FULLNODE_URLS[network],
|
|
108
|
+
network
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return s.client;
|
|
112
|
+
}
|
|
113
|
+
async function getChainInfo(network, rpcUrl) {
|
|
114
|
+
const client = getGrpcClient(network, rpcUrl);
|
|
115
|
+
const s = state[network] ??= {};
|
|
116
|
+
if (!s.chainId) {
|
|
117
|
+
const res = await client.core.getChainIdentifier();
|
|
118
|
+
s.chainId = res.chainIdentifier;
|
|
119
|
+
}
|
|
120
|
+
if (!s.epoch || Date.now() - s.epoch.fetchedAt > EPOCH_TTL_MS) {
|
|
121
|
+
const res = await client.core.getCurrentSystemState();
|
|
122
|
+
s.epoch = { value: String(res.systemState.epoch), fetchedAt: Date.now() };
|
|
123
|
+
}
|
|
124
|
+
return { chain: s.chainId, epoch: s.epoch.value };
|
|
125
|
+
}
|
|
126
|
+
function __resetChainCaches() {
|
|
127
|
+
delete state.mainnet;
|
|
128
|
+
delete state.testnet;
|
|
129
|
+
}
|
|
130
|
+
function __seedChainInfo(network, chain, epoch) {
|
|
131
|
+
const s = state[network] ??= {};
|
|
132
|
+
s.chainId = chain;
|
|
133
|
+
s.epoch = { value: epoch, fetchedAt: Date.now() };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/route.ts
|
|
137
|
+
var MPP_REPORT_URL = "https://mpp.t2000.ai/api/mpp/report";
|
|
138
|
+
var REPORT_TIMEOUT_MS = 2e3;
|
|
139
|
+
var CATALOG_PRICE_CAP_USDC = 5;
|
|
140
|
+
var CORS_HEADERS = {
|
|
141
|
+
"access-control-allow-origin": "*",
|
|
142
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
143
|
+
"access-control-allow-headers": `content-type, ${X402_PAYMENT_HEADER}`,
|
|
144
|
+
"access-control-expose-headers": `${X402_PAYMENT_RESPONSE_HEADER}, WWW-Authenticate`
|
|
145
|
+
};
|
|
146
|
+
function withCors(response) {
|
|
147
|
+
const headers = new Headers(response.headers);
|
|
148
|
+
for (const [k, v] of Object.entries(CORS_HEADERS)) headers.set(k, v);
|
|
149
|
+
return new Response(response.body, { status: response.status, headers });
|
|
150
|
+
}
|
|
151
|
+
function json(status, body, headers) {
|
|
152
|
+
return withCors(
|
|
153
|
+
new Response(JSON.stringify(body), {
|
|
154
|
+
status,
|
|
155
|
+
headers: { "content-type": "application/json", ...headers }
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
function isStandardSchema(schema) {
|
|
160
|
+
return typeof schema === "object" && schema !== null && "~standard" in schema;
|
|
161
|
+
}
|
|
162
|
+
async function validateBody(schema, value) {
|
|
163
|
+
if (isStandardSchema(schema)) {
|
|
164
|
+
const result2 = await schema["~standard"].validate(value);
|
|
165
|
+
if (result2.issues) {
|
|
166
|
+
return { ok: false, message: result2.issues.map((i) => i.message).join("; ") };
|
|
167
|
+
}
|
|
168
|
+
return { ok: true, data: result2.value };
|
|
169
|
+
}
|
|
170
|
+
const result = schema.safeParse(value);
|
|
171
|
+
if (!result.success) {
|
|
172
|
+
return { ok: false, message: result.error.message ?? "invalid body" };
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, data: result.data };
|
|
175
|
+
}
|
|
176
|
+
function assertValidPrice(price, path) {
|
|
177
|
+
if (!/^\d+(\.\d{1,6})?$/.test(price) || Number(price) <= 0) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`[serve] Route "${path}": price must be a positive decimal USDC string with up to 6 decimals, got "${price}"`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (Number(price) > CATALOG_PRICE_CAP_USDC) {
|
|
183
|
+
console.warn(
|
|
184
|
+
`[serve] Route "${path}": price ${price} USDC is above the ${CATALOG_PRICE_CAP_USDC} USDC listing cap \u2014 the route works, but it will not list on mpp.t2000.ai. Job-class work belongs in \`t2 service create\` (escrow).`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
var RouteBuilder = class {
|
|
189
|
+
constructor(runtime, options, register) {
|
|
190
|
+
this.runtime = runtime;
|
|
191
|
+
this.options = options;
|
|
192
|
+
this.register = register;
|
|
193
|
+
}
|
|
194
|
+
priceUsdc;
|
|
195
|
+
bodySchema;
|
|
196
|
+
inputSchema;
|
|
197
|
+
isFree = false;
|
|
198
|
+
/** Charge this many USDC per call (human units, e.g. "0.01"). */
|
|
199
|
+
paid(priceUsdc) {
|
|
200
|
+
assertValidPrice(priceUsdc, this.options.path);
|
|
201
|
+
this.priceUsdc = priceUsdc;
|
|
202
|
+
this.isFree = false;
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
/** Serve without payment (health checks, previews, docs). */
|
|
206
|
+
unprotected() {
|
|
207
|
+
this.isFree = true;
|
|
208
|
+
this.priceUsdc = void 0;
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Validate the JSON request body. zod v4 / valibot / arktype / anything
|
|
213
|
+
* Standard-Schema, or anything with a zod-style safeParse.
|
|
214
|
+
*
|
|
215
|
+
* Pass the JSON Schema as the second argument to publish it in
|
|
216
|
+
* /openapi.json + /llms.txt (zod v4: `z.toJSONSchema(schema)`) — buyers'
|
|
217
|
+
* agents build request bodies from it, and the catalog grades listings
|
|
218
|
+
* without one.
|
|
219
|
+
*/
|
|
220
|
+
body(schema, jsonSchema) {
|
|
221
|
+
this.bodySchema = schema;
|
|
222
|
+
this.inputSchema = jsonSchema;
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
handler(fn) {
|
|
226
|
+
if (!this.isFree && !this.priceUsdc) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`[serve] Route "${this.options.path}": call .paid('<usdc>') or .unprotected() before .handler()`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const runtime = this.runtime;
|
|
232
|
+
const { path, description } = this.options;
|
|
233
|
+
const priceUsdc = this.priceUsdc;
|
|
234
|
+
const bodySchema = this.bodySchema;
|
|
235
|
+
const route = (async (req) => {
|
|
236
|
+
if (req.method === "OPTIONS") {
|
|
237
|
+
return withCors(new Response(null, { status: 204 }));
|
|
238
|
+
}
|
|
239
|
+
let parsedBody;
|
|
240
|
+
let bodyParseError;
|
|
241
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
242
|
+
const text = await req.text();
|
|
243
|
+
if (text.trim().length > 0) {
|
|
244
|
+
try {
|
|
245
|
+
parsedBody = JSON.parse(text);
|
|
246
|
+
} catch {
|
|
247
|
+
bodyParseError = "request body is not valid JSON";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const resource = (runtime.baseUrl ?? new URL(req.url).origin).replace(/\/$/, "") + `/${path}`;
|
|
252
|
+
const runHandler = async (payer) => {
|
|
253
|
+
let validated = parsedBody;
|
|
254
|
+
if (bodySchema) {
|
|
255
|
+
if (bodyParseError) return json(422, { error: bodyParseError });
|
|
256
|
+
const result = await validateBody(bodySchema, parsedBody ?? {});
|
|
257
|
+
if (!result.ok) return json(422, { error: result.message });
|
|
258
|
+
validated = result.data;
|
|
259
|
+
}
|
|
260
|
+
const out = await fn({ body: validated, req, payer });
|
|
261
|
+
if (out instanceof Response) return withCors(out);
|
|
262
|
+
return json(200, out);
|
|
263
|
+
};
|
|
264
|
+
if (!priceUsdc) {
|
|
265
|
+
try {
|
|
266
|
+
return await runHandler();
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error(`[serve] Route "${path}" handler failed:`, err);
|
|
269
|
+
return json(500, { error: "internal error" });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const paymentHeader = req.headers.get(X402_PAYMENT_HEADER);
|
|
273
|
+
if (!paymentHeader) {
|
|
274
|
+
return await respond402(runtime, { resource, priceUsdc });
|
|
275
|
+
}
|
|
276
|
+
if (isX402EscrowHeader(paymentHeader)) {
|
|
277
|
+
return await respond402(runtime, {
|
|
278
|
+
resource,
|
|
279
|
+
priceUsdc,
|
|
280
|
+
error: "this endpoint sells instant per-call work (settle-then-serve), not escrow jobs \u2014 retry with a signed x402 payment"
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
let payment;
|
|
284
|
+
try {
|
|
285
|
+
payment = parseX402Header(paymentHeader);
|
|
286
|
+
verifyX402Payment({
|
|
287
|
+
payment,
|
|
288
|
+
expected: {
|
|
289
|
+
challengeId: payment.payload.challengeId,
|
|
290
|
+
amount: priceUsdc,
|
|
291
|
+
currency: runtime.currency,
|
|
292
|
+
recipient: runtime.payTo,
|
|
293
|
+
network: runtime.network
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return await respond402(runtime, {
|
|
298
|
+
resource,
|
|
299
|
+
priceUsdc,
|
|
300
|
+
error: `invalid payment: ${err instanceof Error ? err.message : String(err)}`
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const challengeKey = `challenge:${payment.payload.challengeId}`;
|
|
304
|
+
if (await runtime.store.has(challengeKey)) {
|
|
305
|
+
return await respond402(runtime, {
|
|
306
|
+
resource,
|
|
307
|
+
priceUsdc,
|
|
308
|
+
error: "payment challenge already used \u2014 sign a fresh payment"
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
let served;
|
|
312
|
+
try {
|
|
313
|
+
served = await runHandler(payment.payload.senderAddress);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error(`[serve] Route "${path}" handler failed (payment NOT settled):`, err);
|
|
316
|
+
return json(500, { error: "internal error \u2014 you were not charged" });
|
|
317
|
+
}
|
|
318
|
+
if (served.status >= 400) return served;
|
|
319
|
+
let settle;
|
|
320
|
+
try {
|
|
321
|
+
settle = await settleX402Payment({
|
|
322
|
+
payment,
|
|
323
|
+
client: getGrpcClient(runtime.network, runtime.rpcUrl),
|
|
324
|
+
store: runtime.store,
|
|
325
|
+
expected: {
|
|
326
|
+
challengeId: payment.payload.challengeId,
|
|
327
|
+
amount: priceUsdc,
|
|
328
|
+
currency: runtime.currency,
|
|
329
|
+
recipient: runtime.payTo,
|
|
330
|
+
network: runtime.network
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return await respond402(runtime, {
|
|
335
|
+
resource,
|
|
336
|
+
priceUsdc,
|
|
337
|
+
error: `settlement failed: ${err instanceof Error ? err.message : String(err)}`
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
await runtime.store.set(challengeKey);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
if (runtime.report) {
|
|
345
|
+
reportPayment(settle.transaction, resource);
|
|
346
|
+
}
|
|
347
|
+
const headers = new Headers(served.headers);
|
|
348
|
+
headers.set(X402_PAYMENT_RESPONSE_HEADER, encodeX402Response(settle));
|
|
349
|
+
return new Response(served.body, { status: served.status, headers });
|
|
350
|
+
});
|
|
351
|
+
route.meta = { path, priceUsdc, description, bodySchema, inputSchema: this.inputSchema };
|
|
352
|
+
this.register(route);
|
|
353
|
+
return route;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
async function respond402(runtime, args) {
|
|
357
|
+
try {
|
|
358
|
+
const { chain, epoch } = await getChainInfo(runtime.network, runtime.rpcUrl);
|
|
359
|
+
const requirements = createX402Requirements({
|
|
360
|
+
challengeId: crypto.randomUUID(),
|
|
361
|
+
amount: args.priceUsdc,
|
|
362
|
+
currency: runtime.currency,
|
|
363
|
+
recipient: runtime.payTo,
|
|
364
|
+
resource: args.resource,
|
|
365
|
+
network: runtime.network,
|
|
366
|
+
chain,
|
|
367
|
+
currentEpoch: epoch
|
|
368
|
+
});
|
|
369
|
+
return json(402, {
|
|
370
|
+
x402Version: X402_VERSION,
|
|
371
|
+
error: args.error ?? "Payment required",
|
|
372
|
+
accepts: [requirements]
|
|
373
|
+
});
|
|
374
|
+
} catch (err) {
|
|
375
|
+
console.error("[serve] failed to build 402 challenge:", err);
|
|
376
|
+
return json(503, { error: "payment challenge temporarily unavailable" });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function reportPayment(digest, url) {
|
|
380
|
+
void fetch(MPP_REPORT_URL, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: { "content-type": "application/json" },
|
|
383
|
+
body: JSON.stringify({ digest, url }),
|
|
384
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
385
|
+
}).catch(() => {
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
var DEFAULT_TTL_SECONDS = 72 * 60 * 60;
|
|
389
|
+
var PREFIX = "serve:digest:";
|
|
390
|
+
var UpstashDigestStore = class {
|
|
391
|
+
url;
|
|
392
|
+
token;
|
|
393
|
+
ttlSeconds;
|
|
394
|
+
constructor(options) {
|
|
395
|
+
this.url = options.url.replace(/\/$/, "");
|
|
396
|
+
this.token = options.token;
|
|
397
|
+
this.ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
398
|
+
}
|
|
399
|
+
async command(segments) {
|
|
400
|
+
const res = await fetch(`${this.url}/${segments.map(encodeURIComponent).join("/")}`, {
|
|
401
|
+
headers: { authorization: `Bearer ${this.token}` }
|
|
402
|
+
});
|
|
403
|
+
if (!res.ok) {
|
|
404
|
+
throw new Error(`Upstash command failed: ${res.status} ${await res.text()}`);
|
|
405
|
+
}
|
|
406
|
+
const body = await res.json();
|
|
407
|
+
if (body.error) throw new Error(`Upstash error: ${body.error}`);
|
|
408
|
+
return body.result;
|
|
409
|
+
}
|
|
410
|
+
async has(digest) {
|
|
411
|
+
const result = await this.command(["GET", PREFIX + digest]);
|
|
412
|
+
return result !== null && result !== void 0;
|
|
413
|
+
}
|
|
414
|
+
async set(digest) {
|
|
415
|
+
const result = await this.command([
|
|
416
|
+
"SET",
|
|
417
|
+
PREFIX + digest,
|
|
418
|
+
"1",
|
|
419
|
+
"EX",
|
|
420
|
+
String(this.ttlSeconds),
|
|
421
|
+
"NX"
|
|
422
|
+
]);
|
|
423
|
+
if (result === null || result === void 0) {
|
|
424
|
+
throw new Error(`Digest already used: ${digest}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// src/serve.ts
|
|
430
|
+
var Serve = class {
|
|
431
|
+
runtime;
|
|
432
|
+
/** Every route built through this instance, keyed by path (discovery). */
|
|
433
|
+
routes = /* @__PURE__ */ new Map();
|
|
434
|
+
payTo;
|
|
435
|
+
network;
|
|
436
|
+
name;
|
|
437
|
+
description;
|
|
438
|
+
baseUrl;
|
|
439
|
+
constructor(config) {
|
|
440
|
+
if (!config.payTo || !isValidSuiAddress(config.payTo)) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`[serve] payTo must be a valid Sui address (the wallet your payments settle to), got "${config.payTo}"`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
this.payTo = normalizeSuiAddress(config.payTo);
|
|
446
|
+
this.network = config.network ?? "mainnet";
|
|
447
|
+
this.name = config.name;
|
|
448
|
+
this.description = config.description;
|
|
449
|
+
this.baseUrl = config.baseUrl?.replace(/\/$/, "");
|
|
450
|
+
const currency = this.network === "testnet" ? USDC_TESTNET : USDC;
|
|
451
|
+
this.runtime = {
|
|
452
|
+
payTo: this.payTo,
|
|
453
|
+
network: this.network,
|
|
454
|
+
currency,
|
|
455
|
+
store: config.store ?? new InMemoryDigestStore(),
|
|
456
|
+
baseUrl: this.baseUrl,
|
|
457
|
+
rpcUrl: config.rpcUrl,
|
|
458
|
+
report: config.report ?? true
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/** Start building a route. Chain `.paid()` / `.body()` / `.handler()`. */
|
|
462
|
+
route(options) {
|
|
463
|
+
const path = options.path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
464
|
+
if (!path) throw new Error("[serve] route path must be non-empty");
|
|
465
|
+
return new RouteBuilder({ ...this.runtime }, { ...options, path }, (route) => {
|
|
466
|
+
this.routes.set(path, route);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Discovery: GET handler for /openapi.json. OpenAPI 3.1 with the
|
|
471
|
+
* `x-payment-info` pricing extension on every paid operation — the shape
|
|
472
|
+
* the mpp.t2000.ai catalog (and x402 tooling generally) indexes.
|
|
473
|
+
*
|
|
474
|
+
* export const GET = serve.openapi(); // app/openapi.json/route.ts
|
|
475
|
+
*/
|
|
476
|
+
openapi() {
|
|
477
|
+
return (req) => new Response(JSON.stringify(buildOpenApiDocument(this, new URL(req.url).origin), null, 2), {
|
|
478
|
+
headers: { "content-type": "application/json", "access-control-allow-origin": "*" }
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Discovery: GET handler for /llms.txt — plain-text guidance agents read
|
|
483
|
+
* to understand what the API sells, what it costs, and how to pay.
|
|
484
|
+
*
|
|
485
|
+
* export const GET = serve.llms(); // app/llms.txt/route.ts
|
|
486
|
+
*/
|
|
487
|
+
llms() {
|
|
488
|
+
return (req) => new Response(buildLlmsTxt(this, new URL(req.url).origin), {
|
|
489
|
+
headers: { "content-type": "text/plain; charset=utf-8", "access-control-allow-origin": "*" }
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* One fetch handler for the whole app — routes + discovery docs. For
|
|
494
|
+
* fetch-native runtimes (Bun.serve, Deno.serve, Hono, Cloudflare Workers):
|
|
495
|
+
*
|
|
496
|
+
* Bun.serve({ fetch: serve.fetch });
|
|
497
|
+
* app.all('*', (c) => serve.fetch(c.req.raw)); // Hono
|
|
498
|
+
*
|
|
499
|
+
* Next.js apps can skip this and export route handlers directly.
|
|
500
|
+
*/
|
|
501
|
+
fetch = async (req) => {
|
|
502
|
+
const pathname = new URL(req.url).pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
503
|
+
if (pathname === "openapi.json") return this.openapi()(req);
|
|
504
|
+
if (pathname === "llms.txt") return this.llms()(req);
|
|
505
|
+
const route = this.routes.get(pathname);
|
|
506
|
+
if (route) return route(req);
|
|
507
|
+
return new Response(
|
|
508
|
+
JSON.stringify({ error: "not found", discovery: ["/openapi.json", "/llms.txt"] }),
|
|
509
|
+
{ status: 404, headers: { "content-type": "application/json" } }
|
|
510
|
+
);
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* The curl that lists this API on mpp.t2000.ai / agents.t2000.ai once it
|
|
514
|
+
* is deployed. Listing is a separate, explicit step — the catalog gates
|
|
515
|
+
* (url · probe · dialect · price-cap), not this package, decide outcomes.
|
|
516
|
+
* Dry-run first with /api/catalog/preview.
|
|
517
|
+
*/
|
|
518
|
+
catalogSubmitCommand(deployedUrl) {
|
|
519
|
+
const base = (deployedUrl ?? this.baseUrl ?? "https://<your-deployed-app>").replace(/\/$/, "");
|
|
520
|
+
const paidPaths = [...this.routes.values()].filter((r) => r.meta.priceUsdc);
|
|
521
|
+
const example = paidPaths[0]?.meta.path ?? "<route>";
|
|
522
|
+
return [
|
|
523
|
+
"# Dry-run (shows gate results, changes nothing):",
|
|
524
|
+
`curl -X POST https://mpp.t2000.ai/api/catalog/preview -H 'content-type: application/json' -d '{"url":"${base}/${example}"}'`,
|
|
525
|
+
"# List for real:",
|
|
526
|
+
`curl -X POST https://mpp.t2000.ai/api/catalog/submit -H 'content-type: application/json' -d '{"url":"${base}/${example}"}'`
|
|
527
|
+
].join("\n");
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
function createServe(config) {
|
|
531
|
+
return new Serve(config);
|
|
532
|
+
}
|
|
533
|
+
function createServeFromEnv(env = process.env) {
|
|
534
|
+
const read = (key) => {
|
|
535
|
+
const v = env[key]?.trim();
|
|
536
|
+
return v && v.length > 0 ? v : void 0;
|
|
537
|
+
};
|
|
538
|
+
const payTo = read("T2000_PAY_TO");
|
|
539
|
+
if (!payTo) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
"[serve] T2000_PAY_TO is not set (or is empty). Set it to the Sui address your payments should settle to \u2014 `t2 address` prints yours."
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const network = read("T2000_NETWORK");
|
|
545
|
+
if (network && network !== "mainnet" && network !== "testnet") {
|
|
546
|
+
throw new Error(`[serve] T2000_NETWORK must be 'mainnet' or 'testnet', got "${network}"`);
|
|
547
|
+
}
|
|
548
|
+
const kvUrl = read("KV_REST_API_URL");
|
|
549
|
+
const kvToken = read("KV_REST_API_TOKEN");
|
|
550
|
+
const store = kvUrl && kvToken ? new UpstashDigestStore({ url: kvUrl, token: kvToken }) : void 0;
|
|
551
|
+
if (!store) {
|
|
552
|
+
console.warn(
|
|
553
|
+
"[serve] No KV_REST_API_URL/KV_REST_API_TOKEN \u2014 using the in-memory replay store. Fine for a single long-lived process; on serverless hosts set both so replay protection is durable."
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return new Serve({
|
|
557
|
+
payTo,
|
|
558
|
+
network: network ?? "mainnet",
|
|
559
|
+
baseUrl: read("T2000_BASE_URL"),
|
|
560
|
+
name: read("T2000_NAME"),
|
|
561
|
+
description: read("T2000_DESCRIPTION"),
|
|
562
|
+
store
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export { RouteBuilder, Serve, UpstashDigestStore, __resetChainCaches, __seedChainInfo, buildLlmsTxt, buildOpenApiDocument, createServe, createServeFromEnv };
|
|
567
|
+
//# sourceMappingURL=index.js.map
|
|
568
|
+
//# sourceMappingURL=index.js.map
|