@omnicross/daemon 0.1.2 → 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/dist/cli.cjs +3206 -328
- package/dist/cli.js +3210 -314
- package/dist/index.cjs +3114 -260
- package/dist/index.d.cts +953 -31
- package/dist/index.d.ts +953 -31
- package/dist/index.js +3083 -213
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -32,12 +32,14 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
AdminServer: () => AdminServer,
|
|
34
34
|
ConfigFileProviderConfigSource: () => ConfigFileProviderConfigSource,
|
|
35
|
+
ConfigurableLogger: () => ConfigurableLogger,
|
|
35
36
|
ConsoleLogger: () => ConsoleLogger,
|
|
36
37
|
DEFAULT_ADMIN_PORT: () => DEFAULT_ADMIN_PORT,
|
|
37
38
|
JsonApiServerSettingsStore: () => JsonApiServerSettingsStore,
|
|
38
39
|
JsonOutboundKeyDb: () => JsonOutboundKeyDb,
|
|
39
40
|
JsonSubscriptionCredentialStore: () => JsonSubscriptionCredentialStore,
|
|
40
41
|
buildDaemon: () => buildDaemon,
|
|
42
|
+
buildHealthReport: () => buildHealthReport,
|
|
41
43
|
handleAdminApi: () => handleAdminApi,
|
|
42
44
|
inferApiFormat: () => inferApiFormat,
|
|
43
45
|
loadConfig: () => loadConfig,
|
|
@@ -51,12 +53,19 @@ __export(src_exports, {
|
|
|
51
53
|
module.exports = __toCommonJS(src_exports);
|
|
52
54
|
|
|
53
55
|
// src/bootstrap.ts
|
|
56
|
+
var import_node_fs19 = require("fs");
|
|
57
|
+
var import_audit_types = require("@omnicross/contracts/audit-types");
|
|
58
|
+
var import_billing_types = require("@omnicross/contracts/billing-types");
|
|
54
59
|
var import_GeminiCodeAssistProjectResolver = require("@omnicross/core/auth/GeminiCodeAssistProjectResolver");
|
|
55
60
|
var import_ApiKeyPoolService = require("@omnicross/core/completion/ApiKeyPoolService");
|
|
56
|
-
var
|
|
61
|
+
var import_outbound_api4 = require("@omnicross/core/outbound-api");
|
|
57
62
|
var import_subscriptionRegistryPort = require("@omnicross/core/outbound-api/subscriptionRegistryPort");
|
|
63
|
+
var import_SubscriptionAccountHealth2 = require("@omnicross/core/pipeline/SubscriptionAccountHealth");
|
|
64
|
+
var import_upstreamFetch7 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
65
|
+
var import_SubscriptionIdentityStore2 = require("@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore");
|
|
58
66
|
var import_gemini_code_assist_resolver = require("@omnicross/core/ports/gemini-code-assist-resolver");
|
|
59
67
|
var import_provider_proxy = require("@omnicross/core/provider-proxy");
|
|
68
|
+
var import_outbound_api5 = require("@omnicross/core/outbound-api");
|
|
60
69
|
var import_usage = require("@omnicross/core/usage");
|
|
61
70
|
var import_subscriptions4 = require("@omnicross/subscriptions");
|
|
62
71
|
|
|
@@ -154,10 +163,136 @@ function handleCodexOAuthStatus(sessionId, deps) {
|
|
|
154
163
|
// src/admin/AdminServer.ts
|
|
155
164
|
var import_node_crypto7 = require("crypto");
|
|
156
165
|
var import_node_http2 = __toESM(require("http"), 1);
|
|
166
|
+
var import_health_logging_types = require("@omnicross/contracts/health-logging-types");
|
|
167
|
+
|
|
168
|
+
// src/admin/accountProbesApi.ts
|
|
169
|
+
function handleAccountProbes(res, reader) {
|
|
170
|
+
const accounts = reader ? reader.getAllHistory() : [];
|
|
171
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
172
|
+
res.end(JSON.stringify({ accounts }));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/admin/auditQueryApi.ts
|
|
176
|
+
function intParam(value) {
|
|
177
|
+
if (value === null || value.trim() === "") return void 0;
|
|
178
|
+
const n = Number(value);
|
|
179
|
+
return Number.isFinite(n) ? Math.trunc(n) : void 0;
|
|
180
|
+
}
|
|
181
|
+
function handleAuditQuery(req, res, reader) {
|
|
182
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
183
|
+
const query = {};
|
|
184
|
+
const keyId = url.searchParams.get("keyId");
|
|
185
|
+
if (keyId && keyId.trim()) query.keyId = keyId.trim();
|
|
186
|
+
const from = intParam(url.searchParams.get("from"));
|
|
187
|
+
if (from !== void 0) query.from = from;
|
|
188
|
+
const to = intParam(url.searchParams.get("to"));
|
|
189
|
+
if (to !== void 0) query.to = to;
|
|
190
|
+
const limit = intParam(url.searchParams.get("limit"));
|
|
191
|
+
if (limit !== void 0) query.limit = limit;
|
|
192
|
+
const records = reader ? reader(query) : [];
|
|
193
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
194
|
+
res.end(JSON.stringify({ records }));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/admin/billingStatusApi.ts
|
|
198
|
+
function handleBillingStatus(res, reader) {
|
|
199
|
+
const status = reader ? reader() : { total: 0, delivered: 0, pending: 0 };
|
|
200
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
201
|
+
res.end(JSON.stringify({ status }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/webhook/webhookRuntime.ts
|
|
205
|
+
var import_webhookEmit = require("@omnicross/core/pipeline/webhookEmit");
|
|
206
|
+
var dispatcher = null;
|
|
207
|
+
var health = null;
|
|
208
|
+
var unsubscribers = [];
|
|
209
|
+
var wired = false;
|
|
210
|
+
function setWebhookRuntime(d, h) {
|
|
211
|
+
dispatcher = d;
|
|
212
|
+
health = h;
|
|
213
|
+
}
|
|
214
|
+
function applyWebhookConfig(config) {
|
|
215
|
+
if (!dispatcher) return;
|
|
216
|
+
dispatcher.setConfig(config);
|
|
217
|
+
const shouldWire = config?.enabled === true && (config?.destinations.length ?? 0) > 0;
|
|
218
|
+
if (shouldWire && !wired) {
|
|
219
|
+
const active = dispatcher;
|
|
220
|
+
(0, import_webhookEmit.setWebhookSink)((event) => active.emit(event));
|
|
221
|
+
if (health) {
|
|
222
|
+
unsubscribers.push(
|
|
223
|
+
health.onRecovered(
|
|
224
|
+
(e) => active.emit({ kind: "account.recovery", at: e.at, providerId: e.providerId, accountId: e.accountId })
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
unsubscribers.push(
|
|
228
|
+
health.onAnomaly(
|
|
229
|
+
(e) => active.emit({
|
|
230
|
+
kind: "account.anomaly",
|
|
231
|
+
at: e.at,
|
|
232
|
+
providerId: e.providerId,
|
|
233
|
+
accountId: e.accountId,
|
|
234
|
+
state: e.state
|
|
235
|
+
})
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
wired = true;
|
|
240
|
+
} else if (!shouldWire && wired) {
|
|
241
|
+
teardown();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function deliverWebhookTest(destinationId) {
|
|
245
|
+
if (!dispatcher) return { ok: false, error: "webhook dispatcher not wired" };
|
|
246
|
+
return dispatcher.deliverTest(destinationId);
|
|
247
|
+
}
|
|
248
|
+
function teardown() {
|
|
249
|
+
(0, import_webhookEmit.setWebhookSink)(null);
|
|
250
|
+
for (const unsub of unsubscribers) unsub();
|
|
251
|
+
unsubscribers = [];
|
|
252
|
+
wired = false;
|
|
253
|
+
}
|
|
254
|
+
function resetWebhookRuntimeForTests() {
|
|
255
|
+
if (wired) teardown();
|
|
256
|
+
dispatcher = null;
|
|
257
|
+
health = null;
|
|
258
|
+
unsubscribers = [];
|
|
259
|
+
wired = false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/admin/webhookTestApi.ts
|
|
263
|
+
function readJsonBody(req) {
|
|
264
|
+
return new Promise((resolve) => {
|
|
265
|
+
const chunks = [];
|
|
266
|
+
req.on("data", (c) => chunks.push(c));
|
|
267
|
+
req.on("end", () => {
|
|
268
|
+
try {
|
|
269
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
270
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
271
|
+
resolve(parsed && typeof parsed === "object" ? parsed : {});
|
|
272
|
+
} catch {
|
|
273
|
+
resolve({});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
req.on("error", () => resolve({}));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async function handleWebhookTest(req, res) {
|
|
280
|
+
const body = await readJsonBody(req);
|
|
281
|
+
const destinationId = body["destinationId"];
|
|
282
|
+
if (typeof destinationId !== "string" || !destinationId.trim()) {
|
|
283
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
284
|
+
res.end(JSON.stringify({ error: { type: "bad_request", message: "destinationId is required" } }));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const result = await deliverWebhookTest(destinationId.trim());
|
|
288
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
289
|
+
res.end(JSON.stringify({ result }));
|
|
290
|
+
}
|
|
157
291
|
|
|
158
292
|
// src/admin/adminApi.ts
|
|
159
293
|
var import_node_http = __toESM(require("http"), 1);
|
|
160
|
-
var
|
|
294
|
+
var import_outbound_api2 = require("@omnicross/core/outbound-api");
|
|
295
|
+
var import_upstreamFetch2 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
161
296
|
|
|
162
297
|
// src/config.ts
|
|
163
298
|
var import_node_fs2 = require("fs");
|
|
@@ -353,6 +488,70 @@ var SecretBox = class {
|
|
|
353
488
|
};
|
|
354
489
|
|
|
355
490
|
// src/secrets/secretFields.ts
|
|
491
|
+
function urlHasInlineCredential(url) {
|
|
492
|
+
try {
|
|
493
|
+
const u = new URL(url);
|
|
494
|
+
return u.username.length > 0 || u.password.length > 0;
|
|
495
|
+
} catch {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function transformProxyConfig(cfg, fn) {
|
|
500
|
+
if ("url" in cfg) {
|
|
501
|
+
if (isEnvelope(cfg.url) || urlHasInlineCredential(cfg.url)) {
|
|
502
|
+
return { url: fn(cfg.url) };
|
|
503
|
+
}
|
|
504
|
+
return cfg;
|
|
505
|
+
}
|
|
506
|
+
if (typeof cfg.password === "string" && cfg.password.length > 0) {
|
|
507
|
+
return { ...cfg, password: fn(cfg.password) };
|
|
508
|
+
}
|
|
509
|
+
return cfg;
|
|
510
|
+
}
|
|
511
|
+
function transformOutboundProxy(proxy, fn) {
|
|
512
|
+
const next = {};
|
|
513
|
+
if (proxy.global) next.global = transformProxyConfig(proxy.global, fn);
|
|
514
|
+
if (proxy.byProvider) {
|
|
515
|
+
const byProvider = {};
|
|
516
|
+
for (const [key, value] of Object.entries(proxy.byProvider)) {
|
|
517
|
+
byProvider[key] = transformProxyConfig(value, fn);
|
|
518
|
+
}
|
|
519
|
+
next.byProvider = byProvider;
|
|
520
|
+
}
|
|
521
|
+
return next;
|
|
522
|
+
}
|
|
523
|
+
function encryptProxySegment(proxy, box) {
|
|
524
|
+
return transformOutboundProxy(proxy, (v) => box.encryptMaybe(v));
|
|
525
|
+
}
|
|
526
|
+
function decryptProxySegment(proxy, box) {
|
|
527
|
+
return transformOutboundProxy(proxy, (v) => box.decryptMaybe(v));
|
|
528
|
+
}
|
|
529
|
+
function transformWebhookSegment(webhook, fn) {
|
|
530
|
+
return {
|
|
531
|
+
...webhook,
|
|
532
|
+
destinations: webhook.destinations.map(
|
|
533
|
+
(d) => typeof d.secret === "string" && d.secret.length > 0 ? { ...d, secret: fn(d.secret) } : d
|
|
534
|
+
)
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function encryptWebhookSegment(webhook, box) {
|
|
538
|
+
return transformWebhookSegment(webhook, (v) => box.encryptMaybe(v));
|
|
539
|
+
}
|
|
540
|
+
function decryptWebhookSegment(webhook, box) {
|
|
541
|
+
return transformWebhookSegment(webhook, (v) => box.decryptMaybe(v));
|
|
542
|
+
}
|
|
543
|
+
function transformBillingSegment(billing, fn) {
|
|
544
|
+
if (typeof billing.secret === "string" && billing.secret.length > 0) {
|
|
545
|
+
return { ...billing, secret: fn(billing.secret) };
|
|
546
|
+
}
|
|
547
|
+
return billing;
|
|
548
|
+
}
|
|
549
|
+
function encryptBillingSegment(billing, box) {
|
|
550
|
+
return transformBillingSegment(billing, (v) => box.encryptMaybe(v));
|
|
551
|
+
}
|
|
552
|
+
function decryptBillingSegment(billing, box) {
|
|
553
|
+
return transformBillingSegment(billing, (v) => box.decryptMaybe(v));
|
|
554
|
+
}
|
|
356
555
|
function transformProvider(provider, fn) {
|
|
357
556
|
const next = { ...provider, apiKey: fn(provider.apiKey) };
|
|
358
557
|
if (provider.apiKeys) {
|
|
@@ -376,6 +575,17 @@ function transformConfigSecrets(cfg, fn) {
|
|
|
376
575
|
if (cfg.admin && typeof cfg.admin.token === "string" && cfg.admin.token.length > 0) {
|
|
377
576
|
next.admin = { ...cfg.admin, token: fn(cfg.admin.token) };
|
|
378
577
|
}
|
|
578
|
+
const proxy = cfg.server?.proxy;
|
|
579
|
+
const webhook = cfg.server?.webhook;
|
|
580
|
+
const billing = cfg.server?.billing;
|
|
581
|
+
if (cfg.server && (proxy?.global || proxy?.byProvider || webhook || billing?.secret)) {
|
|
582
|
+
next.server = { ...cfg.server };
|
|
583
|
+
if (proxy && (proxy.global || proxy.byProvider)) {
|
|
584
|
+
next.server.proxy = transformOutboundProxy(proxy, fn);
|
|
585
|
+
}
|
|
586
|
+
if (webhook) next.server.webhook = transformWebhookSegment(webhook, fn);
|
|
587
|
+
if (billing?.secret) next.server.billing = transformBillingSegment(billing, fn);
|
|
588
|
+
}
|
|
379
589
|
return next;
|
|
380
590
|
}
|
|
381
591
|
function encryptConfigSecrets(cfg, box) {
|
|
@@ -413,7 +623,7 @@ function transformTokens(tokens, fn) {
|
|
|
413
623
|
if (Array.isArray(accounts)) {
|
|
414
624
|
bag[accountsKey] = accounts.map((entry) => {
|
|
415
625
|
if (entry && typeof entry === "object" && "tokens" in entry && entry.tokens && typeof entry.tokens === "object") {
|
|
416
|
-
|
|
626
|
+
const nextEntry = {
|
|
417
627
|
...entry,
|
|
418
628
|
tokens: transformTokenBlock(
|
|
419
629
|
entry.tokens,
|
|
@@ -421,6 +631,11 @@ function transformTokens(tokens, fn) {
|
|
|
421
631
|
fn
|
|
422
632
|
)
|
|
423
633
|
};
|
|
634
|
+
const proxy = entry.proxy;
|
|
635
|
+
if (proxy && typeof proxy === "object") {
|
|
636
|
+
nextEntry.proxy = transformProxyConfig(proxy, fn);
|
|
637
|
+
}
|
|
638
|
+
return nextEntry;
|
|
424
639
|
}
|
|
425
640
|
return entry;
|
|
426
641
|
});
|
|
@@ -455,6 +670,17 @@ function resolveAdminConfig(admin) {
|
|
|
455
670
|
token: typeof admin?.token === "string" && admin.token.length > 0 ? admin.token : void 0
|
|
456
671
|
};
|
|
457
672
|
}
|
|
673
|
+
function validateLogging(raw) {
|
|
674
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
675
|
+
const l = raw;
|
|
676
|
+
const out = {};
|
|
677
|
+
if (l["level"] === "error" || l["level"] === "warn" || l["level"] === "info" || l["level"] === "debug") {
|
|
678
|
+
out.level = l["level"];
|
|
679
|
+
}
|
|
680
|
+
if (l["format"] === "text" || l["format"] === "json") out.format = l["format"];
|
|
681
|
+
if (typeof l["file"] === "string" && l["file"].length > 0) out.file = l["file"];
|
|
682
|
+
return out.level !== void 0 || out.format !== void 0 || out.file !== void 0 ? out : void 0;
|
|
683
|
+
}
|
|
458
684
|
var VALID_FORMATS = ["openai", "anthropic", "gemini"];
|
|
459
685
|
function validateApiKeys(raw) {
|
|
460
686
|
if (!Array.isArray(raw)) return void 0;
|
|
@@ -629,7 +855,8 @@ function validateConfig(raw) {
|
|
|
629
855
|
const providers = providersRaw.map((p, i) => validateProvider(p, i));
|
|
630
856
|
const server = obj["server"];
|
|
631
857
|
const admin = validateAdmin(obj["admin"]);
|
|
632
|
-
|
|
858
|
+
const logging = validateLogging(obj["logging"]);
|
|
859
|
+
return { providers, server, admin, logging };
|
|
633
860
|
}
|
|
634
861
|
var secretBox = null;
|
|
635
862
|
function setSecretBox(box) {
|
|
@@ -729,6 +956,161 @@ function listMappablePresets() {
|
|
|
729
956
|
return { mappable, excluded };
|
|
730
957
|
}
|
|
731
958
|
|
|
959
|
+
// src/proxy/sanitizeProxy.ts
|
|
960
|
+
function sanitizeProxyConfig(cfg) {
|
|
961
|
+
if ("url" in cfg) {
|
|
962
|
+
let endpoint;
|
|
963
|
+
let username;
|
|
964
|
+
let hasPassword = false;
|
|
965
|
+
try {
|
|
966
|
+
const u = new URL(cfg.url);
|
|
967
|
+
endpoint = u.port ? `${u.hostname}:${u.port}` : u.hostname;
|
|
968
|
+
username = u.username ? decodeURIComponent(u.username) : void 0;
|
|
969
|
+
hasPassword = u.password.length > 0;
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
972
|
+
return { kind: "url", endpoint, username, hasPassword };
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
kind: cfg.type,
|
|
976
|
+
endpoint: `${cfg.host}:${cfg.port}`,
|
|
977
|
+
username: cfg.username,
|
|
978
|
+
hasPassword: typeof cfg.password === "string" && cfg.password.length > 0
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function redactProxyConfig(cfg) {
|
|
982
|
+
if ("url" in cfg) {
|
|
983
|
+
try {
|
|
984
|
+
const u = new URL(cfg.url);
|
|
985
|
+
if (u.password) u.password = "";
|
|
986
|
+
return { url: u.toString() };
|
|
987
|
+
} catch {
|
|
988
|
+
return cfg;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const { password: _password, ...rest } = cfg;
|
|
992
|
+
return rest;
|
|
993
|
+
}
|
|
994
|
+
function redactOutboundProxy(proxy) {
|
|
995
|
+
const out = {};
|
|
996
|
+
if (proxy.global) out.global = redactProxyConfig(proxy.global);
|
|
997
|
+
if (proxy.byProvider) {
|
|
998
|
+
const byProvider = {};
|
|
999
|
+
for (const [key, value] of Object.entries(proxy.byProvider)) {
|
|
1000
|
+
byProvider[key] = redactProxyConfig(value);
|
|
1001
|
+
}
|
|
1002
|
+
out.byProvider = byProvider;
|
|
1003
|
+
}
|
|
1004
|
+
return out;
|
|
1005
|
+
}
|
|
1006
|
+
function preserveProxyConfigSecret(incoming, current) {
|
|
1007
|
+
if (!current) return incoming;
|
|
1008
|
+
if ("url" in incoming) {
|
|
1009
|
+
if ("url" in current) {
|
|
1010
|
+
try {
|
|
1011
|
+
const inU = new URL(incoming.url);
|
|
1012
|
+
const curU = new URL(current.url);
|
|
1013
|
+
if (!inU.password && curU.password) {
|
|
1014
|
+
inU.password = curU.password;
|
|
1015
|
+
return { url: inU.toString() };
|
|
1016
|
+
}
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return incoming;
|
|
1021
|
+
}
|
|
1022
|
+
if ("url" in current) return incoming;
|
|
1023
|
+
const blank = incoming.password === void 0 || incoming.password === "";
|
|
1024
|
+
if (blank && typeof current.password === "string" && current.password.length > 0) {
|
|
1025
|
+
return { ...incoming, password: current.password };
|
|
1026
|
+
}
|
|
1027
|
+
return incoming;
|
|
1028
|
+
}
|
|
1029
|
+
function preserveOutboundProxySecrets(incoming, current) {
|
|
1030
|
+
const out = {};
|
|
1031
|
+
if (incoming.global) out.global = preserveProxyConfigSecret(incoming.global, current?.global);
|
|
1032
|
+
if (incoming.byProvider) {
|
|
1033
|
+
const byProvider = {};
|
|
1034
|
+
for (const [key, value] of Object.entries(incoming.byProvider)) {
|
|
1035
|
+
byProvider[key] = preserveProxyConfigSecret(value, current?.byProvider?.[key]);
|
|
1036
|
+
}
|
|
1037
|
+
out.byProvider = byProvider;
|
|
1038
|
+
}
|
|
1039
|
+
return out;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/proxy/upstreamProxyResolver.ts
|
|
1043
|
+
var import_upstreamFetch = require("@omnicross/core/pipeline/upstreamFetch");
|
|
1044
|
+
var serverProxy;
|
|
1045
|
+
function setServerProxyConfig(proxy) {
|
|
1046
|
+
serverProxy = proxy;
|
|
1047
|
+
(0, import_upstreamFetch.bumpUpstreamProxyGeneration)();
|
|
1048
|
+
}
|
|
1049
|
+
function getServerProxyConfig() {
|
|
1050
|
+
return serverProxy;
|
|
1051
|
+
}
|
|
1052
|
+
var envProxyLoggedFor;
|
|
1053
|
+
function maskProxyUrl(url) {
|
|
1054
|
+
return url.replace(/\/\/[^/@]*@/, "//***@");
|
|
1055
|
+
}
|
|
1056
|
+
function hostFromCtx(ctx) {
|
|
1057
|
+
if (!ctx.url) return void 0;
|
|
1058
|
+
try {
|
|
1059
|
+
return new URL(ctx.url).hostname.toLowerCase();
|
|
1060
|
+
} catch {
|
|
1061
|
+
return void 0;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function isLoopbackHost(host) {
|
|
1065
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || host.startsWith("127.") || host.endsWith(".localhost");
|
|
1066
|
+
}
|
|
1067
|
+
function noProxyMatches(noProxy, host) {
|
|
1068
|
+
if (!noProxy) return false;
|
|
1069
|
+
for (const raw of noProxy.split(",")) {
|
|
1070
|
+
const entry = raw.trim().toLowerCase();
|
|
1071
|
+
if (!entry) continue;
|
|
1072
|
+
if (entry === "*") return true;
|
|
1073
|
+
const bare = entry.startsWith(".") ? entry.slice(1) : entry;
|
|
1074
|
+
if (host === bare || host.endsWith(`.${bare}`)) return true;
|
|
1075
|
+
}
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
function resolveEnvProxy(ctx, env = process.env) {
|
|
1079
|
+
const raw = env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? env.HTTP_PROXY ?? env.http_proxy;
|
|
1080
|
+
if (!raw || !raw.trim()) return void 0;
|
|
1081
|
+
const host = hostFromCtx(ctx);
|
|
1082
|
+
if (host && (isLoopbackHost(host) || noProxyMatches(env.NO_PROXY ?? env.no_proxy, host))) {
|
|
1083
|
+
return void 0;
|
|
1084
|
+
}
|
|
1085
|
+
const url = raw.trim();
|
|
1086
|
+
if (envProxyLoggedFor !== url) {
|
|
1087
|
+
envProxyLoggedFor = url;
|
|
1088
|
+
console.info(`[upstream-proxy] routing upstream egress through the environment proxy: ${maskProxyUrl(url)}`);
|
|
1089
|
+
}
|
|
1090
|
+
return { url };
|
|
1091
|
+
}
|
|
1092
|
+
function createUpstreamProxyResolver(src = {}) {
|
|
1093
|
+
const readServer = src.getServerProxy ?? getServerProxyConfig;
|
|
1094
|
+
return (ctx) => {
|
|
1095
|
+
const host = hostFromCtx(ctx);
|
|
1096
|
+
if (host) {
|
|
1097
|
+
if (isLoopbackHost(host)) return void 0;
|
|
1098
|
+
const env = src.env ?? process.env;
|
|
1099
|
+
if (noProxyMatches(env.NO_PROXY ?? env.no_proxy, host)) return void 0;
|
|
1100
|
+
}
|
|
1101
|
+
if (src.getAccountProxy && ctx.providerId && ctx.accountId) {
|
|
1102
|
+
const account = src.getAccountProxy(ctx.providerId, ctx.accountId);
|
|
1103
|
+
if (account) return account;
|
|
1104
|
+
}
|
|
1105
|
+
const server = readServer();
|
|
1106
|
+
if (ctx.providerId && server?.byProvider?.[ctx.providerId]) {
|
|
1107
|
+
return server.byProvider[ctx.providerId];
|
|
1108
|
+
}
|
|
1109
|
+
if (server?.global) return server.global;
|
|
1110
|
+
return resolveEnvProxy(ctx, src.env);
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
732
1114
|
// src/admin/accountsOAuth.ts
|
|
733
1115
|
var import_subscriptions2 = require("@omnicross/subscriptions");
|
|
734
1116
|
|
|
@@ -851,6 +1233,24 @@ function validateTokenBody(providerId, body) {
|
|
|
851
1233
|
return null;
|
|
852
1234
|
}
|
|
853
1235
|
}
|
|
1236
|
+
function validateSupportedModelsBody(raw) {
|
|
1237
|
+
if (raw === null || raw === void 0) return { ok: true, value: void 0 };
|
|
1238
|
+
if (Array.isArray(raw)) {
|
|
1239
|
+
if (raw.length === 0) return { ok: false };
|
|
1240
|
+
if (!raw.every((x) => typeof x === "string" && x.trim().length > 0)) return { ok: false };
|
|
1241
|
+
return { ok: true, value: raw };
|
|
1242
|
+
}
|
|
1243
|
+
if (typeof raw === "object") {
|
|
1244
|
+
const entries = Object.entries(raw);
|
|
1245
|
+
if (entries.length === 0) return { ok: false };
|
|
1246
|
+
const valid = entries.every(
|
|
1247
|
+
([k, v]) => k.trim().length > 0 && typeof v === "string" && v.trim().length > 0
|
|
1248
|
+
);
|
|
1249
|
+
if (!valid) return { ok: false };
|
|
1250
|
+
return { ok: true, value: Object.fromEntries(entries) };
|
|
1251
|
+
}
|
|
1252
|
+
return { ok: false };
|
|
1253
|
+
}
|
|
854
1254
|
async function statusEntryFor(reader, providerId) {
|
|
855
1255
|
const all = await reader.listAll();
|
|
856
1256
|
return all.find((a) => a.providerId === providerId) ?? null;
|
|
@@ -1127,6 +1527,438 @@ async function handleCliLaunch(cli, body, ctx) {
|
|
|
1127
1527
|
return { status: 200, body: { sessionId: id, providerId: target.providerId, model: target.model } };
|
|
1128
1528
|
}
|
|
1129
1529
|
|
|
1530
|
+
// src/admin/auditConfigBody.ts
|
|
1531
|
+
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1532
|
+
function validateAuditSegment(patch) {
|
|
1533
|
+
const errors = [];
|
|
1534
|
+
const audit = patch.audit;
|
|
1535
|
+
if (audit === void 0) return errors;
|
|
1536
|
+
if (!isPlainObject(audit)) {
|
|
1537
|
+
errors.push("audit must be an object");
|
|
1538
|
+
return errors;
|
|
1539
|
+
}
|
|
1540
|
+
for (const flag of ["enabled", "captureBodies", "trustForwardedFor"]) {
|
|
1541
|
+
if (audit[flag] !== void 0 && typeof audit[flag] !== "boolean") {
|
|
1542
|
+
errors.push(`audit.${flag} must be a boolean`);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const maxBodyBytes = audit["maxBodyBytes"];
|
|
1546
|
+
if (maxBodyBytes !== void 0 && (typeof maxBodyBytes !== "number" || !Number.isFinite(maxBodyBytes) || maxBodyBytes < 0)) {
|
|
1547
|
+
errors.push("audit.maxBodyBytes must be a non-negative number");
|
|
1548
|
+
}
|
|
1549
|
+
const retentionDays = audit["retentionDays"];
|
|
1550
|
+
if (retentionDays !== void 0 && (typeof retentionDays !== "number" || !Number.isFinite(retentionDays) || retentionDays < 0)) {
|
|
1551
|
+
errors.push("audit.retentionDays must be a non-negative number");
|
|
1552
|
+
}
|
|
1553
|
+
return errors;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/admin/billingConfigBody.ts
|
|
1557
|
+
var BILLING_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
1558
|
+
var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1559
|
+
function validateBillingSegment(patch) {
|
|
1560
|
+
const errors = [];
|
|
1561
|
+
const billing = patch.billing;
|
|
1562
|
+
if (billing === void 0) return errors;
|
|
1563
|
+
if (!isPlainObject2(billing)) {
|
|
1564
|
+
errors.push("billing must be an object");
|
|
1565
|
+
return errors;
|
|
1566
|
+
}
|
|
1567
|
+
if (billing["enabled"] !== void 0 && typeof billing["enabled"] !== "boolean") {
|
|
1568
|
+
errors.push("billing.enabled must be a boolean");
|
|
1569
|
+
}
|
|
1570
|
+
if (billing["endpoint"] !== void 0 && typeof billing["endpoint"] !== "string") {
|
|
1571
|
+
errors.push("billing.endpoint must be a string");
|
|
1572
|
+
}
|
|
1573
|
+
if (billing["secret"] !== void 0 && typeof billing["secret"] !== "string") {
|
|
1574
|
+
errors.push("billing.secret must be a string");
|
|
1575
|
+
}
|
|
1576
|
+
const maxRetryAgeMs = billing["maxRetryAgeMs"];
|
|
1577
|
+
if (maxRetryAgeMs !== void 0 && (typeof maxRetryAgeMs !== "number" || !Number.isFinite(maxRetryAgeMs) || maxRetryAgeMs < 0)) {
|
|
1578
|
+
errors.push("billing.maxRetryAgeMs must be a non-negative number");
|
|
1579
|
+
}
|
|
1580
|
+
return errors;
|
|
1581
|
+
}
|
|
1582
|
+
function redactBillingConfig(billing) {
|
|
1583
|
+
if (typeof billing.secret === "string" && billing.secret.length > 0) {
|
|
1584
|
+
return { ...billing, secret: BILLING_SECRET_MASK };
|
|
1585
|
+
}
|
|
1586
|
+
return billing;
|
|
1587
|
+
}
|
|
1588
|
+
function preserveBillingSecret(incoming, current) {
|
|
1589
|
+
const isMaskedOrBlank = incoming.secret === void 0 || incoming.secret === "" || incoming.secret === BILLING_SECRET_MASK;
|
|
1590
|
+
if (isMaskedOrBlank) {
|
|
1591
|
+
if (current?.secret) return { ...incoming, secret: current.secret };
|
|
1592
|
+
const { secret: _secret, ...rest } = incoming;
|
|
1593
|
+
return rest;
|
|
1594
|
+
}
|
|
1595
|
+
return incoming;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/admin/dashboard.ts
|
|
1599
|
+
function startOfLocalDayMs(ts) {
|
|
1600
|
+
const d = new Date(ts);
|
|
1601
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
1602
|
+
}
|
|
1603
|
+
function accountProviderId(entry) {
|
|
1604
|
+
if (!entry || typeof entry !== "object") return null;
|
|
1605
|
+
const e = entry;
|
|
1606
|
+
if (typeof e["providerId"] === "string" && e["providerId"]) return e["providerId"];
|
|
1607
|
+
if (typeof e["provider"] === "string" && e["provider"]) return e["provider"];
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
async function handleDashboard(deps) {
|
|
1611
|
+
const now = Date.now();
|
|
1612
|
+
const today = await deps.usageRecorder.getTotals({ startTs: startOfLocalDayMs(now), endTs: now });
|
|
1613
|
+
const total = await deps.usageRecorder.getTotals({ startTs: 0, endTs: now });
|
|
1614
|
+
const providerList = loadConfig(deps.configPath).providers;
|
|
1615
|
+
const providers = {
|
|
1616
|
+
total: providerList.length,
|
|
1617
|
+
enabled: providerList.filter((p) => p.enabled !== false).length
|
|
1618
|
+
};
|
|
1619
|
+
const keys = await deps.keyDb.outboundApiKeysList();
|
|
1620
|
+
const outboundKeys = {
|
|
1621
|
+
total: keys.length,
|
|
1622
|
+
active: keys.filter((k) => k.enabled && k.revokedAt === null).length
|
|
1623
|
+
};
|
|
1624
|
+
const accountsList = await deps.subscriptionAccounts.listAll();
|
|
1625
|
+
const byProvider = {};
|
|
1626
|
+
for (const entry of accountsList) {
|
|
1627
|
+
const providerId = accountProviderId(entry);
|
|
1628
|
+
if (providerId) byProvider[providerId] = (byProvider[providerId] ?? 0) + 1;
|
|
1629
|
+
}
|
|
1630
|
+
const accounts = { total: accountsList.length, byProvider };
|
|
1631
|
+
const status = deps.outboundApiServer.getStatus();
|
|
1632
|
+
const server = {
|
|
1633
|
+
running: status.running,
|
|
1634
|
+
port: status.port,
|
|
1635
|
+
uptimeMs: Math.round(process.uptime() * 1e3)
|
|
1636
|
+
};
|
|
1637
|
+
const summary = { today, total, providers, outboundKeys, accounts, server, generatedAt: now };
|
|
1638
|
+
return { status: 200, body: summary };
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/admin/keyPolicyBody.ts
|
|
1642
|
+
function parseKeyPolicyBody(body) {
|
|
1643
|
+
const policy = {};
|
|
1644
|
+
if ("activationMode" in body) {
|
|
1645
|
+
const m = body["activationMode"];
|
|
1646
|
+
if (m === null) policy.activationMode = null;
|
|
1647
|
+
else if (m === "fixed" || m === "activation") policy.activationMode = m;
|
|
1648
|
+
else return { ok: false, message: "activationMode must be 'fixed', 'activation', or null" };
|
|
1649
|
+
}
|
|
1650
|
+
const numericFields = [
|
|
1651
|
+
{ key: "expiresAt", min: 0 },
|
|
1652
|
+
{ key: "activationDays", min: 1, integer: true },
|
|
1653
|
+
{ key: "dailyCostLimitUsd", min: 0 },
|
|
1654
|
+
{ key: "totalCostLimitUsd", min: 0 },
|
|
1655
|
+
{ key: "weeklyCostLimitUsd", min: 0 },
|
|
1656
|
+
{ key: "rateLimitMaxRequests", min: 0, integer: true },
|
|
1657
|
+
{ key: "rateLimitWindowMs", min: 1 }
|
|
1658
|
+
];
|
|
1659
|
+
for (const { key, min, integer } of numericFields) {
|
|
1660
|
+
if (!(key in body)) continue;
|
|
1661
|
+
const v = body[key];
|
|
1662
|
+
if (v === null) {
|
|
1663
|
+
policy[key] = null;
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < min || integer && !Number.isInteger(v)) {
|
|
1667
|
+
return {
|
|
1668
|
+
ok: false,
|
|
1669
|
+
message: `${key} must be ${integer ? "an integer" : "a number"} >= ${min} or null`
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
policy[key] = v;
|
|
1673
|
+
}
|
|
1674
|
+
if ("enableModelRestriction" in body) {
|
|
1675
|
+
const v = body["enableModelRestriction"];
|
|
1676
|
+
if (v === null) policy.enableModelRestriction = null;
|
|
1677
|
+
else if (typeof v === "boolean") policy.enableModelRestriction = v;
|
|
1678
|
+
else return { ok: false, message: "enableModelRestriction must be a boolean or null" };
|
|
1679
|
+
}
|
|
1680
|
+
if ("restrictionMode" in body) {
|
|
1681
|
+
const v = body["restrictionMode"];
|
|
1682
|
+
if (v === null) policy.restrictionMode = null;
|
|
1683
|
+
else if (v === "blacklist" || v === "allowlist") policy.restrictionMode = v;
|
|
1684
|
+
else return { ok: false, message: "restrictionMode must be 'blacklist', 'allowlist', or null" };
|
|
1685
|
+
}
|
|
1686
|
+
if ("restrictedModels" in body) {
|
|
1687
|
+
const v = body["restrictedModels"];
|
|
1688
|
+
if (v === null) {
|
|
1689
|
+
policy.restrictedModels = null;
|
|
1690
|
+
} else if (Array.isArray(v) && v.every((e) => typeof e === "string")) {
|
|
1691
|
+
policy.restrictedModels = v.map((e) => e.trim()).filter((e) => e !== "");
|
|
1692
|
+
} else {
|
|
1693
|
+
return { ok: false, message: "restrictedModels must be an array of strings or null" };
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return { ok: true, policy };
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/admin/voucherAdmin.ts
|
|
1700
|
+
var import_outbound_api = require("@omnicross/core/outbound-api");
|
|
1701
|
+
function writeJson(res, status, body) {
|
|
1702
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1703
|
+
res.end(JSON.stringify(body));
|
|
1704
|
+
}
|
|
1705
|
+
function writeErr(res, status, message) {
|
|
1706
|
+
writeJson(res, status, { error: { type: "voucher_error", message } });
|
|
1707
|
+
}
|
|
1708
|
+
function readJsonBody2(req) {
|
|
1709
|
+
return new Promise((resolve, reject) => {
|
|
1710
|
+
const chunks = [];
|
|
1711
|
+
req.on("data", (c) => chunks.push(c));
|
|
1712
|
+
req.on("end", () => {
|
|
1713
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1714
|
+
if (!raw.trim()) return resolve({});
|
|
1715
|
+
try {
|
|
1716
|
+
const parsed = JSON.parse(raw);
|
|
1717
|
+
resolve(parsed && typeof parsed === "object" ? parsed : {});
|
|
1718
|
+
} catch {
|
|
1719
|
+
reject(new Error("invalid-json"));
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
req.on("error", reject);
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
function optPositive(value, integer) {
|
|
1726
|
+
if (value === void 0 || value === null) return { ok: true, value: void 0 };
|
|
1727
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return { ok: false };
|
|
1728
|
+
if (integer && !Number.isInteger(value)) return { ok: false };
|
|
1729
|
+
return { ok: true, value };
|
|
1730
|
+
}
|
|
1731
|
+
function parseVoucherCreateBody(body) {
|
|
1732
|
+
const type = body["type"];
|
|
1733
|
+
if (type !== "credit" && type !== "renewal") {
|
|
1734
|
+
return { ok: false, message: "type must be 'credit' or 'renewal'" };
|
|
1735
|
+
}
|
|
1736
|
+
const maxTotal = optPositive(body["maxTotalCostLimitUsd"], false);
|
|
1737
|
+
if (!maxTotal.ok) return { ok: false, message: "maxTotalCostLimitUsd must be a positive number" };
|
|
1738
|
+
const maxDays = optPositive(body["maxExpiryDays"], true);
|
|
1739
|
+
if (!maxDays.ok) return { ok: false, message: "maxExpiryDays must be a positive integer" };
|
|
1740
|
+
const input = { type };
|
|
1741
|
+
if (maxTotal.value !== void 0) input.maxTotalCostLimitUsd = maxTotal.value;
|
|
1742
|
+
if (maxDays.value !== void 0) input.maxExpiryDays = maxDays.value;
|
|
1743
|
+
if (type === "credit") {
|
|
1744
|
+
const credit = optPositive(body["creditUsd"], false);
|
|
1745
|
+
if (!credit.ok || credit.value === void 0) {
|
|
1746
|
+
return { ok: false, message: "creditUsd must be a positive number for a credit card" };
|
|
1747
|
+
}
|
|
1748
|
+
input.creditUsd = credit.value;
|
|
1749
|
+
} else {
|
|
1750
|
+
const days = optPositive(body["renewalDays"], true);
|
|
1751
|
+
if (!days.ok || days.value === void 0) {
|
|
1752
|
+
return { ok: false, message: "renewalDays must be a positive integer for a renewal card" };
|
|
1753
|
+
}
|
|
1754
|
+
input.renewalDays = days.value;
|
|
1755
|
+
}
|
|
1756
|
+
return { ok: true, input };
|
|
1757
|
+
}
|
|
1758
|
+
async function voucherEnabled(deps) {
|
|
1759
|
+
const config = await (0, import_outbound_api.loadServerConfig)(deps.settingsStore);
|
|
1760
|
+
return config.voucher?.enabled === true;
|
|
1761
|
+
}
|
|
1762
|
+
async function handleVoucher(req, res, method, rest, deps) {
|
|
1763
|
+
const voucherDb = deps.voucherDb;
|
|
1764
|
+
if (!voucherDb) return writeErr(res, 501, "Voucher feature is not available");
|
|
1765
|
+
if (method === "GET" && rest.length === 0) {
|
|
1766
|
+
const rows = await voucherDb.voucherList();
|
|
1767
|
+
return writeJson(res, 200, { vouchers: rows.map(import_outbound_api.toVoucherInfo) });
|
|
1768
|
+
}
|
|
1769
|
+
if (method === "POST" && rest.length === 0) {
|
|
1770
|
+
if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
|
|
1771
|
+
let body;
|
|
1772
|
+
try {
|
|
1773
|
+
body = await readJsonBody2(req);
|
|
1774
|
+
} catch {
|
|
1775
|
+
return writeErr(res, 400, "Invalid JSON in request body");
|
|
1776
|
+
}
|
|
1777
|
+
const parsed = parseVoucherCreateBody(body);
|
|
1778
|
+
if (!parsed.ok) return writeErr(res, 400, parsed.message);
|
|
1779
|
+
const code = (0, import_outbound_api.generateVoucherCode)();
|
|
1780
|
+
const created = await voucherDb.voucherCreate({
|
|
1781
|
+
id: (0, import_outbound_api.newVoucherId)(),
|
|
1782
|
+
codeHash: (0, import_outbound_api.hashVoucherCode)(code),
|
|
1783
|
+
codePrefix: (0, import_outbound_api.voucherCodePrefix)(code),
|
|
1784
|
+
...parsed.input
|
|
1785
|
+
});
|
|
1786
|
+
return writeJson(res, 201, {
|
|
1787
|
+
id: created.id,
|
|
1788
|
+
codePrefix: created.codePrefix,
|
|
1789
|
+
type: created.type,
|
|
1790
|
+
createdAt: created.createdAt,
|
|
1791
|
+
// `plaintextOnce` is the ONLY place the full code crosses the wire (D3).
|
|
1792
|
+
plaintextOnce: code
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
const id = rest[0];
|
|
1796
|
+
if (method === "POST" && id && rest[1] === "revoke") {
|
|
1797
|
+
if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
|
|
1798
|
+
const ok = await voucherDb.voucherRevokeCas(id, Date.now());
|
|
1799
|
+
return writeJson(res, ok ? 200 : 409, { ok });
|
|
1800
|
+
}
|
|
1801
|
+
return writeErr(res, 405, `method ${method} not allowed on voucher`);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/admin/webhookConfigBody.ts
|
|
1805
|
+
var import_webhook_types = require("@omnicross/contracts/webhook-types");
|
|
1806
|
+
var WEBHOOK_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
1807
|
+
var isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1808
|
+
function validateWebhookSegment(patch) {
|
|
1809
|
+
const errors = [];
|
|
1810
|
+
const webhook = patch.webhook;
|
|
1811
|
+
if (webhook === void 0) return errors;
|
|
1812
|
+
if (!isPlainObject3(webhook)) {
|
|
1813
|
+
errors.push("webhook must be an object");
|
|
1814
|
+
return errors;
|
|
1815
|
+
}
|
|
1816
|
+
if (typeof webhook["enabled"] !== "boolean") {
|
|
1817
|
+
errors.push("webhook.enabled must be a boolean");
|
|
1818
|
+
}
|
|
1819
|
+
const destinations = webhook["destinations"];
|
|
1820
|
+
if (destinations !== void 0 && !Array.isArray(destinations)) {
|
|
1821
|
+
errors.push("webhook.destinations must be an array");
|
|
1822
|
+
return errors;
|
|
1823
|
+
}
|
|
1824
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1825
|
+
for (const [i, raw] of (Array.isArray(destinations) ? destinations : []).entries()) {
|
|
1826
|
+
if (!isPlainObject3(raw)) {
|
|
1827
|
+
errors.push(`webhook.destinations[${i}] must be an object`);
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
const id = raw["id"];
|
|
1831
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
1832
|
+
errors.push(`webhook.destinations[${i}].id must be a non-empty string`);
|
|
1833
|
+
} else if (seenIds.has(id.trim())) {
|
|
1834
|
+
errors.push(`webhook.destinations[${i}].id '${id.trim()}' is duplicated`);
|
|
1835
|
+
} else {
|
|
1836
|
+
seenIds.add(id.trim());
|
|
1837
|
+
}
|
|
1838
|
+
if (typeof raw["type"] !== "string" || !import_webhook_types.WEBHOOK_DESTINATION_TYPES.includes(raw["type"])) {
|
|
1839
|
+
errors.push(`webhook.destinations[${i}].type must be one of ${import_webhook_types.WEBHOOK_DESTINATION_TYPES.join(", ")}`);
|
|
1840
|
+
}
|
|
1841
|
+
if (typeof raw["url"] !== "string" || !raw["url"].trim()) {
|
|
1842
|
+
errors.push(`webhook.destinations[${i}].url must be a non-empty string`);
|
|
1843
|
+
}
|
|
1844
|
+
if (raw["secret"] !== void 0 && typeof raw["secret"] !== "string") {
|
|
1845
|
+
errors.push(`webhook.destinations[${i}].secret must be a string`);
|
|
1846
|
+
}
|
|
1847
|
+
if (raw["enabled"] !== void 0 && typeof raw["enabled"] !== "boolean") {
|
|
1848
|
+
errors.push(`webhook.destinations[${i}].enabled must be a boolean`);
|
|
1849
|
+
}
|
|
1850
|
+
const events = raw["events"];
|
|
1851
|
+
if (events !== void 0) {
|
|
1852
|
+
if (!Array.isArray(events)) {
|
|
1853
|
+
errors.push(`webhook.destinations[${i}].events must be an array`);
|
|
1854
|
+
} else {
|
|
1855
|
+
for (const e of events) {
|
|
1856
|
+
if (typeof e !== "string" || !import_webhook_types.WEBHOOK_EVENT_KINDS.includes(e)) {
|
|
1857
|
+
errors.push(`webhook.destinations[${i}].events contains an unknown kind '${String(e)}'`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return errors;
|
|
1864
|
+
}
|
|
1865
|
+
function redactWebhookConfig(webhook) {
|
|
1866
|
+
return {
|
|
1867
|
+
...webhook,
|
|
1868
|
+
destinations: webhook.destinations.map(
|
|
1869
|
+
(d) => typeof d.secret === "string" && d.secret.length > 0 ? { ...d, secret: WEBHOOK_SECRET_MASK } : d
|
|
1870
|
+
)
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
function preserveWebhookSecrets(incoming, current) {
|
|
1874
|
+
const currentById = /* @__PURE__ */ new Map();
|
|
1875
|
+
for (const d of current?.destinations ?? []) currentById.set(d.id, d);
|
|
1876
|
+
return {
|
|
1877
|
+
...incoming,
|
|
1878
|
+
destinations: incoming.destinations.map((d) => {
|
|
1879
|
+
const isMaskedOrBlank = d.secret === void 0 || d.secret === "" || d.secret === WEBHOOK_SECRET_MASK;
|
|
1880
|
+
if (isMaskedOrBlank) {
|
|
1881
|
+
const prev = currentById.get(d.id);
|
|
1882
|
+
if (prev?.secret) return { ...d, secret: prev.secret };
|
|
1883
|
+
const { secret: _secret, ...rest } = d;
|
|
1884
|
+
return rest;
|
|
1885
|
+
}
|
|
1886
|
+
return d;
|
|
1887
|
+
})
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// src/audit/auditRuntime.ts
|
|
1892
|
+
var import_auditSink = require("@omnicross/core/pipeline/auditSink");
|
|
1893
|
+
var writer = null;
|
|
1894
|
+
var sweeper = null;
|
|
1895
|
+
function setAuditRuntime(w, s) {
|
|
1896
|
+
writer = w;
|
|
1897
|
+
sweeper = s;
|
|
1898
|
+
}
|
|
1899
|
+
function applyAuditConfig(config) {
|
|
1900
|
+
const enabled = config?.enabled === true && writer !== null;
|
|
1901
|
+
if (enabled && config) {
|
|
1902
|
+
(0, import_auditSink.setAuditCaptureConfig)(config);
|
|
1903
|
+
const activeWriter = writer;
|
|
1904
|
+
(0, import_auditSink.setAuditSink)((record) => activeWriter.record(record));
|
|
1905
|
+
if (sweeper) {
|
|
1906
|
+
sweeper.configure(config);
|
|
1907
|
+
sweeper.start();
|
|
1908
|
+
}
|
|
1909
|
+
} else {
|
|
1910
|
+
(0, import_auditSink.setAuditCaptureConfig)(null);
|
|
1911
|
+
(0, import_auditSink.setAuditSink)(null);
|
|
1912
|
+
if (sweeper) {
|
|
1913
|
+
if (config) sweeper.configure(config);
|
|
1914
|
+
sweeper.dispose();
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
function resetAuditRuntimeForTests() {
|
|
1919
|
+
(0, import_auditSink.setAuditCaptureConfig)(null);
|
|
1920
|
+
(0, import_auditSink.setAuditSink)(null);
|
|
1921
|
+
if (sweeper) sweeper.dispose();
|
|
1922
|
+
writer = null;
|
|
1923
|
+
sweeper = null;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/billing/billingRuntime.ts
|
|
1927
|
+
var import_billingEmit = require("@omnicross/core/pipeline/billingEmit");
|
|
1928
|
+
var publisher = null;
|
|
1929
|
+
var sweeper2 = null;
|
|
1930
|
+
function setBillingRuntime(p, s) {
|
|
1931
|
+
publisher = p;
|
|
1932
|
+
sweeper2 = s;
|
|
1933
|
+
}
|
|
1934
|
+
function applyBillingConfig(config) {
|
|
1935
|
+
const enabled = config?.enabled === true && publisher !== null;
|
|
1936
|
+
if (enabled && config) {
|
|
1937
|
+
const activePublisher = publisher;
|
|
1938
|
+
activePublisher.setConfig(config);
|
|
1939
|
+
(0, import_billingEmit.setBillingCaptureConfig)(config);
|
|
1940
|
+
(0, import_billingEmit.setBillingSink)((event) => activePublisher.record(event));
|
|
1941
|
+
if (sweeper2) {
|
|
1942
|
+
sweeper2.configure(config);
|
|
1943
|
+
sweeper2.start();
|
|
1944
|
+
}
|
|
1945
|
+
} else {
|
|
1946
|
+
(0, import_billingEmit.setBillingCaptureConfig)(null);
|
|
1947
|
+
(0, import_billingEmit.setBillingSink)(null);
|
|
1948
|
+
if (sweeper2) {
|
|
1949
|
+
if (config) sweeper2.configure(config);
|
|
1950
|
+
sweeper2.dispose();
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
function resetBillingRuntimeForTests() {
|
|
1955
|
+
(0, import_billingEmit.setBillingCaptureConfig)(null);
|
|
1956
|
+
(0, import_billingEmit.setBillingSink)(null);
|
|
1957
|
+
if (sweeper2) sweeper2.dispose();
|
|
1958
|
+
publisher = null;
|
|
1959
|
+
sweeper2 = null;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1130
1962
|
// src/ports/account-multi.ts
|
|
1131
1963
|
var import_node_crypto5 = require("crypto");
|
|
1132
1964
|
var PROVIDER_KEYS = {
|
|
@@ -1238,6 +2070,9 @@ function getAccountById(config, p, id) {
|
|
|
1238
2070
|
const account = getAccounts(config, p).find((a) => a.id === id);
|
|
1239
2071
|
return account ? { id: account.id, tokens: account.tokens } : void 0;
|
|
1240
2072
|
}
|
|
2073
|
+
function getAccountProxy(config, p, id) {
|
|
2074
|
+
return getAccounts(config, p).find((a) => a.id === id)?.proxy;
|
|
2075
|
+
}
|
|
1241
2076
|
function getActiveAccount(config, p) {
|
|
1242
2077
|
const active = getAccounts(config, p).find((a) => a.id === getActiveId(config, p));
|
|
1243
2078
|
return active ? { id: active.id, tokens: active.tokens } : void 0;
|
|
@@ -1278,7 +2113,17 @@ function sanitizeAccounts(config, p) {
|
|
|
1278
2113
|
isSetupToken: t.isSetupToken,
|
|
1279
2114
|
hasAccessToken: !!(t.accessToken || t.apiKey),
|
|
1280
2115
|
isActive: a.id === activeId,
|
|
1281
|
-
|
|
2116
|
+
// Scheduling metadata (subscription-account-scheduling): editable priority
|
|
2117
|
+
// (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
|
|
2118
|
+
priority: a.priority,
|
|
2119
|
+
lastUsedAt: a.lastUsedAt,
|
|
2120
|
+
syncWarning: t.syncWarning,
|
|
2121
|
+
// Per-account proxy (upstream-proxy): masked view — password → hasPassword,
|
|
2122
|
+
// userinfo stripped. The plaintext password is NEVER projected.
|
|
2123
|
+
proxy: a.proxy ? sanitizeProxyConfig(a.proxy) : void 0,
|
|
2124
|
+
// Per-account model support / remap (subscription-account-model-map): model
|
|
2125
|
+
// ids are not token material → carried through verbatim for the editor.
|
|
2126
|
+
supportedModels: a.supportedModels
|
|
1282
2127
|
};
|
|
1283
2128
|
});
|
|
1284
2129
|
}
|
|
@@ -1292,27 +2137,98 @@ function renameAccount(config, p, id, label) {
|
|
|
1292
2137
|
);
|
|
1293
2138
|
return { ok: true };
|
|
1294
2139
|
}
|
|
1295
|
-
function
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
2140
|
+
function setAccountPriority(config, p, id, priority) {
|
|
2141
|
+
const accounts = getAccounts(config, p);
|
|
2142
|
+
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
2143
|
+
setAccounts(
|
|
2144
|
+
config,
|
|
2145
|
+
p,
|
|
2146
|
+
accounts.map((a) => a.id === id ? { ...a, priority } : a)
|
|
2147
|
+
);
|
|
2148
|
+
return { ok: true };
|
|
1299
2149
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
2150
|
+
function setAccountProxy(config, p, id, proxy) {
|
|
2151
|
+
const accounts = getAccounts(config, p);
|
|
2152
|
+
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
2153
|
+
setAccounts(
|
|
2154
|
+
config,
|
|
2155
|
+
p,
|
|
2156
|
+
accounts.map((a) => {
|
|
2157
|
+
if (a.id !== id) return a;
|
|
2158
|
+
if (!proxy) {
|
|
2159
|
+
const { proxy: _drop, ...rest } = a;
|
|
2160
|
+
return rest;
|
|
2161
|
+
}
|
|
2162
|
+
return { ...a, proxy };
|
|
2163
|
+
})
|
|
2164
|
+
);
|
|
2165
|
+
return { ok: true };
|
|
2166
|
+
}
|
|
2167
|
+
function setAccountSupportedModels(config, p, id, supportedModels) {
|
|
2168
|
+
const accounts = getAccounts(config, p);
|
|
2169
|
+
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
2170
|
+
setAccounts(
|
|
2171
|
+
config,
|
|
2172
|
+
p,
|
|
2173
|
+
accounts.map((a) => {
|
|
2174
|
+
if (a.id !== id) return a;
|
|
2175
|
+
if (supportedModels === void 0) {
|
|
2176
|
+
const { supportedModels: _drop, ...rest } = a;
|
|
2177
|
+
return rest;
|
|
2178
|
+
}
|
|
2179
|
+
return { ...a, supportedModels };
|
|
2180
|
+
})
|
|
2181
|
+
);
|
|
2182
|
+
return { ok: true };
|
|
2183
|
+
}
|
|
2184
|
+
function setAccountLastUsed(config, p, id, iso) {
|
|
2185
|
+
const accounts = getAccounts(config, p);
|
|
2186
|
+
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
2187
|
+
setAccounts(
|
|
2188
|
+
config,
|
|
2189
|
+
p,
|
|
2190
|
+
accounts.map((a) => a.id === id ? { ...a, lastUsedAt: iso } : a)
|
|
2191
|
+
);
|
|
2192
|
+
return { ok: true };
|
|
2193
|
+
}
|
|
2194
|
+
function setAccountIdentity(config, p, id, identity) {
|
|
2195
|
+
const accounts = getAccounts(config, p);
|
|
2196
|
+
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
2197
|
+
setAccounts(
|
|
2198
|
+
config,
|
|
2199
|
+
p,
|
|
2200
|
+
accounts.map((a) => {
|
|
2201
|
+
if (a.id !== id) return a;
|
|
2202
|
+
if (identity === void 0) {
|
|
2203
|
+
const { identity: _drop, ...rest } = a;
|
|
2204
|
+
return rest;
|
|
2205
|
+
}
|
|
2206
|
+
return { ...a, identity };
|
|
2207
|
+
})
|
|
2208
|
+
);
|
|
2209
|
+
return { ok: true };
|
|
2210
|
+
}
|
|
2211
|
+
function clearProvider(config, p) {
|
|
2212
|
+
setBlock(config, p, void 0);
|
|
2213
|
+
setAccounts(config, p, void 0);
|
|
2214
|
+
setActiveId(config, p, void 0);
|
|
2215
|
+
}
|
|
2216
|
+
var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
|
|
2217
|
+
|
|
2218
|
+
// src/migration/packCodec.ts
|
|
2219
|
+
var import_node_crypto6 = require("crypto");
|
|
2220
|
+
var PACK_MAGIC = "OMCXPACK";
|
|
2221
|
+
var PACK_VERSION = 1;
|
|
2222
|
+
var KDF_ALGORITHM = "scrypt";
|
|
2223
|
+
var PACK_PREFIX = `${PACK_MAGIC}${PACK_VERSION}.`;
|
|
2224
|
+
var KEY_BYTES3 = 32;
|
|
2225
|
+
var IV_BYTES2 = 12;
|
|
2226
|
+
var TAG_BYTES2 = 16;
|
|
2227
|
+
var SCRYPT_N = 1 << 15;
|
|
2228
|
+
var SCRYPT_R = 8;
|
|
2229
|
+
var SCRYPT_P = 1;
|
|
2230
|
+
var SCRYPT_SALT_BYTES = 16;
|
|
2231
|
+
var SCRYPT_MAXMEM = 128 * SCRYPT_R * SCRYPT_N * 2;
|
|
1316
2232
|
var MIN_PASSPHRASE_LENGTH = 8;
|
|
1317
2233
|
var WeakPassphraseError = class extends Error {
|
|
1318
2234
|
constructor() {
|
|
@@ -1557,6 +2473,12 @@ function parseRange(query) {
|
|
|
1557
2473
|
return { startTs, endTs };
|
|
1558
2474
|
}
|
|
1559
2475
|
var isRange = (v) => v.startTs !== void 0 && !("status" in v);
|
|
2476
|
+
var BUCKET_SPAN_MS = {
|
|
2477
|
+
hour: 36e5,
|
|
2478
|
+
day: 864e5,
|
|
2479
|
+
month: 28 * 864e5
|
|
2480
|
+
};
|
|
2481
|
+
var MAX_TIMESERIES_BUCKETS = 2e3;
|
|
1560
2482
|
async function handleUsageGet(view, query, deps) {
|
|
1561
2483
|
const range = parseRange(query);
|
|
1562
2484
|
if (!isRange(range)) return range;
|
|
@@ -1565,6 +2487,24 @@ async function handleUsageGet(view, query, deps) {
|
|
|
1565
2487
|
return { status: 200, body: await deps.usageRecorder.getTotals(range) };
|
|
1566
2488
|
case "by-model":
|
|
1567
2489
|
return { status: 200, body: await deps.usageRecorder.getByModel(range) };
|
|
2490
|
+
case "timeseries": {
|
|
2491
|
+
const bucket = query.get("bucket");
|
|
2492
|
+
if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
|
|
2493
|
+
return err4(400, "bucket must be one of 'hour', 'day', 'month'");
|
|
2494
|
+
}
|
|
2495
|
+
const now = Date.now();
|
|
2496
|
+
const clamped = { startTs: range.startTs, endTs: Math.min(range.endTs, now) };
|
|
2497
|
+
if (clamped.startTs < clamped.endTs) {
|
|
2498
|
+
const projected = Math.ceil((clamped.endTs - clamped.startTs) / BUCKET_SPAN_MS[bucket]) + 1;
|
|
2499
|
+
if (projected > MAX_TIMESERIES_BUCKETS) {
|
|
2500
|
+
return err4(
|
|
2501
|
+
400,
|
|
2502
|
+
`requested range projects ~${projected} '${bucket}' buckets (max ${MAX_TIMESERIES_BUCKETS}); narrow the range or use a coarser bucket`
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
return { status: 200, body: await deps.usageRecorder.getTimeSeries(clamped, bucket) };
|
|
2507
|
+
}
|
|
1568
2508
|
case "by-api-key": {
|
|
1569
2509
|
const rows = await deps.usageRecorder.getByApiKey(range);
|
|
1570
2510
|
const labels = poolKeyLabels(loadConfig(deps.configPath));
|
|
@@ -1710,7 +2650,7 @@ function readBody(req) {
|
|
|
1710
2650
|
req.on("error", reject);
|
|
1711
2651
|
});
|
|
1712
2652
|
}
|
|
1713
|
-
async function
|
|
2653
|
+
async function readJsonBody3(req) {
|
|
1714
2654
|
const raw = await readBody(req);
|
|
1715
2655
|
if (!raw.trim()) return {};
|
|
1716
2656
|
try {
|
|
@@ -1720,12 +2660,12 @@ async function readJsonBody(req) {
|
|
|
1720
2660
|
return {};
|
|
1721
2661
|
}
|
|
1722
2662
|
}
|
|
1723
|
-
function
|
|
2663
|
+
function writeJson2(res, status, body) {
|
|
1724
2664
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1725
2665
|
res.end(JSON.stringify(body));
|
|
1726
2666
|
}
|
|
1727
2667
|
function writeJsonError(res, status, message) {
|
|
1728
|
-
|
|
2668
|
+
writeJson2(res, status, { error: { type: "admin_api_error", message } });
|
|
1729
2669
|
}
|
|
1730
2670
|
function maskProviderApiKey(apiKey) {
|
|
1731
2671
|
if (!apiKey) return "";
|
|
@@ -1741,7 +2681,23 @@ function toKeyInfo(row) {
|
|
|
1741
2681
|
enabled: row.enabled,
|
|
1742
2682
|
createdAt: row.createdAt,
|
|
1743
2683
|
lastUsedAt: row.lastUsedAt,
|
|
1744
|
-
revoked: row.revokedAt !== null
|
|
2684
|
+
revoked: row.revokedAt !== null,
|
|
2685
|
+
maxConcurrency: row.maxConcurrency,
|
|
2686
|
+
// Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
|
|
2687
|
+
// the UI reads them to render + pre-fill the policy editor.
|
|
2688
|
+
expiresAt: row.expiresAt,
|
|
2689
|
+
activationMode: row.activationMode,
|
|
2690
|
+
activationDays: row.activationDays,
|
|
2691
|
+
activatedAt: row.activatedAt,
|
|
2692
|
+
dailyCostLimitUsd: row.dailyCostLimitUsd,
|
|
2693
|
+
totalCostLimitUsd: row.totalCostLimitUsd,
|
|
2694
|
+
weeklyCostLimitUsd: row.weeklyCostLimitUsd,
|
|
2695
|
+
rateLimitMaxRequests: row.rateLimitMaxRequests,
|
|
2696
|
+
rateLimitWindowMs: row.rateLimitWindowMs,
|
|
2697
|
+
// Per-key model restriction (#6) — secret-free scalars the UI pre-fills.
|
|
2698
|
+
enableModelRestriction: row.enableModelRestriction,
|
|
2699
|
+
restrictionMode: row.restrictionMode,
|
|
2700
|
+
restrictedModels: row.restrictedModels
|
|
1745
2701
|
};
|
|
1746
2702
|
}
|
|
1747
2703
|
function toProviderView(row) {
|
|
@@ -1803,6 +2759,8 @@ async function handleAdminApi(req, res, path2, deps) {
|
|
|
1803
2759
|
return handlePresets(res, method);
|
|
1804
2760
|
case "keys":
|
|
1805
2761
|
return await handleKeys(req, res, method, rest, deps);
|
|
2762
|
+
case "voucher":
|
|
2763
|
+
return await handleVoucher(req, res, method, rest, deps);
|
|
1806
2764
|
case "server":
|
|
1807
2765
|
return await handleServer(req, res, method, deps);
|
|
1808
2766
|
case "accounts":
|
|
@@ -1819,6 +2777,8 @@ async function handleAdminApi(req, res, path2, deps) {
|
|
|
1819
2777
|
return await handleMigrationImport(req, res, method, deps);
|
|
1820
2778
|
case "usage":
|
|
1821
2779
|
return await handleUsage(req, res, method, rest, deps);
|
|
2780
|
+
case "dashboard":
|
|
2781
|
+
return await handleDashboardRoute(res, method, deps);
|
|
1822
2782
|
case "pricing":
|
|
1823
2783
|
return await handlePricing(req, res, method, rest, deps);
|
|
1824
2784
|
default:
|
|
@@ -1834,17 +2794,22 @@ function requestQuery(req) {
|
|
|
1834
2794
|
return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
|
|
1835
2795
|
}
|
|
1836
2796
|
function writeResult(res, result) {
|
|
1837
|
-
|
|
2797
|
+
writeJson2(res, result.status, result.body);
|
|
1838
2798
|
}
|
|
1839
2799
|
async function handleUsage(req, res, method, rest, deps) {
|
|
1840
2800
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
|
|
1841
2801
|
return writeResult(res, await handleUsageGet(rest[0], requestQuery(req), deps));
|
|
1842
2802
|
}
|
|
2803
|
+
async function handleDashboardRoute(res, method, deps) {
|
|
2804
|
+
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
|
|
2805
|
+
const result = await handleDashboard(deps);
|
|
2806
|
+
return writeJson2(res, result.status, result.body);
|
|
2807
|
+
}
|
|
1843
2808
|
async function handlePricing(req, res, method, rest, deps) {
|
|
1844
2809
|
if (rest.length === 0) {
|
|
1845
2810
|
if (method === "GET") return writeResult(res, await handlePricingList(deps));
|
|
1846
2811
|
if (method === "PUT") {
|
|
1847
|
-
return writeResult(res, await handlePricingUpsert(await
|
|
2812
|
+
return writeResult(res, await handlePricingUpsert(await readJsonBody3(req), deps));
|
|
1848
2813
|
}
|
|
1849
2814
|
if (method === "DELETE") {
|
|
1850
2815
|
return writeResult(res, await handlePricingDelete(requestQuery(req), deps));
|
|
@@ -1855,7 +2820,7 @@ async function handlePricing(req, res, method, rest, deps) {
|
|
|
1855
2820
|
return writeResult(res, await handlePricingFetchLatest(deps));
|
|
1856
2821
|
}
|
|
1857
2822
|
if (method === "POST" && rest.length === 1 && rest[0] === "resolve-conflicts") {
|
|
1858
|
-
return writeResult(res, await handlePricingResolveConflicts(await
|
|
2823
|
+
return writeResult(res, await handlePricingResolveConflicts(await readJsonBody3(req), deps));
|
|
1859
2824
|
}
|
|
1860
2825
|
return writeJsonError(res, 404, `unknown pricing route '${rest.join("/")}'`);
|
|
1861
2826
|
}
|
|
@@ -1869,15 +2834,15 @@ function migrationDeps(deps) {
|
|
|
1869
2834
|
}
|
|
1870
2835
|
async function handleMigrationExport(req, res, method, deps) {
|
|
1871
2836
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
|
|
1872
|
-
const body = await
|
|
2837
|
+
const body = await readJsonBody3(req);
|
|
1873
2838
|
const result = await handleExport(body, migrationDeps(deps));
|
|
1874
|
-
return
|
|
2839
|
+
return writeJson2(res, result.status, result.body);
|
|
1875
2840
|
}
|
|
1876
2841
|
async function handleMigrationImport(req, res, method, deps) {
|
|
1877
2842
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
|
|
1878
|
-
const body = await
|
|
2843
|
+
const body = await readJsonBody3(req);
|
|
1879
2844
|
const result = await handleImport(body, migrationDeps(deps));
|
|
1880
|
-
return
|
|
2845
|
+
return writeJson2(res, result.status, result.body);
|
|
1881
2846
|
}
|
|
1882
2847
|
async function handleProviders(req, res, method, rest, deps) {
|
|
1883
2848
|
const cfg = loadConfig(deps.configPath);
|
|
@@ -1908,13 +2873,13 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
1908
2873
|
if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
|
|
1909
2874
|
const row = cfg.providers.find((p) => p.id === rest[0]);
|
|
1910
2875
|
if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
|
|
1911
|
-
return
|
|
2876
|
+
return writeJson2(res, 200, { apiKey: row.apiKey ?? "" });
|
|
1912
2877
|
}
|
|
1913
2878
|
if (method === "GET") {
|
|
1914
|
-
return
|
|
2879
|
+
return writeJson2(res, 200, { providers: cfg.providers.map(toProviderView) });
|
|
1915
2880
|
}
|
|
1916
2881
|
if (method === "POST") {
|
|
1917
|
-
const body = await
|
|
2882
|
+
const body = await readJsonBody3(req);
|
|
1918
2883
|
const provider = parseProviderInput(body, void 0);
|
|
1919
2884
|
if (!provider) return writeJsonError(res, 400, "invalid provider (id, apiFormat, baseUrl required)");
|
|
1920
2885
|
if (cfg.providers.some((p) => p.id === provider.id)) {
|
|
@@ -1922,25 +2887,25 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
1922
2887
|
}
|
|
1923
2888
|
cfg.providers.push(provider);
|
|
1924
2889
|
persistProviders(cfg, deps);
|
|
1925
|
-
return
|
|
2890
|
+
return writeJson2(res, 201, { provider: toProviderView(provider) });
|
|
1926
2891
|
}
|
|
1927
2892
|
const id = rest[0];
|
|
1928
2893
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
1929
2894
|
const idx = cfg.providers.findIndex((p) => p.id === id);
|
|
1930
2895
|
if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
1931
2896
|
if (method === "PUT") {
|
|
1932
|
-
const body = await
|
|
2897
|
+
const body = await readJsonBody3(req);
|
|
1933
2898
|
const existing = cfg.providers[idx];
|
|
1934
2899
|
const updated = parseProviderInput(body, existing);
|
|
1935
2900
|
if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
|
|
1936
2901
|
cfg.providers[idx] = updated;
|
|
1937
2902
|
persistProviders(cfg, deps);
|
|
1938
|
-
return
|
|
2903
|
+
return writeJson2(res, 200, { provider: toProviderView(updated) });
|
|
1939
2904
|
}
|
|
1940
2905
|
if (method === "DELETE") {
|
|
1941
2906
|
cfg.providers.splice(idx, 1);
|
|
1942
2907
|
persistProviders(cfg, deps);
|
|
1943
|
-
return
|
|
2908
|
+
return writeJson2(res, 200, { ok: true });
|
|
1944
2909
|
}
|
|
1945
2910
|
return writeJsonError(res, 405, `method ${method} not allowed on providers`);
|
|
1946
2911
|
}
|
|
@@ -1949,7 +2914,7 @@ function persistProviders(cfg, deps) {
|
|
|
1949
2914
|
deps.llmConfig.reload(cfg);
|
|
1950
2915
|
}
|
|
1951
2916
|
async function handleProviderReorder(req, res, cfg, deps) {
|
|
1952
|
-
const body = await
|
|
2917
|
+
const body = await readJsonBody3(req);
|
|
1953
2918
|
const rawOrder = body["order"];
|
|
1954
2919
|
if (!Array.isArray(rawOrder)) {
|
|
1955
2920
|
return writeJsonError(res, 400, "reorder requires { order: string[] }");
|
|
@@ -1973,14 +2938,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
|
|
|
1973
2938
|
}
|
|
1974
2939
|
cfg.providers = reordered;
|
|
1975
2940
|
persistProviders(cfg, deps);
|
|
1976
|
-
return
|
|
2941
|
+
return writeJson2(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
|
|
1977
2942
|
}
|
|
1978
2943
|
async function handleDiscoverModels(res, id, cfg) {
|
|
1979
2944
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
1980
2945
|
const row = cfg.providers.find((p) => p.id === id);
|
|
1981
2946
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
1982
2947
|
if (row.apiFormat !== "openai") {
|
|
1983
|
-
return
|
|
2948
|
+
return writeJson2(res, 200, { models: [], unsupportedFormat: true });
|
|
1984
2949
|
}
|
|
1985
2950
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
1986
2951
|
const base = row.baseUrl.replace(/\/+$/, "");
|
|
@@ -1988,7 +2953,7 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
1988
2953
|
try {
|
|
1989
2954
|
const headers = { Accept: "application/json" };
|
|
1990
2955
|
if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
1991
|
-
const response = await
|
|
2956
|
+
const response = await (0, import_upstreamFetch2.fetchUpstream)(url, { method: "GET", headers }, { providerId: "byo" });
|
|
1992
2957
|
if (!response.ok) {
|
|
1993
2958
|
const text = await response.text().catch(() => "");
|
|
1994
2959
|
let message = text.slice(0, 300);
|
|
@@ -1997,32 +2962,32 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
1997
2962
|
message = parsed?.error?.message || parsed?.message || message;
|
|
1998
2963
|
} catch {
|
|
1999
2964
|
}
|
|
2000
|
-
return
|
|
2965
|
+
return writeJson2(res, 200, {
|
|
2001
2966
|
models: [],
|
|
2002
2967
|
error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
|
|
2003
2968
|
});
|
|
2004
2969
|
}
|
|
2005
2970
|
const data = await response.json();
|
|
2006
2971
|
const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
|
|
2007
|
-
return
|
|
2972
|
+
return writeJson2(res, 200, { models });
|
|
2008
2973
|
} catch (err5) {
|
|
2009
2974
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
2010
|
-
return
|
|
2975
|
+
return writeJson2(res, 200, { models: [], error: `discovery failed: ${message}` });
|
|
2011
2976
|
}
|
|
2012
2977
|
}
|
|
2013
2978
|
async function handleTestModel(req, res, id, cfg) {
|
|
2014
2979
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
2015
2980
|
const row = cfg.providers.find((p) => p.id === id);
|
|
2016
2981
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
2017
|
-
const body = await
|
|
2982
|
+
const body = await readJsonBody3(req);
|
|
2018
2983
|
const model = typeof body["model"] === "string" ? body["model"].trim() : "";
|
|
2019
2984
|
if (!model) return writeJsonError(res, 400, "test requires a { model } string");
|
|
2020
2985
|
if (row.apiFormat === "gemini") {
|
|
2021
|
-
return
|
|
2986
|
+
return writeJson2(res, 200, { ok: false, unsupportedFormat: true });
|
|
2022
2987
|
}
|
|
2023
2988
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
2024
2989
|
if (!resolvedKey) {
|
|
2025
|
-
return
|
|
2990
|
+
return writeJson2(res, 200, { ok: false, message: "no API key configured for this provider" });
|
|
2026
2991
|
}
|
|
2027
2992
|
const url = row.baseUrl.replace(/\/+$/, "");
|
|
2028
2993
|
const prompt = "Reply with the single word: OK.";
|
|
@@ -2043,11 +3008,11 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
2043
3008
|
}
|
|
2044
3009
|
const startedAt = Date.now();
|
|
2045
3010
|
try {
|
|
2046
|
-
const response = await
|
|
2047
|
-
|
|
2048
|
-
headers,
|
|
2049
|
-
|
|
2050
|
-
|
|
3011
|
+
const response = await (0, import_upstreamFetch2.fetchUpstream)(
|
|
3012
|
+
url,
|
|
3013
|
+
{ method: "POST", headers, body: JSON.stringify(payload) },
|
|
3014
|
+
{ providerId: "byo" }
|
|
3015
|
+
);
|
|
2051
3016
|
const latencyMs = Date.now() - startedAt;
|
|
2052
3017
|
const text = await response.text().catch(() => "");
|
|
2053
3018
|
if (!response.ok) {
|
|
@@ -2057,9 +3022,9 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
2057
3022
|
message = parsed?.error?.message || parsed?.message || message;
|
|
2058
3023
|
} catch {
|
|
2059
3024
|
}
|
|
2060
|
-
return
|
|
3025
|
+
return writeJson2(res, 200, { ok: false, status: response.status, latencyMs, message });
|
|
2061
3026
|
}
|
|
2062
|
-
return
|
|
3027
|
+
return writeJson2(res, 200, {
|
|
2063
3028
|
ok: true,
|
|
2064
3029
|
status: response.status,
|
|
2065
3030
|
latencyMs,
|
|
@@ -2067,7 +3032,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
2067
3032
|
});
|
|
2068
3033
|
} catch (err5) {
|
|
2069
3034
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
2070
|
-
return
|
|
3035
|
+
return writeJson2(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
|
|
2071
3036
|
}
|
|
2072
3037
|
}
|
|
2073
3038
|
function extractSampleText(text, apiFormat) {
|
|
@@ -2089,9 +3054,9 @@ function toPoolKeyView(row, cooldown, deps) {
|
|
|
2089
3054
|
return entries.map((e) => {
|
|
2090
3055
|
const auto = deps.autoDisableStore.get(e.id);
|
|
2091
3056
|
const cd = cooldown[e.id];
|
|
2092
|
-
const
|
|
2093
|
-
if (cd)
|
|
2094
|
-
if (auto)
|
|
3057
|
+
const health2 = {};
|
|
3058
|
+
if (cd) health2.cooldown = cd;
|
|
3059
|
+
if (auto) health2.autoDisabled = { status: auto.status, at: auto.at, reason: auto.reason };
|
|
2095
3060
|
return {
|
|
2096
3061
|
id: e.id,
|
|
2097
3062
|
label: e.label && e.label.length > 0 ? e.label : e.id,
|
|
@@ -2099,7 +3064,7 @@ function toPoolKeyView(row, cooldown, deps) {
|
|
|
2099
3064
|
enabled: e.enabled !== false && !deps.autoDisableStore.isDisabled(e.id),
|
|
2100
3065
|
weight: typeof e.weight === "number" && Number.isFinite(e.weight) ? e.weight : 1,
|
|
2101
3066
|
apiKeyMasked: maskProviderApiKey(e.apiKey),
|
|
2102
|
-
...Object.keys(
|
|
3067
|
+
...Object.keys(health2).length > 0 ? { health: health2 } : {}
|
|
2103
3068
|
};
|
|
2104
3069
|
});
|
|
2105
3070
|
}
|
|
@@ -2108,7 +3073,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
|
|
|
2108
3073
|
const row = cfg.providers.find((p) => p.id === id);
|
|
2109
3074
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
2110
3075
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
2111
|
-
return
|
|
3076
|
+
return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
2112
3077
|
}
|
|
2113
3078
|
function parsePoolKeyInput(body, existing) {
|
|
2114
3079
|
const out = {};
|
|
@@ -2127,7 +3092,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
|
|
|
2127
3092
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
2128
3093
|
const idx = cfg.providers.findIndex((p) => p.id === id);
|
|
2129
3094
|
if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
2130
|
-
const body = await
|
|
3095
|
+
const body = await readJsonBody3(req);
|
|
2131
3096
|
const parsed = parsePoolKeyInput(body);
|
|
2132
3097
|
if (!parsed.apiKey) return writeJsonError(res, 400, "add requires a non-empty apiKey");
|
|
2133
3098
|
const keyId = `key-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -2139,7 +3104,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
|
|
|
2139
3104
|
row.apiKeys = [...row.apiKeys ?? [], entry];
|
|
2140
3105
|
persistProviders(cfg, deps);
|
|
2141
3106
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
2142
|
-
return
|
|
3107
|
+
return writeJson2(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
2143
3108
|
}
|
|
2144
3109
|
async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
2145
3110
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -2149,7 +3114,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
2149
3114
|
const row = cfg.providers[idx];
|
|
2150
3115
|
const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
|
|
2151
3116
|
if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
|
|
2152
|
-
const body = await
|
|
3117
|
+
const body = await readJsonBody3(req);
|
|
2153
3118
|
const existing = row.apiKeys[keyIdx];
|
|
2154
3119
|
const parsed = parsePoolKeyInput(body, existing);
|
|
2155
3120
|
const entry = { id: keyId, apiKey: parsed.apiKey || existing.apiKey };
|
|
@@ -2159,7 +3124,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
2159
3124
|
row.apiKeys[keyIdx] = entry;
|
|
2160
3125
|
persistProviders(cfg, deps);
|
|
2161
3126
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
2162
|
-
return
|
|
3127
|
+
return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
2163
3128
|
}
|
|
2164
3129
|
async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
2165
3130
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -2173,7 +3138,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
|
2173
3138
|
if (row.apiKeys.length === 0) row.apiKeys = void 0;
|
|
2174
3139
|
persistProviders(cfg, deps);
|
|
2175
3140
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
2176
|
-
return
|
|
3141
|
+
return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
2177
3142
|
}
|
|
2178
3143
|
async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
2179
3144
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -2183,11 +3148,11 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
2183
3148
|
const row = cfg.providers[idx];
|
|
2184
3149
|
const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
|
|
2185
3150
|
if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
|
|
2186
|
-
const body = await
|
|
3151
|
+
const body = await readJsonBody3(req);
|
|
2187
3152
|
row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
|
|
2188
3153
|
persistProviders(cfg, deps);
|
|
2189
3154
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
2190
|
-
return
|
|
3155
|
+
return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
2191
3156
|
}
|
|
2192
3157
|
function parseApiKeysInput(raw, existing) {
|
|
2193
3158
|
if (!Array.isArray(raw)) return existing;
|
|
@@ -2358,18 +3323,31 @@ function handlePresets(res, method) {
|
|
|
2358
3323
|
baseUrl: p.baseUrl,
|
|
2359
3324
|
models: p.models
|
|
2360
3325
|
}));
|
|
2361
|
-
return
|
|
3326
|
+
return writeJson2(res, 200, { presets, excluded });
|
|
2362
3327
|
}
|
|
2363
3328
|
async function handleKeys(req, res, method, rest, deps) {
|
|
2364
3329
|
if (method === "GET" && rest.length === 0) {
|
|
2365
3330
|
const rows = await deps.keyDb.outboundApiKeysList();
|
|
2366
|
-
|
|
3331
|
+
const reader = deps.keySpendReader;
|
|
3332
|
+
if (!reader) return writeJson2(res, 200, { keys: rows.map(toKeyInfo) });
|
|
3333
|
+
const now = Date.now();
|
|
3334
|
+
const keys = await Promise.all(
|
|
3335
|
+
rows.map(async (row) => {
|
|
3336
|
+
const info = toKeyInfo(row);
|
|
3337
|
+
if (row.revokedAt === null) {
|
|
3338
|
+
const s = await reader.getSpend(row.id, now);
|
|
3339
|
+
info.spend = { dailyUsd: s.dailyUsd, weeklyUsd: s.weeklyUsd, totalUsd: s.totalUsd };
|
|
3340
|
+
}
|
|
3341
|
+
return info;
|
|
3342
|
+
})
|
|
3343
|
+
);
|
|
3344
|
+
return writeJson2(res, 200, { keys });
|
|
2367
3345
|
}
|
|
2368
3346
|
if (method === "POST" && rest.length === 0) {
|
|
2369
|
-
const body = await
|
|
3347
|
+
const body = await readJsonBody3(req);
|
|
2370
3348
|
const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
|
|
2371
|
-
const created = await (0,
|
|
2372
|
-
return
|
|
3349
|
+
const created = await (0, import_outbound_api2.createNamedKey)(deps.keyDb, name);
|
|
3350
|
+
return writeJson2(res, 201, {
|
|
2373
3351
|
id: created.id,
|
|
2374
3352
|
name: created.name,
|
|
2375
3353
|
keyPrefix: created.keyPrefix,
|
|
@@ -2381,46 +3359,181 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
2381
3359
|
const action = rest[1];
|
|
2382
3360
|
if (method === "POST" && id && action === "revoke") {
|
|
2383
3361
|
const ok = await deps.keyDb.outboundApiKeysRevoke(id);
|
|
2384
|
-
return
|
|
3362
|
+
return writeJson2(res, ok ? 200 : 404, { ok });
|
|
2385
3363
|
}
|
|
2386
3364
|
if (method === "POST" && id && action === "enabled") {
|
|
2387
|
-
const body = await
|
|
3365
|
+
const body = await readJsonBody3(req);
|
|
2388
3366
|
const enabled = body["enabled"] === true;
|
|
2389
3367
|
const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
|
|
2390
|
-
return
|
|
3368
|
+
return writeJson2(res, ok ? 200 : 404, { ok, enabled });
|
|
3369
|
+
}
|
|
3370
|
+
if (method === "POST" && id && action === "max-concurrency") {
|
|
3371
|
+
const body = await readJsonBody3(req);
|
|
3372
|
+
const raw = body["maxConcurrency"];
|
|
3373
|
+
let value;
|
|
3374
|
+
if (raw === null) {
|
|
3375
|
+
value = null;
|
|
3376
|
+
} else if (typeof raw === "number" && Number.isInteger(raw) && raw >= 1 && raw <= 1e3) {
|
|
3377
|
+
value = raw;
|
|
3378
|
+
} else {
|
|
3379
|
+
return writeJsonError(
|
|
3380
|
+
res,
|
|
3381
|
+
400,
|
|
3382
|
+
"maxConcurrency must be an integer 1..1000 or null"
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
|
|
3386
|
+
return writeJson2(res, ok ? 200 : 404, { ok, maxConcurrency: value });
|
|
3387
|
+
}
|
|
3388
|
+
if (method === "POST" && id && action === "policy") {
|
|
3389
|
+
const body = await readJsonBody3(req);
|
|
3390
|
+
const parsed = parseKeyPolicyBody(body);
|
|
3391
|
+
if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
|
|
3392
|
+
const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
|
|
3393
|
+
return writeJson2(res, ok ? 200 : 404, { ok });
|
|
2391
3394
|
}
|
|
2392
3395
|
return writeJsonError(res, 405, `method ${method} not allowed on keys`);
|
|
2393
3396
|
}
|
|
3397
|
+
function validateQueueSegments(patch) {
|
|
3398
|
+
const errors = [];
|
|
3399
|
+
const checkNum = (label, value, min, max) => {
|
|
3400
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
|
3401
|
+
errors.push(`${label} must be a number ${min}..${max}`);
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3404
|
+
const isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3405
|
+
const umq = patch.userMessageQueue;
|
|
3406
|
+
if (umq !== void 0) {
|
|
3407
|
+
if (!isPlainObject4(umq)) {
|
|
3408
|
+
errors.push("userMessageQueue must be an object");
|
|
3409
|
+
} else {
|
|
3410
|
+
if (typeof umq.enabled !== "boolean") {
|
|
3411
|
+
errors.push("userMessageQueue.enabled must be a boolean");
|
|
3412
|
+
}
|
|
3413
|
+
checkNum("userMessageQueue.delayMs", umq.delayMs, 0, 1e4);
|
|
3414
|
+
checkNum("userMessageQueue.waitTimeoutMs", umq.waitTimeoutMs, 1e3, 3e5);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
const cq = patch.concurrencyQueue;
|
|
3418
|
+
if (cq !== void 0) {
|
|
3419
|
+
if (!isPlainObject4(cq)) {
|
|
3420
|
+
errors.push("concurrencyQueue must be an object");
|
|
3421
|
+
} else {
|
|
3422
|
+
checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
|
|
3423
|
+
checkNum("concurrencyQueue.minQueueSize", cq.minQueueSize, 1, 100);
|
|
3424
|
+
checkNum("concurrencyQueue.waitTimeoutMs", cq.waitTimeoutMs, 1e3, 3e5);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
const ah = patch.accountHealth;
|
|
3428
|
+
if (ah !== void 0) {
|
|
3429
|
+
if (!isPlainObject4(ah)) {
|
|
3430
|
+
errors.push("accountHealth must be an object");
|
|
3431
|
+
} else {
|
|
3432
|
+
if (typeof ah.overloadCooldownEnabled !== "boolean") {
|
|
3433
|
+
errors.push("accountHealth.overloadCooldownEnabled must be a boolean");
|
|
3434
|
+
}
|
|
3435
|
+
checkNum("accountHealth.overloadCooldownMs", ah.overloadCooldownMs, 6e4, 36e5);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
return errors;
|
|
3439
|
+
}
|
|
2394
3440
|
async function handleServer(req, res, method, deps) {
|
|
2395
3441
|
if (method === "GET") {
|
|
2396
|
-
const config = await (0,
|
|
2397
|
-
|
|
3442
|
+
const config = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
|
|
3443
|
+
let server = config;
|
|
3444
|
+
if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
|
|
3445
|
+
if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
|
|
3446
|
+
if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
|
|
3447
|
+
return writeJson2(res, 200, { server });
|
|
2398
3448
|
}
|
|
2399
3449
|
if (method === "PUT") {
|
|
2400
|
-
const patch = await
|
|
2401
|
-
const
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
3450
|
+
const patch = await readJsonBody3(req);
|
|
3451
|
+
const queueErrors = validateQueueSegments(patch);
|
|
3452
|
+
if (queueErrors.length > 0) {
|
|
3453
|
+
return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
|
|
3454
|
+
}
|
|
3455
|
+
const webhookErrors = validateWebhookSegment(patch);
|
|
3456
|
+
if (webhookErrors.length > 0) {
|
|
3457
|
+
return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
|
|
3458
|
+
}
|
|
3459
|
+
const auditErrors = validateAuditSegment(patch);
|
|
3460
|
+
if (auditErrors.length > 0) {
|
|
3461
|
+
return writeJsonError(res, 400, `invalid audit config: ${auditErrors.join("; ")}`);
|
|
3462
|
+
}
|
|
3463
|
+
const billingErrors = validateBillingSegment(patch);
|
|
3464
|
+
if (billingErrors.length > 0) {
|
|
3465
|
+
return writeJsonError(res, 400, `invalid billing config: ${billingErrors.join("; ")}`);
|
|
3466
|
+
}
|
|
3467
|
+
const current = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
|
|
3468
|
+
let effectivePatch = patch;
|
|
3469
|
+
if (patch.proxy) {
|
|
3470
|
+
effectivePatch = { ...effectivePatch, proxy: preserveOutboundProxySecrets(patch.proxy, current.proxy) };
|
|
3471
|
+
}
|
|
3472
|
+
if (patch.webhook) {
|
|
3473
|
+
effectivePatch = { ...effectivePatch, webhook: preserveWebhookSecrets(patch.webhook, current.webhook) };
|
|
3474
|
+
}
|
|
3475
|
+
if (patch.billing) {
|
|
3476
|
+
effectivePatch = { ...effectivePatch, billing: preserveBillingSecret(patch.billing, current.billing) };
|
|
3477
|
+
}
|
|
3478
|
+
const merged = (0, import_outbound_api2.mergeServerConfig)(current, effectivePatch);
|
|
3479
|
+
await (0, import_outbound_api2.saveServerConfig)(deps.settingsStore, merged);
|
|
3480
|
+
setServerProxyConfig(merged.proxy);
|
|
3481
|
+
applyWebhookConfig(merged.webhook);
|
|
3482
|
+
applyAuditConfig(merged.audit);
|
|
3483
|
+
applyBillingConfig(merged.billing);
|
|
3484
|
+
if (merged.enabled) {
|
|
3485
|
+
const missing = (0, import_outbound_api2.validateServerModelConfig)(merged);
|
|
3486
|
+
if (missing.length > 0) {
|
|
3487
|
+
if (deps.outboundApiServer.getStatus().running) {
|
|
3488
|
+
await deps.outboundApiServer.stop();
|
|
3489
|
+
}
|
|
3490
|
+
return writeJson2(res, 200, {
|
|
3491
|
+
server: merged,
|
|
3492
|
+
error: { code: "incomplete-model-config", missing }
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
try {
|
|
3497
|
+
await deps.outboundApiServer.applyConfig({
|
|
3498
|
+
enabled: merged.enabled,
|
|
3499
|
+
networkBinding: merged.networkBinding,
|
|
3500
|
+
endpoints: merged.endpoints,
|
|
3501
|
+
port: merged.port,
|
|
3502
|
+
userMessageQueue: merged.userMessageQueue,
|
|
3503
|
+
concurrencyQueue: merged.concurrencyQueue,
|
|
3504
|
+
// voucher-redemption #9: hot-apply the voucher flag so enabling the product
|
|
3505
|
+
// takes effect without a restart.
|
|
3506
|
+
voucher: merged.voucher
|
|
3507
|
+
});
|
|
3508
|
+
} catch (err5) {
|
|
3509
|
+
const missing = incompleteConfigMissing(err5);
|
|
3510
|
+
if (missing) {
|
|
3511
|
+
return writeJson2(res, 200, {
|
|
3512
|
+
server: merged,
|
|
3513
|
+
error: { code: "incomplete-model-config", missing }
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
throw err5;
|
|
3517
|
+
}
|
|
3518
|
+
return writeJson2(res, 200, { server: merged });
|
|
2411
3519
|
}
|
|
2412
3520
|
return writeJsonError(res, 405, `method ${method} not allowed on server`);
|
|
2413
3521
|
}
|
|
3522
|
+
function incompleteConfigMissing(err5) {
|
|
3523
|
+
if (typeof err5 !== "object" || err5 === null) return null;
|
|
3524
|
+
const missing = err5.missing;
|
|
3525
|
+
return Array.isArray(missing) ? missing : null;
|
|
3526
|
+
}
|
|
2414
3527
|
async function handleAccounts(req, res, method, rest, deps) {
|
|
2415
3528
|
if (method === "GET" && rest.length === 0) {
|
|
2416
3529
|
const accounts = await deps.subscriptionAccounts.listAll();
|
|
2417
3530
|
const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
2418
3531
|
const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
|
|
2419
|
-
return
|
|
3532
|
+
return writeJson2(res, 200, { accounts, providerAccounts, externalCli });
|
|
2420
3533
|
}
|
|
2421
3534
|
if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
|
|
2422
3535
|
const result = handleCodexOAuthStatus(rest[2], deps);
|
|
2423
|
-
return
|
|
3536
|
+
return writeJson2(res, result.status, result.body);
|
|
2424
3537
|
}
|
|
2425
3538
|
if (method === "PUT" || method === "POST" || method === "DELETE") {
|
|
2426
3539
|
const providerId = asSubscriptionProviderId(rest[0]);
|
|
@@ -2429,15 +3542,15 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
2429
3542
|
}
|
|
2430
3543
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
|
|
2431
3544
|
const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
|
|
2432
|
-
return
|
|
3545
|
+
return writeJson2(res, result.status, result.body);
|
|
2433
3546
|
}
|
|
2434
3547
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
|
|
2435
|
-
const body2 = await
|
|
3548
|
+
const body2 = await readJsonBody3(req);
|
|
2436
3549
|
const result = await handleOAuthComplete(providerId, body2, deps);
|
|
2437
|
-
return
|
|
3550
|
+
return writeJson2(res, result.status, result.body);
|
|
2438
3551
|
}
|
|
2439
3552
|
if (method === "POST" && rest[1] === "accounts") {
|
|
2440
|
-
const body2 = await
|
|
3553
|
+
const body2 = await readJsonBody3(req);
|
|
2441
3554
|
const block = validateTokenBody(providerId, body2);
|
|
2442
3555
|
if (!block) {
|
|
2443
3556
|
return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
|
|
@@ -2445,79 +3558,113 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
2445
3558
|
const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
|
|
2446
3559
|
await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
|
|
2447
3560
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
2448
|
-
return
|
|
3561
|
+
return writeJson2(res, 200, status2 ? { account: status2 } : { ok: true });
|
|
2449
3562
|
}
|
|
2450
3563
|
if (method === "POST" && rest[1] === "import-external") {
|
|
2451
3564
|
if (providerId !== "claude" && providerId !== "codex") {
|
|
2452
3565
|
return writeJsonError(res, 400, `provider '${providerId}' has no external CLI store`);
|
|
2453
3566
|
}
|
|
2454
|
-
const body2 = await
|
|
3567
|
+
const body2 = await readJsonBody3(req);
|
|
2455
3568
|
const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
|
|
2456
3569
|
const result = await deps.subscriptionTokenWriter.importExternalCliAccount(providerId, label);
|
|
2457
3570
|
if (!result.ok) {
|
|
2458
3571
|
return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
|
|
2459
3572
|
}
|
|
2460
3573
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
2461
|
-
return
|
|
3574
|
+
return writeJson2(res, 200, { ok: true, account: status2 ?? void 0 });
|
|
2462
3575
|
}
|
|
2463
3576
|
if (method === "POST" && rest[1] === "refresh") {
|
|
2464
3577
|
if (providerId === "opencodego") {
|
|
2465
3578
|
return writeJsonError(res, 400, "opencodego credentials are not refreshable");
|
|
2466
3579
|
}
|
|
2467
|
-
const
|
|
2468
|
-
const ok = providerId === "claude" ? await
|
|
3580
|
+
const writer2 = deps.subscriptionTokenWriter;
|
|
3581
|
+
const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
|
|
2469
3582
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
2470
|
-
return
|
|
3583
|
+
return writeJson2(res, 200, { ok, account: status2 ?? void 0 });
|
|
2471
3584
|
}
|
|
2472
3585
|
if (method === "POST" && rest[2] === "label") {
|
|
2473
3586
|
const accountId = rest[1];
|
|
2474
|
-
const body2 = await
|
|
3587
|
+
const body2 = await readJsonBody3(req);
|
|
2475
3588
|
const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
|
|
2476
3589
|
const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
|
|
2477
3590
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
2478
|
-
return
|
|
3591
|
+
return writeJson2(res, 200, { ok: true });
|
|
3592
|
+
}
|
|
3593
|
+
if (method === "POST" && rest[2] === "priority") {
|
|
3594
|
+
const accountId = rest[1];
|
|
3595
|
+
const body2 = await readJsonBody3(req);
|
|
3596
|
+
const raw = body2["priority"];
|
|
3597
|
+
const priority = typeof raw === "number" ? raw : Number(raw);
|
|
3598
|
+
if (!Number.isFinite(priority)) {
|
|
3599
|
+
return writeJsonError(res, 400, "priority must be a finite number");
|
|
3600
|
+
}
|
|
3601
|
+
const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
|
|
3602
|
+
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3603
|
+
return writeJson2(res, 200, { ok: true });
|
|
3604
|
+
}
|
|
3605
|
+
if (method === "POST" && rest[2] === "proxy") {
|
|
3606
|
+
const accountId = rest[1];
|
|
3607
|
+
const body2 = await readJsonBody3(req);
|
|
3608
|
+
const rawProxy = body2["proxy"];
|
|
3609
|
+
let proxy;
|
|
3610
|
+
if (rawProxy !== null && rawProxy !== void 0) {
|
|
3611
|
+
proxy = (0, import_outbound_api2.normalizeProxyConfig)(rawProxy);
|
|
3612
|
+
if (!proxy) return writeJsonError(res, 400, "invalid proxy config");
|
|
3613
|
+
}
|
|
3614
|
+
const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
|
|
3615
|
+
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3616
|
+
return writeJson2(res, 200, { ok: true });
|
|
3617
|
+
}
|
|
3618
|
+
if (method === "POST" && rest[2] === "supported-models") {
|
|
3619
|
+
const accountId = rest[1];
|
|
3620
|
+
const body2 = await readJsonBody3(req);
|
|
3621
|
+
const parsed = validateSupportedModelsBody(body2["supportedModels"]);
|
|
3622
|
+
if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
|
|
3623
|
+
const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
|
|
3624
|
+
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3625
|
+
return writeJson2(res, 200, { ok: true });
|
|
2479
3626
|
}
|
|
2480
3627
|
if (method === "PUT" && rest[1] === "active") {
|
|
2481
|
-
const body2 = await
|
|
3628
|
+
const body2 = await readJsonBody3(req);
|
|
2482
3629
|
const id = typeof body2["id"] === "string" ? body2["id"] : "";
|
|
2483
3630
|
if (!id) return writeJsonError(res, 400, "active switch requires { id }");
|
|
2484
3631
|
const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
|
|
2485
3632
|
if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
|
|
2486
|
-
return
|
|
3633
|
+
return writeJson2(res, 200, { ok: true });
|
|
2487
3634
|
}
|
|
2488
3635
|
if (method === "DELETE" && rest.length >= 2) {
|
|
2489
3636
|
const accountId = rest[1];
|
|
2490
3637
|
const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
|
|
2491
3638
|
if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
2492
|
-
return
|
|
3639
|
+
return writeJson2(res, 200, { ok: true });
|
|
2493
3640
|
}
|
|
2494
3641
|
if (method === "DELETE") {
|
|
2495
3642
|
await deps.subscriptionTokenWriter.clearProvider(providerId);
|
|
2496
|
-
return
|
|
3643
|
+
return writeJson2(res, 200, { ok: true });
|
|
2497
3644
|
}
|
|
2498
|
-
const body = await
|
|
3645
|
+
const body = await readJsonBody3(req);
|
|
2499
3646
|
const config = validateTokenBody(providerId, body);
|
|
2500
3647
|
if (!config) {
|
|
2501
3648
|
return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
|
|
2502
3649
|
}
|
|
2503
3650
|
await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
|
|
2504
3651
|
const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
2505
|
-
return
|
|
3652
|
+
return writeJson2(res, 200, status ? { account: status } : { ok: true });
|
|
2506
3653
|
}
|
|
2507
3654
|
return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
|
|
2508
3655
|
}
|
|
2509
3656
|
async function handleCli(req, res, method, rest, deps) {
|
|
2510
3657
|
if (method === "GET" && rest.length === 0) {
|
|
2511
3658
|
const result = handleCliList(process.platform, deps.cliPathProbe);
|
|
2512
|
-
return
|
|
3659
|
+
return writeJson2(res, result.status, result.body);
|
|
2513
3660
|
}
|
|
2514
3661
|
if (method === "GET" && rest[0] === "sessions") {
|
|
2515
3662
|
const result = handleCliSessions();
|
|
2516
|
-
return
|
|
3663
|
+
return writeJson2(res, result.status, result.body);
|
|
2517
3664
|
}
|
|
2518
3665
|
if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
|
|
2519
3666
|
const result = handleCliStop(rest[1]);
|
|
2520
|
-
return
|
|
3667
|
+
return writeJson2(res, result.status, result.body);
|
|
2521
3668
|
}
|
|
2522
3669
|
if (method === "POST" && rest[1] === "install") {
|
|
2523
3670
|
const cli = rest[0];
|
|
@@ -2525,14 +3672,14 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
2525
3672
|
return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
|
|
2526
3673
|
}
|
|
2527
3674
|
const result = await handleCliInstall(cli, deps.cliCommandRunner);
|
|
2528
|
-
return
|
|
3675
|
+
return writeJson2(res, result.status, result.body);
|
|
2529
3676
|
}
|
|
2530
3677
|
if (method === "POST" && rest[1] === "launch") {
|
|
2531
3678
|
const cli = rest[0];
|
|
2532
3679
|
if (!isLaunchCliId(cli)) {
|
|
2533
3680
|
return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
|
|
2534
3681
|
}
|
|
2535
|
-
const body = await
|
|
3682
|
+
const body = await readJsonBody3(req);
|
|
2536
3683
|
const providers = loadConfig(deps.configPath).providers ?? [];
|
|
2537
3684
|
const result = await handleCliLaunch(cli, body, {
|
|
2538
3685
|
llmConfig: deps.llmConfig,
|
|
@@ -2540,20 +3687,28 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
2540
3687
|
opener: deps.cliTerminalOpener,
|
|
2541
3688
|
probe: deps.cliPathProbe
|
|
2542
3689
|
});
|
|
2543
|
-
return
|
|
3690
|
+
return writeJson2(res, result.status, result.body);
|
|
2544
3691
|
}
|
|
2545
3692
|
return writeJsonError(res, 405, `method ${method} not allowed on cli`);
|
|
2546
3693
|
}
|
|
2547
3694
|
async function handleStatus(res, method, deps) {
|
|
2548
3695
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
|
|
2549
3696
|
const status = deps.outboundApiServer.getStatus();
|
|
2550
|
-
const serverConfig = await (0,
|
|
2551
|
-
const endpoints = serverConfig.endpoints.map((e) =>
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
3697
|
+
const serverConfig = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
|
|
3698
|
+
const endpoints = serverConfig.endpoints.map((e) => {
|
|
3699
|
+
if ((0, import_outbound_api2.isKindMappedEndpoint)(e.endpoint)) {
|
|
3700
|
+
return { endpoint: e.endpoint, kinds: e.modelMap ?? {}, useSubscription: e.useSubscription };
|
|
3701
|
+
}
|
|
3702
|
+
if (e.endpoint === "chat") {
|
|
3703
|
+
return { endpoint: e.endpoint, models: e.models ?? [], useSubscription: e.useSubscription };
|
|
3704
|
+
}
|
|
3705
|
+
return { endpoint: e.endpoint, model: e.defaultModel ?? "", useSubscription: e.useSubscription };
|
|
3706
|
+
});
|
|
3707
|
+
if (status.running) {
|
|
3708
|
+
const queueStatus = deps.outboundApiServer.getQueueStatus();
|
|
3709
|
+
return writeJson2(res, 200, { ...status, endpoints, queueStatus });
|
|
3710
|
+
}
|
|
3711
|
+
return writeJson2(res, 200, { ...status, endpoints });
|
|
2557
3712
|
}
|
|
2558
3713
|
function resolvePlaygroundPath(endpoint, body) {
|
|
2559
3714
|
switch (endpoint) {
|
|
@@ -2573,7 +3728,7 @@ function resolvePlaygroundPath(endpoint, body) {
|
|
|
2573
3728
|
}
|
|
2574
3729
|
async function handlePlayground(req, res, method, deps) {
|
|
2575
3730
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on playground`);
|
|
2576
|
-
const body = await
|
|
3731
|
+
const body = await readJsonBody3(req);
|
|
2577
3732
|
const endpoint = typeof body["endpoint"] === "string" ? body["endpoint"] : "";
|
|
2578
3733
|
const key = typeof body["key"] === "string" ? body["key"] : "";
|
|
2579
3734
|
const payload = body["body"];
|
|
@@ -2719,10 +3874,12 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
2719
3874
|
return true;
|
|
2720
3875
|
}
|
|
2721
3876
|
|
|
3877
|
+
// src/admin/version.ts
|
|
3878
|
+
var DAEMON_VERSION = true ? "0.1.3" : "0.0.0-dev";
|
|
3879
|
+
|
|
2722
3880
|
// src/admin/AdminServer.ts
|
|
2723
3881
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
2724
3882
|
var LAN_ADDR = "0.0.0.0";
|
|
2725
|
-
var DAEMON_VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
2726
3883
|
var AdminServer = class {
|
|
2727
3884
|
constructor(deps) {
|
|
2728
3885
|
this.deps = deps;
|
|
@@ -2743,7 +3900,7 @@ var AdminServer = class {
|
|
|
2743
3900
|
const cfg = this.deps.getAdminConfig();
|
|
2744
3901
|
if (!cfg.enabled) return 0;
|
|
2745
3902
|
if (cfg.networkBinding && !cfg.token) {
|
|
2746
|
-
|
|
3903
|
+
this.deps.logger.error(
|
|
2747
3904
|
"[AdminServer] REFUSING to bind: admin.networkBinding (LAN/0.0.0.0) requires a non-empty admin.token. Set admin.token in config.json or disable networkBinding. Dashboard stays DOWN (fail closed)."
|
|
2748
3905
|
);
|
|
2749
3906
|
return 0;
|
|
@@ -2752,7 +3909,7 @@ var AdminServer = class {
|
|
|
2752
3909
|
const actualPort = await this.listen(bindAddr, cfg.port);
|
|
2753
3910
|
this.boundAddr = bindAddr;
|
|
2754
3911
|
this.boundPort = actualPort;
|
|
2755
|
-
|
|
3912
|
+
this.deps.logger.info(`[AdminServer] Dashboard listening on ${bindAddr}:${actualPort}`);
|
|
2756
3913
|
return actualPort;
|
|
2757
3914
|
}
|
|
2758
3915
|
/** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
|
|
@@ -2774,7 +3931,7 @@ var AdminServer = class {
|
|
|
2774
3931
|
const addr = server.address();
|
|
2775
3932
|
if (addr && typeof addr === "object") {
|
|
2776
3933
|
server.removeListener("error", onError);
|
|
2777
|
-
server.on("error", (e) =>
|
|
3934
|
+
server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
|
|
2778
3935
|
this.server = server;
|
|
2779
3936
|
resolve(addr.port);
|
|
2780
3937
|
} else {
|
|
@@ -2787,7 +3944,7 @@ var AdminServer = class {
|
|
|
2787
3944
|
onRequest(req, res) {
|
|
2788
3945
|
void this.dispatch(req, res).catch((err5) => {
|
|
2789
3946
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
2790
|
-
|
|
3947
|
+
this.deps.logger.error("[AdminServer] unhandled error:", message);
|
|
2791
3948
|
if (!res.headersSent) {
|
|
2792
3949
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2793
3950
|
res.end(JSON.stringify({ error: { type: "admin_error", message } }));
|
|
@@ -2798,18 +3955,42 @@ var AdminServer = class {
|
|
|
2798
3955
|
const cfg = this.deps.getAdminConfig();
|
|
2799
3956
|
res.setHeader("x-omnicross-daemon", DAEMON_VERSION);
|
|
2800
3957
|
res.setHeader("x-omnicross-pid", String(process.pid));
|
|
3958
|
+
const url = req.url ?? "/";
|
|
3959
|
+
const path2 = url.split("?")[0];
|
|
3960
|
+
const healthPath = path2.replace(/\/+$/, "") || "/";
|
|
3961
|
+
if ((req.method === "GET" || req.method === "HEAD") && (healthPath === "/health" || healthPath === "/healthz")) {
|
|
3962
|
+
const report = this.deps.getHealthReport();
|
|
3963
|
+
const code = (0, import_health_logging_types.healthHttpStatus)(report.status);
|
|
3964
|
+
res.writeHead(code, { "Content-Type": "application/json" });
|
|
3965
|
+
res.end(req.method === "HEAD" ? void 0 : JSON.stringify(report));
|
|
3966
|
+
return;
|
|
3967
|
+
}
|
|
2801
3968
|
if (cfg.token && !this.isAuthorized(req, cfg.token)) {
|
|
2802
3969
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
2803
3970
|
res.end(JSON.stringify({ error: { type: "unauthorized", message: "admin token required" } }));
|
|
2804
3971
|
return;
|
|
2805
3972
|
}
|
|
2806
|
-
const url = req.url ?? "/";
|
|
2807
|
-
const path2 = url.split("?")[0];
|
|
2808
3973
|
if ((req.method === "GET" || req.method === "HEAD") && (path2 === "/" || path2 === "/admin")) {
|
|
2809
3974
|
res.writeHead(302, { Location: "/ui/" });
|
|
2810
3975
|
res.end();
|
|
2811
3976
|
return;
|
|
2812
3977
|
}
|
|
3978
|
+
if (path2 === "/admin/api/account-probes" && (req.method === "GET" || req.method === "HEAD")) {
|
|
3979
|
+
handleAccountProbes(res, this.deps.probeHistoryReader);
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
if (path2 === "/admin/api/audit" && (req.method === "GET" || req.method === "HEAD")) {
|
|
3983
|
+
handleAuditQuery(req, res, this.deps.auditReader);
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
if (path2 === "/admin/api/billing-status" && (req.method === "GET" || req.method === "HEAD")) {
|
|
3987
|
+
handleBillingStatus(res, this.deps.billingStatusReader);
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
if (path2 === "/admin/api/webhook-test" && req.method === "POST") {
|
|
3991
|
+
await handleWebhookTest(req, res);
|
|
3992
|
+
return;
|
|
3993
|
+
}
|
|
2813
3994
|
if (path2.startsWith("/admin/api/")) {
|
|
2814
3995
|
await handleAdminApi(req, res, path2, this.deps);
|
|
2815
3996
|
return;
|
|
@@ -2853,6 +4034,51 @@ function constantTimeEquals(a, b) {
|
|
|
2853
4034
|
return (0, import_node_crypto7.timingSafeEqual)(bufA, bufB);
|
|
2854
4035
|
}
|
|
2855
4036
|
|
|
4037
|
+
// src/admin/health.ts
|
|
4038
|
+
var CRITICAL_CHECKS = ["config", "credentialStore"];
|
|
4039
|
+
var READINESS_CHECKS = ["outboundServer"];
|
|
4040
|
+
function safeBool(fn) {
|
|
4041
|
+
try {
|
|
4042
|
+
return fn() === true;
|
|
4043
|
+
} catch {
|
|
4044
|
+
return false;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
function toMb(bytes) {
|
|
4048
|
+
return Math.round(bytes / (1024 * 1024) * 10) / 10;
|
|
4049
|
+
}
|
|
4050
|
+
function buildHealthReport(deps) {
|
|
4051
|
+
const checks = {
|
|
4052
|
+
config: safeBool(deps.configPresent),
|
|
4053
|
+
credentialStore: safeBool(deps.credentialStoreReadable),
|
|
4054
|
+
outboundServer: safeBool(deps.outboundServerRunning),
|
|
4055
|
+
adminServer: safeBool(deps.adminServerRunning)
|
|
4056
|
+
};
|
|
4057
|
+
if (deps.subscriptionAccountsHealthy) {
|
|
4058
|
+
let probeHealthy;
|
|
4059
|
+
try {
|
|
4060
|
+
probeHealthy = deps.subscriptionAccountsHealthy();
|
|
4061
|
+
} catch {
|
|
4062
|
+
probeHealthy = false;
|
|
4063
|
+
}
|
|
4064
|
+
if (probeHealthy !== void 0) checks.subscriptionAccountsHealthy = probeHealthy;
|
|
4065
|
+
}
|
|
4066
|
+
const criticalOk = CRITICAL_CHECKS.every((k) => checks[k]);
|
|
4067
|
+
const readinessOk = READINESS_CHECKS.every((k) => checks[k]);
|
|
4068
|
+
const status = !criticalOk ? "error" : readinessOk ? "ok" : "degraded";
|
|
4069
|
+
const mem = (deps.memoryUsage ?? process.memoryUsage)();
|
|
4070
|
+
const uptime = (deps.uptimeSeconds ?? process.uptime)();
|
|
4071
|
+
const nowMs = (deps.now ?? Date.now)();
|
|
4072
|
+
return {
|
|
4073
|
+
status,
|
|
4074
|
+
version: deps.version,
|
|
4075
|
+
uptimeSeconds: Math.floor(uptime),
|
|
4076
|
+
timestamp: new Date(nowMs).toISOString(),
|
|
4077
|
+
memory: { rssMb: toMb(mem.rss), heapUsedMb: toMb(mem.heapUsed) },
|
|
4078
|
+
checks
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
|
|
2856
4082
|
// src/admin/oauthSessions.ts
|
|
2857
4083
|
var import_node_crypto8 = __toESM(require("crypto"), 1);
|
|
2858
4084
|
var DEFAULT_OAUTH_SESSION_TTL_MS = 10 * 60 * 1e3;
|
|
@@ -3026,12 +4252,21 @@ function createPoolKeysLoader(getProviderRow, autoDisabled) {
|
|
|
3026
4252
|
|
|
3027
4253
|
// src/commands/paths.ts
|
|
3028
4254
|
var import_node_path4 = require("path");
|
|
4255
|
+
function defaultVouchersPath(configPath) {
|
|
4256
|
+
return (0, import_node_path4.join)((0, import_node_path4.dirname)(configPath), "vouchers.json");
|
|
4257
|
+
}
|
|
3029
4258
|
function defaultPricingPath(configPath) {
|
|
3030
4259
|
return (0, import_node_path4.join)((0, import_node_path4.dirname)(configPath), "pricing.json");
|
|
3031
4260
|
}
|
|
3032
4261
|
function defaultUsageEventsPath(configPath) {
|
|
3033
4262
|
return (0, import_node_path4.join)((0, import_node_path4.dirname)(configPath), "usage-events.jsonl");
|
|
3034
4263
|
}
|
|
4264
|
+
function defaultAuditDir(configPath) {
|
|
4265
|
+
return (0, import_node_path4.join)((0, import_node_path4.dirname)(configPath), "audit");
|
|
4266
|
+
}
|
|
4267
|
+
function defaultBillingDir(configPath) {
|
|
4268
|
+
return (0, import_node_path4.join)((0, import_node_path4.dirname)(configPath), "billing");
|
|
4269
|
+
}
|
|
3035
4270
|
|
|
3036
4271
|
// src/ports/ConfigFileProviderConfigSource.ts
|
|
3037
4272
|
var import_core = require("@omnicross/core");
|
|
@@ -3190,50 +4425,203 @@ function toLLMProvider(row) {
|
|
|
3190
4425
|
};
|
|
3191
4426
|
}
|
|
3192
4427
|
|
|
3193
|
-
// src/ports/
|
|
3194
|
-
var
|
|
4428
|
+
// src/ports/ConfigurableLogger.ts
|
|
4429
|
+
var import_node_fs5 = require("fs");
|
|
4430
|
+
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
4431
|
+
var RESERVED_JSON_KEYS = /* @__PURE__ */ new Set(["ts", "level", "msg", "error"]);
|
|
4432
|
+
var ConfigurableLogger = class {
|
|
4433
|
+
threshold;
|
|
4434
|
+
format;
|
|
4435
|
+
filePath;
|
|
4436
|
+
fileStream = null;
|
|
4437
|
+
fileDisabled = false;
|
|
4438
|
+
constructor(cfg) {
|
|
4439
|
+
this.threshold = LEVEL_ORDER[cfg?.level ?? "debug"];
|
|
4440
|
+
this.format = cfg?.format ?? "text";
|
|
4441
|
+
this.filePath = cfg?.file && cfg.file.length > 0 ? cfg.file : void 0;
|
|
4442
|
+
}
|
|
3195
4443
|
info(message, meta) {
|
|
3196
|
-
|
|
3197
|
-
else console.info(message, meta);
|
|
4444
|
+
this.emit("info", message, void 0, meta);
|
|
3198
4445
|
}
|
|
3199
4446
|
warn(message, meta) {
|
|
3200
|
-
|
|
3201
|
-
else console.warn(message, meta);
|
|
4447
|
+
this.emit("warn", message, void 0, meta);
|
|
3202
4448
|
}
|
|
3203
4449
|
error(message, error, meta) {
|
|
3204
|
-
|
|
3205
|
-
else if (meta === void 0) console.error(message, error);
|
|
3206
|
-
else console.error(message, error, meta);
|
|
4450
|
+
this.emit("error", message, error, meta);
|
|
3207
4451
|
}
|
|
3208
4452
|
debug(message, meta) {
|
|
3209
|
-
|
|
3210
|
-
|
|
4453
|
+
this.emit("debug", message, void 0, meta);
|
|
4454
|
+
}
|
|
4455
|
+
/**
|
|
4456
|
+
* Flush + close the file sink (tests / graceful shutdown). Resolves once the
|
|
4457
|
+
* append stream has finished flushing to disk. No-op when no file sink is open.
|
|
4458
|
+
*/
|
|
4459
|
+
close() {
|
|
4460
|
+
const stream = this.fileStream;
|
|
4461
|
+
this.fileStream = null;
|
|
4462
|
+
if (!stream) return Promise.resolve();
|
|
4463
|
+
return new Promise((resolve) => stream.end(() => resolve()));
|
|
4464
|
+
}
|
|
4465
|
+
emit(level, message, error, meta) {
|
|
4466
|
+
if (LEVEL_ORDER[level] > this.threshold) return;
|
|
4467
|
+
this.writeConsole(level, message, error, meta);
|
|
4468
|
+
if (this.filePath) this.writeFile(level, message, error, meta);
|
|
4469
|
+
}
|
|
4470
|
+
/**
|
|
4471
|
+
* Console sink. In `text` format this reproduces the legacy `ConsoleLogger`
|
|
4472
|
+
* EXACTLY (same method + arg shape) so the unconfigured default is a byte-for-
|
|
4473
|
+
* byte drop-in; in `json` format it prints the structured line.
|
|
4474
|
+
*/
|
|
4475
|
+
writeConsole(level, message, error, meta) {
|
|
4476
|
+
if (this.format === "json") {
|
|
4477
|
+
this.consoleFn(level)(this.jsonLine(level, message, error, meta));
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
if (level === "error") {
|
|
4481
|
+
if (error === void 0 && meta === void 0) console.error(message);
|
|
4482
|
+
else if (meta === void 0) console.error(message, error);
|
|
4483
|
+
else console.error(message, error, meta);
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
const fn = this.consoleFn(level);
|
|
4487
|
+
if (meta === void 0) fn(message);
|
|
4488
|
+
else fn(message, meta);
|
|
4489
|
+
}
|
|
4490
|
+
/** Append one line to the file sink; a failure disables the sink (swallowed). */
|
|
4491
|
+
writeFile(level, message, error, meta) {
|
|
4492
|
+
const stream = this.getFileStream();
|
|
4493
|
+
if (!stream) return;
|
|
4494
|
+
try {
|
|
4495
|
+
const line = this.format === "json" ? this.jsonLine(level, message, error, meta) : this.textLine(level, message, error, meta);
|
|
4496
|
+
stream.write(line + "\n");
|
|
4497
|
+
} catch {
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
/** Lazily open the append-only file stream; disable the sink on any error. */
|
|
4501
|
+
getFileStream() {
|
|
4502
|
+
if (this.fileDisabled || !this.filePath) return null;
|
|
4503
|
+
if (this.fileStream) return this.fileStream;
|
|
4504
|
+
try {
|
|
4505
|
+
const stream = (0, import_node_fs5.createWriteStream)(this.filePath, { flags: "a" });
|
|
4506
|
+
stream.on("error", () => {
|
|
4507
|
+
this.fileDisabled = true;
|
|
4508
|
+
this.fileStream = null;
|
|
4509
|
+
});
|
|
4510
|
+
this.fileStream = stream;
|
|
4511
|
+
return stream;
|
|
4512
|
+
} catch {
|
|
4513
|
+
this.fileDisabled = true;
|
|
4514
|
+
return null;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
consoleFn(level) {
|
|
4518
|
+
switch (level) {
|
|
4519
|
+
case "error":
|
|
4520
|
+
return console.error;
|
|
4521
|
+
case "warn":
|
|
4522
|
+
return console.warn;
|
|
4523
|
+
case "info":
|
|
4524
|
+
return console.info;
|
|
4525
|
+
case "debug":
|
|
4526
|
+
return console.debug;
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
/** `{ ts, level, msg, ...meta }` (+ `error` when present) as a single line. */
|
|
4530
|
+
jsonLine(level, message, error, meta) {
|
|
4531
|
+
const obj = {
|
|
4532
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4533
|
+
level,
|
|
4534
|
+
msg: message
|
|
4535
|
+
};
|
|
4536
|
+
if (error !== void 0) obj["error"] = reduceError(error);
|
|
4537
|
+
if (meta !== void 0) {
|
|
4538
|
+
if (meta instanceof Error) obj["meta"] = reduceError(meta);
|
|
4539
|
+
else if (meta && typeof meta === "object") {
|
|
4540
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
4541
|
+
if (!RESERVED_JSON_KEYS.has(k)) obj[k] = v;
|
|
4542
|
+
}
|
|
4543
|
+
} else obj["meta"] = meta;
|
|
4544
|
+
}
|
|
4545
|
+
try {
|
|
4546
|
+
return JSON.stringify(obj);
|
|
4547
|
+
} catch {
|
|
4548
|
+
return JSON.stringify({ ts: obj["ts"], level, msg: message });
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
/** Human-readable file line: `ISO [level] message {metaJson}`. */
|
|
4552
|
+
textLine(level, message, error, meta) {
|
|
4553
|
+
const parts = [`${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}`];
|
|
4554
|
+
if (error !== void 0) parts.push(safeStringify(reduceError(error)));
|
|
4555
|
+
if (meta !== void 0) parts.push(safeStringify(meta instanceof Error ? reduceError(meta) : meta));
|
|
4556
|
+
return parts.join(" ");
|
|
3211
4557
|
}
|
|
3212
4558
|
};
|
|
4559
|
+
function reduceError(error) {
|
|
4560
|
+
if (error instanceof Error) {
|
|
4561
|
+
return error.stack ? { message: error.message, stack: error.stack } : { message: error.message };
|
|
4562
|
+
}
|
|
4563
|
+
return { value: String(error) };
|
|
4564
|
+
}
|
|
4565
|
+
function safeStringify(value) {
|
|
4566
|
+
try {
|
|
4567
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
4568
|
+
} catch {
|
|
4569
|
+
return "[unserializable]";
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
3213
4572
|
|
|
3214
4573
|
// src/ports/JsonApiServerSettingsStore.ts
|
|
3215
|
-
var
|
|
3216
|
-
var
|
|
4574
|
+
var import_node_fs6 = require("fs");
|
|
4575
|
+
var import_outbound_api3 = require("@omnicross/core/outbound-api");
|
|
3217
4576
|
var JsonApiServerSettingsStore = class {
|
|
3218
|
-
|
|
4577
|
+
/**
|
|
4578
|
+
* @param configPath the daemon config.json whose `server` field is backed.
|
|
4579
|
+
* @param box OPTIONAL at-rest `SecretBox` (upstream-proxy). When set, the
|
|
4580
|
+
* `server.proxy.*` passwords are encrypted-on-`set` /
|
|
4581
|
+
* decrypted-on-`get` (the settings-store path is otherwise not
|
|
4582
|
+
* secret-aware — every OTHER server field is non-secret). Null
|
|
4583
|
+
* ⇒ passthrough (legacy/pure tests unchanged).
|
|
4584
|
+
*/
|
|
4585
|
+
constructor(configPath, box = null) {
|
|
3219
4586
|
this.configPath = configPath;
|
|
4587
|
+
this.box = box;
|
|
3220
4588
|
}
|
|
3221
4589
|
configPath;
|
|
4590
|
+
box;
|
|
3222
4591
|
async get(key) {
|
|
3223
|
-
if (key !==
|
|
4592
|
+
if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return void 0;
|
|
3224
4593
|
const file = this.readFile();
|
|
3225
|
-
|
|
4594
|
+
if (file.server === void 0) return void 0;
|
|
4595
|
+
return this.decryptSecrets(file.server);
|
|
3226
4596
|
}
|
|
3227
4597
|
async set(key, value) {
|
|
3228
|
-
if (key !==
|
|
4598
|
+
if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return;
|
|
3229
4599
|
const file = this.readFile();
|
|
3230
|
-
file.server = value;
|
|
3231
|
-
(0,
|
|
4600
|
+
file.server = this.encryptSecrets(value);
|
|
4601
|
+
(0, import_node_fs6.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
4602
|
+
}
|
|
4603
|
+
/** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
|
|
4604
|
+
encryptSecrets(config) {
|
|
4605
|
+
if (!this.box) return config;
|
|
4606
|
+
let out = config;
|
|
4607
|
+
if (out?.proxy) out = { ...out, proxy: encryptProxySegment(out.proxy, this.box) };
|
|
4608
|
+
if (out?.webhook) out = { ...out, webhook: encryptWebhookSegment(out.webhook, this.box) };
|
|
4609
|
+
if (out?.billing) out = { ...out, billing: encryptBillingSegment(out.billing, this.box) };
|
|
4610
|
+
return out;
|
|
4611
|
+
}
|
|
4612
|
+
/** Decrypt the proxy passwords + webhook + billing secrets on read (no-op without a box). */
|
|
4613
|
+
decryptSecrets(config) {
|
|
4614
|
+
if (!this.box) return config;
|
|
4615
|
+
let out = config;
|
|
4616
|
+
if (out?.proxy) out = { ...out, proxy: decryptProxySegment(out.proxy, this.box) };
|
|
4617
|
+
if (out?.webhook) out = { ...out, webhook: decryptWebhookSegment(out.webhook, this.box) };
|
|
4618
|
+
if (out?.billing) out = { ...out, billing: decryptBillingSegment(out.billing, this.box) };
|
|
4619
|
+
return out;
|
|
3232
4620
|
}
|
|
3233
4621
|
/** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
|
|
3234
4622
|
readFile() {
|
|
3235
4623
|
try {
|
|
3236
|
-
const raw = (0,
|
|
4624
|
+
const raw = (0, import_node_fs6.readFileSync)(this.configPath, "utf8");
|
|
3237
4625
|
const parsed = JSON.parse(raw);
|
|
3238
4626
|
if (parsed && typeof parsed === "object") return parsed;
|
|
3239
4627
|
} catch {
|
|
@@ -3244,7 +4632,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
3244
4632
|
|
|
3245
4633
|
// src/ports/JsonlUsageEventStore.ts
|
|
3246
4634
|
var import_node_crypto9 = require("crypto");
|
|
3247
|
-
var
|
|
4635
|
+
var import_node_fs7 = require("fs");
|
|
3248
4636
|
var JsonlUsageEventStore = class {
|
|
3249
4637
|
constructor(eventsPath, isPriced) {
|
|
3250
4638
|
this.eventsPath = eventsPath;
|
|
@@ -3259,7 +4647,7 @@ var JsonlUsageEventStore = class {
|
|
|
3259
4647
|
id: (0, import_node_crypto9.randomUUID)(),
|
|
3260
4648
|
ts: input.ts ?? Date.now()
|
|
3261
4649
|
};
|
|
3262
|
-
(0,
|
|
4650
|
+
(0, import_node_fs7.appendFileSync)(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
|
|
3263
4651
|
return row.id;
|
|
3264
4652
|
}
|
|
3265
4653
|
async getTotals(range) {
|
|
@@ -3348,6 +4736,57 @@ var JsonlUsageEventStore = class {
|
|
|
3348
4736
|
}
|
|
3349
4737
|
return Array.from(groups.values());
|
|
3350
4738
|
}
|
|
4739
|
+
/**
|
|
4740
|
+
* ONE pass over a single key's events (`ts < endTs`) summing its `costUsd` into
|
|
4741
|
+
* `totalUsd` / `dailyUsd` (`ts >= dayStartTs`) / `weeklyUsd` (`ts >= weekStartTs`).
|
|
4742
|
+
* Used to lazily seed the outbound key-policy spend tracker (once per key). A
|
|
4743
|
+
* key with no attributed events yields all zeros.
|
|
4744
|
+
*/
|
|
4745
|
+
async getSpendByKey(query) {
|
|
4746
|
+
let totalUsd = 0;
|
|
4747
|
+
let dailyUsd = 0;
|
|
4748
|
+
let weeklyUsd = 0;
|
|
4749
|
+
for (const row of this.readRows({ startTs: 0, endTs: query.endTs })) {
|
|
4750
|
+
if (row.apiKeyId !== query.apiKeyId) continue;
|
|
4751
|
+
totalUsd += row.costUsd;
|
|
4752
|
+
if (row.ts >= query.dayStartTs) dailyUsd += row.costUsd;
|
|
4753
|
+
if (row.ts >= query.weekStartTs) weeklyUsd += row.costUsd;
|
|
4754
|
+
}
|
|
4755
|
+
return { totalUsd, dailyUsd, weeklyUsd };
|
|
4756
|
+
}
|
|
4757
|
+
/**
|
|
4758
|
+
* Time-series aggregation over LOCAL-time bucket boundaries. Every bucket in
|
|
4759
|
+
* `[floor(startTs), endTs)` is present (empty ones zero-filled), ascending by
|
|
4760
|
+
* `bucketStartTs`; an empty range (`startTs >= endTs`) returns `[]`. Reuses
|
|
4761
|
+
* `readRows` so malformed lines are skipped and only in-range rows contribute.
|
|
4762
|
+
*/
|
|
4763
|
+
async getTimeSeries(range, bucket) {
|
|
4764
|
+
if (range.startTs >= range.endTs) return [];
|
|
4765
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
4766
|
+
for (let b = floorToBucket(range.startTs, bucket); b < range.endTs; b = nextBoundary(b, bucket)) {
|
|
4767
|
+
buckets.set(b, {
|
|
4768
|
+
bucketStartTs: b,
|
|
4769
|
+
label: bucketLabel(b, bucket),
|
|
4770
|
+
requests: 0,
|
|
4771
|
+
inputTokens: 0,
|
|
4772
|
+
outputTokens: 0,
|
|
4773
|
+
cacheReadTokens: 0,
|
|
4774
|
+
cacheCreationTokens: 0,
|
|
4775
|
+
costUsd: 0
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
for (const row of this.readRows(range)) {
|
|
4779
|
+
const g = buckets.get(floorToBucket(row.ts, bucket));
|
|
4780
|
+
if (!g) continue;
|
|
4781
|
+
g.requests += 1;
|
|
4782
|
+
g.inputTokens += row.inputTokens;
|
|
4783
|
+
g.outputTokens += row.outputTokens;
|
|
4784
|
+
g.cacheReadTokens += row.cacheReadTokens;
|
|
4785
|
+
g.cacheCreationTokens += row.cacheCreationTokens;
|
|
4786
|
+
g.costUsd += row.costUsd;
|
|
4787
|
+
}
|
|
4788
|
+
return Array.from(buckets.values());
|
|
4789
|
+
}
|
|
3351
4790
|
async getMessagesForSession(sessionId) {
|
|
3352
4791
|
return this.readAllRows().filter((r) => r.sessionId === sessionId).sort((a, b) => a.ts - b.ts).map((r) => ({
|
|
3353
4792
|
id: r.id,
|
|
@@ -3396,10 +4835,10 @@ var JsonlUsageEventStore = class {
|
|
|
3396
4835
|
}
|
|
3397
4836
|
/** Parse every line, skipping malformed/torn lines defensively. */
|
|
3398
4837
|
readAllRows() {
|
|
3399
|
-
if (!(0,
|
|
4838
|
+
if (!(0, import_node_fs7.existsSync)(this.eventsPath)) return [];
|
|
3400
4839
|
let raw;
|
|
3401
4840
|
try {
|
|
3402
|
-
raw = (0,
|
|
4841
|
+
raw = (0, import_node_fs7.readFileSync)(this.eventsPath, "utf8");
|
|
3403
4842
|
} catch {
|
|
3404
4843
|
return [];
|
|
3405
4844
|
}
|
|
@@ -3416,6 +4855,43 @@ var JsonlUsageEventStore = class {
|
|
|
3416
4855
|
return rows;
|
|
3417
4856
|
}
|
|
3418
4857
|
};
|
|
4858
|
+
function floorToBucket(ts, bucket) {
|
|
4859
|
+
const d = new Date(ts);
|
|
4860
|
+
switch (bucket) {
|
|
4861
|
+
case "hour":
|
|
4862
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime();
|
|
4863
|
+
case "day":
|
|
4864
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
4865
|
+
case "month":
|
|
4866
|
+
return new Date(d.getFullYear(), d.getMonth(), 1).getTime();
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
function nextBoundary(ts, bucket) {
|
|
4870
|
+
const d = new Date(ts);
|
|
4871
|
+
switch (bucket) {
|
|
4872
|
+
case "hour":
|
|
4873
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours() + 1).getTime();
|
|
4874
|
+
case "day":
|
|
4875
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1).getTime();
|
|
4876
|
+
case "month":
|
|
4877
|
+
return new Date(d.getFullYear(), d.getMonth() + 1, 1).getTime();
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
4881
|
+
function bucketLabel(bucketStartTs, bucket) {
|
|
4882
|
+
const d = new Date(bucketStartTs);
|
|
4883
|
+
const y = d.getFullYear();
|
|
4884
|
+
const mo = pad2(d.getMonth() + 1);
|
|
4885
|
+
const day = pad2(d.getDate());
|
|
4886
|
+
switch (bucket) {
|
|
4887
|
+
case "hour":
|
|
4888
|
+
return `${mo}-${day} ${pad2(d.getHours())}:00`;
|
|
4889
|
+
case "day":
|
|
4890
|
+
return `${y}-${mo}-${day}`;
|
|
4891
|
+
case "month":
|
|
4892
|
+
return `${y}-${mo}`;
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
3419
4895
|
var NUMERIC_FIELDS = [
|
|
3420
4896
|
"ts",
|
|
3421
4897
|
"inputTokens",
|
|
@@ -3446,7 +4922,7 @@ function isUsageEventRecord(parsed) {
|
|
|
3446
4922
|
}
|
|
3447
4923
|
|
|
3448
4924
|
// src/ports/JsonOutboundKeyDb.ts
|
|
3449
|
-
var
|
|
4925
|
+
var import_node_fs8 = require("fs");
|
|
3450
4926
|
var JsonOutboundKeyDb = class {
|
|
3451
4927
|
constructor(keysPath) {
|
|
3452
4928
|
this.keysPath = keysPath;
|
|
@@ -3499,32 +4975,76 @@ var JsonOutboundKeyDb = class {
|
|
|
3499
4975
|
return true;
|
|
3500
4976
|
});
|
|
3501
4977
|
}
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
4978
|
+
async outboundApiKeysSetMaxConcurrency(id, maxConcurrency) {
|
|
4979
|
+
return this.mutateRow(id, (row) => {
|
|
4980
|
+
if (row.revokedAt !== null) return false;
|
|
4981
|
+
if (maxConcurrency === null) delete row.maxConcurrency;
|
|
4982
|
+
else row.maxConcurrency = maxConcurrency;
|
|
4983
|
+
return true;
|
|
4984
|
+
});
|
|
4985
|
+
}
|
|
4986
|
+
async outboundApiKeysSetPolicy(id, policy) {
|
|
4987
|
+
return this.mutateRow(id, (row) => {
|
|
4988
|
+
if (row.revokedAt !== null) return false;
|
|
4989
|
+
applyPolicyField(row, "expiresAt", policy.expiresAt);
|
|
4990
|
+
applyPolicyField(row, "activationDays", policy.activationDays);
|
|
4991
|
+
applyPolicyField(row, "dailyCostLimitUsd", policy.dailyCostLimitUsd);
|
|
4992
|
+
applyPolicyField(row, "totalCostLimitUsd", policy.totalCostLimitUsd);
|
|
4993
|
+
applyPolicyField(row, "weeklyCostLimitUsd", policy.weeklyCostLimitUsd);
|
|
4994
|
+
applyPolicyField(row, "rateLimitMaxRequests", policy.rateLimitMaxRequests);
|
|
4995
|
+
applyPolicyField(row, "rateLimitWindowMs", policy.rateLimitWindowMs);
|
|
4996
|
+
if (policy.activationMode === null) delete row.activationMode;
|
|
4997
|
+
else if (policy.activationMode !== void 0) row.activationMode = policy.activationMode;
|
|
4998
|
+
if (policy.enableModelRestriction === null) delete row.enableModelRestriction;
|
|
4999
|
+
else if (policy.enableModelRestriction !== void 0) {
|
|
5000
|
+
row.enableModelRestriction = policy.enableModelRestriction;
|
|
5001
|
+
}
|
|
5002
|
+
if (policy.restrictionMode === null) delete row.restrictionMode;
|
|
5003
|
+
else if (policy.restrictionMode !== void 0) row.restrictionMode = policy.restrictionMode;
|
|
5004
|
+
if (policy.restrictedModels === null) delete row.restrictedModels;
|
|
5005
|
+
else if (policy.restrictedModels !== void 0) row.restrictedModels = policy.restrictedModels;
|
|
5006
|
+
return true;
|
|
5007
|
+
});
|
|
5008
|
+
}
|
|
5009
|
+
async outboundApiKeysMarkActivated(id, activatedAt) {
|
|
5010
|
+
return this.mutateRow(id, (row) => {
|
|
5011
|
+
if (row.revokedAt !== null) return false;
|
|
5012
|
+
if (row.activatedAt != null) return false;
|
|
5013
|
+
row.activatedAt = activatedAt;
|
|
5014
|
+
return true;
|
|
5015
|
+
});
|
|
5016
|
+
}
|
|
5017
|
+
/** Apply `fn` to the row with `id`, persisting when it returns true. */
|
|
5018
|
+
mutateRow(id, fn) {
|
|
5019
|
+
const rows = this.readRows();
|
|
5020
|
+
const row = rows.find((r) => r.id === id);
|
|
5021
|
+
if (!row) return false;
|
|
5022
|
+
const changed = fn(row);
|
|
5023
|
+
if (changed) this.writeRows(rows);
|
|
5024
|
+
return changed;
|
|
5025
|
+
}
|
|
5026
|
+
/** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5027
|
+
readRows() {
|
|
5028
|
+
if (!(0, import_node_fs8.existsSync)(this.keysPath)) return [];
|
|
3514
5029
|
try {
|
|
3515
|
-
const parsed = JSON.parse((0,
|
|
5030
|
+
const parsed = JSON.parse((0, import_node_fs8.readFileSync)(this.keysPath, "utf8"));
|
|
3516
5031
|
return Array.isArray(parsed) ? parsed : [];
|
|
3517
5032
|
} catch {
|
|
3518
5033
|
return [];
|
|
3519
5034
|
}
|
|
3520
5035
|
}
|
|
3521
5036
|
writeRows(rows) {
|
|
3522
|
-
(0,
|
|
5037
|
+
(0, import_node_fs8.writeFileSync)(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
3523
5038
|
}
|
|
3524
5039
|
};
|
|
5040
|
+
function applyPolicyField(row, field, value) {
|
|
5041
|
+
if (value === void 0) return;
|
|
5042
|
+
if (value === null) delete row[field];
|
|
5043
|
+
else row[field] = value;
|
|
5044
|
+
}
|
|
3525
5045
|
|
|
3526
5046
|
// src/ports/JsonPricingStore.ts
|
|
3527
|
-
var
|
|
5047
|
+
var import_node_fs9 = require("fs");
|
|
3528
5048
|
var JsonPricingStore = class {
|
|
3529
5049
|
constructor(pricingPath) {
|
|
3530
5050
|
this.pricingPath = pricingPath;
|
|
@@ -3635,22 +5155,117 @@ var JsonPricingStore = class {
|
|
|
3635
5155
|
}
|
|
3636
5156
|
/** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
|
|
3637
5157
|
readRows() {
|
|
3638
|
-
if (!(0,
|
|
5158
|
+
if (!(0, import_node_fs9.existsSync)(this.pricingPath)) return [];
|
|
5159
|
+
try {
|
|
5160
|
+
const parsed = JSON.parse((0, import_node_fs9.readFileSync)(this.pricingPath, "utf8"));
|
|
5161
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
5162
|
+
} catch {
|
|
5163
|
+
return [];
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
writeRows(rows) {
|
|
5167
|
+
(0, import_node_fs9.writeFileSync)(this.pricingPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
5168
|
+
}
|
|
5169
|
+
};
|
|
5170
|
+
|
|
5171
|
+
// src/ports/JsonVoucherDb.ts
|
|
5172
|
+
var import_node_fs10 = require("fs");
|
|
5173
|
+
var JsonVoucherDb = class {
|
|
5174
|
+
constructor(vouchersPath) {
|
|
5175
|
+
this.vouchersPath = vouchersPath;
|
|
5176
|
+
}
|
|
5177
|
+
vouchersPath;
|
|
5178
|
+
async voucherCreate(input) {
|
|
5179
|
+
const rows = this.readRows();
|
|
5180
|
+
const row = {
|
|
5181
|
+
id: input.id,
|
|
5182
|
+
codeHash: input.codeHash,
|
|
5183
|
+
codePrefix: input.codePrefix,
|
|
5184
|
+
type: input.type,
|
|
5185
|
+
status: "unredeemed",
|
|
5186
|
+
createdAt: input.createdAt ?? Date.now()
|
|
5187
|
+
};
|
|
5188
|
+
if (input.creditUsd != null) row.creditUsd = input.creditUsd;
|
|
5189
|
+
if (input.renewalDays != null) row.renewalDays = input.renewalDays;
|
|
5190
|
+
if (input.maxTotalCostLimitUsd != null) row.maxTotalCostLimitUsd = input.maxTotalCostLimitUsd;
|
|
5191
|
+
if (input.maxExpiryDays != null) row.maxExpiryDays = input.maxExpiryDays;
|
|
5192
|
+
rows.push(row);
|
|
5193
|
+
this.writeRows(rows);
|
|
5194
|
+
return row;
|
|
5195
|
+
}
|
|
5196
|
+
async voucherGetByHash(codeHash) {
|
|
5197
|
+
const rows = this.readRows();
|
|
5198
|
+
return rows.find((r) => r.codeHash === codeHash) ?? null;
|
|
5199
|
+
}
|
|
5200
|
+
async voucherRedeemCas(id, keyId, granted, now) {
|
|
5201
|
+
const rows = this.readRows();
|
|
5202
|
+
const row = rows.find((r) => r.id === id);
|
|
5203
|
+
if (!row || row.status !== "unredeemed") return false;
|
|
5204
|
+
row.status = "redeemed";
|
|
5205
|
+
row.redeemedAt = now;
|
|
5206
|
+
row.redeemedByKeyId = keyId;
|
|
5207
|
+
row.grantApplied = false;
|
|
5208
|
+
if (granted.totalCostLimitUsd != null) row.grantedTotalCostLimitUsd = granted.totalCostLimitUsd;
|
|
5209
|
+
if (granted.expiresAt != null) row.grantedExpiresAt = granted.expiresAt;
|
|
5210
|
+
this.writeRows(rows);
|
|
5211
|
+
return true;
|
|
5212
|
+
}
|
|
5213
|
+
async voucherMarkGrantApplied(id) {
|
|
5214
|
+
const rows = this.readRows();
|
|
5215
|
+
const row = rows.find((r) => r.id === id);
|
|
5216
|
+
if (!row || row.status !== "redeemed") return false;
|
|
5217
|
+
if (row.grantApplied === true) return true;
|
|
5218
|
+
row.grantApplied = true;
|
|
5219
|
+
this.writeRows(rows);
|
|
5220
|
+
return true;
|
|
5221
|
+
}
|
|
5222
|
+
async voucherRevertRedeem(id, keyId) {
|
|
5223
|
+
const rows = this.readRows();
|
|
5224
|
+
const row = rows.find((r) => r.id === id);
|
|
5225
|
+
if (!row || row.status !== "redeemed" || row.grantApplied === true) return false;
|
|
5226
|
+
if (row.redeemedByKeyId !== keyId) return false;
|
|
5227
|
+
row.status = "unredeemed";
|
|
5228
|
+
delete row.redeemedAt;
|
|
5229
|
+
delete row.redeemedByKeyId;
|
|
5230
|
+
delete row.grantApplied;
|
|
5231
|
+
delete row.grantedTotalCostLimitUsd;
|
|
5232
|
+
delete row.grantedExpiresAt;
|
|
5233
|
+
this.writeRows(rows);
|
|
5234
|
+
return true;
|
|
5235
|
+
}
|
|
5236
|
+
async voucherRevokeCas(id, now) {
|
|
5237
|
+
const rows = this.readRows();
|
|
5238
|
+
const row = rows.find((r) => r.id === id);
|
|
5239
|
+
if (!row || row.status !== "unredeemed") return false;
|
|
5240
|
+
row.status = "revoked";
|
|
5241
|
+
row.revokedAt = now;
|
|
5242
|
+
this.writeRows(rows);
|
|
5243
|
+
return true;
|
|
5244
|
+
}
|
|
5245
|
+
async voucherList() {
|
|
5246
|
+
return this.readRows();
|
|
5247
|
+
}
|
|
5248
|
+
/** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5249
|
+
readRows() {
|
|
5250
|
+
if (!(0, import_node_fs10.existsSync)(this.vouchersPath)) return [];
|
|
3639
5251
|
try {
|
|
3640
|
-
const parsed = JSON.parse((0,
|
|
5252
|
+
const parsed = JSON.parse((0, import_node_fs10.readFileSync)(this.vouchersPath, "utf8"));
|
|
3641
5253
|
return Array.isArray(parsed) ? parsed : [];
|
|
3642
5254
|
} catch {
|
|
3643
5255
|
return [];
|
|
3644
5256
|
}
|
|
3645
5257
|
}
|
|
3646
5258
|
writeRows(rows) {
|
|
3647
|
-
(0,
|
|
5259
|
+
(0, import_node_fs10.writeFileSync)(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
3648
5260
|
}
|
|
3649
5261
|
};
|
|
3650
5262
|
|
|
3651
5263
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
3652
|
-
var
|
|
5264
|
+
var import_node_fs13 = require("fs");
|
|
3653
5265
|
var import_node_path7 = require("path");
|
|
5266
|
+
var import_SubscriptionAccountHealth = require("@omnicross/core/pipeline/SubscriptionAccountHealth");
|
|
5267
|
+
var import_upstreamFetch3 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
5268
|
+
var import_SubscriptionIdentityStore = require("@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore");
|
|
3654
5269
|
var import_subscriptions3 = require("@omnicross/subscriptions");
|
|
3655
5270
|
|
|
3656
5271
|
// src/ports/account-sync.ts
|
|
@@ -3727,7 +5342,7 @@ function findDuplicateCredentialIds(accounts) {
|
|
|
3727
5342
|
}
|
|
3728
5343
|
|
|
3729
5344
|
// src/ports/external-cli-credentials.ts
|
|
3730
|
-
var
|
|
5345
|
+
var import_node_fs11 = require("fs");
|
|
3731
5346
|
var import_node_os2 = require("os");
|
|
3732
5347
|
var import_node_path5 = require("path");
|
|
3733
5348
|
function externalStorePath(provider, home = (0, import_node_os2.homedir)()) {
|
|
@@ -3780,10 +5395,10 @@ function parseCodexTokensEnvelope(raw) {
|
|
|
3780
5395
|
}
|
|
3781
5396
|
function readExternalCliCredentials(provider, home = (0, import_node_os2.homedir)()) {
|
|
3782
5397
|
const path2 = externalStorePath(provider, home);
|
|
3783
|
-
if (!(0,
|
|
5398
|
+
if (!(0, import_node_fs11.existsSync)(path2)) return null;
|
|
3784
5399
|
let raw;
|
|
3785
5400
|
try {
|
|
3786
|
-
const parsed = JSON.parse((0,
|
|
5401
|
+
const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
|
|
3787
5402
|
raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3788
5403
|
} catch {
|
|
3789
5404
|
return null;
|
|
@@ -3792,7 +5407,7 @@ function readExternalCliCredentials(provider, home = (0, import_node_os2.homedir
|
|
|
3792
5407
|
}
|
|
3793
5408
|
|
|
3794
5409
|
// src/ports/external-cli-store.ts
|
|
3795
|
-
var
|
|
5410
|
+
var import_node_fs12 = require("fs");
|
|
3796
5411
|
var import_node_os3 = require("os");
|
|
3797
5412
|
var import_node_path6 = require("path");
|
|
3798
5413
|
function markerPath(provider, home) {
|
|
@@ -3820,27 +5435,27 @@ function buildCodexTokensEnvelope(tokens) {
|
|
|
3820
5435
|
return envelope;
|
|
3821
5436
|
}
|
|
3822
5437
|
function readExistingObject(path2) {
|
|
3823
|
-
if (!(0,
|
|
5438
|
+
if (!(0, import_node_fs12.existsSync)(path2)) return {};
|
|
3824
5439
|
try {
|
|
3825
|
-
const parsed = JSON.parse((0,
|
|
5440
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
|
|
3826
5441
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3827
5442
|
} catch {
|
|
3828
5443
|
return {};
|
|
3829
5444
|
}
|
|
3830
5445
|
}
|
|
3831
5446
|
function writeAtomic(path2, content) {
|
|
3832
|
-
(0,
|
|
5447
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
|
|
3833
5448
|
const temp = `${path2}.omnicross-tmp`;
|
|
3834
|
-
(0,
|
|
3835
|
-
(0,
|
|
5449
|
+
(0, import_node_fs12.writeFileSync)(temp, content, "utf8");
|
|
5450
|
+
(0, import_node_fs12.renameSync)(temp, path2);
|
|
3836
5451
|
}
|
|
3837
5452
|
function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
|
|
3838
5453
|
return {
|
|
3839
5454
|
readMarkerAccountId(provider) {
|
|
3840
5455
|
const path2 = markerPath(provider, home);
|
|
3841
|
-
if (!(0,
|
|
5456
|
+
if (!(0, import_node_fs12.existsSync)(path2)) return void 0;
|
|
3842
5457
|
try {
|
|
3843
|
-
const parsed = JSON.parse((0,
|
|
5458
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
|
|
3844
5459
|
return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
|
|
3845
5460
|
} catch {
|
|
3846
5461
|
return void 0;
|
|
@@ -3858,8 +5473,8 @@ function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
|
|
|
3858
5473
|
const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
|
|
3859
5474
|
if (!envelope) return false;
|
|
3860
5475
|
const storePath = externalStorePath(provider, home);
|
|
3861
|
-
if ((0,
|
|
3862
|
-
(0,
|
|
5476
|
+
if ((0, import_node_fs12.existsSync)(storePath) && !(0, import_node_fs12.existsSync)(backupPath(provider, home))) {
|
|
5477
|
+
(0, import_node_fs12.copyFileSync)(storePath, backupPath(provider, home));
|
|
3863
5478
|
}
|
|
3864
5479
|
const existing = readExistingObject(storePath);
|
|
3865
5480
|
const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
|
|
@@ -3870,16 +5485,21 @@ function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
|
|
|
3870
5485
|
}
|
|
3871
5486
|
|
|
3872
5487
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5488
|
+
var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
|
|
3873
5489
|
var JsonSubscriptionCredentialStore = class {
|
|
3874
5490
|
/**
|
|
3875
5491
|
* @param tokensPath on-disk `tokens.json` location.
|
|
3876
5492
|
* @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
|
|
3877
|
-
* @param fetchImpl injectable HTTP port for the OAuth refresh
|
|
3878
|
-
* (oauth design D4).
|
|
3879
|
-
*
|
|
3880
|
-
*
|
|
5493
|
+
* @param fetchImpl OPTIONAL injectable HTTP port for the OAuth refresh
|
|
5494
|
+
* round-trips (oauth design D4). A TEST-injected transport is
|
|
5495
|
+
* used verbatim. When ABSENT (production), each refresh uses a
|
|
5496
|
+
* proxy-aware {@link fetchUpstream} that threads the
|
|
5497
|
+
* `{ providerId, accountId }` ctx (upstream-proxy M1) so a
|
|
5498
|
+
* per-account/per-provider proxy is honored on refresh exactly
|
|
5499
|
+
* as on relay — refresh egresses from the SAME proxy IP as the
|
|
5500
|
+
* account's traffic. NOT used by any read/write path.
|
|
3881
5501
|
*/
|
|
3882
|
-
constructor(tokensPath, box, fetchImpl =
|
|
5502
|
+
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
|
|
3883
5503
|
this.tokensPath = tokensPath;
|
|
3884
5504
|
this.box = box;
|
|
3885
5505
|
this.fetchImpl = fetchImpl;
|
|
@@ -3891,6 +5511,15 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3891
5511
|
fetchImpl;
|
|
3892
5512
|
externalCliReader;
|
|
3893
5513
|
externalCliStore;
|
|
5514
|
+
/**
|
|
5515
|
+
* The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
|
|
5516
|
+
* TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
|
|
5517
|
+
* through {@link fetchUpstream} with the account's `{ providerId, accountId }`
|
|
5518
|
+
* ctx so the per-account/provider proxy applies. `@internal` — also a test seam.
|
|
5519
|
+
*/
|
|
5520
|
+
buildRefreshFetch(providerId, accountId) {
|
|
5521
|
+
return this.fetchImpl ?? ((url, init) => (0, import_upstreamFetch3.fetchUpstream)(url, init, { providerId, accountId }));
|
|
5522
|
+
}
|
|
3894
5523
|
/**
|
|
3895
5524
|
* In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
|
|
3896
5525
|
* SINGLE-USE: two concurrent refreshes of one account each spend the same
|
|
@@ -3922,6 +5551,19 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3922
5551
|
async getValidOpenCodeGoApiKey() {
|
|
3923
5552
|
return this.readConfig().opencodego?.apiKey ?? null;
|
|
3924
5553
|
}
|
|
5554
|
+
/**
|
|
5555
|
+
* DAEMON-ONLY per-account proxy lookup by id (upstream-proxy). Returns the
|
|
5556
|
+
* DECRYPTED `ProxyConfig` for the account (`readConfig` decrypts on read), or
|
|
5557
|
+
* `undefined` for an unknown provider/account or no per-account proxy. Feeds the
|
|
5558
|
+
* winning per-account layer of the upstream-proxy resolver. Synchronous like the
|
|
5559
|
+
* other hot reads. Never returns token material.
|
|
5560
|
+
*/
|
|
5561
|
+
getAccountProxy(providerId, accountId) {
|
|
5562
|
+
if (providerId !== "claude" && providerId !== "codex" && providerId !== "gemini" && providerId !== "opencodego") {
|
|
5563
|
+
return void 0;
|
|
5564
|
+
}
|
|
5565
|
+
return getAccountProxy(this.readConfig(), providerId, accountId);
|
|
5566
|
+
}
|
|
3925
5567
|
/**
|
|
3926
5568
|
* DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
|
|
3927
5569
|
* each provider's accounts to the secret-free `SubscriptionAccountSanitized`
|
|
@@ -3930,10 +5572,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3930
5572
|
*/
|
|
3931
5573
|
async listSanitizedAccounts() {
|
|
3932
5574
|
const config = this.readConfig();
|
|
5575
|
+
const health2 = (0, import_SubscriptionAccountHealth.getSharedAccountHealth)();
|
|
5576
|
+
const identityStore = (0, import_SubscriptionIdentityStore.getSharedIdentityStore)();
|
|
5577
|
+
const fingerprintOn = identityStore.isEnabled();
|
|
5578
|
+
const now = Date.now();
|
|
3933
5579
|
const out = {};
|
|
3934
5580
|
for (const provider of ["claude", "codex", "gemini", "opencodego"]) {
|
|
3935
5581
|
const sanitized = sanitizeAccounts(config, provider);
|
|
3936
|
-
if (sanitized.length
|
|
5582
|
+
if (sanitized.length === 0) continue;
|
|
5583
|
+
for (const account of sanitized) {
|
|
5584
|
+
const status = health2.getStatus(provider, account.id, now);
|
|
5585
|
+
account.health = status.state;
|
|
5586
|
+
account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
|
|
5587
|
+
if (fingerprintOn && provider === "claude") {
|
|
5588
|
+
account.identityCaptured = identityStore.hasIdentity(provider, account.id);
|
|
5589
|
+
const capturedAt = identityStore.capturedAt(provider, account.id);
|
|
5590
|
+
account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
out[provider] = this.attachSyncWarnings(config, provider, sanitized);
|
|
3937
5594
|
}
|
|
3938
5595
|
return out;
|
|
3939
5596
|
}
|
|
@@ -3984,8 +5641,9 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3984
5641
|
if (!active || !claude?.refreshToken) return false;
|
|
3985
5642
|
const capturedId = active.id;
|
|
3986
5643
|
this.materializeMigration(config);
|
|
5644
|
+
const refreshFetch = this.buildRefreshFetch("claude", capturedId);
|
|
3987
5645
|
try {
|
|
3988
|
-
const result = await import_subscriptions3.claudeOAuth.refreshAccessToken(claude.refreshToken,
|
|
5646
|
+
const result = await import_subscriptions3.claudeOAuth.refreshAccessToken(claude.refreshToken, refreshFetch);
|
|
3989
5647
|
const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
|
|
3990
5648
|
const next = {
|
|
3991
5649
|
...claude,
|
|
@@ -4002,7 +5660,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4002
5660
|
return true;
|
|
4003
5661
|
} catch (error) {
|
|
4004
5662
|
if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
|
|
4005
|
-
const r = await import_subscriptions3.claudeOAuth.refreshAccessToken(rt,
|
|
5663
|
+
const r = await import_subscriptions3.claudeOAuth.refreshAccessToken(rt, refreshFetch);
|
|
4006
5664
|
return {
|
|
4007
5665
|
accessToken: r.accessToken,
|
|
4008
5666
|
refreshToken: r.refreshToken,
|
|
@@ -4029,8 +5687,9 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4029
5687
|
if (!active || !codex?.refreshToken) return false;
|
|
4030
5688
|
const capturedId = active.id;
|
|
4031
5689
|
this.materializeMigration(config);
|
|
5690
|
+
const refreshFetch = this.buildRefreshFetch("codex", capturedId);
|
|
4032
5691
|
try {
|
|
4033
|
-
const result = await import_subscriptions3.codexOAuth.refreshAccessToken(codex.refreshToken,
|
|
5692
|
+
const result = await import_subscriptions3.codexOAuth.refreshAccessToken(codex.refreshToken, refreshFetch);
|
|
4034
5693
|
const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
|
|
4035
5694
|
const next = {
|
|
4036
5695
|
...codex,
|
|
@@ -4048,7 +5707,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4048
5707
|
return true;
|
|
4049
5708
|
} catch (error) {
|
|
4050
5709
|
if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
|
|
4051
|
-
const r = await import_subscriptions3.codexOAuth.refreshAccessToken(rt,
|
|
5710
|
+
const r = await import_subscriptions3.codexOAuth.refreshAccessToken(rt, refreshFetch);
|
|
4052
5711
|
return {
|
|
4053
5712
|
accessToken: r.accessToken,
|
|
4054
5713
|
refreshToken: r.refreshToken,
|
|
@@ -4078,8 +5737,9 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4078
5737
|
if (!active || !gemini?.refreshToken) return false;
|
|
4079
5738
|
const capturedId = active.id;
|
|
4080
5739
|
this.materializeMigration(config);
|
|
5740
|
+
const refreshFetch = this.buildRefreshFetch("gemini", capturedId);
|
|
4081
5741
|
try {
|
|
4082
|
-
const result = await import_subscriptions3.geminiOAuth.refreshAccessToken(gemini.refreshToken,
|
|
5742
|
+
const result = await import_subscriptions3.geminiOAuth.refreshAccessToken(gemini.refreshToken, refreshFetch);
|
|
4083
5743
|
const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
|
|
4084
5744
|
const next = {
|
|
4085
5745
|
...gemini,
|
|
@@ -4113,7 +5773,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4113
5773
|
if (!account || !captured?.refreshToken) return false;
|
|
4114
5774
|
this.materializeMigration(config);
|
|
4115
5775
|
try {
|
|
4116
|
-
const refreshed = await this.refreshUpstream(provider, captured.refreshToken);
|
|
5776
|
+
const refreshed = await this.refreshUpstream(provider, captured.refreshToken, id);
|
|
4117
5777
|
const next = {
|
|
4118
5778
|
...captured,
|
|
4119
5779
|
accessToken: refreshed.accessToken,
|
|
@@ -4135,10 +5795,114 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4135
5795
|
}
|
|
4136
5796
|
});
|
|
4137
5797
|
}
|
|
5798
|
+
// ── By-id account-pool surface (subscription-account-scheduling, design D6) ──
|
|
5799
|
+
/**
|
|
5800
|
+
* Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
|
|
5801
|
+
* provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
|
|
5802
|
+
* (refresh is 401-driven, like `getValidClaudeAccessToken`); codex/gemini refresh
|
|
5803
|
+
* a near-expiry token via `refreshAccountById` (like `resolveAccessToken`);
|
|
5804
|
+
* opencodego returns the account's static key. `null` when unknown/expired/
|
|
5805
|
+
* tokenless.
|
|
5806
|
+
*/
|
|
5807
|
+
async getAccessTokenForAccount(providerId, accountId) {
|
|
5808
|
+
const account = getAccountById(this.readConfig(), providerId, accountId);
|
|
5809
|
+
if (!account) return null;
|
|
5810
|
+
if (providerId === "opencodego") {
|
|
5811
|
+
return account.tokens.apiKey ?? null;
|
|
5812
|
+
}
|
|
5813
|
+
const oauth = account.tokens;
|
|
5814
|
+
if (!oauth.accessToken) return null;
|
|
5815
|
+
if (providerId === "codex" || providerId === "gemini") {
|
|
5816
|
+
const expiresAtMs = oauth.expiresAt ? Date.parse(oauth.expiresAt) : 0;
|
|
5817
|
+
const expiringSoon = expiresAtMs > 0 && Date.now() >= expiresAtMs - ACCOUNT_REFRESH_LEAD_MS;
|
|
5818
|
+
if (expiringSoon && oauth.refreshToken) {
|
|
5819
|
+
const ok = await this.refreshAccountById(providerId, accountId);
|
|
5820
|
+
if (!ok) return null;
|
|
5821
|
+
const fresh = getAccountById(this.readConfig(), providerId, accountId);
|
|
5822
|
+
return fresh?.tokens?.accessToken ?? null;
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
if (oauth.status === "expired") return null;
|
|
5826
|
+
return oauth.accessToken;
|
|
5827
|
+
}
|
|
5828
|
+
/**
|
|
5829
|
+
* Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
|
|
5830
|
+
* `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
|
|
5831
|
+
* → `false` (no refresh affordance).
|
|
5832
|
+
*/
|
|
5833
|
+
async refreshAccountToken(providerId, accountId) {
|
|
5834
|
+
if (providerId === "opencodego") return false;
|
|
5835
|
+
return this.refreshAccountById(providerId, accountId);
|
|
5836
|
+
}
|
|
5837
|
+
/**
|
|
5838
|
+
* Best-effort record of a selection time onto the account's `lastUsedAt` by id
|
|
5839
|
+
* (design D4). Entry-metadata only (the token mirror is untouched); a no-op for
|
|
5840
|
+
* an unknown id. The selector throttles the call frequency, so this stays cheap.
|
|
5841
|
+
*/
|
|
5842
|
+
async touchAccountLastUsed(providerId, accountId, iso) {
|
|
5843
|
+
const config = this.readConfig();
|
|
5844
|
+
const result = setAccountLastUsed(config, providerId, accountId, iso);
|
|
5845
|
+
if (!result.ok) return;
|
|
5846
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5847
|
+
}
|
|
5848
|
+
/**
|
|
5849
|
+
* Best-effort write-through of a per-account client `identity`
|
|
5850
|
+
* (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
|
|
5851
|
+
* whitelisted fingerprint headers; the token mirror is untouched); a no-op for
|
|
5852
|
+
* an unknown id. Called by the identity store's persistence port on a first-seen
|
|
5853
|
+
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller — the
|
|
5854
|
+
* store's port wrapper swallows a rejection so the relay hot path is unaffected.
|
|
5855
|
+
*/
|
|
5856
|
+
async setAccountIdentity(providerId, accountId, identity) {
|
|
5857
|
+
const config = this.readConfig();
|
|
5858
|
+
const result = setAccountIdentity(config, providerId, accountId, identity);
|
|
5859
|
+
if (!result.ok) return;
|
|
5860
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5861
|
+
}
|
|
5862
|
+
/**
|
|
5863
|
+
* DAEMON-ONLY set-priority (subscription-account-scheduling, admin write, NOT on
|
|
5864
|
+
* the port). Set one account's scheduling `priority` by id. Secret-free
|
|
5865
|
+
* (entry-metadata only; the mirror invariant is untouched). Rejects an unknown id.
|
|
5866
|
+
*/
|
|
5867
|
+
async setAccountPriority(providerId, accountId, priority) {
|
|
5868
|
+
const config = this.readConfig();
|
|
5869
|
+
const result = setAccountPriority(config, providerId, accountId, priority);
|
|
5870
|
+
if (!result.ok) return result;
|
|
5871
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5872
|
+
return result;
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
|
|
5876
|
+
* the port). Passing `undefined` clears the override. Write-only password: when
|
|
5877
|
+
* the incoming structured proxy omits the password but the account already had
|
|
5878
|
+
* one, the current (decrypted) password is preserved — editing host/port never
|
|
5879
|
+
* wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
|
|
5880
|
+
*/
|
|
5881
|
+
async setAccountProxy(providerId, accountId, proxy) {
|
|
5882
|
+
const config = this.readConfig();
|
|
5883
|
+
const merged = proxy ? preserveProxyConfigSecret(proxy, getAccountProxy(config, providerId, accountId)) : void 0;
|
|
5884
|
+
const result = setAccountProxy(config, providerId, accountId, merged);
|
|
5885
|
+
if (!result.ok) return result;
|
|
5886
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5887
|
+
return result;
|
|
5888
|
+
}
|
|
5889
|
+
/**
|
|
5890
|
+
* DAEMON-ONLY set/clear per-account `supportedModels` (subscription-account-
|
|
5891
|
+
* model-map, admin write, NOT on the port). Passing `undefined` clears it.
|
|
5892
|
+
* Secret-free (model ids only; the mirror invariant is untouched). Rejects an
|
|
5893
|
+
* unknown id.
|
|
5894
|
+
*/
|
|
5895
|
+
async setAccountSupportedModels(providerId, accountId, supportedModels) {
|
|
5896
|
+
const config = this.readConfig();
|
|
5897
|
+
const result = setAccountSupportedModels(config, providerId, accountId, supportedModels);
|
|
5898
|
+
if (!result.ok) return result;
|
|
5899
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5900
|
+
return result;
|
|
5901
|
+
}
|
|
4138
5902
|
/** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
|
|
4139
|
-
async refreshUpstream(provider, refreshToken) {
|
|
5903
|
+
async refreshUpstream(provider, refreshToken, accountId) {
|
|
4140
5904
|
const flow = provider === "claude" ? import_subscriptions3.claudeOAuth : provider === "codex" ? import_subscriptions3.codexOAuth : import_subscriptions3.geminiOAuth;
|
|
4141
|
-
const r = await flow.refreshAccessToken(refreshToken, this.
|
|
5905
|
+
const r = await flow.refreshAccessToken(refreshToken, this.buildRefreshFetch(provider, accountId));
|
|
4142
5906
|
return {
|
|
4143
5907
|
accessToken: r.accessToken,
|
|
4144
5908
|
refreshToken: r.refreshToken,
|
|
@@ -4351,9 +6115,9 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4351
6115
|
* → `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
4352
6116
|
* write — incl. child 4's future refresh writes — lands encrypted. */
|
|
4353
6117
|
persist(config) {
|
|
4354
|
-
(0,
|
|
6118
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path7.dirname)(this.tokensPath), { recursive: true });
|
|
4355
6119
|
const encrypted = encryptTokens(config, this.box);
|
|
4356
|
-
(0,
|
|
6120
|
+
(0, import_node_fs13.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
|
|
4357
6121
|
}
|
|
4358
6122
|
/**
|
|
4359
6123
|
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
@@ -4369,10 +6133,10 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4369
6133
|
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
4370
6134
|
*/
|
|
4371
6135
|
readConfig() {
|
|
4372
|
-
if (!(0,
|
|
6136
|
+
if (!(0, import_node_fs13.existsSync)(this.tokensPath)) return { updatedAt: "" };
|
|
4373
6137
|
let parsed;
|
|
4374
6138
|
try {
|
|
4375
|
-
const raw = JSON.parse((0,
|
|
6139
|
+
const raw = JSON.parse((0, import_node_fs13.readFileSync)(this.tokensPath, "utf8"));
|
|
4376
6140
|
parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
4377
6141
|
} catch {
|
|
4378
6142
|
parsed = null;
|
|
@@ -4383,18 +6147,254 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
4383
6147
|
}
|
|
4384
6148
|
};
|
|
4385
6149
|
|
|
4386
|
-
// src/
|
|
6150
|
+
// src/AccountHealthProbeScheduler.ts
|
|
6151
|
+
var import_upstreamFetch4 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
6152
|
+
|
|
6153
|
+
// src/probe/ProbeStrategy.ts
|
|
6154
|
+
var PROVIDER_PROBE_PLANS = {
|
|
6155
|
+
claude: {
|
|
6156
|
+
kind: "upstream",
|
|
6157
|
+
// VERIFIED free authed list endpoint (no tokens billed). The anthropic OAuth
|
|
6158
|
+
// bearer is accepted here exactly as on the relay path.
|
|
6159
|
+
url: "https://api.anthropic.com/v1/models",
|
|
6160
|
+
buildInit: (token) => ({
|
|
6161
|
+
method: "GET",
|
|
6162
|
+
headers: {
|
|
6163
|
+
Authorization: `Bearer ${token}`,
|
|
6164
|
+
"anthropic-version": "2023-06-01"
|
|
6165
|
+
}
|
|
6166
|
+
})
|
|
6167
|
+
},
|
|
6168
|
+
// UNVERIFIED cheap authed GET — Phase 1 local-only (LEAD OQ1: do not guess a
|
|
6169
|
+
// billable/wrong endpoint). Upgrade to `{ kind:'upstream' }` once verified.
|
|
6170
|
+
codex: { kind: "local" },
|
|
6171
|
+
gemini: { kind: "local" },
|
|
6172
|
+
opencodego: { kind: "local" }
|
|
6173
|
+
};
|
|
6174
|
+
function probePlanFor(providerId) {
|
|
6175
|
+
return PROVIDER_PROBE_PLANS[providerId] ?? { kind: "local" };
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6178
|
+
// src/AccountHealthProbeScheduler.ts
|
|
6179
|
+
var KEY_SEP = "\0";
|
|
6180
|
+
var MAX_BODY_SNIFF = 2048;
|
|
6181
|
+
var PROBE_PROVIDERS = [
|
|
6182
|
+
"claude",
|
|
6183
|
+
"codex",
|
|
6184
|
+
"gemini",
|
|
6185
|
+
"opencodego"
|
|
6186
|
+
];
|
|
6187
|
+
var AccountHealthProbeScheduler = class {
|
|
6188
|
+
constructor(store, health2, logger, config, opts = {}) {
|
|
6189
|
+
this.store = store;
|
|
6190
|
+
this.health = health2;
|
|
6191
|
+
this.logger = logger;
|
|
6192
|
+
this.config = config;
|
|
6193
|
+
this.now = opts.now ?? Date.now;
|
|
6194
|
+
this.fetchImpl = opts.fetchImpl ?? import_upstreamFetch4.fetchUpstream;
|
|
6195
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6196
|
+
this.planFor = opts.planFor ?? probePlanFor;
|
|
6197
|
+
}
|
|
6198
|
+
store;
|
|
6199
|
+
health;
|
|
6200
|
+
logger;
|
|
6201
|
+
config;
|
|
6202
|
+
timer = null;
|
|
6203
|
+
sweeping = false;
|
|
6204
|
+
history = /* @__PURE__ */ new Map();
|
|
6205
|
+
now;
|
|
6206
|
+
fetchImpl;
|
|
6207
|
+
sleep;
|
|
6208
|
+
planFor;
|
|
6209
|
+
/** Whether probing is enabled by the current config. */
|
|
6210
|
+
get enabled() {
|
|
6211
|
+
return this.config.enabled;
|
|
6212
|
+
}
|
|
6213
|
+
/**
|
|
6214
|
+
* Re-apply config to the live instance (the async `start.ts` loads the persisted
|
|
6215
|
+
* `accountProbe` segment after `buildDaemon`). Call BEFORE `start()`.
|
|
6216
|
+
*/
|
|
6217
|
+
configure(config) {
|
|
6218
|
+
this.config = config;
|
|
6219
|
+
}
|
|
6220
|
+
/** Arm the probe interval. No-op when disabled (zero regression). Idempotent. */
|
|
6221
|
+
start() {
|
|
6222
|
+
if (this.timer || !this.config.enabled) return;
|
|
6223
|
+
this.timer = setInterval(() => void this.sweep(), this.config.intervalMs);
|
|
6224
|
+
this.timer.unref?.();
|
|
6225
|
+
}
|
|
6226
|
+
/** Clear the interval (daemon shutdown / test teardown). Idempotent. */
|
|
6227
|
+
dispose() {
|
|
6228
|
+
if (this.timer) {
|
|
6229
|
+
clearInterval(this.timer);
|
|
6230
|
+
this.timer = null;
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
/**
|
|
6234
|
+
* One sweep: probe every ELIGIBLE account SEQUENTIALLY with a `staggerMs` gap.
|
|
6235
|
+
* Disabled ⇒ no-op. `onlyMultiAccount` skips single-account providers. Exposed
|
|
6236
|
+
* for tests; never throws.
|
|
6237
|
+
*/
|
|
6238
|
+
async sweep() {
|
|
6239
|
+
if (!this.config.enabled || this.sweeping) return;
|
|
6240
|
+
this.sweeping = true;
|
|
6241
|
+
try {
|
|
6242
|
+
const config = await this.store.getFullConfig();
|
|
6243
|
+
let probed = 0;
|
|
6244
|
+
let marked = 0;
|
|
6245
|
+
for (const providerId of PROBE_PROVIDERS) {
|
|
6246
|
+
const accounts = listAccounts(config, providerId);
|
|
6247
|
+
if (this.config.onlyMultiAccount && accounts.length < 2) continue;
|
|
6248
|
+
for (const account of accounts) {
|
|
6249
|
+
if (probed > 0 && this.config.staggerMs > 0) await this.sleep(this.config.staggerMs);
|
|
6250
|
+
const outcome = await this.probeAccount(providerId, account.id);
|
|
6251
|
+
probed += 1;
|
|
6252
|
+
if (outcome.marked) marked += 1;
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
6255
|
+
this.logger.debug("account-probe sweep complete", { probed, marked });
|
|
6256
|
+
} catch (error) {
|
|
6257
|
+
this.logger.warn("account-probe sweep failed", {
|
|
6258
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6259
|
+
});
|
|
6260
|
+
} finally {
|
|
6261
|
+
this.sweeping = false;
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6264
|
+
/**
|
|
6265
|
+
* Probe ONE account (design D1). Local tier first (dead token → synthesized 401,
|
|
6266
|
+
* no upstream); else the upstream tier when a verified endpoint exists. Records
|
|
6267
|
+
* the rolling history entry either way; returns whether the tracker was MARKED.
|
|
6268
|
+
*/
|
|
6269
|
+
async probeAccount(providerId, accountId) {
|
|
6270
|
+
const now = this.now();
|
|
6271
|
+
let token = null;
|
|
6272
|
+
let readThrew = false;
|
|
6273
|
+
try {
|
|
6274
|
+
token = await this.store.getAccessTokenForAccount(providerId, accountId);
|
|
6275
|
+
} catch {
|
|
6276
|
+
readThrew = true;
|
|
6277
|
+
}
|
|
6278
|
+
if (readThrew) {
|
|
6279
|
+
this.record(providerId, accountId, { ts: now, ok: false, status: null, tier: "local" });
|
|
6280
|
+
return { ok: false, marked: false };
|
|
6281
|
+
}
|
|
6282
|
+
if (!token) {
|
|
6283
|
+
this.health.recordUpstreamOutcome(providerId, accountId, { status: 401, now });
|
|
6284
|
+
this.record(providerId, accountId, { ts: now, ok: false, status: 401, tier: "local" });
|
|
6285
|
+
return { ok: false, marked: true };
|
|
6286
|
+
}
|
|
6287
|
+
const plan = this.planFor(providerId);
|
|
6288
|
+
if (plan.kind === "local") {
|
|
6289
|
+
this.record(providerId, accountId, { ts: now, ok: true, tier: "local" });
|
|
6290
|
+
return { ok: true, marked: false };
|
|
6291
|
+
}
|
|
6292
|
+
const start = this.now();
|
|
6293
|
+
let status = null;
|
|
6294
|
+
let bodyText;
|
|
6295
|
+
try {
|
|
6296
|
+
const res = await this.fetchImpl(
|
|
6297
|
+
plan.url,
|
|
6298
|
+
{ ...plan.buildInit(token), signal: AbortSignal.timeout(this.config.timeoutMs) },
|
|
6299
|
+
{ providerId, accountId }
|
|
6300
|
+
);
|
|
6301
|
+
status = res.status;
|
|
6302
|
+
if (status === 403) bodyText = await this.readBounded(res);
|
|
6303
|
+
} catch {
|
|
6304
|
+
status = null;
|
|
6305
|
+
}
|
|
6306
|
+
const latencyMs = this.now() - start;
|
|
6307
|
+
const marked = this.applyOutcome(providerId, accountId, status, bodyText, now);
|
|
6308
|
+
this.record(providerId, accountId, {
|
|
6309
|
+
ts: now,
|
|
6310
|
+
ok: status !== null && status >= 200 && status < 300,
|
|
6311
|
+
status,
|
|
6312
|
+
latencyMs,
|
|
6313
|
+
tier: "upstream"
|
|
6314
|
+
});
|
|
6315
|
+
return { ok: status !== null && status < 400, marked };
|
|
6316
|
+
}
|
|
6317
|
+
/** Per-account rolling history for the authed admin surface (design D5). */
|
|
6318
|
+
getAllHistory() {
|
|
6319
|
+
const out = [];
|
|
6320
|
+
for (const [key, records] of this.history) {
|
|
6321
|
+
const [providerId, accountId] = this.parseKey(key);
|
|
6322
|
+
out.push({ providerId, accountId, records: records.slice() });
|
|
6323
|
+
}
|
|
6324
|
+
return out;
|
|
6325
|
+
}
|
|
6326
|
+
/**
|
|
6327
|
+
* The coarse, account-ANONYMOUS `/health` signal (design D5): `true` when no
|
|
6328
|
+
* probed account is currently unhealthy (per #2's tracker). No ids, no counts —
|
|
6329
|
+
* safe for the unauthenticated `/health`. Vacuously `true` when nothing probed.
|
|
6330
|
+
*/
|
|
6331
|
+
probedAccountsHealthy(now = this.now()) {
|
|
6332
|
+
for (const key of this.history.keys()) {
|
|
6333
|
+
const [providerId, accountId] = this.parseKey(key);
|
|
6334
|
+
if (!this.health.isSchedulable(providerId, accountId, now)) return false;
|
|
6335
|
+
}
|
|
6336
|
+
return true;
|
|
6337
|
+
}
|
|
6338
|
+
/**
|
|
6339
|
+
* Feed ONLY the account/auth-decisive statuses to #2 (LEAD constraint):
|
|
6340
|
+
* 401/403 → mark; 2xx → clear transient; 429 / 5xx / other 4xx / null →
|
|
6341
|
+
* NOT forwarded (never a mark). Returns whether a NEGATIVE mark was applied.
|
|
6342
|
+
*/
|
|
6343
|
+
applyOutcome(providerId, accountId, status, bodyText, now) {
|
|
6344
|
+
if (status === null) return false;
|
|
6345
|
+
if (status === 401 || status === 403) {
|
|
6346
|
+
this.health.recordUpstreamOutcome(providerId, accountId, { status, bodyText, now });
|
|
6347
|
+
return true;
|
|
6348
|
+
}
|
|
6349
|
+
if (status >= 200 && status < 300) {
|
|
6350
|
+
this.health.clearTransientMark(providerId, accountId);
|
|
6351
|
+
return false;
|
|
6352
|
+
}
|
|
6353
|
+
return false;
|
|
6354
|
+
}
|
|
6355
|
+
/** Append a record, capping the ring at `historySize` (drop oldest). */
|
|
6356
|
+
record(providerId, accountId, rec) {
|
|
6357
|
+
const key = this.key(providerId, accountId);
|
|
6358
|
+
const list = this.history.get(key) ?? [];
|
|
6359
|
+
list.push(rec);
|
|
6360
|
+
const overflow = list.length - this.config.historySize;
|
|
6361
|
+
if (overflow > 0) list.splice(0, overflow);
|
|
6362
|
+
this.history.set(key, list);
|
|
6363
|
+
}
|
|
6364
|
+
/** Read a bounded slice of the response body for the 403-ban sniff (never throws). */
|
|
6365
|
+
async readBounded(res) {
|
|
6366
|
+
try {
|
|
6367
|
+
return (await res.text()).slice(0, MAX_BODY_SNIFF);
|
|
6368
|
+
} catch {
|
|
6369
|
+
return "";
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
key(providerId, accountId) {
|
|
6373
|
+
return `${providerId}${KEY_SEP}${accountId}`;
|
|
6374
|
+
}
|
|
6375
|
+
parseKey(key) {
|
|
6376
|
+
const idx = key.indexOf(KEY_SEP);
|
|
6377
|
+
return [key.slice(0, idx), key.slice(idx + 1)];
|
|
6378
|
+
}
|
|
6379
|
+
};
|
|
6380
|
+
|
|
6381
|
+
// src/AccountHealthSweeper.ts
|
|
4387
6382
|
var REFRESH_LEAD_MS = 5 * 6e4;
|
|
4388
6383
|
var SWEEP_INTERVAL_MS = 6e4;
|
|
4389
6384
|
var OAUTH_PROVIDERS = ["claude", "codex", "gemini"];
|
|
4390
|
-
|
|
4391
|
-
|
|
6385
|
+
function isOAuthProvider(providerId) {
|
|
6386
|
+
return OAUTH_PROVIDERS.includes(providerId);
|
|
6387
|
+
}
|
|
6388
|
+
var AccountHealthSweeper = class {
|
|
6389
|
+
constructor(store, health2, logger, intervalMs = SWEEP_INTERVAL_MS, leadMs = REFRESH_LEAD_MS) {
|
|
4392
6390
|
this.store = store;
|
|
6391
|
+
this.health = health2;
|
|
4393
6392
|
this.logger = logger;
|
|
4394
6393
|
this.intervalMs = intervalMs;
|
|
4395
6394
|
this.leadMs = leadMs;
|
|
4396
6395
|
}
|
|
4397
6396
|
store;
|
|
6397
|
+
health;
|
|
4398
6398
|
logger;
|
|
4399
6399
|
intervalMs;
|
|
4400
6400
|
leadMs;
|
|
@@ -4413,21 +6413,26 @@ var TokenRefreshScheduler = class {
|
|
|
4413
6413
|
this.timer = null;
|
|
4414
6414
|
}
|
|
4415
6415
|
}
|
|
4416
|
-
/**
|
|
6416
|
+
/**
|
|
6417
|
+
* One sweep: surface accounts that just recovered (emits the recovery signal
|
|
6418
|
+
* through the tracker's hook) and nudge a fresh token for any recovered OAuth
|
|
6419
|
+
* account whose token is near expiry. Exposed for tests. Never throws.
|
|
6420
|
+
*/
|
|
4417
6421
|
async sweep(now = Date.now()) {
|
|
4418
6422
|
if (this.sweeping) return;
|
|
4419
6423
|
this.sweeping = true;
|
|
4420
6424
|
try {
|
|
6425
|
+
const recovered = this.health.sweepRecoveries(now);
|
|
6426
|
+
if (recovered.length === 0) return;
|
|
4421
6427
|
const config = await this.store.getFullConfig();
|
|
4422
|
-
for (const
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
}
|
|
6428
|
+
for (const event of recovered) {
|
|
6429
|
+
if (!isOAuthProvider(event.providerId)) continue;
|
|
6430
|
+
const account = getAccountById(config, event.providerId, event.accountId);
|
|
6431
|
+
if (!account || !this.needsRefresh(account.tokens, now)) continue;
|
|
6432
|
+
await this.refreshOne(event.providerId, event.accountId);
|
|
4428
6433
|
}
|
|
4429
6434
|
} catch (error) {
|
|
4430
|
-
this.logger.warn("
|
|
6435
|
+
this.logger.warn("account-health sweep failed", {
|
|
4431
6436
|
error: error instanceof Error ? error.message : String(error)
|
|
4432
6437
|
});
|
|
4433
6438
|
} finally {
|
|
@@ -4442,54 +6447,777 @@ var TokenRefreshScheduler = class {
|
|
|
4442
6447
|
const expiresAt = Date.parse(t.expiresAt);
|
|
4443
6448
|
return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
|
|
4444
6449
|
}
|
|
4445
|
-
/** Refresh one account; failures are logged, never thrown
|
|
4446
|
-
|
|
4447
|
-
async refreshOne(provider, id, isActive) {
|
|
6450
|
+
/** Refresh one recovered account by id; failures are logged, never thrown. */
|
|
6451
|
+
async refreshOne(provider, id) {
|
|
4448
6452
|
try {
|
|
4449
|
-
const ok =
|
|
4450
|
-
if (
|
|
4451
|
-
|
|
4452
|
-
} else {
|
|
4453
|
-
this.logger.info("background token refresh succeeded", { provider, accountId: id });
|
|
4454
|
-
}
|
|
6453
|
+
const ok = await this.store.refreshAccountById(provider, id);
|
|
6454
|
+
if (ok) this.logger.info("account-health recovery refresh succeeded", { provider, accountId: id });
|
|
6455
|
+
else this.logger.warn("account-health recovery refresh failed", { provider, accountId: id });
|
|
4455
6456
|
} catch (error) {
|
|
4456
|
-
this.logger.warn("
|
|
6457
|
+
this.logger.warn("account-health recovery refresh threw", {
|
|
4457
6458
|
provider,
|
|
4458
6459
|
accountId: id,
|
|
4459
6460
|
error: error instanceof Error ? error.message : String(error)
|
|
4460
6461
|
});
|
|
4461
6462
|
}
|
|
4462
6463
|
}
|
|
4463
|
-
refreshActive(provider) {
|
|
4464
|
-
switch (provider) {
|
|
4465
|
-
case "claude":
|
|
4466
|
-
return this.store.refreshClaudeToken();
|
|
4467
|
-
case "codex":
|
|
4468
|
-
return this.store.refreshCodexToken();
|
|
4469
|
-
case "gemini":
|
|
4470
|
-
return this.store.refreshGeminiToken();
|
|
4471
|
-
}
|
|
4472
|
-
}
|
|
4473
6464
|
};
|
|
4474
6465
|
|
|
4475
|
-
// src/
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
const
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
6466
|
+
// src/audit/AuditPruneSweeper.ts
|
|
6467
|
+
var import_node_fs14 = require("fs");
|
|
6468
|
+
var import_node_path8 = require("path");
|
|
6469
|
+
|
|
6470
|
+
// src/audit/auditFiles.ts
|
|
6471
|
+
var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
6472
|
+
var pad22 = (n) => String(n).padStart(2, "0");
|
|
6473
|
+
function auditFileName(ts) {
|
|
6474
|
+
const d = new Date(ts);
|
|
6475
|
+
return `audit-${d.getFullYear()}-${pad22(d.getMonth() + 1)}-${pad22(d.getDate())}.jsonl`;
|
|
6476
|
+
}
|
|
6477
|
+
function auditFileDateMs(fileName) {
|
|
6478
|
+
const m = AUDIT_FILE_RE.exec(fileName);
|
|
6479
|
+
if (!m) return null;
|
|
6480
|
+
const year = Number(m[1]);
|
|
6481
|
+
const month = Number(m[2]);
|
|
6482
|
+
const day = Number(m[3]);
|
|
6483
|
+
const d = new Date(year, month - 1, day);
|
|
6484
|
+
if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day) {
|
|
6485
|
+
return null;
|
|
6486
|
+
}
|
|
6487
|
+
return d.getTime();
|
|
6488
|
+
}
|
|
6489
|
+
|
|
6490
|
+
// src/audit/AuditPruneSweeper.ts
|
|
6491
|
+
var DAY_MS = 24 * 60 * 6e4;
|
|
6492
|
+
var SWEEP_INTERVAL_MS2 = 60 * 6e4;
|
|
6493
|
+
var AuditPruneSweeper = class {
|
|
6494
|
+
constructor(auditDir, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
|
|
6495
|
+
this.auditDir = auditDir;
|
|
6496
|
+
this.logger = logger;
|
|
6497
|
+
this.config = config;
|
|
6498
|
+
this.intervalMs = intervalMs;
|
|
6499
|
+
this.now = now;
|
|
6500
|
+
}
|
|
6501
|
+
auditDir;
|
|
6502
|
+
logger;
|
|
6503
|
+
config;
|
|
6504
|
+
intervalMs;
|
|
6505
|
+
now;
|
|
6506
|
+
timer = null;
|
|
6507
|
+
sweeping = false;
|
|
6508
|
+
/** Whether pruning is active (audit enabled). */
|
|
6509
|
+
get enabled() {
|
|
6510
|
+
return this.config.enabled;
|
|
6511
|
+
}
|
|
6512
|
+
/** Re-apply config to the live instance (boot + admin PUT hot-reload). */
|
|
6513
|
+
configure(config) {
|
|
6514
|
+
this.config = config;
|
|
6515
|
+
}
|
|
6516
|
+
/**
|
|
6517
|
+
* Arm the prune interval AND run one prune immediately (boot cleanup). No-op
|
|
6518
|
+
* when audit is disabled (zero regression). Idempotent.
|
|
6519
|
+
*/
|
|
6520
|
+
start() {
|
|
6521
|
+
if (this.timer || !this.config.enabled) return;
|
|
6522
|
+
void this.sweep();
|
|
6523
|
+
this.timer = setInterval(() => void this.sweep(), this.intervalMs);
|
|
6524
|
+
this.timer.unref?.();
|
|
6525
|
+
}
|
|
6526
|
+
/** Clear the interval (daemon shutdown / test teardown). Idempotent. */
|
|
6527
|
+
dispose() {
|
|
6528
|
+
if (this.timer) {
|
|
6529
|
+
clearInterval(this.timer);
|
|
6530
|
+
this.timer = null;
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
/**
|
|
6534
|
+
* One prune: unlink every audit date file strictly OLDER than the retention
|
|
6535
|
+
* cutoff (`now - retentionDays` days, at local-midnight granularity). Exposed
|
|
6536
|
+
* for tests; never throws. Returns the number of files removed.
|
|
6537
|
+
*/
|
|
6538
|
+
async sweep() {
|
|
6539
|
+
if (!this.config.enabled || this.sweeping) return 0;
|
|
6540
|
+
this.sweeping = true;
|
|
6541
|
+
try {
|
|
6542
|
+
if (!(0, import_node_fs14.existsSync)(this.auditDir)) return 0;
|
|
6543
|
+
const today = new Date(this.now());
|
|
6544
|
+
const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
|
|
6545
|
+
const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
|
|
6546
|
+
let removed = 0;
|
|
6547
|
+
for (const file of (0, import_node_fs14.readdirSync)(this.auditDir)) {
|
|
6548
|
+
const dateMs = auditFileDateMs(file);
|
|
6549
|
+
if (dateMs === null || dateMs >= cutoff) continue;
|
|
6550
|
+
try {
|
|
6551
|
+
(0, import_node_fs14.unlinkSync)((0, import_node_path8.join)(this.auditDir, file));
|
|
6552
|
+
removed += 1;
|
|
6553
|
+
} catch (error) {
|
|
6554
|
+
this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
|
|
6555
|
+
file,
|
|
6556
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6557
|
+
});
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
if (removed > 0) this.logger.debug("audit prune complete", { removed });
|
|
6561
|
+
return removed;
|
|
6562
|
+
} catch (error) {
|
|
6563
|
+
this.logger.warn("audit prune sweep failed", {
|
|
6564
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6565
|
+
});
|
|
6566
|
+
return 0;
|
|
6567
|
+
} finally {
|
|
6568
|
+
this.sweeping = false;
|
|
6569
|
+
}
|
|
6570
|
+
}
|
|
6571
|
+
};
|
|
6572
|
+
|
|
6573
|
+
// src/audit/auditReader.ts
|
|
6574
|
+
var import_node_fs15 = require("fs");
|
|
6575
|
+
var import_node_path9 = require("path");
|
|
6576
|
+
var DEFAULT_LIMIT = 200;
|
|
6577
|
+
var MAX_LIMIT = 2e3;
|
|
6578
|
+
function readAuditRecords(auditDir, query = {}) {
|
|
6579
|
+
if (!(0, import_node_fs15.existsSync)(auditDir)) return [];
|
|
6580
|
+
let files;
|
|
6581
|
+
try {
|
|
6582
|
+
files = (0, import_node_fs15.readdirSync)(auditDir).filter((f) => AUDIT_FILE_RE.test(f));
|
|
6583
|
+
} catch {
|
|
6584
|
+
return [];
|
|
6585
|
+
}
|
|
6586
|
+
const from = typeof query.from === "number" ? query.from : -Infinity;
|
|
6587
|
+
const to = typeof query.to === "number" ? query.to : Infinity;
|
|
6588
|
+
const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit ?? DEFAULT_LIMIT)));
|
|
6589
|
+
const matched = [];
|
|
6590
|
+
for (const file of files.sort().reverse()) {
|
|
6591
|
+
let raw;
|
|
6592
|
+
try {
|
|
6593
|
+
raw = (0, import_node_fs15.readFileSync)((0, import_node_path9.join)(auditDir, file), "utf8");
|
|
6594
|
+
} catch {
|
|
6595
|
+
continue;
|
|
6596
|
+
}
|
|
6597
|
+
for (const line of raw.split("\n")) {
|
|
6598
|
+
const trimmed = line.trim();
|
|
6599
|
+
if (!trimmed) continue;
|
|
6600
|
+
let rec;
|
|
6601
|
+
try {
|
|
6602
|
+
rec = JSON.parse(trimmed);
|
|
6603
|
+
} catch {
|
|
6604
|
+
continue;
|
|
6605
|
+
}
|
|
6606
|
+
if (!isAuditRecord(rec)) continue;
|
|
6607
|
+
if (query.keyId !== void 0 && rec.keyId !== query.keyId) continue;
|
|
6608
|
+
if (rec.ts < from || rec.ts > to) continue;
|
|
6609
|
+
matched.push(rec);
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
matched.sort((a, b) => b.ts - a.ts);
|
|
6613
|
+
return matched.slice(0, limit);
|
|
6614
|
+
}
|
|
6615
|
+
function isAuditRecord(value) {
|
|
6616
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
6617
|
+
const r = value;
|
|
6618
|
+
return typeof r["id"] === "string" && typeof r["ts"] === "number" && typeof r["method"] === "string" && typeof r["path"] === "string" && typeof r["status"] === "number";
|
|
6619
|
+
}
|
|
6620
|
+
|
|
6621
|
+
// src/audit/AuditWriter.ts
|
|
6622
|
+
var import_node_fs16 = require("fs");
|
|
6623
|
+
var import_node_path10 = require("path");
|
|
6624
|
+
var AuditWriter = class {
|
|
6625
|
+
constructor(auditDir, logger, defer = (fn) => setTimeout(fn, 0)) {
|
|
6626
|
+
this.auditDir = auditDir;
|
|
6627
|
+
this.logger = logger;
|
|
6628
|
+
this.defer = defer;
|
|
6629
|
+
}
|
|
6630
|
+
auditDir;
|
|
6631
|
+
logger;
|
|
6632
|
+
defer;
|
|
6633
|
+
dirEnsured = false;
|
|
6634
|
+
/**
|
|
6635
|
+
* Enqueue one record for append. Returns IMMEDIATELY (fire-and-forget); the fs
|
|
6636
|
+
* write happens on the deferred tick. A failure is logged, never thrown.
|
|
6637
|
+
*/
|
|
6638
|
+
record(record) {
|
|
6639
|
+
this.defer(() => {
|
|
6640
|
+
try {
|
|
6641
|
+
this.appendNow(record);
|
|
6642
|
+
} catch (error) {
|
|
6643
|
+
this.logger.warn("[AuditWriter] failed to append audit record", {
|
|
6644
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6645
|
+
});
|
|
6646
|
+
}
|
|
6647
|
+
});
|
|
6648
|
+
}
|
|
6649
|
+
/**
|
|
6650
|
+
* Append synchronously — the awaitable form tests use to assert the line landed.
|
|
6651
|
+
* Ensures the `audit/` directory exists on first write (lazy, like the usage
|
|
6652
|
+
* store's lazy file creation).
|
|
6653
|
+
*/
|
|
6654
|
+
appendNow(record) {
|
|
6655
|
+
if (!this.dirEnsured) {
|
|
6656
|
+
(0, import_node_fs16.mkdirSync)(this.auditDir, { recursive: true });
|
|
6657
|
+
this.dirEnsured = true;
|
|
6658
|
+
}
|
|
6659
|
+
const file = (0, import_node_path10.join)(this.auditDir, auditFileName(record.ts));
|
|
6660
|
+
(0, import_node_fs16.appendFileSync)(file, JSON.stringify(record) + "\n", "utf8");
|
|
6661
|
+
}
|
|
6662
|
+
};
|
|
6663
|
+
|
|
6664
|
+
// src/billing/BillingPublisher.ts
|
|
6665
|
+
var import_node_fs17 = require("fs");
|
|
6666
|
+
var import_node_crypto10 = require("crypto");
|
|
6667
|
+
var import_node_path11 = require("path");
|
|
6668
|
+
var import_upstreamFetch5 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
6669
|
+
|
|
6670
|
+
// src/billing/billingFiles.ts
|
|
6671
|
+
var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
6672
|
+
var DELIVERED_FILE_RE = /^delivered-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
6673
|
+
var pad23 = (n) => String(n).padStart(2, "0");
|
|
6674
|
+
function dateStamp(ts) {
|
|
6675
|
+
const d = new Date(ts);
|
|
6676
|
+
return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}-${pad23(d.getDate())}`;
|
|
6677
|
+
}
|
|
6678
|
+
function billingFileName(ts) {
|
|
6679
|
+
return `billing-${dateStamp(ts)}.jsonl`;
|
|
6680
|
+
}
|
|
6681
|
+
function deliveredFileName(ts) {
|
|
6682
|
+
return `delivered-${dateStamp(ts)}.jsonl`;
|
|
6683
|
+
}
|
|
6684
|
+
|
|
6685
|
+
// src/billing/BillingPublisher.ts
|
|
6686
|
+
var BILLING_POST_TIMEOUT_MS = 1e4;
|
|
6687
|
+
var BillingPublisher = class {
|
|
6688
|
+
constructor(billingDir, logger, opts = {}) {
|
|
6689
|
+
this.billingDir = billingDir;
|
|
6690
|
+
this.logger = logger;
|
|
6691
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0, import_upstreamFetch5.fetchUpstream)(url, init));
|
|
6692
|
+
this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
|
|
6693
|
+
this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
|
|
6694
|
+
this.now = opts.now ?? Date.now;
|
|
6695
|
+
}
|
|
6696
|
+
billingDir;
|
|
6697
|
+
logger;
|
|
6698
|
+
config;
|
|
6699
|
+
dirEnsured = false;
|
|
6700
|
+
fetchImpl;
|
|
6701
|
+
defer;
|
|
6702
|
+
timeoutMs;
|
|
6703
|
+
now;
|
|
6704
|
+
/** Install/replace the live billing config (endpoint + secret + retry bound). */
|
|
6705
|
+
setConfig(config) {
|
|
6706
|
+
this.config = config;
|
|
6707
|
+
}
|
|
6708
|
+
/**
|
|
6709
|
+
* Record one billing event. DURABLE-FIRST: append synchronously (the event is
|
|
6710
|
+
* now on disk, never lost), THEN schedule a best-effort POST off the caller's
|
|
6711
|
+
* stack (non-blocking; ledger-only when no endpoint). Returns IMMEDIATELY and
|
|
6712
|
+
* NEVER throws — a failing append/POST is logged, never propagated.
|
|
6713
|
+
*/
|
|
6714
|
+
record(event) {
|
|
6715
|
+
let appended = false;
|
|
6716
|
+
try {
|
|
6717
|
+
this.appendNow(event);
|
|
6718
|
+
appended = true;
|
|
6719
|
+
} catch (error) {
|
|
6720
|
+
this.logger.warn("[BillingPublisher] failed to append billing event", {
|
|
6721
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6722
|
+
});
|
|
6723
|
+
}
|
|
6724
|
+
if (appended && this.config?.endpoint) {
|
|
6725
|
+
this.defer(() => {
|
|
6726
|
+
void this.deliverNow(event).catch(() => {
|
|
6727
|
+
});
|
|
6728
|
+
});
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
/**
|
|
6732
|
+
* Append the event as one JSON line to `billing-YYYY-MM-DD.jsonl` (the event's
|
|
6733
|
+
* LOCAL date). Synchronous — the awaitable form tests use to assert the ledger
|
|
6734
|
+
* line landed BEFORE any delivery. Ensures the `billing/` directory on first write.
|
|
6735
|
+
*/
|
|
6736
|
+
appendNow(event) {
|
|
6737
|
+
this.ensureDir();
|
|
6738
|
+
const file = (0, import_node_path11.join)(this.billingDir, billingFileName(event.ts));
|
|
6739
|
+
(0, import_node_fs17.appendFileSync)(file, JSON.stringify(event) + "\n", "utf8");
|
|
6740
|
+
}
|
|
6741
|
+
/**
|
|
6742
|
+
* One best-effort delivery attempt for an event ALREADY in the ledger. POSTs the
|
|
6743
|
+
* event JSON to the configured endpoint (optionally HMAC-signed); on a 2xx ack
|
|
6744
|
+
* appends a delivery marker and returns `true`. Any non-2xx / thrown / timed-out
|
|
6745
|
+
* attempt returns `false` — the event stays UNdelivered in the ledger (never
|
|
6746
|
+
* lost). NEVER rejects. A no-op `false` when no endpoint is configured.
|
|
6747
|
+
*/
|
|
6748
|
+
async deliverNow(event) {
|
|
6749
|
+
const endpoint = this.config?.endpoint;
|
|
6750
|
+
if (!endpoint) return false;
|
|
6751
|
+
try {
|
|
6752
|
+
const body = JSON.stringify(event);
|
|
6753
|
+
const headers = { "Content-Type": "application/json" };
|
|
6754
|
+
const secret = this.config?.secret;
|
|
6755
|
+
if (secret) {
|
|
6756
|
+
const hmac = (0, import_node_crypto10.createHmac)("sha256", secret).update(body).digest("hex");
|
|
6757
|
+
headers["X-Omnicross-Billing-Signature"] = `sha256=${hmac}`;
|
|
6758
|
+
}
|
|
6759
|
+
const res = await this.fetchImpl(endpoint, {
|
|
6760
|
+
method: "POST",
|
|
6761
|
+
headers,
|
|
6762
|
+
body,
|
|
6763
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
6764
|
+
});
|
|
6765
|
+
if (!res.ok) {
|
|
6766
|
+
this.logger.debug(`[billing] delivery failed ${event.id} (HTTP ${res.status})`);
|
|
6767
|
+
return false;
|
|
6768
|
+
}
|
|
6769
|
+
this.markDelivered(event);
|
|
6770
|
+
this.logger.debug(`[billing] delivered ${event.id}`);
|
|
6771
|
+
return true;
|
|
6772
|
+
} catch (error) {
|
|
6773
|
+
this.logger.debug(
|
|
6774
|
+
`[billing] delivery error ${event.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
6775
|
+
);
|
|
6776
|
+
return false;
|
|
6777
|
+
}
|
|
6778
|
+
}
|
|
6779
|
+
/**
|
|
6780
|
+
* Append a delivery marker `{ id, deliveredAt }` to `delivered-YYYY-MM-DD.jsonl`
|
|
6781
|
+
* (keyed by the EVENT's date so the reader finds both together). Idempotent at
|
|
6782
|
+
* the reconciliation layer — the reader unions marker ids into a delivered set,
|
|
6783
|
+
* so a duplicate marker is harmless. A marker-write failure is logged, never thrown.
|
|
6784
|
+
*/
|
|
6785
|
+
markDelivered(event) {
|
|
6786
|
+
try {
|
|
6787
|
+
this.ensureDir();
|
|
6788
|
+
const file = (0, import_node_path11.join)(this.billingDir, deliveredFileName(event.ts));
|
|
6789
|
+
(0, import_node_fs17.appendFileSync)(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
|
|
6790
|
+
} catch (error) {
|
|
6791
|
+
this.logger.warn("[BillingPublisher] failed to append delivery marker", {
|
|
6792
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6793
|
+
});
|
|
6794
|
+
}
|
|
6795
|
+
}
|
|
6796
|
+
ensureDir() {
|
|
6797
|
+
if (this.dirEnsured) return;
|
|
6798
|
+
(0, import_node_fs17.mkdirSync)(this.billingDir, { recursive: true });
|
|
6799
|
+
this.dirEnsured = true;
|
|
6800
|
+
}
|
|
6801
|
+
};
|
|
6802
|
+
|
|
6803
|
+
// src/billing/billingReader.ts
|
|
6804
|
+
var import_node_fs18 = require("fs");
|
|
6805
|
+
var import_node_path12 = require("path");
|
|
6806
|
+
function readBillingLedger(billingDir) {
|
|
6807
|
+
const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
|
|
6808
|
+
if (!(0, import_node_fs18.existsSync)(billingDir)) return view;
|
|
6809
|
+
let files;
|
|
6810
|
+
try {
|
|
6811
|
+
files = (0, import_node_fs18.readdirSync)(billingDir);
|
|
6812
|
+
} catch {
|
|
6813
|
+
return view;
|
|
6814
|
+
}
|
|
6815
|
+
for (const file of files.sort()) {
|
|
6816
|
+
if (BILLING_FILE_RE.test(file)) {
|
|
6817
|
+
for (const rec of parseLines(billingDir, file)) {
|
|
6818
|
+
if (isBillingEvent(rec)) view.events.push(rec);
|
|
6819
|
+
}
|
|
6820
|
+
} else if (DELIVERED_FILE_RE.test(file)) {
|
|
6821
|
+
for (const rec of parseLines(billingDir, file)) {
|
|
6822
|
+
const id = rec.id;
|
|
6823
|
+
if (typeof id === "string") view.deliveredIds.add(id);
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6826
|
+
}
|
|
6827
|
+
return view;
|
|
6828
|
+
}
|
|
6829
|
+
function readUndeliveredEvents(billingDir) {
|
|
6830
|
+
const { events, deliveredIds } = readBillingLedger(billingDir);
|
|
6831
|
+
return events.filter((e) => !deliveredIds.has(e.id)).sort((a, b) => a.ts - b.ts);
|
|
6832
|
+
}
|
|
6833
|
+
function readBillingStatus(billingDir) {
|
|
6834
|
+
const { events, deliveredIds } = readBillingLedger(billingDir);
|
|
6835
|
+
let delivered = 0;
|
|
6836
|
+
for (const e of events) if (deliveredIds.has(e.id)) delivered += 1;
|
|
6837
|
+
return { total: events.length, delivered, pending: events.length - delivered };
|
|
6838
|
+
}
|
|
6839
|
+
function parseLines(dir, file) {
|
|
6840
|
+
let raw;
|
|
6841
|
+
try {
|
|
6842
|
+
raw = (0, import_node_fs18.readFileSync)((0, import_node_path12.join)(dir, file), "utf8");
|
|
6843
|
+
} catch {
|
|
6844
|
+
return [];
|
|
6845
|
+
}
|
|
6846
|
+
const out = [];
|
|
6847
|
+
for (const line of raw.split("\n")) {
|
|
6848
|
+
const trimmed = line.trim();
|
|
6849
|
+
if (!trimmed) continue;
|
|
6850
|
+
try {
|
|
6851
|
+
out.push(JSON.parse(trimmed));
|
|
6852
|
+
} catch {
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
return out;
|
|
6856
|
+
}
|
|
6857
|
+
function isBillingEvent(value) {
|
|
6858
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
6859
|
+
const r = value;
|
|
6860
|
+
return typeof r["id"] === "string" && typeof r["ts"] === "number" && typeof r["model"] === "string" && typeof r["status"] === "number";
|
|
6861
|
+
}
|
|
6862
|
+
|
|
6863
|
+
// src/billing/BillingRetrySweeper.ts
|
|
6864
|
+
var SWEEP_INTERVAL_MS3 = 5 * 6e4;
|
|
6865
|
+
var BillingRetrySweeper = class {
|
|
6866
|
+
constructor(billingDir, publisher2, logger, config, intervalMs = SWEEP_INTERVAL_MS3, now = Date.now) {
|
|
6867
|
+
this.billingDir = billingDir;
|
|
6868
|
+
this.publisher = publisher2;
|
|
6869
|
+
this.logger = logger;
|
|
6870
|
+
this.config = config;
|
|
6871
|
+
this.intervalMs = intervalMs;
|
|
6872
|
+
this.now = now;
|
|
6873
|
+
}
|
|
6874
|
+
billingDir;
|
|
6875
|
+
publisher;
|
|
6876
|
+
logger;
|
|
6877
|
+
config;
|
|
6878
|
+
intervalMs;
|
|
6879
|
+
now;
|
|
6880
|
+
timer = null;
|
|
6881
|
+
sweeping = false;
|
|
6882
|
+
/** Whether retrying is active: billing enabled AND an endpoint is configured. */
|
|
6883
|
+
get enabled() {
|
|
6884
|
+
return this.config.enabled && typeof this.config.endpoint === "string" && this.config.endpoint.length > 0;
|
|
6885
|
+
}
|
|
6886
|
+
/** Re-apply config to the live instance (boot + admin PUT hot-reload). */
|
|
6887
|
+
configure(config) {
|
|
6888
|
+
this.config = config;
|
|
6889
|
+
}
|
|
6890
|
+
/**
|
|
6891
|
+
* Arm the retry interval AND run one sweep immediately (boot catch-up for events
|
|
6892
|
+
* that failed to deliver while the daemon was down). No-op when disabled or in
|
|
6893
|
+
* ledger-only mode (no endpoint to POST to). Idempotent.
|
|
6894
|
+
*/
|
|
6895
|
+
start() {
|
|
6896
|
+
if (this.timer || !this.enabled) return;
|
|
6897
|
+
void this.sweep();
|
|
6898
|
+
this.timer = setInterval(() => void this.sweep(), this.intervalMs);
|
|
6899
|
+
this.timer.unref?.();
|
|
6900
|
+
}
|
|
6901
|
+
/** Clear the interval (daemon shutdown / test teardown). Idempotent. */
|
|
6902
|
+
dispose() {
|
|
6903
|
+
if (this.timer) {
|
|
6904
|
+
clearInterval(this.timer);
|
|
6905
|
+
this.timer = null;
|
|
6906
|
+
}
|
|
6907
|
+
}
|
|
6908
|
+
/**
|
|
6909
|
+
* One sweep: re-POST every UNdelivered ledger event still within
|
|
6910
|
+
* `maxRetryAgeMs`; leave over-age undelivered events for reconciliation (NEVER
|
|
6911
|
+
* deleted). Exposed for tests; never throws. Returns the number of events a
|
|
6912
|
+
* re-POST was attempted for.
|
|
6913
|
+
*/
|
|
6914
|
+
async sweep() {
|
|
6915
|
+
if (!this.enabled || this.sweeping) return 0;
|
|
6916
|
+
this.sweeping = true;
|
|
6917
|
+
try {
|
|
6918
|
+
const cutoff = this.now() - this.config.maxRetryAgeMs;
|
|
6919
|
+
let attempted = 0;
|
|
6920
|
+
for (const event of readUndeliveredEvents(this.billingDir)) {
|
|
6921
|
+
if (event.ts < cutoff) continue;
|
|
6922
|
+
attempted += 1;
|
|
6923
|
+
await this.publisher.deliverNow(event);
|
|
6924
|
+
}
|
|
6925
|
+
if (attempted > 0) this.logger.debug("billing retry sweep complete", { attempted });
|
|
6926
|
+
return attempted;
|
|
6927
|
+
} catch (error) {
|
|
6928
|
+
this.logger.warn("billing retry sweep failed", {
|
|
6929
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6930
|
+
});
|
|
6931
|
+
return 0;
|
|
6932
|
+
} finally {
|
|
6933
|
+
this.sweeping = false;
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
};
|
|
6937
|
+
|
|
6938
|
+
// src/TokenRefreshScheduler.ts
|
|
6939
|
+
var REFRESH_LEAD_MS2 = 5 * 6e4;
|
|
6940
|
+
var SWEEP_INTERVAL_MS4 = 6e4;
|
|
6941
|
+
var OAUTH_PROVIDERS2 = ["claude", "codex", "gemini"];
|
|
6942
|
+
var TokenRefreshScheduler = class {
|
|
6943
|
+
constructor(store, logger, intervalMs = SWEEP_INTERVAL_MS4, leadMs = REFRESH_LEAD_MS2) {
|
|
6944
|
+
this.store = store;
|
|
6945
|
+
this.logger = logger;
|
|
6946
|
+
this.intervalMs = intervalMs;
|
|
6947
|
+
this.leadMs = leadMs;
|
|
6948
|
+
}
|
|
6949
|
+
store;
|
|
6950
|
+
logger;
|
|
6951
|
+
intervalMs;
|
|
6952
|
+
leadMs;
|
|
6953
|
+
timer = null;
|
|
6954
|
+
sweeping = false;
|
|
6955
|
+
/** Arm the sweep interval. Idempotent. The timer never holds the loop open. */
|
|
6956
|
+
start() {
|
|
6957
|
+
if (this.timer) return;
|
|
6958
|
+
this.timer = setInterval(() => void this.sweep(), this.intervalMs);
|
|
6959
|
+
this.timer.unref?.();
|
|
6960
|
+
}
|
|
6961
|
+
/** Clear the interval (daemon shutdown / test teardown). Idempotent. */
|
|
6962
|
+
dispose() {
|
|
6963
|
+
if (this.timer) {
|
|
6964
|
+
clearInterval(this.timer);
|
|
6965
|
+
this.timer = null;
|
|
6966
|
+
}
|
|
6967
|
+
}
|
|
6968
|
+
/** One sweep over every account of every OAuth provider. Exposed for tests. */
|
|
6969
|
+
async sweep(now = Date.now()) {
|
|
6970
|
+
if (this.sweeping) return;
|
|
6971
|
+
this.sweeping = true;
|
|
6972
|
+
try {
|
|
6973
|
+
const config = await this.store.getFullConfig();
|
|
6974
|
+
for (const provider of OAUTH_PROVIDERS2) {
|
|
6975
|
+
const activeId = getActiveAccount(config, provider)?.id;
|
|
6976
|
+
for (const account of listAccounts(config, provider)) {
|
|
6977
|
+
if (!this.needsRefresh(account.tokens, now)) continue;
|
|
6978
|
+
await this.refreshOne(provider, account.id, account.id === activeId);
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
} catch (error) {
|
|
6982
|
+
this.logger.warn("token-refresh sweep failed", {
|
|
6983
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6984
|
+
});
|
|
6985
|
+
} finally {
|
|
6986
|
+
this.sweeping = false;
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
/** Expiring within the lead window, refreshable, and not already dead. */
|
|
6990
|
+
needsRefresh(tokens, now) {
|
|
6991
|
+
const t = tokens;
|
|
6992
|
+
if (!t.refreshToken || t.status === "expired" || t.status === "error") return false;
|
|
6993
|
+
if (!t.expiresAt) return false;
|
|
6994
|
+
const expiresAt = Date.parse(t.expiresAt);
|
|
6995
|
+
return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
|
|
6996
|
+
}
|
|
6997
|
+
/** Refresh one account; failures are logged, never thrown (the store has
|
|
6998
|
+
* already flagged the account `expired`). */
|
|
6999
|
+
async refreshOne(provider, id, isActive) {
|
|
7000
|
+
try {
|
|
7001
|
+
const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
|
|
7002
|
+
if (!ok) {
|
|
7003
|
+
this.logger.warn("background token refresh failed", { provider, accountId: id });
|
|
7004
|
+
} else {
|
|
7005
|
+
this.logger.info("background token refresh succeeded", { provider, accountId: id });
|
|
7006
|
+
}
|
|
7007
|
+
} catch (error) {
|
|
7008
|
+
this.logger.warn("background token refresh threw", {
|
|
7009
|
+
provider,
|
|
7010
|
+
accountId: id,
|
|
7011
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7012
|
+
});
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
refreshActive(provider) {
|
|
7016
|
+
switch (provider) {
|
|
7017
|
+
case "claude":
|
|
7018
|
+
return this.store.refreshClaudeToken();
|
|
7019
|
+
case "codex":
|
|
7020
|
+
return this.store.refreshCodexToken();
|
|
7021
|
+
case "gemini":
|
|
7022
|
+
return this.store.refreshGeminiToken();
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
};
|
|
7026
|
+
|
|
7027
|
+
// src/webhook/WebhookDispatcher.ts
|
|
7028
|
+
var import_node_crypto11 = require("crypto");
|
|
7029
|
+
var import_upstreamFetch6 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
7030
|
+
var WEBHOOK_MAX_ATTEMPTS = 3;
|
|
7031
|
+
var WEBHOOK_QUEUE_MAX = 1e3;
|
|
7032
|
+
var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
|
|
7033
|
+
var WEBHOOK_BASE_BACKOFF_MS = 200;
|
|
7034
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7035
|
+
var WebhookDispatcher = class {
|
|
7036
|
+
config;
|
|
7037
|
+
queue = [];
|
|
7038
|
+
draining = false;
|
|
7039
|
+
warnedFull = false;
|
|
7040
|
+
fetchImpl;
|
|
7041
|
+
logger;
|
|
7042
|
+
maxAttempts;
|
|
7043
|
+
queueMax;
|
|
7044
|
+
timeoutMs;
|
|
7045
|
+
baseBackoffMs;
|
|
7046
|
+
sleep;
|
|
7047
|
+
now;
|
|
7048
|
+
constructor(opts = {}) {
|
|
7049
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0, import_upstreamFetch6.fetchUpstream)(url, init));
|
|
7050
|
+
this.logger = opts.logger;
|
|
7051
|
+
this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
|
|
7052
|
+
this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
|
|
7053
|
+
this.timeoutMs = opts.timeoutMs ?? WEBHOOK_SEND_TIMEOUT_MS;
|
|
7054
|
+
this.baseBackoffMs = opts.baseBackoffMs ?? WEBHOOK_BASE_BACKOFF_MS;
|
|
7055
|
+
this.sleep = opts.sleep ?? defaultSleep;
|
|
7056
|
+
this.now = opts.now ?? Date.now;
|
|
7057
|
+
}
|
|
7058
|
+
/** Install/replace the live webhook config (destinations + master switch). */
|
|
7059
|
+
setConfig(config) {
|
|
7060
|
+
this.config = config;
|
|
7061
|
+
}
|
|
7062
|
+
/**
|
|
7063
|
+
* Enqueue an event and return IMMEDIATELY (fire-and-forget). NEVER awaits a
|
|
7064
|
+
* send, NEVER throws — the drain loop does all sending on a side channel. A
|
|
7065
|
+
* full queue drops the OLDEST event (with a one-shot warn) so a runaway source
|
|
7066
|
+
* can't OOM the process.
|
|
7067
|
+
*/
|
|
7068
|
+
emit(event) {
|
|
7069
|
+
if (this.queue.length >= this.queueMax) {
|
|
7070
|
+
this.queue.shift();
|
|
7071
|
+
if (!this.warnedFull) {
|
|
7072
|
+
this.logger?.warn("[webhook] queue full \u2014 dropping oldest events");
|
|
7073
|
+
this.warnedFull = true;
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
7076
|
+
this.queue.push(event);
|
|
7077
|
+
if (!this.draining) {
|
|
7078
|
+
this.draining = true;
|
|
7079
|
+
queueMicrotask(() => void this.drain());
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
/** Drain the queue, sending each event to its matching destinations concurrently. */
|
|
7083
|
+
async drain() {
|
|
7084
|
+
try {
|
|
7085
|
+
while (this.queue.length > 0) {
|
|
7086
|
+
const event = this.queue.shift();
|
|
7087
|
+
const destinations = this.matchingDestinations(event.kind);
|
|
7088
|
+
if (destinations.length === 0) continue;
|
|
7089
|
+
await Promise.all(destinations.map((d) => this.sendWithRetry(event, d)));
|
|
7090
|
+
}
|
|
7091
|
+
} finally {
|
|
7092
|
+
this.draining = false;
|
|
7093
|
+
if (this.queue.length > 0) {
|
|
7094
|
+
this.draining = true;
|
|
7095
|
+
queueMicrotask(() => void this.drain());
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
7099
|
+
/** The enabled destinations whose event filter admits this kind (empty ⇒ all). */
|
|
7100
|
+
matchingDestinations(kind) {
|
|
7101
|
+
const cfg = this.config;
|
|
7102
|
+
if (!cfg || !cfg.enabled) return [];
|
|
7103
|
+
return cfg.destinations.filter(
|
|
7104
|
+
(d) => d.enabled && (!d.events || d.events.length === 0 || d.events.includes(kind))
|
|
7105
|
+
);
|
|
7106
|
+
}
|
|
7107
|
+
/** Send with bounded exponential backoff; log-and-drop after `maxAttempts`. */
|
|
7108
|
+
async sendWithRetry(event, dest) {
|
|
7109
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
|
|
7110
|
+
const result = await this.sendOnce(event, dest);
|
|
7111
|
+
if (result.ok) {
|
|
7112
|
+
this.logger?.debug(`[webhook] delivered ${event.kind} \u2192 ${dest.id} (${result.status})`);
|
|
7113
|
+
return;
|
|
7114
|
+
}
|
|
7115
|
+
if (attempt < this.maxAttempts) {
|
|
7116
|
+
await this.sleep(this.baseBackoffMs * 2 ** (attempt - 1));
|
|
7117
|
+
} else {
|
|
7118
|
+
this.logger?.warn(
|
|
7119
|
+
`[webhook] dropped ${event.kind} \u2192 ${dest.id} after ${this.maxAttempts} attempts: ${result.error ?? `HTTP ${result.status}`}`
|
|
7120
|
+
);
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
/** One POST attempt. Returns an outcome; a thrown error becomes `{ ok:false }`. */
|
|
7125
|
+
async sendOnce(event, dest) {
|
|
7126
|
+
try {
|
|
7127
|
+
const { body, headers } = buildRequest(event, dest, this.now());
|
|
7128
|
+
const res = await this.fetchImpl(dest.url, {
|
|
7129
|
+
method: "POST",
|
|
7130
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
7131
|
+
body,
|
|
7132
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
7133
|
+
});
|
|
7134
|
+
return res.ok ? { ok: true, status: res.status } : { ok: false, status: res.status };
|
|
7135
|
+
} catch (err5) {
|
|
7136
|
+
return { ok: false, error: err5 instanceof Error ? err5.message : String(err5) };
|
|
7137
|
+
}
|
|
7138
|
+
}
|
|
7139
|
+
/**
|
|
7140
|
+
* ADMIN test path (design D8): deliver a `test` event to ONE destination and
|
|
7141
|
+
* AWAIT the single-attempt result. This is the ONLY awaited send — it runs on
|
|
7142
|
+
* the admin request path (an operator clicking "Test"), NEVER on a relay path,
|
|
7143
|
+
* so awaiting it is safe. Finds the destination regardless of its `enabled`
|
|
7144
|
+
* flag or the master switch (an explicit operator action).
|
|
7145
|
+
*/
|
|
7146
|
+
async deliverTest(destinationId) {
|
|
7147
|
+
const dest = this.config?.destinations.find((d) => d.id === destinationId);
|
|
7148
|
+
if (!dest) return { ok: false, error: "destination not found" };
|
|
7149
|
+
return this.sendOnce({ kind: "test", at: this.now() }, dest);
|
|
7150
|
+
}
|
|
7151
|
+
};
|
|
7152
|
+
function buildRequest(event, dest, nowMs) {
|
|
7153
|
+
if (dest.type === "feishu") return buildFeishu(event, dest, nowMs);
|
|
7154
|
+
return buildCustom(event, dest);
|
|
7155
|
+
}
|
|
7156
|
+
function buildCustom(event, dest) {
|
|
7157
|
+
const body = JSON.stringify(event);
|
|
7158
|
+
const headers = {};
|
|
7159
|
+
if (dest.secret) {
|
|
7160
|
+
const hmac = (0, import_node_crypto11.createHmac)("sha256", dest.secret).update(body).digest("hex");
|
|
7161
|
+
headers["X-Omnicross-Signature"] = `sha256=${hmac}`;
|
|
7162
|
+
}
|
|
7163
|
+
return { body, headers };
|
|
7164
|
+
}
|
|
7165
|
+
function buildFeishu(event, dest, nowMs) {
|
|
7166
|
+
const payload = {
|
|
7167
|
+
msg_type: "text",
|
|
7168
|
+
content: { text: feishuText(event) }
|
|
7169
|
+
};
|
|
7170
|
+
if (dest.secret) {
|
|
7171
|
+
const timestamp = Math.floor(nowMs / 1e3).toString();
|
|
7172
|
+
const stringToSign = `${timestamp}
|
|
7173
|
+
${dest.secret}`;
|
|
7174
|
+
payload["timestamp"] = timestamp;
|
|
7175
|
+
payload["sign"] = (0, import_node_crypto11.createHmac)("sha256", stringToSign).digest("base64");
|
|
7176
|
+
}
|
|
7177
|
+
return { body: JSON.stringify(payload), headers: {} };
|
|
7178
|
+
}
|
|
7179
|
+
function feishuText(event) {
|
|
7180
|
+
switch (event.kind) {
|
|
7181
|
+
case "account.recovery":
|
|
7182
|
+
return `omnicross: account recovered \u2014 ${event.providerId}/${event.accountId}`;
|
|
7183
|
+
case "account.anomaly":
|
|
7184
|
+
return `omnicross: account anomaly [${event.state}] \u2014 ${event.providerId}/${event.accountId}`;
|
|
7185
|
+
case "key.quotaWarning":
|
|
7186
|
+
return `omnicross: key quota warning (${event.scope}) \u2014 $${event.spentUsd.toFixed(2)} of $${event.limitUsd.toFixed(2)} (key ${event.keyId})`;
|
|
7187
|
+
case "key.quotaExceeded":
|
|
7188
|
+
return `omnicross: key quota EXCEEDED (${event.scope}) \u2014 $${event.spentUsd.toFixed(2)} of $${event.limitUsd.toFixed(2)} (key ${event.keyId})`;
|
|
7189
|
+
case "server.error":
|
|
7190
|
+
return `omnicross: server error \u2014 ${event.message}`;
|
|
7191
|
+
case "test":
|
|
7192
|
+
return "omnicross: webhook test";
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
|
|
7196
|
+
// src/bootstrap.ts
|
|
7197
|
+
function buildDaemon(config, paths) {
|
|
7198
|
+
const logger = new ConfigurableLogger(config.logging);
|
|
7199
|
+
const secretBox3 = new SecretBox(() => resolveMasterKey({ keyFilePath: paths.masterKeyFilePath }));
|
|
7200
|
+
setSecretBox(secretBox3);
|
|
7201
|
+
setSecretBox2(secretBox3);
|
|
7202
|
+
const decryptedConfig = decryptConfigSecrets(config, secretBox3);
|
|
7203
|
+
const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
|
|
7204
|
+
const keyDb = new JsonOutboundKeyDb(paths.keysPath);
|
|
7205
|
+
const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
|
|
7206
|
+
const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
|
|
7207
|
+
const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
|
|
7208
|
+
const subscriptionAccounts = new import_subscriptions4.SubscriptionAccountService(credentialStore);
|
|
4487
7209
|
(0, import_subscriptions4.setSubscriptionAccountService)(subscriptionAccounts);
|
|
4488
7210
|
const subscriptionRegistry = new import_subscriptions4.SubscriptionProviderRegistry(
|
|
4489
7211
|
subscriptionAccounts,
|
|
4490
7212
|
credentialStore
|
|
4491
7213
|
);
|
|
4492
7214
|
(0, import_subscriptions4.setSubscriptionProviderRegistry)(subscriptionRegistry);
|
|
7215
|
+
setServerProxyConfig(decryptedConfig.server?.proxy);
|
|
7216
|
+
(0, import_upstreamFetch7.setUpstreamProxyResolver)(
|
|
7217
|
+
createUpstreamProxyResolver({
|
|
7218
|
+
getAccountProxy: (providerId, accountId) => credentialStore.getAccountProxy(providerId, accountId)
|
|
7219
|
+
})
|
|
7220
|
+
);
|
|
4493
7221
|
(0, import_gemini_code_assist_resolver.setGeminiCodeAssistResolver)((0, import_GeminiCodeAssistProjectResolver.getGeminiCodeAssistProjectResolver)());
|
|
4494
7222
|
const autoDisableStore = new AutoDisableStore();
|
|
4495
7223
|
const apiKeyPool = new import_ApiKeyPoolService.ApiKeyPoolService(
|
|
@@ -4510,19 +7238,59 @@ function buildDaemon(config, paths) {
|
|
|
4510
7238
|
defaultUsageEventsPath(paths.configPath),
|
|
4511
7239
|
async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
|
|
4512
7240
|
);
|
|
4513
|
-
const
|
|
7241
|
+
const keySpendTracker = new import_outbound_api5.KeySpendTracker(usageEventStore);
|
|
7242
|
+
const usageRecorder = new import_usage.UsageRecorder(usageEventStore, pricingEngine, logger, {
|
|
7243
|
+
onRecord: (apiKeyId, costUsd, at) => keySpendTracker.add(apiKeyId, costUsd, at)
|
|
7244
|
+
});
|
|
4514
7245
|
const providerProxy = (0, import_provider_proxy.getProviderProxy)({ llmConfig, apiKeyPool, usageRecorder });
|
|
4515
7246
|
llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
|
|
4516
|
-
const
|
|
7247
|
+
const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
|
|
7248
|
+
credentialStore,
|
|
7249
|
+
(0, import_SubscriptionAccountHealth2.getSharedAccountHealth)(),
|
|
7250
|
+
logger,
|
|
7251
|
+
import_outbound_api4.DEFAULT_ACCOUNT_PROBE
|
|
7252
|
+
);
|
|
7253
|
+
const getHealthReport = () => buildHealthReport({
|
|
7254
|
+
version: DAEMON_VERSION,
|
|
7255
|
+
// CRITICAL: the config loaded with a providers array.
|
|
7256
|
+
configPresent: () => Array.isArray(decryptedConfig.providers),
|
|
7257
|
+
// CRITICAL: the credential store's tokens.json is readable WITHOUT
|
|
7258
|
+
// decrypting (a missing file is fine — no accounts yet). A stat/access
|
|
7259
|
+
// only; never reads or decrypts token material.
|
|
7260
|
+
credentialStoreReadable: () => isTokensStoreReadable(paths.tokensPath),
|
|
7261
|
+
outboundServerRunning: () => outboundApiServer.getStatus().running,
|
|
7262
|
+
adminServerRunning: () => adminServer.getStatus().running,
|
|
7263
|
+
// Coarse, account-anonymous probe signal (#8, D5) — added to `checks` ONLY
|
|
7264
|
+
// when probing is ENABLED; disabled ⇒ `undefined` ⇒ key omitted ⇒ the
|
|
7265
|
+
// `/health` body stays byte-identical (zero regression).
|
|
7266
|
+
subscriptionAccountsHealthy: () => accountHealthProbeScheduler.enabled ? accountHealthProbeScheduler.probedAccountsHealthy() : void 0
|
|
7267
|
+
});
|
|
7268
|
+
const outboundApiServer = (0, import_outbound_api4.getOutboundApiServer)({
|
|
4517
7269
|
db: keyDb,
|
|
7270
|
+
// voucher-redemption #9: the key-authenticated `POST /redeem` endpoint redeems
|
|
7271
|
+
// cards against the presenting key (gated on `voucher.enabled`).
|
|
7272
|
+
voucherDb,
|
|
4518
7273
|
llmConfig,
|
|
4519
7274
|
providerProxy,
|
|
4520
|
-
proxyDeps: providerProxy.getDeps()
|
|
7275
|
+
proxyDeps: providerProxy.getDeps(),
|
|
7276
|
+
healthReportProvider: getHealthReport,
|
|
7277
|
+
// outbound-key-policy: the wire layer's 402 cost check reads per-key spend.
|
|
7278
|
+
keySpendTracker,
|
|
7279
|
+
// configurable-logging: route the server's OWN lifecycle + relay dispatch-error
|
|
7280
|
+
// lines through the injected logger (honors level/format/file sink).
|
|
7281
|
+
logger
|
|
4521
7282
|
});
|
|
7283
|
+
const auditDir = defaultAuditDir(paths.configPath);
|
|
7284
|
+
const billingDir = defaultBillingDir(paths.configPath);
|
|
4522
7285
|
const adminServer = new AdminServer({
|
|
4523
7286
|
configPath: paths.configPath,
|
|
4524
7287
|
llmConfig,
|
|
4525
7288
|
keyDb,
|
|
7289
|
+
// voucher-redemption #9: the admin `/admin/api/voucher` surface generates/
|
|
7290
|
+
// lists/revokes redemption cards (gated on `voucher.enabled`).
|
|
7291
|
+
voucherDb,
|
|
7292
|
+
// outbound-key-policy: the admin key list surfaces each key's OWN spend.
|
|
7293
|
+
keySpendReader: keySpendTracker,
|
|
4526
7294
|
settingsStore,
|
|
4527
7295
|
outboundApiServer,
|
|
4528
7296
|
subscriptionAccounts,
|
|
@@ -4544,7 +7312,9 @@ function buildDaemon(config, paths) {
|
|
|
4544
7312
|
oauthSessions: new OAuthSessionStore(),
|
|
4545
7313
|
// Real global fetch by default; a test seam (`paths.oauthExchangeFetch`) can
|
|
4546
7314
|
// inject a mock so no real token endpoint is hit.
|
|
4547
|
-
|
|
7315
|
+
// upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
|
|
7316
|
+
// helper so interactive login honors a configured proxy (global/env layers).
|
|
7317
|
+
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => (0, import_upstreamFetch7.fetchUpstream)(url, init)),
|
|
4548
7318
|
subscriptionAccountAppender: credentialStore,
|
|
4549
7319
|
// Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
|
|
4550
7320
|
// + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
|
|
@@ -4571,9 +7341,48 @@ function buildDaemon(config, paths) {
|
|
|
4571
7341
|
pricingStore,
|
|
4572
7342
|
// Use the DECRYPTED config so `admin.token` (if stored as `enc:`) is the
|
|
4573
7343
|
// plaintext bearer the AdminServer's constant-time compare expects (D4).
|
|
4574
|
-
getAdminConfig: () => resolveAdminConfig(decryptedConfig.admin)
|
|
7344
|
+
getAdminConfig: () => resolveAdminConfig(decryptedConfig.admin),
|
|
7345
|
+
// Unauthenticated `/health` probe (daemon-health-endpoint) — the SAME shared
|
|
7346
|
+
// builder the outbound server uses, served before the admin auth gate.
|
|
7347
|
+
getHealthReport,
|
|
7348
|
+
// configurable-logging: the admin listener's lifecycle lines route through
|
|
7349
|
+
// the injected logger.
|
|
7350
|
+
logger,
|
|
7351
|
+
// subscription-account-probe #8: the AUTHED `GET /admin/api/account-probes`
|
|
7352
|
+
// reads per-account probe history from the scheduler (secret-free — ids +
|
|
7353
|
+
// status labels only). Routed in `AdminServer` (not `adminApi.ts`).
|
|
7354
|
+
probeHistoryReader: accountHealthProbeScheduler,
|
|
7355
|
+
// request-audit-log: the AUTHED `GET /admin/api/audit` reads + filters the
|
|
7356
|
+
// date-rotated audit store. Bound to the store dir here so the AdminServer
|
|
7357
|
+
// carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
|
|
7358
|
+
// NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
|
|
7359
|
+
auditReader: (query) => readAuditRecords(auditDir, query),
|
|
7360
|
+
// billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
|
|
7361
|
+
// secret-free total/delivered/pending counts of the durable ledger.
|
|
7362
|
+
billingStatusReader: () => readBillingStatus(billingDir)
|
|
4575
7363
|
});
|
|
7364
|
+
const webhookDispatcher = new WebhookDispatcher({
|
|
7365
|
+
logger,
|
|
7366
|
+
fetchImpl: (url, init) => (0, import_upstreamFetch7.fetchUpstream)(url, init)
|
|
7367
|
+
});
|
|
7368
|
+
setWebhookRuntime(webhookDispatcher, (0, import_SubscriptionAccountHealth2.getSharedAccountHealth)());
|
|
7369
|
+
const auditWriter = new AuditWriter(auditDir, logger);
|
|
7370
|
+
const auditPruneSweeper = new AuditPruneSweeper(auditDir, logger, import_audit_types.DEFAULT_AUDIT_CONFIG);
|
|
7371
|
+
setAuditRuntime(auditWriter, auditPruneSweeper);
|
|
7372
|
+
const billingPublisher = new BillingPublisher(billingDir, logger);
|
|
7373
|
+
const billingRetrySweeper = new BillingRetrySweeper(
|
|
7374
|
+
billingDir,
|
|
7375
|
+
billingPublisher,
|
|
7376
|
+
logger,
|
|
7377
|
+
import_billing_types.DEFAULT_BILLING_CONFIG
|
|
7378
|
+
);
|
|
7379
|
+
setBillingRuntime(billingPublisher, billingRetrySweeper);
|
|
4576
7380
|
const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
|
|
7381
|
+
const accountHealthSweeper = new AccountHealthSweeper(
|
|
7382
|
+
credentialStore,
|
|
7383
|
+
(0, import_SubscriptionAccountHealth2.getSharedAccountHealth)(),
|
|
7384
|
+
logger
|
|
7385
|
+
);
|
|
4577
7386
|
return {
|
|
4578
7387
|
logger,
|
|
4579
7388
|
llmConfig,
|
|
@@ -4590,19 +7399,62 @@ function buildDaemon(config, paths) {
|
|
|
4590
7399
|
pricingEngine,
|
|
4591
7400
|
usageRecorder,
|
|
4592
7401
|
adminServer,
|
|
4593
|
-
tokenRefreshScheduler
|
|
7402
|
+
tokenRefreshScheduler,
|
|
7403
|
+
accountHealthSweeper,
|
|
7404
|
+
accountHealthProbeScheduler,
|
|
7405
|
+
webhookDispatcher,
|
|
7406
|
+
auditWriter,
|
|
7407
|
+
auditPruneSweeper,
|
|
7408
|
+
billingPublisher,
|
|
7409
|
+
billingRetrySweeper
|
|
4594
7410
|
};
|
|
4595
7411
|
}
|
|
4596
7412
|
function resetDaemonSingletonsForTests() {
|
|
4597
7413
|
(0, import_provider_proxy.__resetProviderProxyForTests)();
|
|
4598
|
-
(0,
|
|
7414
|
+
(0, import_outbound_api4.__resetOutboundApiServerForTests)();
|
|
4599
7415
|
(0, import_subscriptionRegistryPort.setSubscriptionRegistryForOutbound)(null);
|
|
4600
7416
|
(0, import_subscriptions4.setSubscriptionProviderRegistry)(null);
|
|
4601
7417
|
(0, import_subscriptions4.setSubscriptionAccountService)(null);
|
|
7418
|
+
(0, import_upstreamFetch7.setUpstreamProxyResolver)(null);
|
|
7419
|
+
setServerProxyConfig(void 0);
|
|
4602
7420
|
(0, import_gemini_code_assist_resolver.setGeminiCodeAssistResolver)(null);
|
|
4603
7421
|
setSecretBox(null);
|
|
4604
7422
|
setSecretBox2(null);
|
|
7423
|
+
resetWebhookRuntimeForTests();
|
|
7424
|
+
resetAuditRuntimeForTests();
|
|
7425
|
+
resetBillingRuntimeForTests();
|
|
7426
|
+
(0, import_SubscriptionIdentityStore2.__resetSharedIdentityStoreForTests)();
|
|
4605
7427
|
}
|
|
7428
|
+
function isTokensStoreReadable(tokensPath) {
|
|
7429
|
+
try {
|
|
7430
|
+
if (!(0, import_node_fs19.existsSync)(tokensPath)) return true;
|
|
7431
|
+
(0, import_node_fs19.accessSync)(tokensPath, import_node_fs19.constants.R_OK);
|
|
7432
|
+
return true;
|
|
7433
|
+
} catch {
|
|
7434
|
+
return false;
|
|
7435
|
+
}
|
|
7436
|
+
}
|
|
7437
|
+
|
|
7438
|
+
// src/ports/ConsoleLogger.ts
|
|
7439
|
+
var ConsoleLogger = class {
|
|
7440
|
+
info(message, meta) {
|
|
7441
|
+
if (meta === void 0) console.info(message);
|
|
7442
|
+
else console.info(message, meta);
|
|
7443
|
+
}
|
|
7444
|
+
warn(message, meta) {
|
|
7445
|
+
if (meta === void 0) console.warn(message);
|
|
7446
|
+
else console.warn(message, meta);
|
|
7447
|
+
}
|
|
7448
|
+
error(message, error, meta) {
|
|
7449
|
+
if (error === void 0 && meta === void 0) console.error(message);
|
|
7450
|
+
else if (meta === void 0) console.error(message, error);
|
|
7451
|
+
else console.error(message, error, meta);
|
|
7452
|
+
}
|
|
7453
|
+
debug(message, meta) {
|
|
7454
|
+
if (meta === void 0) console.debug(message);
|
|
7455
|
+
else console.debug(message, meta);
|
|
7456
|
+
}
|
|
7457
|
+
};
|
|
4606
7458
|
|
|
4607
7459
|
// src/ccr-import.ts
|
|
4608
7460
|
function parseCcrConfig(raw) {
|
|
@@ -4679,12 +7531,14 @@ function mapCcrToOmnicross(ccr) {
|
|
|
4679
7531
|
0 && (module.exports = {
|
|
4680
7532
|
AdminServer,
|
|
4681
7533
|
ConfigFileProviderConfigSource,
|
|
7534
|
+
ConfigurableLogger,
|
|
4682
7535
|
ConsoleLogger,
|
|
4683
7536
|
DEFAULT_ADMIN_PORT,
|
|
4684
7537
|
JsonApiServerSettingsStore,
|
|
4685
7538
|
JsonOutboundKeyDb,
|
|
4686
7539
|
JsonSubscriptionCredentialStore,
|
|
4687
7540
|
buildDaemon,
|
|
7541
|
+
buildHealthReport,
|
|
4688
7542
|
handleAdminApi,
|
|
4689
7543
|
inferApiFormat,
|
|
4690
7544
|
loadConfig,
|