@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/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 import_outbound_api3 = require("@omnicross/core/outbound-api");
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 import_outbound_api = require("@omnicross/core/outbound-api");
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
- return {
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
- return { providers, server, admin };
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
- syncWarning: t.syncWarning
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 clearProvider(config, p) {
1296
- setBlock(config, p, void 0);
1297
- setAccounts(config, p, void 0);
1298
- setActiveId(config, p, void 0);
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
- var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
1301
-
1302
- // src/migration/packCodec.ts
1303
- var import_node_crypto6 = require("crypto");
1304
- var PACK_MAGIC = "OMCXPACK";
1305
- var PACK_VERSION = 1;
1306
- var KDF_ALGORITHM = "scrypt";
1307
- var PACK_PREFIX = `${PACK_MAGIC}${PACK_VERSION}.`;
1308
- var KEY_BYTES3 = 32;
1309
- var IV_BYTES2 = 12;
1310
- var TAG_BYTES2 = 16;
1311
- var SCRYPT_N = 1 << 15;
1312
- var SCRYPT_R = 8;
1313
- var SCRYPT_P = 1;
1314
- var SCRYPT_SALT_BYTES = 16;
1315
- var SCRYPT_MAXMEM = 128 * SCRYPT_R * SCRYPT_N * 2;
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 readJsonBody(req) {
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 writeJson(res, status, body) {
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
- writeJson(res, status, { error: { type: "admin_api_error", message } });
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
- writeJson(res, result.status, result.body);
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 readJsonBody(req), deps));
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 readJsonBody(req), deps));
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 readJsonBody(req);
2837
+ const body = await readJsonBody3(req);
1873
2838
  const result = await handleExport(body, migrationDeps(deps));
1874
- return writeJson(res, result.status, result.body);
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 readJsonBody(req);
2843
+ const body = await readJsonBody3(req);
1879
2844
  const result = await handleImport(body, migrationDeps(deps));
1880
- return writeJson(res, result.status, result.body);
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 writeJson(res, 200, { apiKey: row.apiKey ?? "" });
2876
+ return writeJson2(res, 200, { apiKey: row.apiKey ?? "" });
1912
2877
  }
1913
2878
  if (method === "GET") {
1914
- return writeJson(res, 200, { providers: cfg.providers.map(toProviderView) });
2879
+ return writeJson2(res, 200, { providers: cfg.providers.map(toProviderView) });
1915
2880
  }
