routstrd 0.2.5 → 0.2.8
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/bun.lock +16 -217
- package/dist/daemon/index.js +472 -397
- package/dist/index.js +10239 -31673
- package/package.json +2 -1
- package/src/cli.ts +291 -208
- package/src/daemon/wallet/cocod-client.ts +2 -1
- package/src/integrations/claudecode.ts +19 -40
- package/src/integrations/openclaw.ts +8 -34
- package/src/integrations/opencode.ts +8 -34
- package/src/integrations/pi.ts +7 -34
- package/src/integrations/registry.ts +4 -12
- package/src/tui/usage/data.ts +19 -7
- package/src/utils/clients.ts +304 -0
- package/src/utils/config.ts +2 -0
- package/src/utils/daemon-client.ts +84 -13
- package/src/utils/nip98.ts +102 -0
- package/src/daemon/http/index.ts +0 -1130
- package/src/daemon/index.ts +0 -242
- package/src/daemon/wallet/index.ts +0 -122
- package/src/index.ts +0 -4
- package/src/integrations/index.ts +0 -76
- package/src/tui/usage/index.ts +0 -1
package/src/daemon/http/index.ts
DELETED
|
@@ -1,1130 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
|
-
import { type IncomingMessage, type ServerResponse } from "http";
|
|
3
|
-
import { Readable } from "stream";
|
|
4
|
-
import {
|
|
5
|
-
routeRequests,
|
|
6
|
-
InsufficientBalanceError,
|
|
7
|
-
ProviderManager,
|
|
8
|
-
} from "@routstr/sdk";
|
|
9
|
-
import type { UsageTrackingDriver } from "@routstr/sdk";
|
|
10
|
-
import { logger } from "../../utils/logger";
|
|
11
|
-
import {
|
|
12
|
-
CocodHttpError,
|
|
13
|
-
type CocodClient,
|
|
14
|
-
type CocodState,
|
|
15
|
-
} from "../wallet/cocod-client";
|
|
16
|
-
import { decodeCashuTokenAmount } from "../wallet";
|
|
17
|
-
|
|
18
|
-
type ClientMode = "xcashu" | "lazyrefund" | "apikeys";
|
|
19
|
-
|
|
20
|
-
type WalletStatusOutput = {
|
|
21
|
-
daemon: "running";
|
|
22
|
-
wallet: "connected" | "error";
|
|
23
|
-
walletState: CocodState;
|
|
24
|
-
balances?: Record<string, number>;
|
|
25
|
-
mode: ClientMode;
|
|
26
|
-
error?: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type DaemonDeps = {
|
|
30
|
-
provider: string | null;
|
|
31
|
-
server: { close(cb?: () => void): void };
|
|
32
|
-
store: any;
|
|
33
|
-
walletClient: CocodClient;
|
|
34
|
-
walletAdapter: any;
|
|
35
|
-
storageAdapter: any;
|
|
36
|
-
providerRegistry: any;
|
|
37
|
-
discoveryAdapter: any;
|
|
38
|
-
modelManager: any;
|
|
39
|
-
ensureProvidersBootstrapped: () => Promise<void>;
|
|
40
|
-
getRoutstr21Models: (forceRefresh?: boolean) => Promise<any[]>;
|
|
41
|
-
getModelProviders: (modelId: string) => Promise<any>;
|
|
42
|
-
mode?: ClientMode;
|
|
43
|
-
providerManager: ProviderManager;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Extracts the client ID from an incoming request by looking up the API key
|
|
48
|
-
* in the store's clientIds list.
|
|
49
|
-
*/
|
|
50
|
-
function getClientIdFromRequest(
|
|
51
|
-
req: IncomingMessage,
|
|
52
|
-
store: { getState(): any },
|
|
53
|
-
): string | undefined {
|
|
54
|
-
const authHeader = req.headers.authorization;
|
|
55
|
-
|
|
56
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const apiKey = authHeader.slice(7); // Remove "Bearer " prefix
|
|
61
|
-
|
|
62
|
-
if (!apiKey.startsWith("sk-")) {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const state = store.getState();
|
|
67
|
-
const clientIds = state.clientIds || [];
|
|
68
|
-
|
|
69
|
-
const matchingClient = (
|
|
70
|
-
clientIds as { clientId: string; apiKey: string }[]
|
|
71
|
-
).find((c) => c.apiKey === apiKey);
|
|
72
|
-
|
|
73
|
-
return matchingClient?.clientId;
|
|
74
|
-
}
|
|
75
|
-
function generateApiKey(): string {
|
|
76
|
-
const bytes = randomBytes(24);
|
|
77
|
-
return `sk-${bytes.toString("hex")}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function readBody(req: IncomingMessage): Promise<string> {
|
|
81
|
-
return new Promise((resolve, reject) => {
|
|
82
|
-
let data = "";
|
|
83
|
-
req.on("data", (chunk) => {
|
|
84
|
-
data += chunk.toString();
|
|
85
|
-
});
|
|
86
|
-
req.on("end", () => resolve(data));
|
|
87
|
-
req.on("error", reject);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function readJsonBody(
|
|
92
|
-
req: IncomingMessage,
|
|
93
|
-
): Promise<Record<string, unknown>> {
|
|
94
|
-
const bodyText = await readBody(req);
|
|
95
|
-
if (!bodyText) {
|
|
96
|
-
return {};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
return JSON.parse(bodyText) as Record<string, unknown>;
|
|
101
|
-
} catch {
|
|
102
|
-
throw new CocodHttpError(400, "Invalid JSON body.");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function parseLimit(value: string | null, fallback = 10): number {
|
|
107
|
-
const requested = Number.parseInt(value || String(fallback), 10);
|
|
108
|
-
return Number.isFinite(requested) && requested > 0
|
|
109
|
-
? Math.min(requested, 100000) // Cap at 100k entries
|
|
110
|
-
: fallback;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function sendJson(
|
|
114
|
-
res: ServerResponse,
|
|
115
|
-
status: number,
|
|
116
|
-
payload: Record<string, unknown>,
|
|
117
|
-
): void {
|
|
118
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
119
|
-
res.end(JSON.stringify(payload));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function toErrorMessage(error: unknown): string {
|
|
123
|
-
return error instanceof Error ? error.message : String(error);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getWalletStateMessage(state: CocodState): string {
|
|
127
|
-
switch (state) {
|
|
128
|
-
case "LOCKED":
|
|
129
|
-
return "Wallet is locked. Unlock it before performing wallet operations.";
|
|
130
|
-
case "UNINITIALIZED":
|
|
131
|
-
return "Wallet is not initialized. Run 'routstrd onboard' first.";
|
|
132
|
-
case "ERROR":
|
|
133
|
-
return "Wallet is in an error state.";
|
|
134
|
-
default:
|
|
135
|
-
return "Wallet is unavailable.";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function respondWithError(
|
|
140
|
-
res: ServerResponse,
|
|
141
|
-
error: unknown,
|
|
142
|
-
fallbackStatus = 500,
|
|
143
|
-
): void {
|
|
144
|
-
if (error instanceof CocodHttpError) {
|
|
145
|
-
sendJson(res, error.status, { error: error.message });
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
sendJson(res, fallbackStatus, { error: toErrorMessage(error) });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function respond(
|
|
153
|
-
res: ServerResponse,
|
|
154
|
-
getPayload: () => Promise<Record<string, unknown>>,
|
|
155
|
-
): Promise<void> {
|
|
156
|
-
try {
|
|
157
|
-
sendJson(res, 200, await getPayload());
|
|
158
|
-
} catch (error) {
|
|
159
|
-
respondWithError(res, error);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function requireStringField(
|
|
164
|
-
body: Record<string, unknown>,
|
|
165
|
-
field: string,
|
|
166
|
-
): string | null {
|
|
167
|
-
const value = body[field];
|
|
168
|
-
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function getRequiredStringField(
|
|
172
|
-
body: Record<string, unknown>,
|
|
173
|
-
field: string,
|
|
174
|
-
): string {
|
|
175
|
-
const value = requireStringField(body, field);
|
|
176
|
-
if (!value) {
|
|
177
|
-
throw new CocodHttpError(400, `Missing required '${field}' field.`);
|
|
178
|
-
}
|
|
179
|
-
return value;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function getRequiredPositiveNumberField(
|
|
183
|
-
body: Record<string, unknown>,
|
|
184
|
-
field: string,
|
|
185
|
-
): number {
|
|
186
|
-
const value = body[field];
|
|
187
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
188
|
-
return value;
|
|
189
|
-
}
|
|
190
|
-
if (typeof value === "string" && value.trim()) {
|
|
191
|
-
const parsed = Number.parseInt(value.trim(), 10);
|
|
192
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
193
|
-
return parsed;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
throw new CocodHttpError(400, `Missing required '${field}' field.`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function optionalStringField(
|
|
200
|
-
body: Record<string, unknown>,
|
|
201
|
-
field: string,
|
|
202
|
-
): string | undefined {
|
|
203
|
-
const value = body[field];
|
|
204
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function getCurrentMode(deps: DaemonDeps): ClientMode {
|
|
208
|
-
const stateMode = deps.store.getState()?.mode;
|
|
209
|
-
return stateMode || deps.mode || "apikeys";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function buildStatusOutput(
|
|
213
|
-
deps: DaemonDeps,
|
|
214
|
-
): Promise<WalletStatusOutput> {
|
|
215
|
-
const mode = getCurrentMode(deps);
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
const walletState = await deps.walletClient.getStatus();
|
|
219
|
-
if (walletState !== "UNLOCKED") {
|
|
220
|
-
return {
|
|
221
|
-
daemon: "running",
|
|
222
|
-
wallet: "error",
|
|
223
|
-
walletState,
|
|
224
|
-
mode,
|
|
225
|
-
error: getWalletStateMessage(walletState),
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const balances = await deps.walletAdapter.getBalances();
|
|
230
|
-
return {
|
|
231
|
-
daemon: "running",
|
|
232
|
-
wallet: "connected",
|
|
233
|
-
walletState,
|
|
234
|
-
balances,
|
|
235
|
-
mode,
|
|
236
|
-
};
|
|
237
|
-
} catch (error) {
|
|
238
|
-
return {
|
|
239
|
-
daemon: "running",
|
|
240
|
-
wallet: "error",
|
|
241
|
-
walletState: "ERROR",
|
|
242
|
-
mode,
|
|
243
|
-
error: toErrorMessage(error),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async function buildWalletDetails(deps: DaemonDeps): Promise<{
|
|
249
|
-
state: CocodState;
|
|
250
|
-
ready: boolean;
|
|
251
|
-
balances?: Record<string, number>;
|
|
252
|
-
unit?: "sat";
|
|
253
|
-
activeMint?: string | null;
|
|
254
|
-
}> {
|
|
255
|
-
const state = await deps.walletClient.getStatus();
|
|
256
|
-
if (state !== "UNLOCKED") {
|
|
257
|
-
return { state, ready: false };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const balances = await deps.walletAdapter.getBalances();
|
|
261
|
-
return {
|
|
262
|
-
state,
|
|
263
|
-
ready: true,
|
|
264
|
-
balances,
|
|
265
|
-
unit: "sat",
|
|
266
|
-
activeMint: deps.walletAdapter.getActiveMintUrl(),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function createDaemonRequestHandler(deps: {
|
|
271
|
-
provider: string | null;
|
|
272
|
-
server: { close(cb?: () => void): void };
|
|
273
|
-
store: any;
|
|
274
|
-
walletClient: CocodClient;
|
|
275
|
-
walletAdapter: any;
|
|
276
|
-
storageAdapter: any;
|
|
277
|
-
providerRegistry: any;
|
|
278
|
-
discoveryAdapter: any;
|
|
279
|
-
modelManager: any;
|
|
280
|
-
ensureProvidersBootstrapped: () => Promise<void>;
|
|
281
|
-
getRoutstr21Models: (forceRefresh?: boolean) => Promise<any[]>;
|
|
282
|
-
getModelProviders: (modelId: string) => Promise<any>;
|
|
283
|
-
mode?: "xcashu" | "apikeys";
|
|
284
|
-
usageTrackingDriver: UsageTrackingDriver;
|
|
285
|
-
providerManager: ProviderManager;
|
|
286
|
-
}) {
|
|
287
|
-
return async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
288
|
-
const host = req.headers.host || "localhost";
|
|
289
|
-
const url = new URL(req.url || "/", `http://${host}`);
|
|
290
|
-
|
|
291
|
-
if (req.method === "GET" && url.pathname === "/health") {
|
|
292
|
-
sendJson(res, 200, { ok: true });
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (req.method === "GET" && url.pathname === "/ping") {
|
|
297
|
-
sendJson(res, 200, { output: "pong" });
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (req.method === "GET" && url.pathname === "/status") {
|
|
302
|
-
const output = await buildStatusOutput(deps);
|
|
303
|
-
sendJson(res, 200, { output });
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (req.method === "GET" && url.pathname === "/wallet/status") {
|
|
308
|
-
await respond(res, async () => ({
|
|
309
|
-
output: await buildWalletDetails(deps),
|
|
310
|
-
}));
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (req.method === "POST" && url.pathname === "/wallet/unlock") {
|
|
315
|
-
await respond(res, async () => {
|
|
316
|
-
const body = await readJsonBody(req);
|
|
317
|
-
const passphrase = getRequiredStringField(body, "passphrase");
|
|
318
|
-
const message = await deps.walletClient.unlock(passphrase);
|
|
319
|
-
const state = await deps.walletClient.getStatus();
|
|
320
|
-
return { output: { message, state } };
|
|
321
|
-
});
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (req.method === "GET" && url.pathname === "/wallet/balance") {
|
|
326
|
-
await respond(res, async () => {
|
|
327
|
-
const balances = await deps.walletAdapter.getBalances();
|
|
328
|
-
return {
|
|
329
|
-
output: {
|
|
330
|
-
balances,
|
|
331
|
-
unit: "sat",
|
|
332
|
-
activeMint: deps.walletAdapter.getActiveMintUrl(),
|
|
333
|
-
walletState: "UNLOCKED",
|
|
334
|
-
},
|
|
335
|
-
};
|
|
336
|
-
});
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (req.method === "POST" && url.pathname === "/wallet/receive/cashu") {
|
|
341
|
-
await respond(res, async () => {
|
|
342
|
-
const body = await readJsonBody(req);
|
|
343
|
-
const token = getRequiredStringField(body, "token");
|
|
344
|
-
const message = await deps.walletClient.receiveCashu(token);
|
|
345
|
-
const { amount, unit } = decodeCashuTokenAmount(token);
|
|
346
|
-
return { output: { message, amount, unit } };
|
|
347
|
-
});
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (req.method === "POST" && url.pathname === "/wallet/receive/bolt11") {
|
|
352
|
-
await respond(res, async () => {
|
|
353
|
-
const body = await readJsonBody(req);
|
|
354
|
-
const amount = getRequiredPositiveNumberField(body, "amount");
|
|
355
|
-
const mintUrl = optionalStringField(body, "mintUrl");
|
|
356
|
-
const invoice = await deps.walletClient.receiveBolt11(amount, mintUrl);
|
|
357
|
-
return { output: { invoice, amount, mintUrl } };
|
|
358
|
-
});
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (req.method === "POST" && url.pathname === "/wallet/send/cashu") {
|
|
363
|
-
await respond(res, async () => {
|
|
364
|
-
const body = await readJsonBody(req);
|
|
365
|
-
const amount = getRequiredPositiveNumberField(body, "amount");
|
|
366
|
-
const mintUrl = optionalStringField(body, "mintUrl");
|
|
367
|
-
const token = await deps.walletClient.sendCashu(amount, mintUrl);
|
|
368
|
-
return { output: { token, amount, mintUrl } };
|
|
369
|
-
});
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (req.method === "POST" && url.pathname === "/wallet/send/bolt11") {
|
|
374
|
-
await respond(res, async () => {
|
|
375
|
-
const body = await readJsonBody(req);
|
|
376
|
-
const invoice = getRequiredStringField(body, "invoice");
|
|
377
|
-
const mintUrl = optionalStringField(body, "mintUrl");
|
|
378
|
-
const message = await deps.walletClient.sendBolt11(invoice, mintUrl);
|
|
379
|
-
return { output: { message, invoice, mintUrl } };
|
|
380
|
-
});
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (req.method === "GET" && url.pathname === "/wallet/mints") {
|
|
385
|
-
await respond(res, async () => {
|
|
386
|
-
const mints = await deps.walletClient.listMints();
|
|
387
|
-
return {
|
|
388
|
-
output: {
|
|
389
|
-
mints,
|
|
390
|
-
activeMint: mints[0] || null,
|
|
391
|
-
},
|
|
392
|
-
};
|
|
393
|
-
});
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (req.method === "POST" && url.pathname === "/wallet/mints") {
|
|
398
|
-
await respond(res, async () => {
|
|
399
|
-
const body = await readJsonBody(req);
|
|
400
|
-
const mintUrl = getRequiredStringField(body, "url");
|
|
401
|
-
const message = await deps.walletClient.addMint(mintUrl);
|
|
402
|
-
return { output: { message, url: mintUrl } };
|
|
403
|
-
});
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (req.method === "POST" && url.pathname === "/wallet/mints/info") {
|
|
408
|
-
await respond(res, async () => {
|
|
409
|
-
const body = await readJsonBody(req);
|
|
410
|
-
const mintUrl = getRequiredStringField(body, "url");
|
|
411
|
-
const info = await deps.walletClient.getMintInfo(mintUrl);
|
|
412
|
-
return { output: { url: mintUrl, info } };
|
|
413
|
-
});
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (req.method === "GET" && url.pathname === "/models") {
|
|
418
|
-
try {
|
|
419
|
-
const forceRefresh =
|
|
420
|
-
url.searchParams.get("refresh")?.toLowerCase() === "true";
|
|
421
|
-
const models = await deps.getRoutstr21Models(forceRefresh);
|
|
422
|
-
sendJson(res, 200, { output: { models } });
|
|
423
|
-
} catch (error) {
|
|
424
|
-
sendJson(res, 500, { error: toErrorMessage(error) });
|
|
425
|
-
}
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Get providers for a specific model
|
|
430
|
-
const modelProvidersMatch = url.pathname.match(/^\/models\/([^\/]+)\/providers$/);
|
|
431
|
-
if (req.method === "GET" && modelProvidersMatch && modelProvidersMatch[1]) {
|
|
432
|
-
try {
|
|
433
|
-
const modelId = decodeURIComponent(modelProvidersMatch[1]);
|
|
434
|
-
const modelWithProviders = await deps.getModelProviders(modelId);
|
|
435
|
-
if (!modelWithProviders) {
|
|
436
|
-
sendJson(res, 404, { error: `Model '${modelId}' not found` });
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
sendJson(res, 200, { output: modelWithProviders });
|
|
440
|
-
} catch (error) {
|
|
441
|
-
sendJson(res, 500, { error: toErrorMessage(error) });
|
|
442
|
-
}
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (req.method === "GET" && url.pathname === "/v1/models") {
|
|
447
|
-
try {
|
|
448
|
-
const forceRefresh =
|
|
449
|
-
url.searchParams.get("refresh")?.toLowerCase() === "true";
|
|
450
|
-
const models = await deps.getRoutstr21Models(forceRefresh);
|
|
451
|
-
sendJson(res, 200, {
|
|
452
|
-
object: "list",
|
|
453
|
-
data: models.map((model) => ({ ...model, object: "model" })),
|
|
454
|
-
});
|
|
455
|
-
} catch (error) {
|
|
456
|
-
sendJson(res, 500, { error: toErrorMessage(error) });
|
|
457
|
-
}
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (req.method === "POST" && url.pathname === "/stop") {
|
|
462
|
-
sendJson(res, 200, { output: "stopping" });
|
|
463
|
-
setTimeout(() => {
|
|
464
|
-
deps.server.close(() => {
|
|
465
|
-
process.exit(0);
|
|
466
|
-
});
|
|
467
|
-
}, 50);
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (req.method === "POST" && url.pathname === "/refund") {
|
|
472
|
-
try {
|
|
473
|
-
const body = await readJsonBody(req);
|
|
474
|
-
const mintUrl = getRequiredStringField(body, "mintUrl");
|
|
475
|
-
|
|
476
|
-
const state = deps.store.getState();
|
|
477
|
-
const pendingDistribution = (state.cachedTokens || []).map(
|
|
478
|
-
(t: { baseUrl: string; balance?: number }) => ({
|
|
479
|
-
baseUrl: t.baseUrl,
|
|
480
|
-
amount: t.balance || 0,
|
|
481
|
-
}),
|
|
482
|
-
);
|
|
483
|
-
const apiKeysStored = (state.apiKeys || []).map(
|
|
484
|
-
(k: { baseUrl: string; balance?: number }) => ({
|
|
485
|
-
baseUrl: k.baseUrl,
|
|
486
|
-
amount: k.balance || 0,
|
|
487
|
-
}),
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
|
|
491
|
-
sendJson(res, 200, {
|
|
492
|
-
output: { message: "No pending tokens to refund", results: [] },
|
|
493
|
-
});
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const refundBaseUrls = pendingDistribution
|
|
498
|
-
.map((p: { baseUrl: string }) => p.baseUrl)
|
|
499
|
-
.concat(apiKeysStored.map((p: { baseUrl: string }) => p.baseUrl));
|
|
500
|
-
|
|
501
|
-
const { RoutstrClient } = await import("@routstr/sdk");
|
|
502
|
-
const client = new RoutstrClient(
|
|
503
|
-
deps.walletAdapter,
|
|
504
|
-
deps.storageAdapter,
|
|
505
|
-
deps.providerRegistry,
|
|
506
|
-
"min",
|
|
507
|
-
"apikeys",
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
const spender = client.getCashuSpender();
|
|
511
|
-
const results = await spender.refundProviders(mintUrl, true);
|
|
512
|
-
|
|
513
|
-
sendJson(res, 200, {
|
|
514
|
-
output: {
|
|
515
|
-
message: `Refunded to ${mintUrl}`,
|
|
516
|
-
pendingTokens: pendingDistribution.length,
|
|
517
|
-
apiKeys: apiKeysStored.length,
|
|
518
|
-
results: results.map(
|
|
519
|
-
(r: { baseUrl: string; success: boolean }) => ({
|
|
520
|
-
baseUrl: r.baseUrl,
|
|
521
|
-
success: r.success,
|
|
522
|
-
}),
|
|
523
|
-
),
|
|
524
|
-
},
|
|
525
|
-
});
|
|
526
|
-
} catch (error) {
|
|
527
|
-
logger.error(`Refund error: ${toErrorMessage(error)}`);
|
|
528
|
-
respondWithError(res, error);
|
|
529
|
-
}
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (req.method === "GET" && url.pathname === "/balance") {
|
|
534
|
-
try {
|
|
535
|
-
const balances = await deps.walletAdapter.getBalances();
|
|
536
|
-
sendJson(res, 200, {
|
|
537
|
-
output: {
|
|
538
|
-
balances,
|
|
539
|
-
unit: "sat",
|
|
540
|
-
activeMint: deps.walletAdapter.getActiveMintUrl(),
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
} catch (error) {
|
|
544
|
-
respondWithError(res, error);
|
|
545
|
-
}
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (req.method === "GET" && url.pathname === "/keys/balance") {
|
|
550
|
-
try {
|
|
551
|
-
const walletBalances = await deps.walletAdapter.getBalances();
|
|
552
|
-
const totalWallet = Object.values(walletBalances).reduce<number>(
|
|
553
|
-
(sum, balance) => sum + Number(balance),
|
|
554
|
-
0,
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
const state = deps.store.getState();
|
|
558
|
-
const cachedTokens = state.cachedTokens || [];
|
|
559
|
-
const totalCached = cachedTokens.reduce(
|
|
560
|
-
(sum: number, t: { balance?: number }) => sum + (t.balance || 0),
|
|
561
|
-
0,
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
const apiKeys = state.apiKeys || [];
|
|
565
|
-
const totalApiKeys = apiKeys.reduce(
|
|
566
|
-
(sum: number, k: { balance?: number }) => sum + (k.balance || 0),
|
|
567
|
-
0,
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
const keys: Array<{ id: string; name: string; balance: number }> = [
|
|
571
|
-
{ id: "wallet", name: "Wallet", balance: totalWallet },
|
|
572
|
-
...cachedTokens.map((t: { baseUrl: string; balance?: number }) => ({
|
|
573
|
-
id: `cached:${t.baseUrl}`,
|
|
574
|
-
name: `Cached: ${t.baseUrl}`,
|
|
575
|
-
balance: t.balance || 0,
|
|
576
|
-
})),
|
|
577
|
-
...apiKeys.map((k: { baseUrl: string; balance?: number }) => ({
|
|
578
|
-
id: `apikey:${k.baseUrl}`,
|
|
579
|
-
name: `API Key: ${k.baseUrl}`,
|
|
580
|
-
balance: k.balance || 0,
|
|
581
|
-
})),
|
|
582
|
-
];
|
|
583
|
-
|
|
584
|
-
sendJson(res, 200, {
|
|
585
|
-
output: {
|
|
586
|
-
keys,
|
|
587
|
-
total: totalWallet + totalCached + totalApiKeys,
|
|
588
|
-
unit: "sat",
|
|
589
|
-
apikeysCalled: apiKeys.length,
|
|
590
|
-
},
|
|
591
|
-
});
|
|
592
|
-
} catch (error) {
|
|
593
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
594
|
-
res.end(JSON.stringify({ error: String(error) }));
|
|
595
|
-
}
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (req.method === "POST" && url.pathname === "/providers/disable") {
|
|
600
|
-
try {
|
|
601
|
-
const bodyText = await readBody(req);
|
|
602
|
-
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
603
|
-
const indices = body.indices as number[] | undefined;
|
|
604
|
-
|
|
605
|
-
if (!Array.isArray(indices)) {
|
|
606
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
607
|
-
res.end(
|
|
608
|
-
JSON.stringify({
|
|
609
|
-
error: "Missing or invalid 'indices' field (expected number[]).",
|
|
610
|
-
}),
|
|
611
|
-
);
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const state = deps.store.getState();
|
|
616
|
-
const baseUrlsList: string[] = state.baseUrlsList || [];
|
|
617
|
-
const disabledProviders: string[] = [
|
|
618
|
-
...(state.disabledProviders || []),
|
|
619
|
-
];
|
|
620
|
-
|
|
621
|
-
const toDisable: string[] = [];
|
|
622
|
-
for (const idx of indices) {
|
|
623
|
-
if (
|
|
624
|
-
typeof idx === "number" &&
|
|
625
|
-
idx >= 0 &&
|
|
626
|
-
idx < baseUrlsList.length
|
|
627
|
-
) {
|
|
628
|
-
const baseUrl = baseUrlsList[idx]!;
|
|
629
|
-
if (!disabledProviders.includes(baseUrl)) {
|
|
630
|
-
disabledProviders.push(baseUrl);
|
|
631
|
-
toDisable.push(baseUrl);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
deps.store.getState().setDisabledProviders(disabledProviders);
|
|
637
|
-
|
|
638
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
639
|
-
res.end(
|
|
640
|
-
JSON.stringify({
|
|
641
|
-
output: {
|
|
642
|
-
message: `Disabled ${toDisable.length} provider(s)`,
|
|
643
|
-
disabled: toDisable,
|
|
644
|
-
},
|
|
645
|
-
}),
|
|
646
|
-
);
|
|
647
|
-
} catch (error) {
|
|
648
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
649
|
-
res.end(JSON.stringify({ error: String(error) }));
|
|
650
|
-
}
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
if (req.method === "POST" && url.pathname === "/providers/enable") {
|
|
655
|
-
try {
|
|
656
|
-
const bodyText = await readBody(req);
|
|
657
|
-
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
658
|
-
const indices = body.indices as number[] | undefined;
|
|
659
|
-
|
|
660
|
-
if (!Array.isArray(indices)) {
|
|
661
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
662
|
-
res.end(
|
|
663
|
-
JSON.stringify({
|
|
664
|
-
error: "Missing or invalid 'indices' field (expected number[]).",
|
|
665
|
-
}),
|
|
666
|
-
);
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const state = deps.store.getState();
|
|
671
|
-
const baseUrlsList: string[] = state.baseUrlsList || [];
|
|
672
|
-
const disabledProviders: string[] = [
|
|
673
|
-
...(state.disabledProviders || []),
|
|
674
|
-
];
|
|
675
|
-
|
|
676
|
-
const toEnable: string[] = [];
|
|
677
|
-
for (const idx of indices) {
|
|
678
|
-
if (
|
|
679
|
-
typeof idx === "number" &&
|
|
680
|
-
idx >= 0 &&
|
|
681
|
-
idx < baseUrlsList.length
|
|
682
|
-
) {
|
|
683
|
-
const baseUrl = baseUrlsList[idx]!;
|
|
684
|
-
const pos = disabledProviders.indexOf(baseUrl);
|
|
685
|
-
if (pos !== -1) {
|
|
686
|
-
disabledProviders.splice(pos, 1);
|
|
687
|
-
toEnable.push(baseUrl);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
deps.store.getState().setDisabledProviders(disabledProviders);
|
|
693
|
-
|
|
694
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
695
|
-
res.end(
|
|
696
|
-
JSON.stringify({
|
|
697
|
-
output: {
|
|
698
|
-
message: `Enabled ${toEnable.length} provider(s)`,
|
|
699
|
-
enabled: toEnable,
|
|
700
|
-
},
|
|
701
|
-
}),
|
|
702
|
-
);
|
|
703
|
-
} catch (error) {
|
|
704
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
705
|
-
res.end(JSON.stringify({ error: String(error) }));
|
|
706
|
-
}
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Client management endpoints
|
|
711
|
-
if (req.method === "GET" && url.pathname === "/clients") {
|
|
712
|
-
try {
|
|
713
|
-
const state = deps.store.getState();
|
|
714
|
-
const clientIds = state.clientIds || [];
|
|
715
|
-
|
|
716
|
-
const clients = clientIds.map(
|
|
717
|
-
(c: {
|
|
718
|
-
clientId: string;
|
|
719
|
-
name: string;
|
|
720
|
-
apiKey: string;
|
|
721
|
-
createdAt: number;
|
|
722
|
-
lastUsed?: number | null;
|
|
723
|
-
}) => ({
|
|
724
|
-
id: c.clientId,
|
|
725
|
-
name: c.name,
|
|
726
|
-
apiKey: c.apiKey,
|
|
727
|
-
createdAt: c.createdAt,
|
|
728
|
-
lastUsed: c.lastUsed,
|
|
729
|
-
}),
|
|
730
|
-
);
|
|
731
|
-
|
|
732
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
733
|
-
res.end(
|
|
734
|
-
JSON.stringify({
|
|
735
|
-
output: {
|
|
736
|
-
clients,
|
|
737
|
-
totalCount: clients.length,
|
|
738
|
-
},
|
|
739
|
-
}),
|
|
740
|
-
);
|
|
741
|
-
} catch (error) {
|
|
742
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
743
|
-
res.end(JSON.stringify({ error: String(error) }));
|
|
744
|
-
}
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
if (req.method === "POST" && url.pathname === "/clients/add") {
|
|
749
|
-
try {
|
|
750
|
-
const bodyText = await readBody(req);
|
|
751
|
-
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
752
|
-
const name = body.name as string | undefined;
|
|
753
|
-
|
|
754
|
-
if (!name || typeof name !== "string" || name.trim() === "") {
|
|
755
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
756
|
-
res.end(
|
|
757
|
-
JSON.stringify({
|
|
758
|
-
error:
|
|
759
|
-
"Missing required 'name' field (must be a non-empty string).",
|
|
760
|
-
}),
|
|
761
|
-
);
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const clientId = name
|
|
766
|
-
.toLowerCase()
|
|
767
|
-
.replace(/\s+/g, "-")
|
|
768
|
-
.replace(/[^a-z0-9-]/g, "");
|
|
769
|
-
|
|
770
|
-
if (!clientId) {
|
|
771
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
772
|
-
res.end(
|
|
773
|
-
JSON.stringify({
|
|
774
|
-
error:
|
|
775
|
-
"Invalid client name. Must contain alphanumeric characters.",
|
|
776
|
-
}),
|
|
777
|
-
);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const state = deps.store.getState();
|
|
782
|
-
const existingClients = state.clientIds || [];
|
|
783
|
-
const existingClient = existingClients.find(
|
|
784
|
-
(c: { clientId: string }) => c.clientId === clientId,
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
if (existingClient) {
|
|
788
|
-
res.writeHead(409, { "Content-Type": "application/json" });
|
|
789
|
-
res.end(
|
|
790
|
-
JSON.stringify({
|
|
791
|
-
error: `Client with id '${clientId}' already exists.`,
|
|
792
|
-
}),
|
|
793
|
-
);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const apiKey = generateApiKey();
|
|
798
|
-
const newClient = {
|
|
799
|
-
clientId,
|
|
800
|
-
name: name.trim(),
|
|
801
|
-
apiKey,
|
|
802
|
-
createdAt: Date.now(),
|
|
803
|
-
};
|
|
804
|
-
|
|
805
|
-
deps.store
|
|
806
|
-
.getState()
|
|
807
|
-
.setClientIds((prev: typeof existingClients) => [
|
|
808
|
-
...(prev || []),
|
|
809
|
-
newClient,
|
|
810
|
-
]);
|
|
811
|
-
|
|
812
|
-
logger.log(`Added client '${name}' with id '${clientId}'`);
|
|
813
|
-
|
|
814
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
815
|
-
res.end(
|
|
816
|
-
JSON.stringify({
|
|
817
|
-
output: {
|
|
818
|
-
message: `Client '${name}' added successfully`,
|
|
819
|
-
client: {
|
|
820
|
-
id: clientId,
|
|
821
|
-
name: name.trim(),
|
|
822
|
-
apiKey,
|
|
823
|
-
createdAt: newClient.createdAt,
|
|
824
|
-
},
|
|
825
|
-
},
|
|
826
|
-
}),
|
|
827
|
-
);
|
|
828
|
-
} catch (error) {
|
|
829
|
-
respondWithError(res, error);
|
|
830
|
-
}
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (req.method === "POST" && url.pathname === "/clients/delete") {
|
|
835
|
-
try {
|
|
836
|
-
const bodyText = await readBody(req);
|
|
837
|
-
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
838
|
-
const id = body.id as string | undefined;
|
|
839
|
-
|
|
840
|
-
if (!id || typeof id !== "string" || id.trim() === "") {
|
|
841
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
842
|
-
res.end(
|
|
843
|
-
JSON.stringify({
|
|
844
|
-
error:
|
|
845
|
-
"Missing required 'id' field (must be a non-empty string).",
|
|
846
|
-
}),
|
|
847
|
-
);
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const state = deps.store.getState();
|
|
852
|
-
const existingClients = state.clientIds || [];
|
|
853
|
-
const index = existingClients.findIndex(
|
|
854
|
-
(c: { clientId: string }) => c.clientId === id,
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
if (index === -1) {
|
|
858
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
859
|
-
res.end(
|
|
860
|
-
JSON.stringify({
|
|
861
|
-
error: `Client with id '${id}' not found.`,
|
|
862
|
-
}),
|
|
863
|
-
);
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
const removedClient = existingClients[index];
|
|
868
|
-
const updatedClients = existingClients.filter(
|
|
869
|
-
(_c: unknown, i: number) => i !== index,
|
|
870
|
-
);
|
|
871
|
-
|
|
872
|
-
deps.store.getState().setClientIds(updatedClients);
|
|
873
|
-
|
|
874
|
-
logger.log(
|
|
875
|
-
`Deleted client '${removedClient.name}' with id '${id}'`,
|
|
876
|
-
);
|
|
877
|
-
|
|
878
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
879
|
-
res.end(
|
|
880
|
-
JSON.stringify({
|
|
881
|
-
output: {
|
|
882
|
-
message: `Client '${removedClient.name}' deleted successfully`,
|
|
883
|
-
id,
|
|
884
|
-
},
|
|
885
|
-
}),
|
|
886
|
-
);
|
|
887
|
-
} catch (error) {
|
|
888
|
-
respondWithError(res, error);
|
|
889
|
-
}
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
if (req.method === "GET" && url.pathname === "/providers") {
|
|
894
|
-
try {
|
|
895
|
-
const state = deps.store.getState();
|
|
896
|
-
const baseUrlsList: string[] = state.baseUrlsList || [];
|
|
897
|
-
const disabledProviders: string[] = state.disabledProviders || [];
|
|
898
|
-
|
|
899
|
-
const providers = baseUrlsList.map((baseUrl, index) => ({
|
|
900
|
-
index,
|
|
901
|
-
baseUrl,
|
|
902
|
-
disabled: disabledProviders.includes(baseUrl),
|
|
903
|
-
}));
|
|
904
|
-
|
|
905
|
-
// Only count disabled providers that are actually in the current list
|
|
906
|
-
// (filter out stale entries from previously disabled providers that are no longer present)
|
|
907
|
-
const activeDisabledCount = providers.filter((p) => p.disabled).length;
|
|
908
|
-
|
|
909
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
910
|
-
res.end(
|
|
911
|
-
JSON.stringify({
|
|
912
|
-
output: {
|
|
913
|
-
providers,
|
|
914
|
-
disabledCount: activeDisabledCount,
|
|
915
|
-
totalCount: baseUrlsList.length,
|
|
916
|
-
},
|
|
917
|
-
}),
|
|
918
|
-
);
|
|
919
|
-
} catch (error) {
|
|
920
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
921
|
-
res.end(JSON.stringify({ error: String(error) }));
|
|
922
|
-
}
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
if (req.method === "GET" && url.pathname === "/usage") {
|
|
927
|
-
try {
|
|
928
|
-
const output = await deps.usageTrackingDriver.list({
|
|
929
|
-
limit: parseLimit(url.searchParams.get("limit")),
|
|
930
|
-
});
|
|
931
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
932
|
-
res.end(JSON.stringify({ output }));
|
|
933
|
-
} catch (error) {
|
|
934
|
-
sendJson(res, 500, { error: toErrorMessage(error) });
|
|
935
|
-
}
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (req.method === "GET" && url.pathname === "/usagePi") {
|
|
940
|
-
try {
|
|
941
|
-
const timestamp = (url.searchParams.get("timestamp") || "").trim();
|
|
942
|
-
if (!timestamp) {
|
|
943
|
-
sendJson(res, 400, {
|
|
944
|
-
error: "Missing required 'timestamp' query parameter.",
|
|
945
|
-
});
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const usageDriver = deps.usageTrackingDriver;
|
|
950
|
-
const limit = parseLimit(url.searchParams.get("limit"));
|
|
951
|
-
const allMatching = await usageDriver.list();
|
|
952
|
-
const requestIdPrefix = `gen-${timestamp}-`;
|
|
953
|
-
const filtered = allMatching.filter((entry) =>
|
|
954
|
-
entry.requestId.startsWith(requestIdPrefix),
|
|
955
|
-
);
|
|
956
|
-
const entries = filtered.slice(0, limit);
|
|
957
|
-
const totalEntries = filtered.length;
|
|
958
|
-
const totalSatsCost = filtered.reduce(
|
|
959
|
-
(sum, entry) => sum + (entry.satsCost || 0),
|
|
960
|
-
0,
|
|
961
|
-
);
|
|
962
|
-
const recentSatsCost = entries.reduce(
|
|
963
|
-
(sum, entry) => sum + (entry.satsCost || 0),
|
|
964
|
-
0,
|
|
965
|
-
);
|
|
966
|
-
|
|
967
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
968
|
-
res.end(
|
|
969
|
-
JSON.stringify({
|
|
970
|
-
output: {
|
|
971
|
-
entries,
|
|
972
|
-
totalEntries,
|
|
973
|
-
totalSatsCost,
|
|
974
|
-
recentSatsCost,
|
|
975
|
-
limit,
|
|
976
|
-
timestamp,
|
|
977
|
-
},
|
|
978
|
-
}),
|
|
979
|
-
);
|
|
980
|
-
} catch (error) {
|
|
981
|
-
sendJson(res, 500, { error: toErrorMessage(error) });
|
|
982
|
-
}
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// Allow client management endpoints through
|
|
987
|
-
if (req.method !== "POST" && !url.pathname.startsWith("/clients")) {
|
|
988
|
-
res.writeHead(405, { "Content-Type": "application/json" });
|
|
989
|
-
res.end(JSON.stringify({ error: "Only POST is supported." }));
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
let requestBody: unknown = {};
|
|
994
|
-
try {
|
|
995
|
-
requestBody = await readJsonBody(req);
|
|
996
|
-
} catch (error) {
|
|
997
|
-
sendJson(res, 400, {
|
|
998
|
-
error: "Invalid JSON body.",
|
|
999
|
-
details: toErrorMessage(error),
|
|
1000
|
-
});
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
const bodyObj = requestBody as Record<string, unknown>;
|
|
1005
|
-
const modelId = typeof bodyObj.model === "string" ? bodyObj.model : "";
|
|
1006
|
-
|
|
1007
|
-
if (!modelId) {
|
|
1008
|
-
sendJson(res, 400, { error: "Missing required 'model' field." });
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
const forcedProvider: string | undefined =
|
|
1013
|
-
url.searchParams.get("provider") ||
|
|
1014
|
-
(req.headers["x-routstr-provider"] as string | undefined) ||
|
|
1015
|
-
deps.provider ||
|
|
1016
|
-
undefined;
|
|
1017
|
-
|
|
1018
|
-
// Convert req.headers to Record<string, string>
|
|
1019
|
-
const incomingHeaders: Record<string, string> = {};
|
|
1020
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
1021
|
-
if (typeof value === "string") {
|
|
1022
|
-
incomingHeaders[key] = value;
|
|
1023
|
-
} else if (Array.isArray(value) && value.length > 0) {
|
|
1024
|
-
incomingHeaders[key] = value[0]!;
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
try {
|
|
1029
|
-
await deps.ensureProvidersBootstrapped();
|
|
1030
|
-
logger.log("Routing request with path: ", url.pathname);
|
|
1031
|
-
|
|
1032
|
-
const response = await routeRequests({
|
|
1033
|
-
modelId,
|
|
1034
|
-
requestBody,
|
|
1035
|
-
path: url.pathname,
|
|
1036
|
-
forcedProvider,
|
|
1037
|
-
headers: incomingHeaders,
|
|
1038
|
-
walletAdapter: deps.walletAdapter,
|
|
1039
|
-
storageAdapter: deps.storageAdapter,
|
|
1040
|
-
providerRegistry: deps.providerRegistry,
|
|
1041
|
-
discoveryAdapter: deps.discoveryAdapter,
|
|
1042
|
-
modelManager: deps.modelManager,
|
|
1043
|
-
debugLevel: "DEBUG",
|
|
1044
|
-
mode: deps.mode,
|
|
1045
|
-
usageTrackingDriver: deps.usageTrackingDriver,
|
|
1046
|
-
sdkStore: deps.store,
|
|
1047
|
-
providerManager: deps.providerManager,
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
// Bridge the Web `Response` to the Node `ServerResponse` with no
|
|
1051
|
-
// transforms: status + headers + pipe(body → res).
|
|
1052
|
-
res.statusCode = response.status;
|
|
1053
|
-
response.headers.forEach((value, key) => {
|
|
1054
|
-
res.setHeader(key, value);
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
const finalize = (response as any).finalize as
|
|
1058
|
-
| (() => Promise<number>)
|
|
1059
|
-
| undefined;
|
|
1060
|
-
|
|
1061
|
-
if (!response.body) {
|
|
1062
|
-
res.end();
|
|
1063
|
-
if (finalize) {
|
|
1064
|
-
try {
|
|
1065
|
-
await finalize();
|
|
1066
|
-
} catch (err) {
|
|
1067
|
-
logger.error(`[daemon] finalize error: ${toErrorMessage(err)}`);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
const nodeReadable = Readable.fromWeb(response.body as any);
|
|
1074
|
-
await new Promise<void>((resolve, reject) => {
|
|
1075
|
-
let settled = false;
|
|
1076
|
-
const finish = () => {
|
|
1077
|
-
if (settled) return;
|
|
1078
|
-
settled = true;
|
|
1079
|
-
resolve();
|
|
1080
|
-
};
|
|
1081
|
-
const fail = (err: unknown) => {
|
|
1082
|
-
if (settled) return;
|
|
1083
|
-
settled = true;
|
|
1084
|
-
reject(err);
|
|
1085
|
-
};
|
|
1086
|
-
res.once("finish", finish);
|
|
1087
|
-
res.once("close", finish);
|
|
1088
|
-
res.once("error", fail);
|
|
1089
|
-
nodeReadable.once("error", fail);
|
|
1090
|
-
nodeReadable.pipe(res);
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
if (finalize) {
|
|
1094
|
-
try {
|
|
1095
|
-
await finalize();
|
|
1096
|
-
} catch (err) {
|
|
1097
|
-
logger.error(`[daemon] finalize error: ${toErrorMessage(err)}`);
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
return;
|
|
1101
|
-
} catch (error) {
|
|
1102
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1103
|
-
logger.error(`[daemon] Error: ${message}`);
|
|
1104
|
-
|
|
1105
|
-
if (error instanceof InsufficientBalanceError) {
|
|
1106
|
-
const balanceError = error as {
|
|
1107
|
-
required?: number;
|
|
1108
|
-
available?: number;
|
|
1109
|
-
maxMintBalance?: number;
|
|
1110
|
-
maxMintUrl?: string;
|
|
1111
|
-
};
|
|
1112
|
-
res.writeHead(402, { "Content-Type": "application/json" });
|
|
1113
|
-
res.end(
|
|
1114
|
-
JSON.stringify({
|
|
1115
|
-
error: message,
|
|
1116
|
-
error_type: "insufficient_balance",
|
|
1117
|
-
required: balanceError.required,
|
|
1118
|
-
available: balanceError.available,
|
|
1119
|
-
maxMintBalance: balanceError.maxMintBalance,
|
|
1120
|
-
maxMintUrl: balanceError.maxMintUrl,
|
|
1121
|
-
}),
|
|
1122
|
-
);
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1127
|
-
res.end(JSON.stringify({ error: message }));
|
|
1128
|
-
}
|
|
1129
|
-
};
|
|
1130
|
-
}
|