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