1916
2881
  if (method === "POST") {
1917
- const body = await readJsonBody(req);
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 writeJson(res, 201, { provider: toProviderView(provider) });
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 readJsonBody(req);
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 writeJson(res, 200, { provider: toProviderView(updated) });
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 writeJson(res, 200, { ok: true });
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 readJsonBody(req);
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 writeJson(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
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 writeJson(res, 200, { models: [], unsupportedFormat: true });
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 fetch(url, { method: "GET", headers });
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 writeJson(res, 200, {
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 writeJson(res, 200, { models });
2972
+ return writeJson2(res, 200, { models });
2008
2973
  } catch (err5) {
2009
2974
  const message = err5 instanceof Error ? err5.message : String(err5);
2010
- return writeJson(res, 200, { models: [], error: `discovery failed: ${message}` });
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 readJsonBody(req);
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 writeJson(res, 200, { ok: false, unsupportedFormat: true });
2986
+ return writeJson2(res, 200, { ok: false, unsupportedFormat: true });
2022
2987
  }
2023
2988
  const resolvedKey = resolveEnvKey(row.apiKey);
2024
2989
  if (!resolvedKey) {
2025
- return writeJson(res, 200, { ok: false, message: "no API key configured for this provider" });
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 fetch(url, {
2047
- method: "POST",
2048
- headers,
2049
- body: JSON.stringify(payload)
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 writeJson(res, 200, { ok: false, status: response.status, latencyMs, message });
3025
+ return writeJson2(res, 200, { ok: false, status: response.status, latencyMs, message });
2061
3026
  }
2062
- return writeJson(res, 200, {
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 writeJson(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
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 health = {};
2093
- if (cd) health.cooldown = cd;
2094
- if (auto) health.autoDisabled = { status: auto.status, at: auto.at, reason: auto.reason };
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(health).length > 0 ? { health } : {}
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 writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
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 readJsonBody(req);
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 writeJson(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
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 readJsonBody(req);
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 writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
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 writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
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 readJsonBody(req);
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 writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
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 writeJson(res, 200, { presets, excluded });
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
- return writeJson(res, 200, { keys: rows.map(toKeyInfo) });
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 readJsonBody(req);
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, import_outbound_api.createNamedKey)(deps.keyDb, name);
2372
- return writeJson(res, 201, {
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 writeJson(res, ok ? 200 : 404, { ok });
3362
+ return writeJson2(res, ok ? 200 : 404, { ok });
2385
3363
  }
2386
3364
  if (method === "POST" && id && action === "enabled") {
2387
- const body = await readJsonBody(req);
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 writeJson(res, ok ? 200 : 404, { ok, enabled });
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, import_outbound_api.loadServerConfig)(deps.settingsStore);
2397
- return writeJson(res, 200, { server: config });
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 readJsonBody(req);
2401
- const current = await (0, import_outbound_api.loadServerConfig)(deps.settingsStore);
2402
- const merged = (0, import_outbound_api.mergeServerConfig)(current, patch);
2403
- await (0, import_outbound_api.saveServerConfig)(deps.settingsStore, merged);
2404
- await deps.outboundApiServer.applyConfig({
2405
- enabled: merged.enabled,
2406
- networkBinding: merged.networkBinding,
2407
- endpoints: merged.endpoints,
2408
- port: merged.port
2409
- });
2410
- return writeJson(res, 200, { server: merged });
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 writeJson(res, 200, { accounts, providerAccounts, externalCli });
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 writeJson(res, result.status, result.body);
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 writeJson(res, result.status, result.body);
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 readJsonBody(req);
3548
+ const body2 = await readJsonBody3(req);
2436
3549
  const result = await handleOAuthComplete(providerId, body2, deps);
2437
- return writeJson(res, result.status, result.body);
3550
+ return writeJson2(res, result.status, result.body);
2438
3551
  }
2439
3552
  if (method === "POST" && rest[1] === "accounts") {
2440
- const body2 = await readJsonBody(req);
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 writeJson(res, 200, status2 ? { account: status2 } : { ok: true });
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 readJsonBody(req);
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 writeJson(res, 200, { ok: true, account: status2 ?? void 0 });
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 writer = deps.subscriptionTokenWriter;
2468
- const ok = providerId === "claude" ? await writer.refreshClaudeToken() : providerId === "codex" ? await writer.refreshCodexToken() : await writer.refreshGeminiToken();
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 writeJson(res, 200, { ok, account: status2 ?? void 0 });
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 readJsonBody(req);
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 writeJson(res, 200, { ok: true });
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 readJsonBody(req);
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 writeJson(res, 200, { ok: true });
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 writeJson(res, 200, { ok: true });
3639
+ return writeJson2(res, 200, { ok: true });
2493
3640
  }
2494
3641
  if (method === "DELETE") {
2495
3642
  await deps.subscriptionTokenWriter.clearProvider(providerId);
2496
- return writeJson(res, 200, { ok: true });
3643
+ return writeJson2(res, 200, { ok: true });
2497
3644
  }
2498
- const body = await readJsonBody(req);
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 writeJson(res, 200, status ? { account: status } : { ok: true });
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 writeJson(res, result.status, result.body);
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 writeJson(res, result.status, result.body);
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 writeJson(res, result.status, result.body);
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 writeJson(res, result.status, result.body);
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 readJsonBody(req);
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 writeJson(res, result.status, result.body);
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, import_outbound_api.loadServerConfig)(deps.settingsStore);
2551
- const endpoints = serverConfig.endpoints.map((e) => ({
2552
- endpoint: e.endpoint,
2553
- model: e.defaultModel,
2554
- useSubscription: e.useSubscription
2555
- }));
2556
- return writeJson(res, 200, { ...status, endpoints });
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 readJsonBody(req);
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
- console.error(
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
- console.info(`[AdminServer] Dashboard listening on ${bindAddr}:${actualPort}`);
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) => console.error("[AdminServer] server 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
- console.error("[AdminServer] unhandled error:", message);
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/ConsoleLogger.ts
3194
- var ConsoleLogger = class {
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
- if (meta === void 0) console.info(message);
3197
- else console.info(message, meta);
4444
+ this.emit("info", message, void 0, meta);
3198
4445
  }
3199
4446
  warn(message, meta) {
3200
- if (meta === void 0) console.warn(message);
3201
- else console.warn(message, meta);
4447
+ this.emit("warn", message, void 0, meta);
3202
4448
  }
3203
4449
  error(message, error, meta) {
3204
- if (error === void 0 && meta === void 0) console.error(message);
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
- if (meta === void 0) console.debug(message);
3210
- else console.debug(message, meta);
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 import_node_fs5 = require("fs");
3216
- var import_outbound_api2 = require("@omnicross/core/outbound-api");
4574
+ var import_node_fs6 = require("fs");
4575
+ var import_outbound_api3 = require("@omnicross/core/outbound-api");
3217
4576
  var JsonApiServerSettingsStore = class {
3218
- constructor(configPath) {
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 !== import_outbound_api2.OUTBOUND_API_SERVER_CONFIG_KEY) return void 0;
4592
+ if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return void 0;
3224
4593
  const file = this.readFile();
3225
- return file.server ?? void 0;
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 !== import_outbound_api2.OUTBOUND_API_SERVER_CONFIG_KEY) return;
4598
+ if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return;
3229
4599
  const file = this.readFile();
3230
- file.server = value;
3231
- (0, import_node_fs5.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
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, import_node_fs5.readFileSync)(this.configPath, "utf8");
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 import_node_fs6 = require("fs");
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, import_node_fs6.appendFileSync)(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
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, import_node_fs6.existsSync)(this.eventsPath)) return [];
4838
+ if (!(0, import_node_fs7.existsSync)(this.eventsPath)) return [];
3400
4839
  let raw;
3401
4840
  try {
3402
- raw = (0, import_node_fs6.readFileSync)(this.eventsPath, "utf8");
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 import_node_fs7 = require("fs");
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
- /** Apply `fn` to the row with `id`, persisting when it returns true. */
3503
- mutateRow(id, fn) {
3504
- const rows = this.readRows();
3505
- const row = rows.find((r) => r.id === id);
3506
- if (!row) return false;
3507
- const changed = fn(row);
3508
- if (changed) this.writeRows(rows);
3509
- return changed;
3510
- }
3511
- /** Read the key rows, tolerating a missing/corrupt file ( empty list). */
3512
- readRows() {
3513
- if (!(0, import_node_fs7.existsSync)(this.keysPath)) return [];
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, import_node_fs7.readFileSync)(this.keysPath, "utf8"));
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, import_node_fs7.writeFileSync)(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
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 import_node_fs8 = require("fs");
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, import_node_fs8.existsSync)(this.pricingPath)) return [];
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, import_node_fs8.readFileSync)(this.pricingPath, "utf8"));
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, import_node_fs8.writeFileSync)(this.pricingPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
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 import_node_fs11 = require("fs");
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 import_node_fs9 = require("fs");
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, import_node_fs9.existsSync)(path2)) return null;
5398
+ if (!(0, import_node_fs11.existsSync)(path2)) return null;
3784
5399
  let raw;
3785
5400
  try {
3786
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)(path2, "utf8"));
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 import_node_fs10 = require("fs");
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, import_node_fs10.existsSync)(path2)) return {};
5438
+ if (!(0, import_node_fs12.existsSync)(path2)) return {};
3824
5439
  try {
3825
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
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, import_node_fs10.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
5447
+ (0, import_node_fs12.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
3833
5448
  const temp = `${path2}.omnicross-tmp`;
3834
- (0, import_node_fs10.writeFileSync)(temp, content, "utf8");
3835
- (0, import_node_fs10.renameSync)(temp, path2);
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, import_node_fs10.existsSync)(path2)) return void 0;
5456
+ if (!(0, import_node_fs12.existsSync)(path2)) return void 0;
3842
5457
  try {
3843
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
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, import_node_fs10.existsSync)(storePath) && !(0, import_node_fs10.existsSync)(backupPath(provider, home))) {
3862
- (0, import_node_fs10.copyFileSync)(storePath, backupPath(provider, home));
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 round-trips
3878
- * (oauth design D4). Defaults to the global `fetch` so boot
3879
- * is unchanged; tests inject a mock fetch. NOT used by any
3880
- * read/write path only by `refresh*Token`.
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 = (url, init) => fetch(url, init), externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
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 > 0) out[provider] = this.attachSyncWarnings(config, provider, sanitized);
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, this.fetchImpl);
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, this.fetchImpl);
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, this.fetchImpl);
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, this.fetchImpl);
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, this.fetchImpl);
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.fetchImpl);
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, import_node_fs11.mkdirSync)((0, import_node_path7.dirname)(this.tokensPath), { recursive: true });
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, import_node_fs11.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
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, import_node_fs11.existsSync)(this.tokensPath)) return { updatedAt: "" };
6136
+ if (!(0, import_node_fs13.existsSync)(this.tokensPath)) return { updatedAt: "" };
4373
6137
  let parsed;
4374
6138
  try {
4375
- const raw = JSON.parse((0, import_node_fs11.readFileSync)(this.tokensPath, "utf8"));
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/TokenRefreshScheduler.ts
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
- var TokenRefreshScheduler = class {
4391
- constructor(store, logger, intervalMs = SWEEP_INTERVAL_MS, leadMs = REFRESH_LEAD_MS) {
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
- /** One sweep over every account of every OAuth provider. Exposed for tests. */
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 provider of OAUTH_PROVIDERS) {
4423
- const activeId = getActiveAccount(config, provider)?.id;
4424
- for (const account of listAccounts(config, provider)) {
4425
- if (!this.needsRefresh(account.tokens, now)) continue;
4426
- await this.refreshOne(provider, account.id, account.id === activeId);
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("token-refresh sweep failed", {
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 (the store has
4446
- * already flagged the account `expired`). */
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 = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
4450
- if (!ok) {
4451
- this.logger.warn("background token refresh failed", { provider, accountId: id });
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("background token refresh threw", {
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/bootstrap.ts
4476
- function buildDaemon(config, paths) {
4477
- const logger = new ConsoleLogger();
4478
- const secretBox3 = new SecretBox(() => resolveMasterKey({ keyFilePath: paths.masterKeyFilePath }));
4479
- setSecretBox(secretBox3);
4480
- setSecretBox2(secretBox3);
4481
- const decryptedConfig = decryptConfigSecrets(config, secretBox3);
4482
- const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
4483
- const keyDb = new JsonOutboundKeyDb(paths.keysPath);
4484
- const settingsStore = new JsonApiServerSettingsStore(paths.configPath);
4485
- const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
4486
- const subscriptionAccounts = new import_subscriptions4.SubscriptionAccountService(credentialStore);
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 usageRecorder = new import_usage.UsageRecorder(usageEventStore, pricingEngine, logger);
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 outboundApiServer = (0, import_outbound_api3.getOutboundApiServer)({
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
- oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetch(url, init)),
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, import_outbound_api3.__resetOutboundApiServerForTests)();
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,