@omnicross/daemon 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -293,6 +293,70 @@ var SecretBox = class {
293
293
  };
294
294
 
295
295
  // src/secrets/secretFields.ts
296
+ function urlHasInlineCredential(url) {
297
+ try {
298
+ const u = new URL(url);
299
+ return u.username.length > 0 || u.password.length > 0;
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
304
+ function transformProxyConfig(cfg, fn) {
305
+ if ("url" in cfg) {
306
+ if (isEnvelope(cfg.url) || urlHasInlineCredential(cfg.url)) {
307
+ return { url: fn(cfg.url) };
308
+ }
309
+ return cfg;
310
+ }
311
+ if (typeof cfg.password === "string" && cfg.password.length > 0) {
312
+ return { ...cfg, password: fn(cfg.password) };
313
+ }
314
+ return cfg;
315
+ }
316
+ function transformOutboundProxy(proxy, fn) {
317
+ const next = {};
318
+ if (proxy.global) next.global = transformProxyConfig(proxy.global, fn);
319
+ if (proxy.byProvider) {
320
+ const byProvider = {};
321
+ for (const [key, value] of Object.entries(proxy.byProvider)) {
322
+ byProvider[key] = transformProxyConfig(value, fn);
323
+ }
324
+ next.byProvider = byProvider;
325
+ }
326
+ return next;
327
+ }
328
+ function encryptProxySegment(proxy, box) {
329
+ return transformOutboundProxy(proxy, (v) => box.encryptMaybe(v));
330
+ }
331
+ function decryptProxySegment(proxy, box) {
332
+ return transformOutboundProxy(proxy, (v) => box.decryptMaybe(v));
333
+ }
334
+ function transformWebhookSegment(webhook, fn) {
335
+ return {
336
+ ...webhook,
337
+ destinations: webhook.destinations.map(
338
+ (d) => typeof d.secret === "string" && d.secret.length > 0 ? { ...d, secret: fn(d.secret) } : d
339
+ )
340
+ };
341
+ }
342
+ function encryptWebhookSegment(webhook, box) {
343
+ return transformWebhookSegment(webhook, (v) => box.encryptMaybe(v));
344
+ }
345
+ function decryptWebhookSegment(webhook, box) {
346
+ return transformWebhookSegment(webhook, (v) => box.decryptMaybe(v));
347
+ }
348
+ function transformBillingSegment(billing, fn) {
349
+ if (typeof billing.secret === "string" && billing.secret.length > 0) {
350
+ return { ...billing, secret: fn(billing.secret) };
351
+ }
352
+ return billing;
353
+ }
354
+ function encryptBillingSegment(billing, box) {
355
+ return transformBillingSegment(billing, (v) => box.encryptMaybe(v));
356
+ }
357
+ function decryptBillingSegment(billing, box) {
358
+ return transformBillingSegment(billing, (v) => box.decryptMaybe(v));
359
+ }
296
360
  function transformProvider(provider, fn) {
297
361
  const next = { ...provider, apiKey: fn(provider.apiKey) };
298
362
  if (provider.apiKeys) {
@@ -316,6 +380,17 @@ function transformConfigSecrets(cfg, fn) {
316
380
  if (cfg.admin && typeof cfg.admin.token === "string" && cfg.admin.token.length > 0) {
317
381
  next.admin = { ...cfg.admin, token: fn(cfg.admin.token) };
318
382
  }
383
+ const proxy = cfg.server?.proxy;
384
+ const webhook = cfg.server?.webhook;
385
+ const billing = cfg.server?.billing;
386
+ if (cfg.server && (proxy?.global || proxy?.byProvider || webhook || billing?.secret)) {
387
+ next.server = { ...cfg.server };
388
+ if (proxy && (proxy.global || proxy.byProvider)) {
389
+ next.server.proxy = transformOutboundProxy(proxy, fn);
390
+ }
391
+ if (webhook) next.server.webhook = transformWebhookSegment(webhook, fn);
392
+ if (billing?.secret) next.server.billing = transformBillingSegment(billing, fn);
393
+ }
319
394
  return next;
320
395
  }
321
396
  function encryptConfigSecrets(cfg, box) {
@@ -353,7 +428,7 @@ function transformTokens(tokens, fn) {
353
428
  if (Array.isArray(accounts)) {
354
429
  bag[accountsKey] = accounts.map((entry) => {
355
430
  if (entry && typeof entry === "object" && "tokens" in entry && entry.tokens && typeof entry.tokens === "object") {
356
- return {
431
+ const nextEntry = {
357
432
  ...entry,
358
433
  tokens: transformTokenBlock(
359
434
  entry.tokens,
@@ -361,6 +436,11 @@ function transformTokens(tokens, fn) {
361
436
  fn
362
437
  )
363
438
  };
439
+ const proxy = entry.proxy;
440
+ if (proxy && typeof proxy === "object") {
441
+ nextEntry.proxy = transformProxyConfig(proxy, fn);
442
+ }
443
+ return nextEntry;
364
444
  }
365
445
  return entry;
366
446
  });
@@ -395,6 +475,17 @@ function resolveAdminConfig(admin) {
395
475
  token: typeof admin?.token === "string" && admin.token.length > 0 ? admin.token : void 0
396
476
  };
397
477
  }
478
+ function validateLogging(raw) {
479
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
480
+ const l = raw;
481
+ const out = {};
482
+ if (l["level"] === "error" || l["level"] === "warn" || l["level"] === "info" || l["level"] === "debug") {
483
+ out.level = l["level"];
484
+ }
485
+ if (l["format"] === "text" || l["format"] === "json") out.format = l["format"];
486
+ if (typeof l["file"] === "string" && l["file"].length > 0) out.file = l["file"];
487
+ return out.level !== void 0 || out.format !== void 0 || out.file !== void 0 ? out : void 0;
488
+ }
398
489
  var VALID_FORMATS = ["openai", "anthropic", "gemini"];
399
490
  function validateApiKeys(raw) {
400
491
  if (!Array.isArray(raw)) return void 0;
@@ -569,7 +660,8 @@ function validateConfig(raw) {
569
660
  const providers = providersRaw.map((p, i) => validateProvider(p, i));
570
661
  const server = obj["server"];
571
662
  const admin = validateAdmin(obj["admin"]);
572
- return { providers, server, admin };
663
+ const logging = validateLogging(obj["logging"]);
664
+ return { providers, server, admin, logging };
573
665
  }
574
666
  var secretBox = null;
575
667
  function setSecretBox(box) {
@@ -601,6 +693,9 @@ var import_node_path2 = require("path");
601
693
  function defaultKeysPath(configPath) {
602
694
  return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "keys.json");
603
695
  }
696
+ function defaultVouchersPath(configPath) {
697
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "vouchers.json");
698
+ }
604
699
  function defaultTokensPath(configPath) {
605
700
  return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "tokens.json");
606
701
  }
@@ -610,6 +705,12 @@ function defaultPricingPath(configPath) {
610
705
  function defaultUsageEventsPath(configPath) {
611
706
  return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "usage-events.jsonl");
612
707
  }
708
+ function defaultAuditDir(configPath) {
709
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "audit");
710
+ }
711
+ function defaultBillingDir(configPath) {
712
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "billing");
713
+ }
613
714
  function resolveSecretBox(masterKeyFilePath) {
614
715
  return new SecretBox(() => resolveMasterKey({ keyFilePath: masterKeyFilePath }));
615
716
  }
@@ -709,6 +810,45 @@ var JsonOutboundKeyDb = class {
709
810
  return true;
710
811
  });
711
812
  }
813
+ async outboundApiKeysSetMaxConcurrency(id, maxConcurrency) {
814
+ return this.mutateRow(id, (row) => {
815
+ if (row.revokedAt !== null) return false;
816
+ if (maxConcurrency === null) delete row.maxConcurrency;
817
+ else row.maxConcurrency = maxConcurrency;
818
+ return true;
819
+ });
820
+ }
821
+ async outboundApiKeysSetPolicy(id, policy) {
822
+ return this.mutateRow(id, (row) => {
823
+ if (row.revokedAt !== null) return false;
824
+ applyPolicyField(row, "expiresAt", policy.expiresAt);
825
+ applyPolicyField(row, "activationDays", policy.activationDays);
826
+ applyPolicyField(row, "dailyCostLimitUsd", policy.dailyCostLimitUsd);
827
+ applyPolicyField(row, "totalCostLimitUsd", policy.totalCostLimitUsd);
828
+ applyPolicyField(row, "weeklyCostLimitUsd", policy.weeklyCostLimitUsd);
829
+ applyPolicyField(row, "rateLimitMaxRequests", policy.rateLimitMaxRequests);
830
+ applyPolicyField(row, "rateLimitWindowMs", policy.rateLimitWindowMs);
831
+ if (policy.activationMode === null) delete row.activationMode;
832
+ else if (policy.activationMode !== void 0) row.activationMode = policy.activationMode;
833
+ if (policy.enableModelRestriction === null) delete row.enableModelRestriction;
834
+ else if (policy.enableModelRestriction !== void 0) {
835
+ row.enableModelRestriction = policy.enableModelRestriction;
836
+ }
837
+ if (policy.restrictionMode === null) delete row.restrictionMode;
838
+ else if (policy.restrictionMode !== void 0) row.restrictionMode = policy.restrictionMode;
839
+ if (policy.restrictedModels === null) delete row.restrictedModels;
840
+ else if (policy.restrictedModels !== void 0) row.restrictedModels = policy.restrictedModels;
841
+ return true;
842
+ });
843
+ }
844
+ async outboundApiKeysMarkActivated(id, activatedAt) {
845
+ return this.mutateRow(id, (row) => {
846
+ if (row.revokedAt !== null) return false;
847
+ if (row.activatedAt != null) return false;
848
+ row.activatedAt = activatedAt;
849
+ return true;
850
+ });
851
+ }
712
852
  /** Apply `fn` to the row with `id`, persisting when it returns true. */
713
853
  mutateRow(id, fn) {
714
854
  const rows = this.readRows();
@@ -732,6 +872,11 @@ var JsonOutboundKeyDb = class {
732
872
  (0, import_node_fs4.writeFileSync)(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
733
873
  }
734
874
  };
875
+ function applyPolicyField(row, field, value) {
876
+ if (value === void 0) return;
877
+ if (value === null) delete row[field];
878
+ else row[field] = value;
879
+ }
735
880
 
736
881
  // src/commands/keys.ts
737
882
  async function runKeys(argv) {
@@ -788,18 +933,25 @@ async function keysRevoke(db, id) {
788
933
 
789
934
  // src/commands/launch.ts
790
935
  var import_node_child_process2 = require("child_process");
791
- var import_node_fs13 = require("fs");
792
- var import_node_path8 = require("path");
936
+ var import_node_fs21 = require("fs");
937
+ var import_node_path13 = require("path");
793
938
  var import_node_util3 = require("util");
794
939
  var import_cli_launcher2 = require("@omnicross/cli-launcher");
795
940
 
796
941
  // src/bootstrap.ts
942
+ var import_node_fs20 = require("fs");
943
+ var import_audit_types = require("@omnicross/contracts/audit-types");
944
+ var import_billing_types = require("@omnicross/contracts/billing-types");
797
945
  var import_GeminiCodeAssistProjectResolver = require("@omnicross/core/auth/GeminiCodeAssistProjectResolver");
798
946
  var import_ApiKeyPoolService = require("@omnicross/core/completion/ApiKeyPoolService");
799
- var import_outbound_api4 = require("@omnicross/core/outbound-api");
947
+ var import_outbound_api5 = require("@omnicross/core/outbound-api");
800
948
  var import_subscriptionRegistryPort = require("@omnicross/core/outbound-api/subscriptionRegistryPort");
949
+ var import_SubscriptionAccountHealth2 = require("@omnicross/core/pipeline/SubscriptionAccountHealth");
950
+ var import_upstreamFetch7 = require("@omnicross/core/pipeline/upstreamFetch");
951
+ var import_SubscriptionIdentityStore2 = require("@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore");
801
952
  var import_gemini_code_assist_resolver = require("@omnicross/core/ports/gemini-code-assist-resolver");
802
953
  var import_provider_proxy = require("@omnicross/core/provider-proxy");
954
+ var import_outbound_api6 = require("@omnicross/core/outbound-api");
803
955
  var import_usage = require("@omnicross/core/usage");
804
956
  var import_subscriptions4 = require("@omnicross/subscriptions");
805
957
 
@@ -897,10 +1049,129 @@ function handleCodexOAuthStatus(sessionId, deps) {
897
1049
  // src/admin/AdminServer.ts
898
1050
  var import_node_crypto7 = require("crypto");
899
1051
  var import_node_http2 = __toESM(require("http"), 1);
1052
+ var import_health_logging_types = require("@omnicross/contracts/health-logging-types");
1053
+
1054
+ // src/admin/accountProbesApi.ts
1055
+ function handleAccountProbes(res, reader) {
1056
+ const accounts = reader ? reader.getAllHistory() : [];
1057
+ res.writeHead(200, { "Content-Type": "application/json" });
1058
+ res.end(JSON.stringify({ accounts }));
1059
+ }
1060
+
1061
+ // src/admin/auditQueryApi.ts
1062
+ function intParam(value) {
1063
+ if (value === null || value.trim() === "") return void 0;
1064
+ const n = Number(value);
1065
+ return Number.isFinite(n) ? Math.trunc(n) : void 0;
1066
+ }
1067
+ function handleAuditQuery(req, res, reader) {
1068
+ const url = new URL(req.url ?? "/", "http://localhost");
1069
+ const query = {};
1070
+ const keyId = url.searchParams.get("keyId");
1071
+ if (keyId && keyId.trim()) query.keyId = keyId.trim();
1072
+ const from = intParam(url.searchParams.get("from"));
1073
+ if (from !== void 0) query.from = from;
1074
+ const to = intParam(url.searchParams.get("to"));
1075
+ if (to !== void 0) query.to = to;
1076
+ const limit = intParam(url.searchParams.get("limit"));
1077
+ if (limit !== void 0) query.limit = limit;
1078
+ const records = reader ? reader(query) : [];
1079
+ res.writeHead(200, { "Content-Type": "application/json" });
1080
+ res.end(JSON.stringify({ records }));
1081
+ }
1082
+
1083
+ // src/admin/billingStatusApi.ts
1084
+ function handleBillingStatus(res, reader) {
1085
+ const status = reader ? reader() : { total: 0, delivered: 0, pending: 0 };
1086
+ res.writeHead(200, { "Content-Type": "application/json" });
1087
+ res.end(JSON.stringify({ status }));
1088
+ }
1089
+
1090
+ // src/webhook/webhookRuntime.ts
1091
+ var import_webhookEmit = require("@omnicross/core/pipeline/webhookEmit");
1092
+ var dispatcher = null;
1093
+ var health = null;
1094
+ var unsubscribers = [];
1095
+ var wired = false;
1096
+ function setWebhookRuntime(d, h) {
1097
+ dispatcher = d;
1098
+ health = h;
1099
+ }
1100
+ function applyWebhookConfig(config) {
1101
+ if (!dispatcher) return;
1102
+ dispatcher.setConfig(config);
1103
+ const shouldWire = config?.enabled === true && (config?.destinations.length ?? 0) > 0;
1104
+ if (shouldWire && !wired) {
1105
+ const active = dispatcher;
1106
+ (0, import_webhookEmit.setWebhookSink)((event) => active.emit(event));
1107
+ if (health) {
1108
+ unsubscribers.push(
1109
+ health.onRecovered(
1110
+ (e) => active.emit({ kind: "account.recovery", at: e.at, providerId: e.providerId, accountId: e.accountId })
1111
+ )
1112
+ );
1113
+ unsubscribers.push(
1114
+ health.onAnomaly(
1115
+ (e) => active.emit({
1116
+ kind: "account.anomaly",
1117
+ at: e.at,
1118
+ providerId: e.providerId,
1119
+ accountId: e.accountId,
1120
+ state: e.state
1121
+ })
1122
+ )
1123
+ );
1124
+ }
1125
+ wired = true;
1126
+ } else if (!shouldWire && wired) {
1127
+ teardown();
1128
+ }
1129
+ }
1130
+ async function deliverWebhookTest(destinationId) {
1131
+ if (!dispatcher) return { ok: false, error: "webhook dispatcher not wired" };
1132
+ return dispatcher.deliverTest(destinationId);
1133
+ }
1134
+ function teardown() {
1135
+ (0, import_webhookEmit.setWebhookSink)(null);
1136
+ for (const unsub of unsubscribers) unsub();
1137
+ unsubscribers = [];
1138
+ wired = false;
1139
+ }
1140
+
1141
+ // src/admin/webhookTestApi.ts
1142
+ function readJsonBody(req) {
1143
+ return new Promise((resolve) => {
1144
+ const chunks = [];
1145
+ req.on("data", (c) => chunks.push(c));
1146
+ req.on("end", () => {
1147
+ try {
1148
+ const raw = Buffer.concat(chunks).toString("utf8");
1149
+ const parsed = raw ? JSON.parse(raw) : {};
1150
+ resolve(parsed && typeof parsed === "object" ? parsed : {});
1151
+ } catch {
1152
+ resolve({});
1153
+ }
1154
+ });
1155
+ req.on("error", () => resolve({}));
1156
+ });
1157
+ }
1158
+ async function handleWebhookTest(req, res) {
1159
+ const body = await readJsonBody(req);
1160
+ const destinationId = body["destinationId"];
1161
+ if (typeof destinationId !== "string" || !destinationId.trim()) {
1162
+ res.writeHead(400, { "Content-Type": "application/json" });
1163
+ res.end(JSON.stringify({ error: { type: "bad_request", message: "destinationId is required" } }));
1164
+ return;
1165
+ }
1166
+ const result = await deliverWebhookTest(destinationId.trim());
1167
+ res.writeHead(200, { "Content-Type": "application/json" });
1168
+ res.end(JSON.stringify({ result }));
1169
+ }
900
1170
 
901
1171
  // src/admin/adminApi.ts
902
1172
  var import_node_http = __toESM(require("http"), 1);
903
- var import_outbound_api2 = require("@omnicross/core/outbound-api");
1173
+ var import_outbound_api3 = require("@omnicross/core/outbound-api");
1174
+ var import_upstreamFetch2 = require("@omnicross/core/pipeline/upstreamFetch");
904
1175
 
905
1176
  // src/pool/resolveEnvKey.ts
906
1177
  function resolveEnvKey(rawKey) {
@@ -995,6 +1266,161 @@ function listMappablePresets() {
995
1266
  return { mappable, excluded };
996
1267
  }
997
1268
 
1269
+ // src/proxy/sanitizeProxy.ts
1270
+ function sanitizeProxyConfig(cfg) {
1271
+ if ("url" in cfg) {
1272
+ let endpoint;
1273
+ let username;
1274
+ let hasPassword = false;
1275
+ try {
1276
+ const u = new URL(cfg.url);
1277
+ endpoint = u.port ? `${u.hostname}:${u.port}` : u.hostname;
1278
+ username = u.username ? decodeURIComponent(u.username) : void 0;
1279
+ hasPassword = u.password.length > 0;
1280
+ } catch {
1281
+ }
1282
+ return { kind: "url", endpoint, username, hasPassword };
1283
+ }
1284
+ return {
1285
+ kind: cfg.type,
1286
+ endpoint: `${cfg.host}:${cfg.port}`,
1287
+ username: cfg.username,
1288
+ hasPassword: typeof cfg.password === "string" && cfg.password.length > 0
1289
+ };
1290
+ }
1291
+ function redactProxyConfig(cfg) {
1292
+ if ("url" in cfg) {
1293
+ try {
1294
+ const u = new URL(cfg.url);
1295
+ if (u.password) u.password = "";
1296
+ return { url: u.toString() };
1297
+ } catch {
1298
+ return cfg;
1299
+ }
1300
+ }
1301
+ const { password: _password, ...rest } = cfg;
1302
+ return rest;
1303
+ }
1304
+ function redactOutboundProxy(proxy) {
1305
+ const out = {};
1306
+ if (proxy.global) out.global = redactProxyConfig(proxy.global);
1307
+ if (proxy.byProvider) {
1308
+ const byProvider = {};
1309
+ for (const [key, value] of Object.entries(proxy.byProvider)) {
1310
+ byProvider[key] = redactProxyConfig(value);
1311
+ }
1312
+ out.byProvider = byProvider;
1313
+ }
1314
+ return out;
1315
+ }
1316
+ function preserveProxyConfigSecret(incoming, current) {
1317
+ if (!current) return incoming;
1318
+ if ("url" in incoming) {
1319
+ if ("url" in current) {
1320
+ try {
1321
+ const inU = new URL(incoming.url);
1322
+ const curU = new URL(current.url);
1323
+ if (!inU.password && curU.password) {
1324
+ inU.password = curU.password;
1325
+ return { url: inU.toString() };
1326
+ }
1327
+ } catch {
1328
+ }
1329
+ }
1330
+ return incoming;
1331
+ }
1332
+ if ("url" in current) return incoming;
1333
+ const blank = incoming.password === void 0 || incoming.password === "";
1334
+ if (blank && typeof current.password === "string" && current.password.length > 0) {
1335
+ return { ...incoming, password: current.password };
1336
+ }
1337
+ return incoming;
1338
+ }
1339
+ function preserveOutboundProxySecrets(incoming, current) {
1340
+ const out = {};
1341
+ if (incoming.global) out.global = preserveProxyConfigSecret(incoming.global, current?.global);
1342
+ if (incoming.byProvider) {
1343
+ const byProvider = {};
1344
+ for (const [key, value] of Object.entries(incoming.byProvider)) {
1345
+ byProvider[key] = preserveProxyConfigSecret(value, current?.byProvider?.[key]);
1346
+ }
1347
+ out.byProvider = byProvider;
1348
+ }
1349
+ return out;
1350
+ }
1351
+
1352
+ // src/proxy/upstreamProxyResolver.ts
1353
+ var import_upstreamFetch = require("@omnicross/core/pipeline/upstreamFetch");
1354
+ var serverProxy;
1355
+ function setServerProxyConfig(proxy) {
1356
+ serverProxy = proxy;
1357
+ (0, import_upstreamFetch.bumpUpstreamProxyGeneration)();
1358
+ }
1359
+ function getServerProxyConfig() {
1360
+ return serverProxy;
1361
+ }
1362
+ var envProxyLoggedFor;
1363
+ function maskProxyUrl(url) {
1364
+ return url.replace(/\/\/[^/@]*@/, "//***@");
1365
+ }
1366
+ function hostFromCtx(ctx) {
1367
+ if (!ctx.url) return void 0;
1368
+ try {
1369
+ return new URL(ctx.url).hostname.toLowerCase();
1370
+ } catch {
1371
+ return void 0;
1372
+ }
1373
+ }
1374
+ function isLoopbackHost(host) {
1375
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || host.startsWith("127.") || host.endsWith(".localhost");
1376
+ }
1377
+ function noProxyMatches(noProxy, host) {
1378
+ if (!noProxy) return false;
1379
+ for (const raw of noProxy.split(",")) {
1380
+ const entry = raw.trim().toLowerCase();
1381
+ if (!entry) continue;
1382
+ if (entry === "*") return true;
1383
+ const bare = entry.startsWith(".") ? entry.slice(1) : entry;
1384
+ if (host === bare || host.endsWith(`.${bare}`)) return true;
1385
+ }
1386
+ return false;
1387
+ }
1388
+ function resolveEnvProxy(ctx, env = process.env) {
1389
+ const raw = env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? env.HTTP_PROXY ?? env.http_proxy;
1390
+ if (!raw || !raw.trim()) return void 0;
1391
+ const host = hostFromCtx(ctx);
1392
+ if (host && (isLoopbackHost(host) || noProxyMatches(env.NO_PROXY ?? env.no_proxy, host))) {
1393
+ return void 0;
1394
+ }
1395
+ const url = raw.trim();
1396
+ if (envProxyLoggedFor !== url) {
1397
+ envProxyLoggedFor = url;
1398
+ console.info(`[upstream-proxy] routing upstream egress through the environment proxy: ${maskProxyUrl(url)}`);
1399
+ }
1400
+ return { url };
1401
+ }
1402
+ function createUpstreamProxyResolver(src = {}) {
1403
+ const readServer = src.getServerProxy ?? getServerProxyConfig;
1404
+ return (ctx) => {
1405
+ const host = hostFromCtx(ctx);
1406
+ if (host) {
1407
+ if (isLoopbackHost(host)) return void 0;
1408
+ const env = src.env ?? process.env;
1409
+ if (noProxyMatches(env.NO_PROXY ?? env.no_proxy, host)) return void 0;
1410
+ }
1411
+ if (src.getAccountProxy && ctx.providerId && ctx.accountId) {
1412
+ const account = src.getAccountProxy(ctx.providerId, ctx.accountId);
1413
+ if (account) return account;
1414
+ }
1415
+ const server = readServer();
1416
+ if (ctx.providerId && server?.byProvider?.[ctx.providerId]) {
1417
+ return server.byProvider[ctx.providerId];
1418
+ }
1419
+ if (server?.global) return server.global;
1420
+ return resolveEnvProxy(ctx, src.env);
1421
+ };
1422
+ }
1423
+
998
1424
  // src/admin/accountsOAuth.ts
999
1425
  var import_subscriptions2 = require("@omnicross/subscriptions");
1000
1426
 
@@ -1117,6 +1543,24 @@ function validateTokenBody(providerId, body) {
1117
1543
  return null;
1118
1544
  }
1119
1545
  }
1546
+ function validateSupportedModelsBody(raw) {
1547
+ if (raw === null || raw === void 0) return { ok: true, value: void 0 };
1548
+ if (Array.isArray(raw)) {
1549
+ if (raw.length === 0) return { ok: false };
1550
+ if (!raw.every((x) => typeof x === "string" && x.trim().length > 0)) return { ok: false };
1551
+ return { ok: true, value: raw };
1552
+ }
1553
+ if (typeof raw === "object") {
1554
+ const entries = Object.entries(raw);
1555
+ if (entries.length === 0) return { ok: false };
1556
+ const valid = entries.every(
1557
+ ([k, v]) => k.trim().length > 0 && typeof v === "string" && v.trim().length > 0
1558
+ );
1559
+ if (!valid) return { ok: false };
1560
+ return { ok: true, value: Object.fromEntries(entries) };
1561
+ }
1562
+ return { ok: false };
1563
+ }
1120
1564
  async function statusEntryFor(reader, providerId) {
1121
1565
  const all = await reader.listAll();
1122
1566
  return all.find((a) => a.providerId === providerId) ?? null;
@@ -1393,6 +1837,424 @@ async function handleCliLaunch(cli, body, ctx) {
1393
1837
  return { status: 200, body: { sessionId: id, providerId: target.providerId, model: target.model } };
1394
1838
  }
1395
1839
 
1840
+ // src/admin/auditConfigBody.ts
1841
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1842
+ function validateAuditSegment(patch) {
1843
+ const errors = [];
1844
+ const audit = patch.audit;
1845
+ if (audit === void 0) return errors;
1846
+ if (!isPlainObject(audit)) {
1847
+ errors.push("audit must be an object");
1848
+ return errors;
1849
+ }
1850
+ for (const flag of ["enabled", "captureBodies", "trustForwardedFor"]) {
1851
+ if (audit[flag] !== void 0 && typeof audit[flag] !== "boolean") {
1852
+ errors.push(`audit.${flag} must be a boolean`);
1853
+ }
1854
+ }
1855
+ const maxBodyBytes = audit["maxBodyBytes"];
1856
+ if (maxBodyBytes !== void 0 && (typeof maxBodyBytes !== "number" || !Number.isFinite(maxBodyBytes) || maxBodyBytes < 0)) {
1857
+ errors.push("audit.maxBodyBytes must be a non-negative number");
1858
+ }
1859
+ const retentionDays = audit["retentionDays"];
1860
+ if (retentionDays !== void 0 && (typeof retentionDays !== "number" || !Number.isFinite(retentionDays) || retentionDays < 0)) {
1861
+ errors.push("audit.retentionDays must be a non-negative number");
1862
+ }
1863
+ return errors;
1864
+ }
1865
+
1866
+ // src/admin/billingConfigBody.ts
1867
+ var BILLING_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
1868
+ var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1869
+ function validateBillingSegment(patch) {
1870
+ const errors = [];
1871
+ const billing = patch.billing;
1872
+ if (billing === void 0) return errors;
1873
+ if (!isPlainObject2(billing)) {
1874
+ errors.push("billing must be an object");
1875
+ return errors;
1876
+ }
1877
+ if (billing["enabled"] !== void 0 && typeof billing["enabled"] !== "boolean") {
1878
+ errors.push("billing.enabled must be a boolean");
1879
+ }
1880
+ if (billing["endpoint"] !== void 0 && typeof billing["endpoint"] !== "string") {
1881
+ errors.push("billing.endpoint must be a string");
1882
+ }
1883
+ if (billing["secret"] !== void 0 && typeof billing["secret"] !== "string") {
1884
+ errors.push("billing.secret must be a string");
1885
+ }
1886
+ const maxRetryAgeMs = billing["maxRetryAgeMs"];
1887
+ if (maxRetryAgeMs !== void 0 && (typeof maxRetryAgeMs !== "number" || !Number.isFinite(maxRetryAgeMs) || maxRetryAgeMs < 0)) {
1888
+ errors.push("billing.maxRetryAgeMs must be a non-negative number");
1889
+ }
1890
+ return errors;
1891
+ }
1892
+ function redactBillingConfig(billing) {
1893
+ if (typeof billing.secret === "string" && billing.secret.length > 0) {
1894
+ return { ...billing, secret: BILLING_SECRET_MASK };
1895
+ }
1896
+ return billing;
1897
+ }
1898
+ function preserveBillingSecret(incoming, current) {
1899
+ const isMaskedOrBlank = incoming.secret === void 0 || incoming.secret === "" || incoming.secret === BILLING_SECRET_MASK;
1900
+ if (isMaskedOrBlank) {
1901
+ if (current?.secret) return { ...incoming, secret: current.secret };
1902
+ const { secret: _secret, ...rest } = incoming;
1903
+ return rest;
1904
+ }
1905
+ return incoming;
1906
+ }
1907
+
1908
+ // src/admin/dashboard.ts
1909
+ function startOfLocalDayMs(ts) {
1910
+ const d = new Date(ts);
1911
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
1912
+ }
1913
+ function accountProviderId(entry) {
1914
+ if (!entry || typeof entry !== "object") return null;
1915
+ const e = entry;
1916
+ if (typeof e["providerId"] === "string" && e["providerId"]) return e["providerId"];
1917
+ if (typeof e["provider"] === "string" && e["provider"]) return e["provider"];
1918
+ return null;
1919
+ }
1920
+ async function handleDashboard(deps) {
1921
+ const now = Date.now();
1922
+ const today = await deps.usageRecorder.getTotals({ startTs: startOfLocalDayMs(now), endTs: now });
1923
+ const total = await deps.usageRecorder.getTotals({ startTs: 0, endTs: now });
1924
+ const providerList = loadConfig(deps.configPath).providers;
1925
+ const providers = {
1926
+ total: providerList.length,
1927
+ enabled: providerList.filter((p) => p.enabled !== false).length
1928
+ };
1929
+ const keys = await deps.keyDb.outboundApiKeysList();
1930
+ const outboundKeys = {
1931
+ total: keys.length,
1932
+ active: keys.filter((k) => k.enabled && k.revokedAt === null).length
1933
+ };
1934
+ const accountsList = await deps.subscriptionAccounts.listAll();
1935
+ const byProvider = {};
1936
+ for (const entry of accountsList) {
1937
+ const providerId = accountProviderId(entry);
1938
+ if (providerId) byProvider[providerId] = (byProvider[providerId] ?? 0) + 1;
1939
+ }
1940
+ const accounts = { total: accountsList.length, byProvider };
1941
+ const status = deps.outboundApiServer.getStatus();
1942
+ const server = {
1943
+ running: status.running,
1944
+ port: status.port,
1945
+ uptimeMs: Math.round(process.uptime() * 1e3)
1946
+ };
1947
+ const summary = { today, total, providers, outboundKeys, accounts, server, generatedAt: now };
1948
+ return { status: 200, body: summary };
1949
+ }
1950
+
1951
+ // src/admin/keyPolicyBody.ts
1952
+ function parseKeyPolicyBody(body) {
1953
+ const policy = {};
1954
+ if ("activationMode" in body) {
1955
+ const m = body["activationMode"];
1956
+ if (m === null) policy.activationMode = null;
1957
+ else if (m === "fixed" || m === "activation") policy.activationMode = m;
1958
+ else return { ok: false, message: "activationMode must be 'fixed', 'activation', or null" };
1959
+ }
1960
+ const numericFields = [
1961
+ { key: "expiresAt", min: 0 },
1962
+ { key: "activationDays", min: 1, integer: true },
1963
+ { key: "dailyCostLimitUsd", min: 0 },
1964
+ { key: "totalCostLimitUsd", min: 0 },
1965
+ { key: "weeklyCostLimitUsd", min: 0 },
1966
+ { key: "rateLimitMaxRequests", min: 0, integer: true },
1967
+ { key: "rateLimitWindowMs", min: 1 }
1968
+ ];
1969
+ for (const { key, min, integer } of numericFields) {
1970
+ if (!(key in body)) continue;
1971
+ const v = body[key];
1972
+ if (v === null) {
1973
+ policy[key] = null;
1974
+ continue;
1975
+ }
1976
+ if (typeof v !== "number" || !Number.isFinite(v) || v < min || integer && !Number.isInteger(v)) {
1977
+ return {
1978
+ ok: false,
1979
+ message: `${key} must be ${integer ? "an integer" : "a number"} >= ${min} or null`
1980
+ };
1981
+ }
1982
+ policy[key] = v;
1983
+ }
1984
+ if ("enableModelRestriction" in body) {
1985
+ const v = body["enableModelRestriction"];
1986
+ if (v === null) policy.enableModelRestriction = null;
1987
+ else if (typeof v === "boolean") policy.enableModelRestriction = v;
1988
+ else return { ok: false, message: "enableModelRestriction must be a boolean or null" };
1989
+ }
1990
+ if ("restrictionMode" in body) {
1991
+ const v = body["restrictionMode"];
1992
+ if (v === null) policy.restrictionMode = null;
1993
+ else if (v === "blacklist" || v === "allowlist") policy.restrictionMode = v;
1994
+ else return { ok: false, message: "restrictionMode must be 'blacklist', 'allowlist', or null" };
1995
+ }
1996
+ if ("restrictedModels" in body) {
1997
+ const v = body["restrictedModels"];
1998
+ if (v === null) {
1999
+ policy.restrictedModels = null;
2000
+ } else if (Array.isArray(v) && v.every((e) => typeof e === "string")) {
2001
+ policy.restrictedModels = v.map((e) => e.trim()).filter((e) => e !== "");
2002
+ } else {
2003
+ return { ok: false, message: "restrictedModels must be an array of strings or null" };
2004
+ }
2005
+ }
2006
+ return { ok: true, policy };
2007
+ }
2008
+
2009
+ // src/admin/voucherAdmin.ts
2010
+ var import_outbound_api2 = require("@omnicross/core/outbound-api");
2011
+ function writeJson(res, status, body) {
2012
+ res.writeHead(status, { "Content-Type": "application/json" });
2013
+ res.end(JSON.stringify(body));
2014
+ }
2015
+ function writeErr(res, status, message) {
2016
+ writeJson(res, status, { error: { type: "voucher_error", message } });
2017
+ }
2018
+ function readJsonBody2(req) {
2019
+ return new Promise((resolve, reject) => {
2020
+ const chunks = [];
2021
+ req.on("data", (c) => chunks.push(c));
2022
+ req.on("end", () => {
2023
+ const raw = Buffer.concat(chunks).toString("utf8");
2024
+ if (!raw.trim()) return resolve({});
2025
+ try {
2026
+ const parsed = JSON.parse(raw);
2027
+ resolve(parsed && typeof parsed === "object" ? parsed : {});
2028
+ } catch {
2029
+ reject(new Error("invalid-json"));
2030
+ }
2031
+ });
2032
+ req.on("error", reject);
2033
+ });
2034
+ }
2035
+ function optPositive(value, integer) {
2036
+ if (value === void 0 || value === null) return { ok: true, value: void 0 };
2037
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return { ok: false };
2038
+ if (integer && !Number.isInteger(value)) return { ok: false };
2039
+ return { ok: true, value };
2040
+ }
2041
+ function parseVoucherCreateBody(body) {
2042
+ const type = body["type"];
2043
+ if (type !== "credit" && type !== "renewal") {
2044
+ return { ok: false, message: "type must be 'credit' or 'renewal'" };
2045
+ }
2046
+ const maxTotal = optPositive(body["maxTotalCostLimitUsd"], false);
2047
+ if (!maxTotal.ok) return { ok: false, message: "maxTotalCostLimitUsd must be a positive number" };
2048
+ const maxDays = optPositive(body["maxExpiryDays"], true);
2049
+ if (!maxDays.ok) return { ok: false, message: "maxExpiryDays must be a positive integer" };
2050
+ const input = { type };
2051
+ if (maxTotal.value !== void 0) input.maxTotalCostLimitUsd = maxTotal.value;
2052
+ if (maxDays.value !== void 0) input.maxExpiryDays = maxDays.value;
2053
+ if (type === "credit") {
2054
+ const credit = optPositive(body["creditUsd"], false);
2055
+ if (!credit.ok || credit.value === void 0) {
2056
+ return { ok: false, message: "creditUsd must be a positive number for a credit card" };
2057
+ }
2058
+ input.creditUsd = credit.value;
2059
+ } else {
2060
+ const days = optPositive(body["renewalDays"], true);
2061
+ if (!days.ok || days.value === void 0) {
2062
+ return { ok: false, message: "renewalDays must be a positive integer for a renewal card" };
2063
+ }
2064
+ input.renewalDays = days.value;
2065
+ }
2066
+ return { ok: true, input };
2067
+ }
2068
+ async function voucherEnabled(deps) {
2069
+ const config = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
2070
+ return config.voucher?.enabled === true;
2071
+ }
2072
+ async function handleVoucher(req, res, method, rest, deps) {
2073
+ const voucherDb = deps.voucherDb;
2074
+ if (!voucherDb) return writeErr(res, 501, "Voucher feature is not available");
2075
+ if (method === "GET" && rest.length === 0) {
2076
+ const rows = await voucherDb.voucherList();
2077
+ return writeJson(res, 200, { vouchers: rows.map(import_outbound_api2.toVoucherInfo) });
2078
+ }
2079
+ if (method === "POST" && rest.length === 0) {
2080
+ if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
2081
+ let body;
2082
+ try {
2083
+ body = await readJsonBody2(req);
2084
+ } catch {
2085
+ return writeErr(res, 400, "Invalid JSON in request body");
2086
+ }
2087
+ const parsed = parseVoucherCreateBody(body);
2088
+ if (!parsed.ok) return writeErr(res, 400, parsed.message);
2089
+ const code = (0, import_outbound_api2.generateVoucherCode)();
2090
+ const created = await voucherDb.voucherCreate({
2091
+ id: (0, import_outbound_api2.newVoucherId)(),
2092
+ codeHash: (0, import_outbound_api2.hashVoucherCode)(code),
2093
+ codePrefix: (0, import_outbound_api2.voucherCodePrefix)(code),
2094
+ ...parsed.input
2095
+ });
2096
+ return writeJson(res, 201, {
2097
+ id: created.id,
2098
+ codePrefix: created.codePrefix,
2099
+ type: created.type,
2100
+ createdAt: created.createdAt,
2101
+ // `plaintextOnce` is the ONLY place the full code crosses the wire (D3).
2102
+ plaintextOnce: code
2103
+ });
2104
+ }
2105
+ const id = rest[0];
2106
+ if (method === "POST" && id && rest[1] === "revoke") {
2107
+ if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
2108
+ const ok = await voucherDb.voucherRevokeCas(id, Date.now());
2109
+ return writeJson(res, ok ? 200 : 409, { ok });
2110
+ }
2111
+ return writeErr(res, 405, `method ${method} not allowed on voucher`);
2112
+ }
2113
+
2114
+ // src/admin/webhookConfigBody.ts
2115
+ var import_webhook_types = require("@omnicross/contracts/webhook-types");
2116
+ var WEBHOOK_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
2117
+ var isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2118
+ function validateWebhookSegment(patch) {
2119
+ const errors = [];
2120
+ const webhook = patch.webhook;
2121
+ if (webhook === void 0) return errors;
2122
+ if (!isPlainObject3(webhook)) {
2123
+ errors.push("webhook must be an object");
2124
+ return errors;
2125
+ }
2126
+ if (typeof webhook["enabled"] !== "boolean") {
2127
+ errors.push("webhook.enabled must be a boolean");
2128
+ }
2129
+ const destinations = webhook["destinations"];
2130
+ if (destinations !== void 0 && !Array.isArray(destinations)) {
2131
+ errors.push("webhook.destinations must be an array");
2132
+ return errors;
2133
+ }
2134
+ const seenIds = /* @__PURE__ */ new Set();
2135
+ for (const [i, raw] of (Array.isArray(destinations) ? destinations : []).entries()) {
2136
+ if (!isPlainObject3(raw)) {
2137
+ errors.push(`webhook.destinations[${i}] must be an object`);
2138
+ continue;
2139
+ }
2140
+ const id = raw["id"];
2141
+ if (typeof id !== "string" || !id.trim()) {
2142
+ errors.push(`webhook.destinations[${i}].id must be a non-empty string`);
2143
+ } else if (seenIds.has(id.trim())) {
2144
+ errors.push(`webhook.destinations[${i}].id '${id.trim()}' is duplicated`);
2145
+ } else {
2146
+ seenIds.add(id.trim());
2147
+ }
2148
+ if (typeof raw["type"] !== "string" || !import_webhook_types.WEBHOOK_DESTINATION_TYPES.includes(raw["type"])) {
2149
+ errors.push(`webhook.destinations[${i}].type must be one of ${import_webhook_types.WEBHOOK_DESTINATION_TYPES.join(", ")}`);
2150
+ }
2151
+ if (typeof raw["url"] !== "string" || !raw["url"].trim()) {
2152
+ errors.push(`webhook.destinations[${i}].url must be a non-empty string`);
2153
+ }
2154
+ if (raw["secret"] !== void 0 && typeof raw["secret"] !== "string") {
2155
+ errors.push(`webhook.destinations[${i}].secret must be a string`);
2156
+ }
2157
+ if (raw["enabled"] !== void 0 && typeof raw["enabled"] !== "boolean") {
2158
+ errors.push(`webhook.destinations[${i}].enabled must be a boolean`);
2159
+ }
2160
+ const events = raw["events"];
2161
+ if (events !== void 0) {
2162
+ if (!Array.isArray(events)) {
2163
+ errors.push(`webhook.destinations[${i}].events must be an array`);
2164
+ } else {
2165
+ for (const e of events) {
2166
+ if (typeof e !== "string" || !import_webhook_types.WEBHOOK_EVENT_KINDS.includes(e)) {
2167
+ errors.push(`webhook.destinations[${i}].events contains an unknown kind '${String(e)}'`);
2168
+ }
2169
+ }
2170
+ }
2171
+ }
2172
+ }
2173
+ return errors;
2174
+ }
2175
+ function redactWebhookConfig(webhook) {
2176
+ return {
2177
+ ...webhook,
2178
+ destinations: webhook.destinations.map(
2179
+ (d) => typeof d.secret === "string" && d.secret.length > 0 ? { ...d, secret: WEBHOOK_SECRET_MASK } : d
2180
+ )
2181
+ };
2182
+ }
2183
+ function preserveWebhookSecrets(incoming, current) {
2184
+ const currentById = /* @__PURE__ */ new Map();
2185
+ for (const d of current?.destinations ?? []) currentById.set(d.id, d);
2186
+ return {
2187
+ ...incoming,
2188
+ destinations: incoming.destinations.map((d) => {
2189
+ const isMaskedOrBlank = d.secret === void 0 || d.secret === "" || d.secret === WEBHOOK_SECRET_MASK;
2190
+ if (isMaskedOrBlank) {
2191
+ const prev = currentById.get(d.id);
2192
+ if (prev?.secret) return { ...d, secret: prev.secret };
2193
+ const { secret: _secret, ...rest } = d;
2194
+ return rest;
2195
+ }
2196
+ return d;
2197
+ })
2198
+ };
2199
+ }
2200
+
2201
+ // src/audit/auditRuntime.ts
2202
+ var import_auditSink = require("@omnicross/core/pipeline/auditSink");
2203
+ var writer = null;
2204
+ var sweeper = null;
2205
+ function setAuditRuntime(w, s) {
2206
+ writer = w;
2207
+ sweeper = s;
2208
+ }
2209
+ function applyAuditConfig(config) {
2210
+ const enabled = config?.enabled === true && writer !== null;
2211
+ if (enabled && config) {
2212
+ (0, import_auditSink.setAuditCaptureConfig)(config);
2213
+ const activeWriter = writer;
2214
+ (0, import_auditSink.setAuditSink)((record) => activeWriter.record(record));
2215
+ if (sweeper) {
2216
+ sweeper.configure(config);
2217
+ sweeper.start();
2218
+ }
2219
+ } else {
2220
+ (0, import_auditSink.setAuditCaptureConfig)(null);
2221
+ (0, import_auditSink.setAuditSink)(null);
2222
+ if (sweeper) {
2223
+ if (config) sweeper.configure(config);
2224
+ sweeper.dispose();
2225
+ }
2226
+ }
2227
+ }
2228
+
2229
+ // src/billing/billingRuntime.ts
2230
+ var import_billingEmit = require("@omnicross/core/pipeline/billingEmit");
2231
+ var publisher = null;
2232
+ var sweeper2 = null;
2233
+ function setBillingRuntime(p, s) {
2234
+ publisher = p;
2235
+ sweeper2 = s;
2236
+ }
2237
+ function applyBillingConfig(config) {
2238
+ const enabled = config?.enabled === true && publisher !== null;
2239
+ if (enabled && config) {
2240
+ const activePublisher = publisher;
2241
+ activePublisher.setConfig(config);
2242
+ (0, import_billingEmit.setBillingCaptureConfig)(config);
2243
+ (0, import_billingEmit.setBillingSink)((event) => activePublisher.record(event));
2244
+ if (sweeper2) {
2245
+ sweeper2.configure(config);
2246
+ sweeper2.start();
2247
+ }
2248
+ } else {
2249
+ (0, import_billingEmit.setBillingCaptureConfig)(null);
2250
+ (0, import_billingEmit.setBillingSink)(null);
2251
+ if (sweeper2) {
2252
+ if (config) sweeper2.configure(config);
2253
+ sweeper2.dispose();
2254
+ }
2255
+ }
2256
+ }
2257
+
1396
2258
  // src/ports/account-multi.ts
1397
2259
  var import_node_crypto5 = require("crypto");
1398
2260
  var PROVIDER_KEYS = {
@@ -1504,6 +2366,9 @@ function getAccountById(config, p, id) {
1504
2366
  const account = getAccounts(config, p).find((a) => a.id === id);
1505
2367
  return account ? { id: account.id, tokens: account.tokens } : void 0;
1506
2368
  }
2369
+ function getAccountProxy(config, p, id) {
2370
+ return getAccounts(config, p).find((a) => a.id === id)?.proxy;
2371
+ }
1507
2372
  function getActiveAccount(config, p) {
1508
2373
  const active = getAccounts(config, p).find((a) => a.id === getActiveId(config, p));
1509
2374
  return active ? { id: active.id, tokens: active.tokens } : void 0;
@@ -1544,7 +2409,17 @@ function sanitizeAccounts(config, p) {
1544
2409
  isSetupToken: t.isSetupToken,
1545
2410
  hasAccessToken: !!(t.accessToken || t.apiKey),
1546
2411
  isActive: a.id === activeId,
1547
- syncWarning: t.syncWarning
2412
+ // Scheduling metadata (subscription-account-scheduling): editable priority
2413
+ // (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
2414
+ priority: a.priority,
2415
+ lastUsedAt: a.lastUsedAt,
2416
+ syncWarning: t.syncWarning,
2417
+ // Per-account proxy (upstream-proxy): masked view — password → hasPassword,
2418
+ // userinfo stripped. The plaintext password is NEVER projected.
2419
+ proxy: a.proxy ? sanitizeProxyConfig(a.proxy) : void 0,
2420
+ // Per-account model support / remap (subscription-account-model-map): model
2421
+ // ids are not token material → carried through verbatim for the editor.
2422
+ supportedModels: a.supportedModels
1548
2423
  };
1549
2424
  });
1550
2425
  }
@@ -1558,13 +2433,84 @@ function renameAccount(config, p, id, label) {
1558
2433
  );
1559
2434
  return { ok: true };
1560
2435
  }
1561
- function clearProvider(config, p) {
1562
- setBlock(config, p, void 0);
1563
- setAccounts(config, p, void 0);
1564
- setActiveId(config, p, void 0);
1565
- }
1566
- var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
1567
-
2436
+ function setAccountPriority(config, p, id, priority) {
2437
+ const accounts = getAccounts(config, p);
2438
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2439
+ setAccounts(
2440
+ config,
2441
+ p,
2442
+ accounts.map((a) => a.id === id ? { ...a, priority } : a)
2443
+ );
2444
+ return { ok: true };
2445
+ }
2446
+ function setAccountProxy(config, p, id, proxy) {
2447
+ const accounts = getAccounts(config, p);
2448
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2449
+ setAccounts(
2450
+ config,
2451
+ p,
2452
+ accounts.map((a) => {
2453
+ if (a.id !== id) return a;
2454
+ if (!proxy) {
2455
+ const { proxy: _drop, ...rest } = a;
2456
+ return rest;
2457
+ }
2458
+ return { ...a, proxy };
2459
+ })
2460
+ );
2461
+ return { ok: true };
2462
+ }
2463
+ function setAccountSupportedModels(config, p, id, supportedModels) {
2464
+ const accounts = getAccounts(config, p);
2465
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2466
+ setAccounts(
2467
+ config,
2468
+ p,
2469
+ accounts.map((a) => {
2470
+ if (a.id !== id) return a;
2471
+ if (supportedModels === void 0) {
2472
+ const { supportedModels: _drop, ...rest } = a;
2473
+ return rest;
2474
+ }
2475
+ return { ...a, supportedModels };
2476
+ })
2477
+ );
2478
+ return { ok: true };
2479
+ }
2480
+ function setAccountLastUsed(config, p, id, iso) {
2481
+ const accounts = getAccounts(config, p);
2482
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2483
+ setAccounts(
2484
+ config,
2485
+ p,
2486
+ accounts.map((a) => a.id === id ? { ...a, lastUsedAt: iso } : a)
2487
+ );
2488
+ return { ok: true };
2489
+ }
2490
+ function setAccountIdentity(config, p, id, identity) {
2491
+ const accounts = getAccounts(config, p);
2492
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2493
+ setAccounts(
2494
+ config,
2495
+ p,
2496
+ accounts.map((a) => {
2497
+ if (a.id !== id) return a;
2498
+ if (identity === void 0) {
2499
+ const { identity: _drop, ...rest } = a;
2500
+ return rest;
2501
+ }
2502
+ return { ...a, identity };
2503
+ })
2504
+ );
2505
+ return { ok: true };
2506
+ }
2507
+ function clearProvider(config, p) {
2508
+ setBlock(config, p, void 0);
2509
+ setAccounts(config, p, void 0);
2510
+ setActiveId(config, p, void 0);
2511
+ }
2512
+ var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
2513
+
1568
2514
  // src/migration/packCodec.ts
1569
2515
  var import_node_crypto6 = require("crypto");
1570
2516
  var PACK_MAGIC = "OMCXPACK";
@@ -1823,6 +2769,12 @@ function parseRange(query) {
1823
2769
  return { startTs, endTs };
1824
2770
  }
1825
2771
  var isRange = (v) => v.startTs !== void 0 && !("status" in v);
2772
+ var BUCKET_SPAN_MS = {
2773
+ hour: 36e5,
2774
+ day: 864e5,
2775
+ month: 28 * 864e5
2776
+ };
2777
+ var MAX_TIMESERIES_BUCKETS = 2e3;
1826
2778
  async function handleUsageGet(view, query, deps) {
1827
2779
  const range = parseRange(query);
1828
2780
  if (!isRange(range)) return range;
@@ -1831,6 +2783,24 @@ async function handleUsageGet(view, query, deps) {
1831
2783
  return { status: 200, body: await deps.usageRecorder.getTotals(range) };
1832
2784
  case "by-model":
1833
2785
  return { status: 200, body: await deps.usageRecorder.getByModel(range) };
2786
+ case "timeseries": {
2787
+ const bucket = query.get("bucket");
2788
+ if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
2789
+ return err4(400, "bucket must be one of 'hour', 'day', 'month'");
2790
+ }
2791
+ const now = Date.now();
2792
+ const clamped = { startTs: range.startTs, endTs: Math.min(range.endTs, now) };
2793
+ if (clamped.startTs < clamped.endTs) {
2794
+ const projected = Math.ceil((clamped.endTs - clamped.startTs) / BUCKET_SPAN_MS[bucket]) + 1;
2795
+ if (projected > MAX_TIMESERIES_BUCKETS) {
2796
+ return err4(
2797
+ 400,
2798
+ `requested range projects ~${projected} '${bucket}' buckets (max ${MAX_TIMESERIES_BUCKETS}); narrow the range or use a coarser bucket`
2799
+ );
2800
+ }
2801
+ }
2802
+ return { status: 200, body: await deps.usageRecorder.getTimeSeries(clamped, bucket) };
2803
+ }
1834
2804
  case "by-api-key": {
1835
2805
  const rows = await deps.usageRecorder.getByApiKey(range);
1836
2806
  const labels = poolKeyLabels(loadConfig(deps.configPath));
@@ -1976,7 +2946,7 @@ function readBody(req) {
1976
2946
  req.on("error", reject);
1977
2947
  });
1978
2948
  }
1979
- async function readJsonBody(req) {
2949
+ async function readJsonBody3(req) {
1980
2950
  const raw = await readBody(req);
1981
2951
  if (!raw.trim()) return {};
1982
2952
  try {
@@ -1986,12 +2956,12 @@ async function readJsonBody(req) {
1986
2956
  return {};
1987
2957
  }
1988
2958
  }
1989
- function writeJson(res, status, body) {
2959
+ function writeJson2(res, status, body) {
1990
2960
  res.writeHead(status, { "Content-Type": "application/json" });
1991
2961
  res.end(JSON.stringify(body));
1992
2962
  }
1993
2963
  function writeJsonError(res, status, message) {
1994
- writeJson(res, status, { error: { type: "admin_api_error", message } });
2964
+ writeJson2(res, status, { error: { type: "admin_api_error", message } });
1995
2965
  }
1996
2966
  function maskProviderApiKey(apiKey) {
1997
2967
  if (!apiKey) return "";
@@ -2007,7 +2977,23 @@ function toKeyInfo(row) {
2007
2977
  enabled: row.enabled,
2008
2978
  createdAt: row.createdAt,
2009
2979
  lastUsedAt: row.lastUsedAt,
2010
- revoked: row.revokedAt !== null
2980
+ revoked: row.revokedAt !== null,
2981
+ maxConcurrency: row.maxConcurrency,
2982
+ // Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
2983
+ // the UI reads them to render + pre-fill the policy editor.
2984
+ expiresAt: row.expiresAt,
2985
+ activationMode: row.activationMode,
2986
+ activationDays: row.activationDays,
2987
+ activatedAt: row.activatedAt,
2988
+ dailyCostLimitUsd: row.dailyCostLimitUsd,
2989
+ totalCostLimitUsd: row.totalCostLimitUsd,
2990
+ weeklyCostLimitUsd: row.weeklyCostLimitUsd,
2991
+ rateLimitMaxRequests: row.rateLimitMaxRequests,
2992
+ rateLimitWindowMs: row.rateLimitWindowMs,
2993
+ // Per-key model restriction (#6) — secret-free scalars the UI pre-fills.
2994
+ enableModelRestriction: row.enableModelRestriction,
2995
+ restrictionMode: row.restrictionMode,
2996
+ restrictedModels: row.restrictedModels
2011
2997
  };
2012
2998
  }
2013
2999
  function toProviderView(row) {
@@ -2069,6 +3055,8 @@ async function handleAdminApi(req, res, path2, deps) {
2069
3055
  return handlePresets(res, method);
2070
3056
  case "keys":
2071
3057
  return await handleKeys(req, res, method, rest, deps);
3058
+ case "voucher":
3059
+ return await handleVoucher(req, res, method, rest, deps);
2072
3060
  case "server":
2073
3061
  return await handleServer(req, res, method, deps);
2074
3062
  case "accounts":
@@ -2085,6 +3073,8 @@ async function handleAdminApi(req, res, path2, deps) {
2085
3073
  return await handleMigrationImport(req, res, method, deps);
2086
3074
  case "usage":
2087
3075
  return await handleUsage(req, res, method, rest, deps);
3076
+ case "dashboard":
3077
+ return await handleDashboardRoute(res, method, deps);
2088
3078
  case "pricing":
2089
3079
  return await handlePricing(req, res, method, rest, deps);
2090
3080
  default:
@@ -2100,17 +3090,22 @@ function requestQuery(req) {
2100
3090
  return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
2101
3091
  }
2102
3092
  function writeResult(res, result) {
2103
- writeJson(res, result.status, result.body);
3093
+ writeJson2(res, result.status, result.body);
2104
3094
  }
2105
3095
  async function handleUsage(req, res, method, rest, deps) {
2106
3096
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
2107
3097
  return writeResult(res, await handleUsageGet(rest[0], requestQuery(req), deps));
2108
3098
  }
3099
+ async function handleDashboardRoute(res, method, deps) {
3100
+ if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
3101
+ const result = await handleDashboard(deps);
3102
+ return writeJson2(res, result.status, result.body);
3103
+ }
2109
3104
  async function handlePricing(req, res, method, rest, deps) {
2110
3105
  if (rest.length === 0) {
2111
3106
  if (method === "GET") return writeResult(res, await handlePricingList(deps));
2112
3107
  if (method === "PUT") {
2113
- return writeResult(res, await handlePricingUpsert(await readJsonBody(req), deps));
3108
+ return writeResult(res, await handlePricingUpsert(await readJsonBody3(req), deps));
2114
3109
  }
2115
3110
  if (method === "DELETE") {
2116
3111
  return writeResult(res, await handlePricingDelete(requestQuery(req), deps));
@@ -2121,7 +3116,7 @@ async function handlePricing(req, res, method, rest, deps) {
2121
3116
  return writeResult(res, await handlePricingFetchLatest(deps));
2122
3117
  }
2123
3118
  if (method === "POST" && rest.length === 1 && rest[0] === "resolve-conflicts") {
2124
- return writeResult(res, await handlePricingResolveConflicts(await readJsonBody(req), deps));
3119
+ return writeResult(res, await handlePricingResolveConflicts(await readJsonBody3(req), deps));
2125
3120
  }
2126
3121
  return writeJsonError(res, 404, `unknown pricing route '${rest.join("/")}'`);
2127
3122
  }
@@ -2135,15 +3130,15 @@ function migrationDeps(deps) {
2135
3130
  }
2136
3131
  async function handleMigrationExport(req, res, method, deps) {
2137
3132
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
2138
- const body = await readJsonBody(req);
3133
+ const body = await readJsonBody3(req);
2139
3134
  const result = await handleExport(body, migrationDeps(deps));
2140
- return writeJson(res, result.status, result.body);
3135
+ return writeJson2(res, result.status, result.body);
2141
3136
  }
2142
3137
  async function handleMigrationImport(req, res, method, deps) {
2143
3138
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
2144
- const body = await readJsonBody(req);
3139
+ const body = await readJsonBody3(req);
2145
3140
  const result = await handleImport(body, migrationDeps(deps));
2146
- return writeJson(res, result.status, result.body);
3141
+ return writeJson2(res, result.status, result.body);
2147
3142
  }
2148
3143
  async function handleProviders(req, res, method, rest, deps) {
2149
3144
  const cfg = loadConfig(deps.configPath);
@@ -2174,13 +3169,13 @@ async function handleProviders(req, res, method, rest, deps) {
2174
3169
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
2175
3170
  const row = cfg.providers.find((p) => p.id === rest[0]);
2176
3171
  if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
2177
- return writeJson(res, 200, { apiKey: row.apiKey ?? "" });
3172
+ return writeJson2(res, 200, { apiKey: row.apiKey ?? "" });
2178
3173
  }
2179
3174
  if (method === "GET") {
2180
- return writeJson(res, 200, { providers: cfg.providers.map(toProviderView) });
3175
+ return writeJson2(res, 200, { providers: cfg.providers.map(toProviderView) });
2181
3176
  }
2182
3177
  if (method === "POST") {
2183
- const body = await readJsonBody(req);
3178
+ const body = await readJsonBody3(req);
2184
3179
  const provider = parseProviderInput(body, void 0);
2185
3180
  if (!provider) return writeJsonError(res, 400, "invalid provider (id, apiFormat, baseUrl required)");
2186
3181
  if (cfg.providers.some((p) => p.id === provider.id)) {
@@ -2188,25 +3183,25 @@ async function handleProviders(req, res, method, rest, deps) {
2188
3183
  }
2189
3184
  cfg.providers.push(provider);
2190
3185
  persistProviders(cfg, deps);
2191
- return writeJson(res, 201, { provider: toProviderView(provider) });
3186
+ return writeJson2(res, 201, { provider: toProviderView(provider) });
2192
3187
  }
2193
3188
  const id = rest[0];
2194
3189
  if (!id) return writeJsonError(res, 400, "provider id required in path");
2195
3190
  const idx = cfg.providers.findIndex((p) => p.id === id);
2196
3191
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
2197
3192
  if (method === "PUT") {
2198
- const body = await readJsonBody(req);
3193
+ const body = await readJsonBody3(req);
2199
3194
  const existing = cfg.providers[idx];
2200
3195
  const updated = parseProviderInput(body, existing);
2201
3196
  if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
2202
3197
  cfg.providers[idx] = updated;
2203
3198
  persistProviders(cfg, deps);
2204
- return writeJson(res, 200, { provider: toProviderView(updated) });
3199
+ return writeJson2(res, 200, { provider: toProviderView(updated) });
2205
3200
  }
2206
3201
  if (method === "DELETE") {
2207
3202
  cfg.providers.splice(idx, 1);
2208
3203
  persistProviders(cfg, deps);
2209
- return writeJson(res, 200, { ok: true });
3204
+ return writeJson2(res, 200, { ok: true });
2210
3205
  }
2211
3206
  return writeJsonError(res, 405, `method ${method} not allowed on providers`);
2212
3207
  }
@@ -2215,7 +3210,7 @@ function persistProviders(cfg, deps) {
2215
3210
  deps.llmConfig.reload(cfg);
2216
3211
  }
2217
3212
  async function handleProviderReorder(req, res, cfg, deps) {
2218
- const body = await readJsonBody(req);
3213
+ const body = await readJsonBody3(req);
2219
3214
  const rawOrder = body["order"];
2220
3215
  if (!Array.isArray(rawOrder)) {
2221
3216
  return writeJsonError(res, 400, "reorder requires { order: string[] }");
@@ -2239,14 +3234,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
2239
3234
  }
2240
3235
  cfg.providers = reordered;
2241
3236
  persistProviders(cfg, deps);
2242
- return writeJson(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
3237
+ return writeJson2(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
2243
3238
  }
2244
3239
  async function handleDiscoverModels(res, id, cfg) {
2245
3240
  if (!id) return writeJsonError(res, 400, "provider id required in path");
2246
3241
  const row = cfg.providers.find((p) => p.id === id);
2247
3242
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
2248
3243
  if (row.apiFormat !== "openai") {
2249
- return writeJson(res, 200, { models: [], unsupportedFormat: true });
3244
+ return writeJson2(res, 200, { models: [], unsupportedFormat: true });
2250
3245
  }
2251
3246
  const resolvedKey = resolveEnvKey(row.apiKey);
2252
3247
  const base = row.baseUrl.replace(/\/+$/, "");
@@ -2254,7 +3249,7 @@ async function handleDiscoverModels(res, id, cfg) {
2254
3249
  try {
2255
3250
  const headers = { Accept: "application/json" };
2256
3251
  if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
2257
- const response = await fetch(url, { method: "GET", headers });
3252
+ const response = await (0, import_upstreamFetch2.fetchUpstream)(url, { method: "GET", headers }, { providerId: "byo" });
2258
3253
  if (!response.ok) {
2259
3254
  const text = await response.text().catch(() => "");
2260
3255
  let message = text.slice(0, 300);
@@ -2263,32 +3258,32 @@ async function handleDiscoverModels(res, id, cfg) {
2263
3258
  message = parsed?.error?.message || parsed?.message || message;
2264
3259
  } catch {
2265
3260
  }
2266
- return writeJson(res, 200, {
3261
+ return writeJson2(res, 200, {
2267
3262
  models: [],
2268
3263
  error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
2269
3264
  });
2270
3265
  }
2271
3266
  const data = await response.json();
2272
3267
  const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
2273
- return writeJson(res, 200, { models });
3268
+ return writeJson2(res, 200, { models });
2274
3269
  } catch (err5) {
2275
3270
  const message = err5 instanceof Error ? err5.message : String(err5);
2276
- return writeJson(res, 200, { models: [], error: `discovery failed: ${message}` });
3271
+ return writeJson2(res, 200, { models: [], error: `discovery failed: ${message}` });
2277
3272
  }
2278
3273
  }
2279
3274
  async function handleTestModel(req, res, id, cfg) {
2280
3275
  if (!id) return writeJsonError(res, 400, "provider id required in path");
2281
3276
  const row = cfg.providers.find((p) => p.id === id);
2282
3277
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
2283
- const body = await readJsonBody(req);
3278
+ const body = await readJsonBody3(req);
2284
3279
  const model = typeof body["model"] === "string" ? body["model"].trim() : "";
2285
3280
  if (!model) return writeJsonError(res, 400, "test requires a { model } string");
2286
3281
  if (row.apiFormat === "gemini") {
2287
- return writeJson(res, 200, { ok: false, unsupportedFormat: true });
3282
+ return writeJson2(res, 200, { ok: false, unsupportedFormat: true });
2288
3283
  }
2289
3284
  const resolvedKey = resolveEnvKey(row.apiKey);
2290
3285
  if (!resolvedKey) {
2291
- return writeJson(res, 200, { ok: false, message: "no API key configured for this provider" });
3286
+ return writeJson2(res, 200, { ok: false, message: "no API key configured for this provider" });
2292
3287
  }
2293
3288
  const url = row.baseUrl.replace(/\/+$/, "");
2294
3289
  const prompt = "Reply with the single word: OK.";
@@ -2309,11 +3304,11 @@ async function handleTestModel(req, res, id, cfg) {
2309
3304
  }
2310
3305
  const startedAt = Date.now();
2311
3306
  try {
2312
- const response = await fetch(url, {
2313
- method: "POST",
2314
- headers,
2315
- body: JSON.stringify(payload)
2316
- });
3307
+ const response = await (0, import_upstreamFetch2.fetchUpstream)(
3308
+ url,
3309
+ { method: "POST", headers, body: JSON.stringify(payload) },
3310
+ { providerId: "byo" }
3311
+ );
2317
3312
  const latencyMs = Date.now() - startedAt;
2318
3313
  const text = await response.text().catch(() => "");
2319
3314
  if (!response.ok) {
@@ -2323,9 +3318,9 @@ async function handleTestModel(req, res, id, cfg) {
2323
3318
  message = parsed?.error?.message || parsed?.message || message;
2324
3319
  } catch {
2325
3320
  }
2326
- return writeJson(res, 200, { ok: false, status: response.status, latencyMs, message });
3321
+ return writeJson2(res, 200, { ok: false, status: response.status, latencyMs, message });
2327
3322
  }
2328
- return writeJson(res, 200, {
3323
+ return writeJson2(res, 200, {
2329
3324
  ok: true,
2330
3325
  status: response.status,
2331
3326
  latencyMs,
@@ -2333,7 +3328,7 @@ async function handleTestModel(req, res, id, cfg) {
2333
3328
  });
2334
3329
  } catch (err5) {
2335
3330
  const message = err5 instanceof Error ? err5.message : String(err5);
2336
- return writeJson(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
3331
+ return writeJson2(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
2337
3332
  }
2338
3333
  }
2339
3334
  function extractSampleText(text, apiFormat) {
@@ -2355,9 +3350,9 @@ function toPoolKeyView(row, cooldown, deps) {
2355
3350
  return entries.map((e) => {
2356
3351
  const auto = deps.autoDisableStore.get(e.id);
2357
3352
  const cd = cooldown[e.id];
2358
- const health = {};
2359
- if (cd) health.cooldown = cd;
2360
- if (auto) health.autoDisabled = { status: auto.status, at: auto.at, reason: auto.reason };
3353
+ const health2 = {};
3354
+ if (cd) health2.cooldown = cd;
3355
+ if (auto) health2.autoDisabled = { status: auto.status, at: auto.at, reason: auto.reason };
2361
3356
  return {
2362
3357
  id: e.id,
2363
3358
  label: e.label && e.label.length > 0 ? e.label : e.id,
@@ -2365,7 +3360,7 @@ function toPoolKeyView(row, cooldown, deps) {
2365
3360
  enabled: e.enabled !== false && !deps.autoDisableStore.isDisabled(e.id),
2366
3361
  weight: typeof e.weight === "number" && Number.isFinite(e.weight) ? e.weight : 1,
2367
3362
  apiKeyMasked: maskProviderApiKey(e.apiKey),
2368
- ...Object.keys(health).length > 0 ? { health } : {}
3363
+ ...Object.keys(health2).length > 0 ? { health: health2 } : {}
2369
3364
  };
2370
3365
  });
2371
3366
  }
@@ -2374,7 +3369,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
2374
3369
  const row = cfg.providers.find((p) => p.id === id);
2375
3370
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
2376
3371
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2377
- return writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3372
+ return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
2378
3373
  }
2379
3374
  function parsePoolKeyInput(body, existing) {
2380
3375
  const out = {};
@@ -2393,7 +3388,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
2393
3388
  if (!id) return writeJsonError(res, 400, "provider id required in path");
2394
3389
  const idx = cfg.providers.findIndex((p) => p.id === id);
2395
3390
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
2396
- const body = await readJsonBody(req);
3391
+ const body = await readJsonBody3(req);
2397
3392
  const parsed = parsePoolKeyInput(body);
2398
3393
  if (!parsed.apiKey) return writeJsonError(res, 400, "add requires a non-empty apiKey");
2399
3394
  const keyId = `key-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -2405,7 +3400,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
2405
3400
  row.apiKeys = [...row.apiKeys ?? [], entry];
2406
3401
  persistProviders(cfg, deps);
2407
3402
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2408
- return writeJson(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
3403
+ return writeJson2(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
2409
3404
  }
2410
3405
  async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
2411
3406
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -2415,7 +3410,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
2415
3410
  const row = cfg.providers[idx];
2416
3411
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
2417
3412
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
2418
- const body = await readJsonBody(req);
3413
+ const body = await readJsonBody3(req);
2419
3414
  const existing = row.apiKeys[keyIdx];
2420
3415
  const parsed = parsePoolKeyInput(body, existing);
2421
3416
  const entry = { id: keyId, apiKey: parsed.apiKey || existing.apiKey };
@@ -2425,7 +3420,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
2425
3420
  row.apiKeys[keyIdx] = entry;
2426
3421
  persistProviders(cfg, deps);
2427
3422
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2428
- return writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3423
+ return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
2429
3424
  }
2430
3425
  async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
2431
3426
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -2439,7 +3434,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
2439
3434
  if (row.apiKeys.length === 0) row.apiKeys = void 0;
2440
3435
  persistProviders(cfg, deps);
2441
3436
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2442
- return writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3437
+ return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
2443
3438
  }
2444
3439
  async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
2445
3440
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -2449,11 +3444,11 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
2449
3444
  const row = cfg.providers[idx];
2450
3445
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
2451
3446
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
2452
- const body = await readJsonBody(req);
3447
+ const body = await readJsonBody3(req);
2453
3448
  row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
2454
3449
  persistProviders(cfg, deps);
2455
3450
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2456
- return writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3451
+ return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
2457
3452
  }
2458
3453
  function parseApiKeysInput(raw, existing) {
2459
3454
  if (!Array.isArray(raw)) return existing;
@@ -2624,18 +3619,31 @@ function handlePresets(res, method) {
2624
3619
  baseUrl: p.baseUrl,
2625
3620
  models: p.models
2626
3621
  }));
2627
- return writeJson(res, 200, { presets, excluded });
3622
+ return writeJson2(res, 200, { presets, excluded });
2628
3623
  }
2629
3624
  async function handleKeys(req, res, method, rest, deps) {
2630
3625
  if (method === "GET" && rest.length === 0) {
2631
3626
  const rows = await deps.keyDb.outboundApiKeysList();
2632
- return writeJson(res, 200, { keys: rows.map(toKeyInfo) });
3627
+ const reader = deps.keySpendReader;
3628
+ if (!reader) return writeJson2(res, 200, { keys: rows.map(toKeyInfo) });
3629
+ const now = Date.now();
3630
+ const keys = await Promise.all(
3631
+ rows.map(async (row) => {
3632
+ const info = toKeyInfo(row);
3633
+ if (row.revokedAt === null) {
3634
+ const s = await reader.getSpend(row.id, now);
3635
+ info.spend = { dailyUsd: s.dailyUsd, weeklyUsd: s.weeklyUsd, totalUsd: s.totalUsd };
3636
+ }
3637
+ return info;
3638
+ })
3639
+ );
3640
+ return writeJson2(res, 200, { keys });
2633
3641
  }
2634
3642
  if (method === "POST" && rest.length === 0) {
2635
- const body = await readJsonBody(req);
3643
+ const body = await readJsonBody3(req);
2636
3644
  const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
2637
- const created = await (0, import_outbound_api2.createNamedKey)(deps.keyDb, name);
2638
- return writeJson(res, 201, {
3645
+ const created = await (0, import_outbound_api3.createNamedKey)(deps.keyDb, name);
3646
+ return writeJson2(res, 201, {
2639
3647
  id: created.id,
2640
3648
  name: created.name,
2641
3649
  keyPrefix: created.keyPrefix,
@@ -2647,46 +3655,181 @@ async function handleKeys(req, res, method, rest, deps) {
2647
3655
  const action = rest[1];
2648
3656
  if (method === "POST" && id && action === "revoke") {
2649
3657
  const ok = await deps.keyDb.outboundApiKeysRevoke(id);
2650
- return writeJson(res, ok ? 200 : 404, { ok });
3658
+ return writeJson2(res, ok ? 200 : 404, { ok });
2651
3659
  }
2652
3660
  if (method === "POST" && id && action === "enabled") {
2653
- const body = await readJsonBody(req);
3661
+ const body = await readJsonBody3(req);
2654
3662
  const enabled = body["enabled"] === true;
2655
3663
  const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
2656
- return writeJson(res, ok ? 200 : 404, { ok, enabled });
3664
+ return writeJson2(res, ok ? 200 : 404, { ok, enabled });
3665
+ }
3666
+ if (method === "POST" && id && action === "max-concurrency") {
3667
+ const body = await readJsonBody3(req);
3668
+ const raw = body["maxConcurrency"];
3669
+ let value;
3670
+ if (raw === null) {
3671
+ value = null;
3672
+ } else if (typeof raw === "number" && Number.isInteger(raw) && raw >= 1 && raw <= 1e3) {
3673
+ value = raw;
3674
+ } else {
3675
+ return writeJsonError(
3676
+ res,
3677
+ 400,
3678
+ "maxConcurrency must be an integer 1..1000 or null"
3679
+ );
3680
+ }
3681
+ const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
3682
+ return writeJson2(res, ok ? 200 : 404, { ok, maxConcurrency: value });
3683
+ }
3684
+ if (method === "POST" && id && action === "policy") {
3685
+ const body = await readJsonBody3(req);
3686
+ const parsed = parseKeyPolicyBody(body);
3687
+ if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
3688
+ const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
3689
+ return writeJson2(res, ok ? 200 : 404, { ok });
2657
3690
  }
2658
3691
  return writeJsonError(res, 405, `method ${method} not allowed on keys`);
2659
3692
  }
3693
+ function validateQueueSegments(patch) {
3694
+ const errors = [];
3695
+ const checkNum = (label, value, min, max) => {
3696
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
3697
+ errors.push(`${label} must be a number ${min}..${max}`);
3698
+ }
3699
+ };
3700
+ const isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3701
+ const umq = patch.userMessageQueue;
3702
+ if (umq !== void 0) {
3703
+ if (!isPlainObject4(umq)) {
3704
+ errors.push("userMessageQueue must be an object");
3705
+ } else {
3706
+ if (typeof umq.enabled !== "boolean") {
3707
+ errors.push("userMessageQueue.enabled must be a boolean");
3708
+ }
3709
+ checkNum("userMessageQueue.delayMs", umq.delayMs, 0, 1e4);
3710
+ checkNum("userMessageQueue.waitTimeoutMs", umq.waitTimeoutMs, 1e3, 3e5);
3711
+ }
3712
+ }
3713
+ const cq = patch.concurrencyQueue;
3714
+ if (cq !== void 0) {
3715
+ if (!isPlainObject4(cq)) {
3716
+ errors.push("concurrencyQueue must be an object");
3717
+ } else {
3718
+ checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
3719
+ checkNum("concurrencyQueue.minQueueSize", cq.minQueueSize, 1, 100);
3720
+ checkNum("concurrencyQueue.waitTimeoutMs", cq.waitTimeoutMs, 1e3, 3e5);
3721
+ }
3722
+ }
3723
+ const ah = patch.accountHealth;
3724
+ if (ah !== void 0) {
3725
+ if (!isPlainObject4(ah)) {
3726
+ errors.push("accountHealth must be an object");
3727
+ } else {
3728
+ if (typeof ah.overloadCooldownEnabled !== "boolean") {
3729
+ errors.push("accountHealth.overloadCooldownEnabled must be a boolean");
3730
+ }
3731
+ checkNum("accountHealth.overloadCooldownMs", ah.overloadCooldownMs, 6e4, 36e5);
3732
+ }
3733
+ }
3734
+ return errors;
3735
+ }
2660
3736
  async function handleServer(req, res, method, deps) {
2661
3737
  if (method === "GET") {
2662
- const config = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
2663
- return writeJson(res, 200, { server: config });
3738
+ const config = await (0, import_outbound_api3.loadServerConfig)(deps.settingsStore);
3739
+ let server = config;
3740
+ if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
3741
+ if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
3742
+ if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
3743
+ return writeJson2(res, 200, { server });
2664
3744
  }
2665
3745
  if (method === "PUT") {
2666
- const patch = await readJsonBody(req);
2667
- const current = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
2668
- const merged = (0, import_outbound_api2.mergeServerConfig)(current, patch);
2669
- await (0, import_outbound_api2.saveServerConfig)(deps.settingsStore, merged);
2670
- await deps.outboundApiServer.applyConfig({
2671
- enabled: merged.enabled,
2672
- networkBinding: merged.networkBinding,
2673
- endpoints: merged.endpoints,
2674
- port: merged.port
2675
- });
2676
- return writeJson(res, 200, { server: merged });
3746
+ const patch = await readJsonBody3(req);
3747
+ const queueErrors = validateQueueSegments(patch);
3748
+ if (queueErrors.length > 0) {
3749
+ return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
3750
+ }
3751
+ const webhookErrors = validateWebhookSegment(patch);
3752
+ if (webhookErrors.length > 0) {
3753
+ return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
3754
+ }
3755
+ const auditErrors = validateAuditSegment(patch);
3756
+ if (auditErrors.length > 0) {
3757
+ return writeJsonError(res, 400, `invalid audit config: ${auditErrors.join("; ")}`);
3758
+ }
3759
+ const billingErrors = validateBillingSegment(patch);
3760
+ if (billingErrors.length > 0) {
3761
+ return writeJsonError(res, 400, `invalid billing config: ${billingErrors.join("; ")}`);
3762
+ }
3763
+ const current = await (0, import_outbound_api3.loadServerConfig)(deps.settingsStore);
3764
+ let effectivePatch = patch;
3765
+ if (patch.proxy) {
3766
+ effectivePatch = { ...effectivePatch, proxy: preserveOutboundProxySecrets(patch.proxy, current.proxy) };
3767
+ }
3768
+ if (patch.webhook) {
3769
+ effectivePatch = { ...effectivePatch, webhook: preserveWebhookSecrets(patch.webhook, current.webhook) };
3770
+ }
3771
+ if (patch.billing) {
3772
+ effectivePatch = { ...effectivePatch, billing: preserveBillingSecret(patch.billing, current.billing) };
3773
+ }
3774
+ const merged = (0, import_outbound_api3.mergeServerConfig)(current, effectivePatch);
3775
+ await (0, import_outbound_api3.saveServerConfig)(deps.settingsStore, merged);
3776
+ setServerProxyConfig(merged.proxy);
3777
+ applyWebhookConfig(merged.webhook);
3778
+ applyAuditConfig(merged.audit);
3779
+ applyBillingConfig(merged.billing);
3780
+ if (merged.enabled) {
3781
+ const missing = (0, import_outbound_api3.validateServerModelConfig)(merged);
3782
+ if (missing.length > 0) {
3783
+ if (deps.outboundApiServer.getStatus().running) {
3784
+ await deps.outboundApiServer.stop();
3785
+ }
3786
+ return writeJson2(res, 200, {
3787
+ server: merged,
3788
+ error: { code: "incomplete-model-config", missing }
3789
+ });
3790
+ }
3791
+ }
3792
+ try {
3793
+ await deps.outboundApiServer.applyConfig({
3794
+ enabled: merged.enabled,
3795
+ networkBinding: merged.networkBinding,
3796
+ endpoints: merged.endpoints,
3797
+ port: merged.port,
3798
+ userMessageQueue: merged.userMessageQueue,
3799
+ concurrencyQueue: merged.concurrencyQueue,
3800
+ // voucher-redemption #9: hot-apply the voucher flag so enabling the product
3801
+ // takes effect without a restart.
3802
+ voucher: merged.voucher
3803
+ });
3804
+ } catch (err5) {
3805
+ const missing = incompleteConfigMissing(err5);
3806
+ if (missing) {
3807
+ return writeJson2(res, 200, {
3808
+ server: merged,
3809
+ error: { code: "incomplete-model-config", missing }
3810
+ });
3811
+ }
3812
+ throw err5;
3813
+ }
3814
+ return writeJson2(res, 200, { server: merged });
2677
3815
  }
2678
3816
  return writeJsonError(res, 405, `method ${method} not allowed on server`);
2679
3817
  }
3818
+ function incompleteConfigMissing(err5) {
3819
+ if (typeof err5 !== "object" || err5 === null) return null;
3820
+ const missing = err5.missing;
3821
+ return Array.isArray(missing) ? missing : null;
3822
+ }
2680
3823
  async function handleAccounts(req, res, method, rest, deps) {
2681
3824
  if (method === "GET" && rest.length === 0) {
2682
3825
  const accounts = await deps.subscriptionAccounts.listAll();
2683
3826
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
2684
3827
  const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
2685
- return writeJson(res, 200, { accounts, providerAccounts, externalCli });
3828
+ return writeJson2(res, 200, { accounts, providerAccounts, externalCli });
2686
3829
  }
2687
3830
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
2688
3831
  const result = handleCodexOAuthStatus(rest[2], deps);
2689
- return writeJson(res, result.status, result.body);
3832
+ return writeJson2(res, result.status, result.body);
2690
3833
  }
2691
3834
  if (method === "PUT" || method === "POST" || method === "DELETE") {
2692
3835
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -2695,15 +3838,15 @@ async function handleAccounts(req, res, method, rest, deps) {
2695
3838
  }
2696
3839
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
2697
3840
  const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
2698
- return writeJson(res, result.status, result.body);
3841
+ return writeJson2(res, result.status, result.body);
2699
3842
  }
2700
3843
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
2701
- const body2 = await readJsonBody(req);
3844
+ const body2 = await readJsonBody3(req);
2702
3845
  const result = await handleOAuthComplete(providerId, body2, deps);
2703
- return writeJson(res, result.status, result.body);
3846
+ return writeJson2(res, result.status, result.body);
2704
3847
  }
2705
3848
  if (method === "POST" && rest[1] === "accounts") {
2706
- const body2 = await readJsonBody(req);
3849
+ const body2 = await readJsonBody3(req);
2707
3850
  const block = validateTokenBody(providerId, body2);
2708
3851
  if (!block) {
2709
3852
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
@@ -2711,79 +3854,113 @@ async function handleAccounts(req, res, method, rest, deps) {
2711
3854
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
2712
3855
  await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
2713
3856
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2714
- return writeJson(res, 200, status2 ? { account: status2 } : { ok: true });
3857
+ return writeJson2(res, 200, status2 ? { account: status2 } : { ok: true });
2715
3858
  }
2716
3859
  if (method === "POST" && rest[1] === "import-external") {
2717
3860
  if (providerId !== "claude" && providerId !== "codex") {
2718
3861
  return writeJsonError(res, 400, `provider '${providerId}' has no external CLI store`);
2719
3862
  }
2720
- const body2 = await readJsonBody(req);
3863
+ const body2 = await readJsonBody3(req);
2721
3864
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
2722
3865
  const result = await deps.subscriptionTokenWriter.importExternalCliAccount(providerId, label);
2723
3866
  if (!result.ok) {
2724
3867
  return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
2725
3868
  }
2726
3869
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2727
- return writeJson(res, 200, { ok: true, account: status2 ?? void 0 });
3870
+ return writeJson2(res, 200, { ok: true, account: status2 ?? void 0 });
2728
3871
  }
2729
3872
  if (method === "POST" && rest[1] === "refresh") {
2730
3873
  if (providerId === "opencodego") {
2731
3874
  return writeJsonError(res, 400, "opencodego credentials are not refreshable");
2732
3875
  }
2733
- const writer = deps.subscriptionTokenWriter;
2734
- const ok = providerId === "claude" ? await writer.refreshClaudeToken() : providerId === "codex" ? await writer.refreshCodexToken() : await writer.refreshGeminiToken();
3876
+ const writer2 = deps.subscriptionTokenWriter;
3877
+ const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
2735
3878
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2736
- return writeJson(res, 200, { ok, account: status2 ?? void 0 });
3879
+ return writeJson2(res, 200, { ok, account: status2 ?? void 0 });
2737
3880
  }
2738
3881
  if (method === "POST" && rest[2] === "label") {
2739
3882
  const accountId = rest[1];
2740
- const body2 = await readJsonBody(req);
3883
+ const body2 = await readJsonBody3(req);
2741
3884
  const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
2742
3885
  const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
2743
3886
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
2744
- return writeJson(res, 200, { ok: true });
3887
+ return writeJson2(res, 200, { ok: true });
3888
+ }
3889
+ if (method === "POST" && rest[2] === "priority") {
3890
+ const accountId = rest[1];
3891
+ const body2 = await readJsonBody3(req);
3892
+ const raw = body2["priority"];
3893
+ const priority = typeof raw === "number" ? raw : Number(raw);
3894
+ if (!Number.isFinite(priority)) {
3895
+ return writeJsonError(res, 400, "priority must be a finite number");
3896
+ }
3897
+ const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
3898
+ if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3899
+ return writeJson2(res, 200, { ok: true });
3900
+ }
3901
+ if (method === "POST" && rest[2] === "proxy") {
3902
+ const accountId = rest[1];
3903
+ const body2 = await readJsonBody3(req);
3904
+ const rawProxy = body2["proxy"];
3905
+ let proxy;
3906
+ if (rawProxy !== null && rawProxy !== void 0) {
3907
+ proxy = (0, import_outbound_api3.normalizeProxyConfig)(rawProxy);
3908
+ if (!proxy) return writeJsonError(res, 400, "invalid proxy config");
3909
+ }
3910
+ const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
3911
+ if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3912
+ return writeJson2(res, 200, { ok: true });
3913
+ }
3914
+ if (method === "POST" && rest[2] === "supported-models") {
3915
+ const accountId = rest[1];
3916
+ const body2 = await readJsonBody3(req);
3917
+ const parsed = validateSupportedModelsBody(body2["supportedModels"]);
3918
+ if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
3919
+ const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
3920
+ if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3921
+ return writeJson2(res, 200, { ok: true });
2745
3922
  }
2746
3923
  if (method === "PUT" && rest[1] === "active") {
2747
- const body2 = await readJsonBody(req);
3924
+ const body2 = await readJsonBody3(req);
2748
3925
  const id = typeof body2["id"] === "string" ? body2["id"] : "";
2749
3926
  if (!id) return writeJsonError(res, 400, "active switch requires { id }");
2750
3927
  const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
2751
3928
  if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
2752
- return writeJson(res, 200, { ok: true });
3929
+ return writeJson2(res, 200, { ok: true });
2753
3930
  }
2754
3931
  if (method === "DELETE" && rest.length >= 2) {
2755
3932
  const accountId = rest[1];
2756
3933
  const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
2757
3934
  if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
2758
- return writeJson(res, 200, { ok: true });
3935
+ return writeJson2(res, 200, { ok: true });
2759
3936
  }
2760
3937
  if (method === "DELETE") {
2761
3938
  await deps.subscriptionTokenWriter.clearProvider(providerId);
2762
- return writeJson(res, 200, { ok: true });
3939
+ return writeJson2(res, 200, { ok: true });
2763
3940
  }
2764
- const body = await readJsonBody(req);
3941
+ const body = await readJsonBody3(req);
2765
3942
  const config = validateTokenBody(providerId, body);
2766
3943
  if (!config) {
2767
3944
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
2768
3945
  }
2769
3946
  await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
2770
3947
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
2771
- return writeJson(res, 200, status ? { account: status } : { ok: true });
3948
+ return writeJson2(res, 200, status ? { account: status } : { ok: true });
2772
3949
  }
2773
3950
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
2774
3951
  }
2775
3952
  async function handleCli(req, res, method, rest, deps) {
2776
3953
  if (method === "GET" && rest.length === 0) {
2777
3954
  const result = handleCliList(process.platform, deps.cliPathProbe);
2778
- return writeJson(res, result.status, result.body);
3955
+ return writeJson2(res, result.status, result.body);
2779
3956
  }
2780
3957
  if (method === "GET" && rest[0] === "sessions") {
2781
3958
  const result = handleCliSessions();
2782
- return writeJson(res, result.status, result.body);
3959
+ return writeJson2(res, result.status, result.body);
2783
3960
  }
2784
3961
  if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
2785
3962
  const result = handleCliStop(rest[1]);
2786
- return writeJson(res, result.status, result.body);
3963
+ return writeJson2(res, result.status, result.body);
2787
3964
  }
2788
3965
  if (method === "POST" && rest[1] === "install") {
2789
3966
  const cli = rest[0];
@@ -2791,14 +3968,14 @@ async function handleCli(req, res, method, rest, deps) {
2791
3968
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
2792
3969
  }
2793
3970
  const result = await handleCliInstall(cli, deps.cliCommandRunner);
2794
- return writeJson(res, result.status, result.body);
3971
+ return writeJson2(res, result.status, result.body);
2795
3972
  }
2796
3973
  if (method === "POST" && rest[1] === "launch") {
2797
3974
  const cli = rest[0];
2798
3975
  if (!isLaunchCliId(cli)) {
2799
3976
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
2800
3977
  }
2801
- const body = await readJsonBody(req);
3978
+ const body = await readJsonBody3(req);
2802
3979
  const providers = loadConfig(deps.configPath).providers ?? [];
2803
3980
  const result = await handleCliLaunch(cli, body, {
2804
3981
  llmConfig: deps.llmConfig,
@@ -2806,20 +3983,28 @@ async function handleCli(req, res, method, rest, deps) {
2806
3983
  opener: deps.cliTerminalOpener,
2807
3984
  probe: deps.cliPathProbe
2808
3985
  });
2809
- return writeJson(res, result.status, result.body);
3986
+ return writeJson2(res, result.status, result.body);
2810
3987
  }
2811
3988
  return writeJsonError(res, 405, `method ${method} not allowed on cli`);
2812
3989
  }
2813
3990
  async function handleStatus(res, method, deps) {
2814
3991
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
2815
3992
  const status = deps.outboundApiServer.getStatus();
2816
- const serverConfig = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
2817
- const endpoints = serverConfig.endpoints.map((e) => ({
2818
- endpoint: e.endpoint,
2819
- model: e.defaultModel,
2820
- useSubscription: e.useSubscription
2821
- }));
2822
- return writeJson(res, 200, { ...status, endpoints });
3993
+ const serverConfig = await (0, import_outbound_api3.loadServerConfig)(deps.settingsStore);
3994
+ const endpoints = serverConfig.endpoints.map((e) => {
3995
+ if ((0, import_outbound_api3.isKindMappedEndpoint)(e.endpoint)) {
3996
+ return { endpoint: e.endpoint, kinds: e.modelMap ?? {}, useSubscription: e.useSubscription };
3997
+ }
3998
+ if (e.endpoint === "chat") {
3999
+ return { endpoint: e.endpoint, models: e.models ?? [], useSubscription: e.useSubscription };
4000
+ }
4001
+ return { endpoint: e.endpoint, model: e.defaultModel ?? "", useSubscription: e.useSubscription };
4002
+ });
4003
+ if (status.running) {
4004
+ const queueStatus = deps.outboundApiServer.getQueueStatus();
4005
+ return writeJson2(res, 200, { ...status, endpoints, queueStatus });
4006
+ }
4007
+ return writeJson2(res, 200, { ...status, endpoints });
2823
4008
  }
2824
4009
  function resolvePlaygroundPath(endpoint, body) {
2825
4010
  switch (endpoint) {
@@ -2839,7 +4024,7 @@ function resolvePlaygroundPath(endpoint, body) {
2839
4024
  }
2840
4025
  async function handlePlayground(req, res, method, deps) {
2841
4026
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on playground`);
2842
- const body = await readJsonBody(req);
4027
+ const body = await readJsonBody3(req);
2843
4028
  const endpoint = typeof body["endpoint"] === "string" ? body["endpoint"] : "";
2844
4029
  const key = typeof body["key"] === "string" ? body["key"] : "";
2845
4030
  const payload = body["body"];
@@ -2985,10 +4170,12 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
2985
4170
  return true;
2986
4171
  }
2987
4172
 
4173
+ // src/admin/version.ts
4174
+ var DAEMON_VERSION = true ? "0.1.3" : "0.0.0-dev";
4175
+
2988
4176
  // src/admin/AdminServer.ts
2989
4177
  var LOOPBACK_ADDR = "127.0.0.1";
2990
4178
  var LAN_ADDR = "0.0.0.0";
2991
- var DAEMON_VERSION = true ? "0.1.2" : "0.0.0-dev";
2992
4179
  var AdminServer = class {
2993
4180
  constructor(deps) {
2994
4181
  this.deps = deps;
@@ -3009,7 +4196,7 @@ var AdminServer = class {
3009
4196
  const cfg = this.deps.getAdminConfig();
3010
4197
  if (!cfg.enabled) return 0;
3011
4198
  if (cfg.networkBinding && !cfg.token) {
3012
- console.error(
4199
+ this.deps.logger.error(
3013
4200
  "[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)."
3014
4201
  );
3015
4202
  return 0;
@@ -3018,7 +4205,7 @@ var AdminServer = class {
3018
4205
  const actualPort = await this.listen(bindAddr, cfg.port);
3019
4206
  this.boundAddr = bindAddr;
3020
4207
  this.boundPort = actualPort;
3021
- console.info(`[AdminServer] Dashboard listening on ${bindAddr}:${actualPort}`);
4208
+ this.deps.logger.info(`[AdminServer] Dashboard listening on ${bindAddr}:${actualPort}`);
3022
4209
  return actualPort;
3023
4210
  }
3024
4211
  /** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
@@ -3040,7 +4227,7 @@ var AdminServer = class {
3040
4227
  const addr = server.address();
3041
4228
  if (addr && typeof addr === "object") {
3042
4229
  server.removeListener("error", onError);
3043
- server.on("error", (e) => console.error("[AdminServer] server error", e));
4230
+ server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
3044
4231
  this.server = server;
3045
4232
  resolve(addr.port);
3046
4233
  } else {
@@ -3053,7 +4240,7 @@ var AdminServer = class {
3053
4240
  onRequest(req, res) {
3054
4241
  void this.dispatch(req, res).catch((err5) => {
3055
4242
  const message = err5 instanceof Error ? err5.message : String(err5);
3056
- console.error("[AdminServer] unhandled error:", message);
4243
+ this.deps.logger.error("[AdminServer] unhandled error:", message);
3057
4244
  if (!res.headersSent) {
3058
4245
  res.writeHead(500, { "Content-Type": "application/json" });
3059
4246
  res.end(JSON.stringify({ error: { type: "admin_error", message } }));
@@ -3064,18 +4251,42 @@ var AdminServer = class {
3064
4251
  const cfg = this.deps.getAdminConfig();
3065
4252
  res.setHeader("x-omnicross-daemon", DAEMON_VERSION);
3066
4253
  res.setHeader("x-omnicross-pid", String(process.pid));
4254
+ const url = req.url ?? "/";
4255
+ const path2 = url.split("?")[0];
4256
+ const healthPath = path2.replace(/\/+$/, "") || "/";
4257
+ if ((req.method === "GET" || req.method === "HEAD") && (healthPath === "/health" || healthPath === "/healthz")) {
4258
+ const report = this.deps.getHealthReport();
4259
+ const code = (0, import_health_logging_types.healthHttpStatus)(report.status);
4260
+ res.writeHead(code, { "Content-Type": "application/json" });
4261
+ res.end(req.method === "HEAD" ? void 0 : JSON.stringify(report));
4262
+ return;
4263
+ }
3067
4264
  if (cfg.token && !this.isAuthorized(req, cfg.token)) {
3068
4265
  res.writeHead(401, { "Content-Type": "application/json" });
3069
4266
  res.end(JSON.stringify({ error: { type: "unauthorized", message: "admin token required" } }));
3070
4267
  return;
3071
4268
  }
3072
- const url = req.url ?? "/";
3073
- const path2 = url.split("?")[0];
3074
4269
  if ((req.method === "GET" || req.method === "HEAD") && (path2 === "/" || path2 === "/admin")) {
3075
4270
  res.writeHead(302, { Location: "/ui/" });
3076
4271
  res.end();
3077
4272
  return;
3078
4273
  }
4274
+ if (path2 === "/admin/api/account-probes" && (req.method === "GET" || req.method === "HEAD")) {
4275
+ handleAccountProbes(res, this.deps.probeHistoryReader);
4276
+ return;
4277
+ }
4278
+ if (path2 === "/admin/api/audit" && (req.method === "GET" || req.method === "HEAD")) {
4279
+ handleAuditQuery(req, res, this.deps.auditReader);
4280
+ return;
4281
+ }
4282
+ if (path2 === "/admin/api/billing-status" && (req.method === "GET" || req.method === "HEAD")) {
4283
+ handleBillingStatus(res, this.deps.billingStatusReader);
4284
+ return;
4285
+ }
4286
+ if (path2 === "/admin/api/webhook-test" && req.method === "POST") {
4287
+ await handleWebhookTest(req, res);
4288
+ return;
4289
+ }
3079
4290
  if (path2.startsWith("/admin/api/")) {
3080
4291
  await handleAdminApi(req, res, path2, this.deps);
3081
4292
  return;
@@ -3119,6 +4330,51 @@ function constantTimeEquals(a, b) {
3119
4330
  return (0, import_node_crypto7.timingSafeEqual)(bufA, bufB);
3120
4331
  }
3121
4332
 
4333
+ // src/admin/health.ts
4334
+ var CRITICAL_CHECKS = ["config", "credentialStore"];
4335
+ var READINESS_CHECKS = ["outboundServer"];
4336
+ function safeBool(fn) {
4337
+ try {
4338
+ return fn() === true;
4339
+ } catch {
4340
+ return false;
4341
+ }
4342
+ }
4343
+ function toMb(bytes) {
4344
+ return Math.round(bytes / (1024 * 1024) * 10) / 10;
4345
+ }
4346
+ function buildHealthReport(deps) {
4347
+ const checks = {
4348
+ config: safeBool(deps.configPresent),
4349
+ credentialStore: safeBool(deps.credentialStoreReadable),
4350
+ outboundServer: safeBool(deps.outboundServerRunning),
4351
+ adminServer: safeBool(deps.adminServerRunning)
4352
+ };
4353
+ if (deps.subscriptionAccountsHealthy) {
4354
+ let probeHealthy;
4355
+ try {
4356
+ probeHealthy = deps.subscriptionAccountsHealthy();
4357
+ } catch {
4358
+ probeHealthy = false;
4359
+ }
4360
+ if (probeHealthy !== void 0) checks.subscriptionAccountsHealthy = probeHealthy;
4361
+ }
4362
+ const criticalOk = CRITICAL_CHECKS.every((k) => checks[k]);
4363
+ const readinessOk = READINESS_CHECKS.every((k) => checks[k]);
4364
+ const status = !criticalOk ? "error" : readinessOk ? "ok" : "degraded";
4365
+ const mem = (deps.memoryUsage ?? process.memoryUsage)();
4366
+ const uptime = (deps.uptimeSeconds ?? process.uptime)();
4367
+ const nowMs = (deps.now ?? Date.now)();
4368
+ return {
4369
+ status,
4370
+ version: deps.version,
4371
+ uptimeSeconds: Math.floor(uptime),
4372
+ timestamp: new Date(nowMs).toISOString(),
4373
+ memory: { rssMb: toMb(mem.rss), heapUsedMb: toMb(mem.heapUsed) },
4374
+ checks
4375
+ };
4376
+ }
4377
+
3122
4378
  // src/admin/oauthSessions.ts
3123
4379
  var import_node_crypto8 = __toESM(require("crypto"), 1);
3124
4380
  var DEFAULT_OAUTH_SESSION_TTL_MS = 10 * 60 * 1e3;
@@ -3447,50 +4703,203 @@ function toLLMProvider(row) {
3447
4703
  };
3448
4704
  }
3449
4705
 
3450
- // src/ports/ConsoleLogger.ts
3451
- var ConsoleLogger = class {
4706
+ // src/ports/ConfigurableLogger.ts
4707
+ var import_node_fs7 = require("fs");
4708
+ var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
4709
+ var RESERVED_JSON_KEYS = /* @__PURE__ */ new Set(["ts", "level", "msg", "error"]);
4710
+ var ConfigurableLogger = class {
4711
+ threshold;
4712
+ format;
4713
+ filePath;
4714
+ fileStream = null;
4715
+ fileDisabled = false;
4716
+ constructor(cfg) {
4717
+ this.threshold = LEVEL_ORDER[cfg?.level ?? "debug"];
4718
+ this.format = cfg?.format ?? "text";
4719
+ this.filePath = cfg?.file && cfg.file.length > 0 ? cfg.file : void 0;
4720
+ }
3452
4721
  info(message, meta) {
3453
- if (meta === void 0) console.info(message);
3454
- else console.info(message, meta);
4722
+ this.emit("info", message, void 0, meta);
3455
4723
  }
3456
4724
  warn(message, meta) {
3457
- if (meta === void 0) console.warn(message);
3458
- else console.warn(message, meta);
4725
+ this.emit("warn", message, void 0, meta);
3459
4726
  }
3460
4727
  error(message, error, meta) {
3461
- if (error === void 0 && meta === void 0) console.error(message);
3462
- else if (meta === void 0) console.error(message, error);
3463
- else console.error(message, error, meta);
4728
+ this.emit("error", message, error, meta);
3464
4729
  }
3465
4730
  debug(message, meta) {
3466
- if (meta === void 0) console.debug(message);
3467
- else console.debug(message, meta);
4731
+ this.emit("debug", message, void 0, meta);
4732
+ }
4733
+ /**
4734
+ * Flush + close the file sink (tests / graceful shutdown). Resolves once the
4735
+ * append stream has finished flushing to disk. No-op when no file sink is open.
4736
+ */
4737
+ close() {
4738
+ const stream = this.fileStream;
4739
+ this.fileStream = null;
4740
+ if (!stream) return Promise.resolve();
4741
+ return new Promise((resolve) => stream.end(() => resolve()));
4742
+ }
4743
+ emit(level, message, error, meta) {
4744
+ if (LEVEL_ORDER[level] > this.threshold) return;
4745
+ this.writeConsole(level, message, error, meta);
4746
+ if (this.filePath) this.writeFile(level, message, error, meta);
4747
+ }
4748
+ /**
4749
+ * Console sink. In `text` format this reproduces the legacy `ConsoleLogger`
4750
+ * EXACTLY (same method + arg shape) so the unconfigured default is a byte-for-
4751
+ * byte drop-in; in `json` format it prints the structured line.
4752
+ */
4753
+ writeConsole(level, message, error, meta) {
4754
+ if (this.format === "json") {
4755
+ this.consoleFn(level)(this.jsonLine(level, message, error, meta));
4756
+ return;
4757
+ }
4758
+ if (level === "error") {
4759
+ if (error === void 0 && meta === void 0) console.error(message);
4760
+ else if (meta === void 0) console.error(message, error);
4761
+ else console.error(message, error, meta);
4762
+ return;
4763
+ }
4764
+ const fn = this.consoleFn(level);
4765
+ if (meta === void 0) fn(message);
4766
+ else fn(message, meta);
4767
+ }
4768
+ /** Append one line to the file sink; a failure disables the sink (swallowed). */
4769
+ writeFile(level, message, error, meta) {
4770
+ const stream = this.getFileStream();
4771
+ if (!stream) return;
4772
+ try {
4773
+ const line = this.format === "json" ? this.jsonLine(level, message, error, meta) : this.textLine(level, message, error, meta);
4774
+ stream.write(line + "\n");
4775
+ } catch {
4776
+ }
4777
+ }
4778
+ /** Lazily open the append-only file stream; disable the sink on any error. */
4779
+ getFileStream() {
4780
+ if (this.fileDisabled || !this.filePath) return null;
4781
+ if (this.fileStream) return this.fileStream;
4782
+ try {
4783
+ const stream = (0, import_node_fs7.createWriteStream)(this.filePath, { flags: "a" });
4784
+ stream.on("error", () => {
4785
+ this.fileDisabled = true;
4786
+ this.fileStream = null;
4787
+ });
4788
+ this.fileStream = stream;
4789
+ return stream;
4790
+ } catch {
4791
+ this.fileDisabled = true;
4792
+ return null;
4793
+ }
4794
+ }
4795
+ consoleFn(level) {
4796
+ switch (level) {
4797
+ case "error":
4798
+ return console.error;
4799
+ case "warn":
4800
+ return console.warn;
4801
+ case "info":
4802
+ return console.info;
4803
+ case "debug":
4804
+ return console.debug;
4805
+ }
4806
+ }
4807
+ /** `{ ts, level, msg, ...meta }` (+ `error` when present) as a single line. */
4808
+ jsonLine(level, message, error, meta) {
4809
+ const obj = {
4810
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4811
+ level,
4812
+ msg: message
4813
+ };
4814
+ if (error !== void 0) obj["error"] = reduceError(error);
4815
+ if (meta !== void 0) {
4816
+ if (meta instanceof Error) obj["meta"] = reduceError(meta);
4817
+ else if (meta && typeof meta === "object") {
4818
+ for (const [k, v] of Object.entries(meta)) {
4819
+ if (!RESERVED_JSON_KEYS.has(k)) obj[k] = v;
4820
+ }
4821
+ } else obj["meta"] = meta;
4822
+ }
4823
+ try {
4824
+ return JSON.stringify(obj);
4825
+ } catch {
4826
+ return JSON.stringify({ ts: obj["ts"], level, msg: message });
4827
+ }
4828
+ }
4829
+ /** Human-readable file line: `ISO [level] message {metaJson}`. */
4830
+ textLine(level, message, error, meta) {
4831
+ const parts = [`${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}`];
4832
+ if (error !== void 0) parts.push(safeStringify(reduceError(error)));
4833
+ if (meta !== void 0) parts.push(safeStringify(meta instanceof Error ? reduceError(meta) : meta));
4834
+ return parts.join(" ");
3468
4835
  }
3469
4836
  };
4837
+ function reduceError(error) {
4838
+ if (error instanceof Error) {
4839
+ return error.stack ? { message: error.message, stack: error.stack } : { message: error.message };
4840
+ }
4841
+ return { value: String(error) };
4842
+ }
4843
+ function safeStringify(value) {
4844
+ try {
4845
+ return typeof value === "string" ? value : JSON.stringify(value);
4846
+ } catch {
4847
+ return "[unserializable]";
4848
+ }
4849
+ }
3470
4850
 
3471
4851
  // src/ports/JsonApiServerSettingsStore.ts
3472
- var import_node_fs7 = require("fs");
3473
- var import_outbound_api3 = require("@omnicross/core/outbound-api");
4852
+ var import_node_fs8 = require("fs");
4853
+ var import_outbound_api4 = require("@omnicross/core/outbound-api");
3474
4854
  var JsonApiServerSettingsStore = class {
3475
- constructor(configPath) {
4855
+ /**
4856
+ * @param configPath the daemon config.json whose `server` field is backed.
4857
+ * @param box OPTIONAL at-rest `SecretBox` (upstream-proxy). When set, the
4858
+ * `server.proxy.*` passwords are encrypted-on-`set` /
4859
+ * decrypted-on-`get` (the settings-store path is otherwise not
4860
+ * secret-aware — every OTHER server field is non-secret). Null
4861
+ * ⇒ passthrough (legacy/pure tests unchanged).
4862
+ */
4863
+ constructor(configPath, box = null) {
3476
4864
  this.configPath = configPath;
4865
+ this.box = box;
3477
4866
  }
3478
4867
  configPath;
4868
+ box;
3479
4869
  async get(key) {
3480
- if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return void 0;
4870
+ if (key !== import_outbound_api4.OUTBOUND_API_SERVER_CONFIG_KEY) return void 0;
3481
4871
  const file = this.readFile();
3482
- return file.server ?? void 0;
4872
+ if (file.server === void 0) return void 0;
4873
+ return this.decryptSecrets(file.server);
3483
4874
  }
3484
4875
  async set(key, value) {
3485
- if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return;
4876
+ if (key !== import_outbound_api4.OUTBOUND_API_SERVER_CONFIG_KEY) return;
3486
4877
  const file = this.readFile();
3487
- file.server = value;
3488
- (0, import_node_fs7.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
4878
+ file.server = this.encryptSecrets(value);
4879
+ (0, import_node_fs8.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
4880
+ }
4881
+ /** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
4882
+ encryptSecrets(config) {
4883
+ if (!this.box) return config;
4884
+ let out = config;
4885
+ if (out?.proxy) out = { ...out, proxy: encryptProxySegment(out.proxy, this.box) };
4886
+ if (out?.webhook) out = { ...out, webhook: encryptWebhookSegment(out.webhook, this.box) };
4887
+ if (out?.billing) out = { ...out, billing: encryptBillingSegment(out.billing, this.box) };
4888
+ return out;
4889
+ }
4890
+ /** Decrypt the proxy passwords + webhook + billing secrets on read (no-op without a box). */
4891
+ decryptSecrets(config) {
4892
+ if (!this.box) return config;
4893
+ let out = config;
4894
+ if (out?.proxy) out = { ...out, proxy: decryptProxySegment(out.proxy, this.box) };
4895
+ if (out?.webhook) out = { ...out, webhook: decryptWebhookSegment(out.webhook, this.box) };
4896
+ if (out?.billing) out = { ...out, billing: decryptBillingSegment(out.billing, this.box) };
4897
+ return out;
3489
4898
  }
3490
4899
  /** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
3491
4900
  readFile() {
3492
4901
  try {
3493
- const raw = (0, import_node_fs7.readFileSync)(this.configPath, "utf8");
4902
+ const raw = (0, import_node_fs8.readFileSync)(this.configPath, "utf8");
3494
4903
  const parsed = JSON.parse(raw);
3495
4904
  if (parsed && typeof parsed === "object") return parsed;
3496
4905
  } catch {
@@ -3501,7 +4910,7 @@ var JsonApiServerSettingsStore = class {
3501
4910
 
3502
4911
  // src/ports/JsonlUsageEventStore.ts
3503
4912
  var import_node_crypto9 = require("crypto");
3504
- var import_node_fs8 = require("fs");
4913
+ var import_node_fs9 = require("fs");
3505
4914
  var JsonlUsageEventStore = class {
3506
4915
  constructor(eventsPath, isPriced) {
3507
4916
  this.eventsPath = eventsPath;
@@ -3516,7 +4925,7 @@ var JsonlUsageEventStore = class {
3516
4925
  id: (0, import_node_crypto9.randomUUID)(),
3517
4926
  ts: input.ts ?? Date.now()
3518
4927
  };
3519
- (0, import_node_fs8.appendFileSync)(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
4928
+ (0, import_node_fs9.appendFileSync)(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
3520
4929
  return row.id;
3521
4930
  }
3522
4931
  async getTotals(range) {
@@ -3605,6 +5014,57 @@ var JsonlUsageEventStore = class {
3605
5014
  }
3606
5015
  return Array.from(groups.values());
3607
5016
  }
5017
+ /**
5018
+ * ONE pass over a single key's events (`ts < endTs`) summing its `costUsd` into
5019
+ * `totalUsd` / `dailyUsd` (`ts >= dayStartTs`) / `weeklyUsd` (`ts >= weekStartTs`).
5020
+ * Used to lazily seed the outbound key-policy spend tracker (once per key). A
5021
+ * key with no attributed events yields all zeros.
5022
+ */
5023
+ async getSpendByKey(query) {
5024
+ let totalUsd = 0;
5025
+ let dailyUsd = 0;
5026
+ let weeklyUsd = 0;
5027
+ for (const row of this.readRows({ startTs: 0, endTs: query.endTs })) {
5028
+ if (row.apiKeyId !== query.apiKeyId) continue;
5029
+ totalUsd += row.costUsd;
5030
+ if (row.ts >= query.dayStartTs) dailyUsd += row.costUsd;
5031
+ if (row.ts >= query.weekStartTs) weeklyUsd += row.costUsd;
5032
+ }
5033
+ return { totalUsd, dailyUsd, weeklyUsd };
5034
+ }
5035
+ /**
5036
+ * Time-series aggregation over LOCAL-time bucket boundaries. Every bucket in
5037
+ * `[floor(startTs), endTs)` is present (empty ones zero-filled), ascending by
5038
+ * `bucketStartTs`; an empty range (`startTs >= endTs`) returns `[]`. Reuses
5039
+ * `readRows` so malformed lines are skipped and only in-range rows contribute.
5040
+ */
5041
+ async getTimeSeries(range, bucket) {
5042
+ if (range.startTs >= range.endTs) return [];
5043
+ const buckets = /* @__PURE__ */ new Map();
5044
+ for (let b = floorToBucket(range.startTs, bucket); b < range.endTs; b = nextBoundary(b, bucket)) {
5045
+ buckets.set(b, {
5046
+ bucketStartTs: b,
5047
+ label: bucketLabel(b, bucket),
5048
+ requests: 0,
5049
+ inputTokens: 0,
5050
+ outputTokens: 0,
5051
+ cacheReadTokens: 0,
5052
+ cacheCreationTokens: 0,
5053
+ costUsd: 0
5054
+ });
5055
+ }
5056
+ for (const row of this.readRows(range)) {
5057
+ const g = buckets.get(floorToBucket(row.ts, bucket));
5058
+ if (!g) continue;
5059
+ g.requests += 1;
5060
+ g.inputTokens += row.inputTokens;
5061
+ g.outputTokens += row.outputTokens;
5062
+ g.cacheReadTokens += row.cacheReadTokens;
5063
+ g.cacheCreationTokens += row.cacheCreationTokens;
5064
+ g.costUsd += row.costUsd;
5065
+ }
5066
+ return Array.from(buckets.values());
5067
+ }
3608
5068
  async getMessagesForSession(sessionId) {
3609
5069
  return this.readAllRows().filter((r) => r.sessionId === sessionId).sort((a, b) => a.ts - b.ts).map((r) => ({
3610
5070
  id: r.id,
@@ -3653,10 +5113,10 @@ var JsonlUsageEventStore = class {
3653
5113
  }
3654
5114
  /** Parse every line, skipping malformed/torn lines defensively. */
3655
5115
  readAllRows() {
3656
- if (!(0, import_node_fs8.existsSync)(this.eventsPath)) return [];
5116
+ if (!(0, import_node_fs9.existsSync)(this.eventsPath)) return [];
3657
5117
  let raw;
3658
5118
  try {
3659
- raw = (0, import_node_fs8.readFileSync)(this.eventsPath, "utf8");
5119
+ raw = (0, import_node_fs9.readFileSync)(this.eventsPath, "utf8");
3660
5120
  } catch {
3661
5121
  return [];
3662
5122
  }
@@ -3673,6 +5133,43 @@ var JsonlUsageEventStore = class {
3673
5133
  return rows;
3674
5134
  }
3675
5135
  };
5136
+ function floorToBucket(ts, bucket) {
5137
+ const d = new Date(ts);
5138
+ switch (bucket) {
5139
+ case "hour":
5140
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime();
5141
+ case "day":
5142
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
5143
+ case "month":
5144
+ return new Date(d.getFullYear(), d.getMonth(), 1).getTime();
5145
+ }
5146
+ }
5147
+ function nextBoundary(ts, bucket) {
5148
+ const d = new Date(ts);
5149
+ switch (bucket) {
5150
+ case "hour":
5151
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours() + 1).getTime();
5152
+ case "day":
5153
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1).getTime();
5154
+ case "month":
5155
+ return new Date(d.getFullYear(), d.getMonth() + 1, 1).getTime();
5156
+ }
5157
+ }
5158
+ var pad2 = (n) => String(n).padStart(2, "0");
5159
+ function bucketLabel(bucketStartTs, bucket) {
5160
+ const d = new Date(bucketStartTs);
5161
+ const y = d.getFullYear();
5162
+ const mo = pad2(d.getMonth() + 1);
5163
+ const day = pad2(d.getDate());
5164
+ switch (bucket) {
5165
+ case "hour":
5166
+ return `${mo}-${day} ${pad2(d.getHours())}:00`;
5167
+ case "day":
5168
+ return `${y}-${mo}-${day}`;
5169
+ case "month":
5170
+ return `${y}-${mo}`;
5171
+ }
5172
+ }
3676
5173
  var NUMERIC_FIELDS = [
3677
5174
  "ts",
3678
5175
  "inputTokens",
@@ -3703,7 +5200,7 @@ function isUsageEventRecord(parsed) {
3703
5200
  }
3704
5201
 
3705
5202
  // src/ports/JsonPricingStore.ts
3706
- var import_node_fs9 = require("fs");
5203
+ var import_node_fs10 = require("fs");
3707
5204
  var JsonPricingStore = class {
3708
5205
  constructor(pricingPath) {
3709
5206
  this.pricingPath = pricingPath;
@@ -3814,50 +5311,145 @@ var JsonPricingStore = class {
3814
5311
  }
3815
5312
  /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
3816
5313
  readRows() {
3817
- if (!(0, import_node_fs9.existsSync)(this.pricingPath)) return [];
5314
+ if (!(0, import_node_fs10.existsSync)(this.pricingPath)) return [];
3818
5315
  try {
3819
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)(this.pricingPath, "utf8"));
5316
+ const parsed = JSON.parse((0, import_node_fs10.readFileSync)(this.pricingPath, "utf8"));
3820
5317
  return Array.isArray(parsed) ? parsed : [];
3821
5318
  } catch {
3822
5319
  return [];
3823
5320
  }
3824
5321
  }
3825
5322
  writeRows(rows) {
3826
- (0, import_node_fs9.writeFileSync)(this.pricingPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
5323
+ (0, import_node_fs10.writeFileSync)(this.pricingPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
3827
5324
  }
3828
5325
  };
3829
5326
 
3830
- // src/ports/JsonSubscriptionCredentialStore.ts
3831
- var import_node_fs12 = require("fs");
3832
- var import_node_path7 = require("path");
3833
- var import_subscriptions3 = require("@omnicross/subscriptions");
3834
-
3835
- // src/ports/account-sync.ts
3836
- var IMPORT_EXPIRY_MARGIN_MS = 6e4;
3837
- function viewOf(tokens) {
3838
- return tokens;
3839
- }
3840
- function decideExternalImport(captured, external, now = Date.now()) {
3841
- if (!external?.accessToken) return "no-credential";
3842
- const capturedRt = viewOf(captured).refreshToken;
3843
- const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
3844
- const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
3845
- return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
3846
- }
3847
- function buildImportedTokens(captured, external) {
3848
- const imported = {
3849
- ...captured,
3850
- accessToken: external.accessToken,
3851
- status: "authorized",
3852
- errorMessage: void 0,
3853
- syncWarning: void 0,
3854
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
3855
- };
3856
- if (external.refreshToken) imported.refreshToken = external.refreshToken;
3857
- if (external.expiresAt) imported.expiresAt = external.expiresAt;
3858
- else delete imported.expiresAt;
3859
- if (external.idToken) imported.idToken = external.idToken;
3860
- if (external.scopes) imported.scopes = external.scopes;
5327
+ // src/ports/JsonVoucherDb.ts
5328
+ var import_node_fs11 = require("fs");
5329
+ var JsonVoucherDb = class {
5330
+ constructor(vouchersPath) {
5331
+ this.vouchersPath = vouchersPath;
5332
+ }
5333
+ vouchersPath;
5334
+ async voucherCreate(input) {
5335
+ const rows = this.readRows();
5336
+ const row = {
5337
+ id: input.id,
5338
+ codeHash: input.codeHash,
5339
+ codePrefix: input.codePrefix,
5340
+ type: input.type,
5341
+ status: "unredeemed",
5342
+ createdAt: input.createdAt ?? Date.now()
5343
+ };
5344
+ if (input.creditUsd != null) row.creditUsd = input.creditUsd;
5345
+ if (input.renewalDays != null) row.renewalDays = input.renewalDays;
5346
+ if (input.maxTotalCostLimitUsd != null) row.maxTotalCostLimitUsd = input.maxTotalCostLimitUsd;
5347
+ if (input.maxExpiryDays != null) row.maxExpiryDays = input.maxExpiryDays;
5348
+ rows.push(row);
5349
+ this.writeRows(rows);
5350
+ return row;
5351
+ }
5352
+ async voucherGetByHash(codeHash) {
5353
+ const rows = this.readRows();
5354
+ return rows.find((r) => r.codeHash === codeHash) ?? null;
5355
+ }
5356
+ async voucherRedeemCas(id, keyId, granted, now) {
5357
+ const rows = this.readRows();
5358
+ const row = rows.find((r) => r.id === id);
5359
+ if (!row || row.status !== "unredeemed") return false;
5360
+ row.status = "redeemed";
5361
+ row.redeemedAt = now;
5362
+ row.redeemedByKeyId = keyId;
5363
+ row.grantApplied = false;
5364
+ if (granted.totalCostLimitUsd != null) row.grantedTotalCostLimitUsd = granted.totalCostLimitUsd;
5365
+ if (granted.expiresAt != null) row.grantedExpiresAt = granted.expiresAt;
5366
+ this.writeRows(rows);
5367
+ return true;
5368
+ }
5369
+ async voucherMarkGrantApplied(id) {
5370
+ const rows = this.readRows();
5371
+ const row = rows.find((r) => r.id === id);
5372
+ if (!row || row.status !== "redeemed") return false;
5373
+ if (row.grantApplied === true) return true;
5374
+ row.grantApplied = true;
5375
+ this.writeRows(rows);
5376
+ return true;
5377
+ }
5378
+ async voucherRevertRedeem(id, keyId) {
5379
+ const rows = this.readRows();
5380
+ const row = rows.find((r) => r.id === id);
5381
+ if (!row || row.status !== "redeemed" || row.grantApplied === true) return false;
5382
+ if (row.redeemedByKeyId !== keyId) return false;
5383
+ row.status = "unredeemed";
5384
+ delete row.redeemedAt;
5385
+ delete row.redeemedByKeyId;
5386
+ delete row.grantApplied;
5387
+ delete row.grantedTotalCostLimitUsd;
5388
+ delete row.grantedExpiresAt;
5389
+ this.writeRows(rows);
5390
+ return true;
5391
+ }
5392
+ async voucherRevokeCas(id, now) {
5393
+ const rows = this.readRows();
5394
+ const row = rows.find((r) => r.id === id);
5395
+ if (!row || row.status !== "unredeemed") return false;
5396
+ row.status = "revoked";
5397
+ row.revokedAt = now;
5398
+ this.writeRows(rows);
5399
+ return true;
5400
+ }
5401
+ async voucherList() {
5402
+ return this.readRows();
5403
+ }
5404
+ /** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
5405
+ readRows() {
5406
+ if (!(0, import_node_fs11.existsSync)(this.vouchersPath)) return [];
5407
+ try {
5408
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(this.vouchersPath, "utf8"));
5409
+ return Array.isArray(parsed) ? parsed : [];
5410
+ } catch {
5411
+ return [];
5412
+ }
5413
+ }
5414
+ writeRows(rows) {
5415
+ (0, import_node_fs11.writeFileSync)(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
5416
+ }
5417
+ };
5418
+
5419
+ // src/ports/JsonSubscriptionCredentialStore.ts
5420
+ var import_node_fs14 = require("fs");
5421
+ var import_node_path7 = require("path");
5422
+ var import_SubscriptionAccountHealth = require("@omnicross/core/pipeline/SubscriptionAccountHealth");
5423
+ var import_upstreamFetch3 = require("@omnicross/core/pipeline/upstreamFetch");
5424
+ var import_SubscriptionIdentityStore = require("@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore");
5425
+ var import_subscriptions3 = require("@omnicross/subscriptions");
5426
+
5427
+ // src/ports/account-sync.ts
5428
+ var IMPORT_EXPIRY_MARGIN_MS = 6e4;
5429
+ function viewOf(tokens) {
5430
+ return tokens;
5431
+ }
5432
+ function decideExternalImport(captured, external, now = Date.now()) {
5433
+ if (!external?.accessToken) return "no-credential";
5434
+ const capturedRt = viewOf(captured).refreshToken;
5435
+ const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
5436
+ const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
5437
+ return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
5438
+ }
5439
+ function buildImportedTokens(captured, external) {
5440
+ const imported = {
5441
+ ...captured,
5442
+ accessToken: external.accessToken,
5443
+ status: "authorized",
5444
+ errorMessage: void 0,
5445
+ syncWarning: void 0,
5446
+ lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
5447
+ };
5448
+ if (external.refreshToken) imported.refreshToken = external.refreshToken;
5449
+ if (external.expiresAt) imported.expiresAt = external.expiresAt;
5450
+ else delete imported.expiresAt;
5451
+ if (external.idToken) imported.idToken = external.idToken;
5452
+ if (external.scopes) imported.scopes = external.scopes;
3861
5453
  return imported;
3862
5454
  }
3863
5455
  function buildTokensFromExternal(provider, external) {
@@ -3906,7 +5498,7 @@ function findDuplicateCredentialIds(accounts) {
3906
5498
  }
3907
5499
 
3908
5500
  // src/ports/external-cli-credentials.ts
3909
- var import_node_fs10 = require("fs");
5501
+ var import_node_fs12 = require("fs");
3910
5502
  var import_node_os2 = require("os");
3911
5503
  var import_node_path5 = require("path");
3912
5504
  function externalStorePath(provider, home = (0, import_node_os2.homedir)()) {
@@ -3959,10 +5551,10 @@ function parseCodexTokensEnvelope(raw) {
3959
5551
  }
3960
5552
  function readExternalCliCredentials(provider, home = (0, import_node_os2.homedir)()) {
3961
5553
  const path2 = externalStorePath(provider, home);
3962
- if (!(0, import_node_fs10.existsSync)(path2)) return null;
5554
+ if (!(0, import_node_fs12.existsSync)(path2)) return null;
3963
5555
  let raw;
3964
5556
  try {
3965
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
5557
+ const parsed = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
3966
5558
  raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
3967
5559
  } catch {
3968
5560
  return null;
@@ -3971,7 +5563,7 @@ function readExternalCliCredentials(provider, home = (0, import_node_os2.homedir
3971
5563
  }
3972
5564
 
3973
5565
  // src/ports/external-cli-store.ts
3974
- var import_node_fs11 = require("fs");
5566
+ var import_node_fs13 = require("fs");
3975
5567
  var import_node_os3 = require("os");
3976
5568
  var import_node_path6 = require("path");
3977
5569
  function markerPath(provider, home) {
@@ -3999,27 +5591,27 @@ function buildCodexTokensEnvelope(tokens) {
3999
5591
  return envelope;
4000
5592
  }
4001
5593
  function readExistingObject(path2) {
4002
- if (!(0, import_node_fs11.existsSync)(path2)) return {};
5594
+ if (!(0, import_node_fs13.existsSync)(path2)) return {};
4003
5595
  try {
4004
- const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
5596
+ const parsed = JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
4005
5597
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4006
5598
  } catch {
4007
5599
  return {};
4008
5600
  }
4009
5601
  }
4010
5602
  function writeAtomic(path2, content) {
4011
- (0, import_node_fs11.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
5603
+ (0, import_node_fs13.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
4012
5604
  const temp = `${path2}.omnicross-tmp`;
4013
- (0, import_node_fs11.writeFileSync)(temp, content, "utf8");
4014
- (0, import_node_fs11.renameSync)(temp, path2);
5605
+ (0, import_node_fs13.writeFileSync)(temp, content, "utf8");
5606
+ (0, import_node_fs13.renameSync)(temp, path2);
4015
5607
  }
4016
5608
  function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
4017
5609
  return {
4018
5610
  readMarkerAccountId(provider) {
4019
5611
  const path2 = markerPath(provider, home);
4020
- if (!(0, import_node_fs11.existsSync)(path2)) return void 0;
5612
+ if (!(0, import_node_fs13.existsSync)(path2)) return void 0;
4021
5613
  try {
4022
- const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
5614
+ const parsed = JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
4023
5615
  return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
4024
5616
  } catch {
4025
5617
  return void 0;
@@ -4037,8 +5629,8 @@ function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
4037
5629
  const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
4038
5630
  if (!envelope) return false;
4039
5631
  const storePath = externalStorePath(provider, home);
4040
- if ((0, import_node_fs11.existsSync)(storePath) && !(0, import_node_fs11.existsSync)(backupPath(provider, home))) {
4041
- (0, import_node_fs11.copyFileSync)(storePath, backupPath(provider, home));
5632
+ if ((0, import_node_fs13.existsSync)(storePath) && !(0, import_node_fs13.existsSync)(backupPath(provider, home))) {
5633
+ (0, import_node_fs13.copyFileSync)(storePath, backupPath(provider, home));
4042
5634
  }
4043
5635
  const existing = readExistingObject(storePath);
4044
5636
  const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
@@ -4049,16 +5641,21 @@ function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
4049
5641
  }
4050
5642
 
4051
5643
  // src/ports/JsonSubscriptionCredentialStore.ts
5644
+ var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
4052
5645
  var JsonSubscriptionCredentialStore = class {
4053
5646
  /**
4054
5647
  * @param tokensPath on-disk `tokens.json` location.
4055
5648
  * @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
4056
- * @param fetchImpl injectable HTTP port for the OAuth refresh round-trips
4057
- * (oauth design D4). Defaults to the global `fetch` so boot
4058
- * is unchanged; tests inject a mock fetch. NOT used by any
4059
- * read/write path only by `refresh*Token`.
5649
+ * @param fetchImpl OPTIONAL injectable HTTP port for the OAuth refresh
5650
+ * round-trips (oauth design D4). A TEST-injected transport is
5651
+ * used verbatim. When ABSENT (production), each refresh uses a
5652
+ * proxy-aware {@link fetchUpstream} that threads the
5653
+ * `{ providerId, accountId }` ctx (upstream-proxy M1) so a
5654
+ * per-account/per-provider proxy is honored on refresh exactly
5655
+ * as on relay — refresh egresses from the SAME proxy IP as the
5656
+ * account's traffic. NOT used by any read/write path.
4060
5657
  */
4061
- constructor(tokensPath, box, fetchImpl = (url, init) => fetch(url, init), externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
5658
+ constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
4062
5659
  this.tokensPath = tokensPath;
4063
5660
  this.box = box;
4064
5661
  this.fetchImpl = fetchImpl;
@@ -4070,6 +5667,15 @@ var JsonSubscriptionCredentialStore = class {
4070
5667
  fetchImpl;
4071
5668
  externalCliReader;
4072
5669
  externalCliStore;
5670
+ /**
5671
+ * The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
5672
+ * TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
5673
+ * through {@link fetchUpstream} with the account's `{ providerId, accountId }`
5674
+ * ctx so the per-account/provider proxy applies. `@internal` — also a test seam.
5675
+ */
5676
+ buildRefreshFetch(providerId, accountId) {
5677
+ return this.fetchImpl ?? ((url, init) => (0, import_upstreamFetch3.fetchUpstream)(url, init, { providerId, accountId }));
5678
+ }
4073
5679
  /**
4074
5680
  * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
4075
5681
  * SINGLE-USE: two concurrent refreshes of one account each spend the same
@@ -4101,6 +5707,19 @@ var JsonSubscriptionCredentialStore = class {
4101
5707
  async getValidOpenCodeGoApiKey() {
4102
5708
  return this.readConfig().opencodego?.apiKey ?? null;
4103
5709
  }
5710
+ /**
5711
+ * DAEMON-ONLY per-account proxy lookup by id (upstream-proxy). Returns the
5712
+ * DECRYPTED `ProxyConfig` for the account (`readConfig` decrypts on read), or
5713
+ * `undefined` for an unknown provider/account or no per-account proxy. Feeds the
5714
+ * winning per-account layer of the upstream-proxy resolver. Synchronous like the
5715
+ * other hot reads. Never returns token material.
5716
+ */
5717
+ getAccountProxy(providerId, accountId) {
5718
+ if (providerId !== "claude" && providerId !== "codex" && providerId !== "gemini" && providerId !== "opencodego") {
5719
+ return void 0;
5720
+ }
5721
+ return getAccountProxy(this.readConfig(), providerId, accountId);
5722
+ }
4104
5723
  /**
4105
5724
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
4106
5725
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
@@ -4109,10 +5728,25 @@ var JsonSubscriptionCredentialStore = class {
4109
5728
  */
4110
5729
  async listSanitizedAccounts() {
4111
5730
  const config = this.readConfig();
5731
+ const health2 = (0, import_SubscriptionAccountHealth.getSharedAccountHealth)();
5732
+ const identityStore = (0, import_SubscriptionIdentityStore.getSharedIdentityStore)();
5733
+ const fingerprintOn = identityStore.isEnabled();
5734
+ const now = Date.now();
4112
5735
  const out = {};
4113
5736
  for (const provider of ["claude", "codex", "gemini", "opencodego"]) {
4114
5737
  const sanitized = sanitizeAccounts(config, provider);
4115
- if (sanitized.length > 0) out[provider] = this.attachSyncWarnings(config, provider, sanitized);
5738
+ if (sanitized.length === 0) continue;
5739
+ for (const account of sanitized) {
5740
+ const status = health2.getStatus(provider, account.id, now);
5741
+ account.health = status.state;
5742
+ account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
5743
+ if (fingerprintOn && provider === "claude") {
5744
+ account.identityCaptured = identityStore.hasIdentity(provider, account.id);
5745
+ const capturedAt = identityStore.capturedAt(provider, account.id);
5746
+ account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
5747
+ }
5748
+ }
5749
+ out[provider] = this.attachSyncWarnings(config, provider, sanitized);
4116
5750
  }
4117
5751
  return out;
4118
5752
  }
@@ -4163,8 +5797,9 @@ var JsonSubscriptionCredentialStore = class {
4163
5797
  if (!active || !claude?.refreshToken) return false;
4164
5798
  const capturedId = active.id;
4165
5799
  this.materializeMigration(config);
5800
+ const refreshFetch = this.buildRefreshFetch("claude", capturedId);
4166
5801
  try {
4167
- const result = await import_subscriptions3.claudeOAuth.refreshAccessToken(claude.refreshToken, this.fetchImpl);
5802
+ const result = await import_subscriptions3.claudeOAuth.refreshAccessToken(claude.refreshToken, refreshFetch);
4168
5803
  const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
4169
5804
  const next = {
4170
5805
  ...claude,
@@ -4181,7 +5816,7 @@ var JsonSubscriptionCredentialStore = class {
4181
5816
  return true;
4182
5817
  } catch (error) {
4183
5818
  if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
4184
- const r = await import_subscriptions3.claudeOAuth.refreshAccessToken(rt, this.fetchImpl);
5819
+ const r = await import_subscriptions3.claudeOAuth.refreshAccessToken(rt, refreshFetch);
4185
5820
  return {
4186
5821
  accessToken: r.accessToken,
4187
5822
  refreshToken: r.refreshToken,
@@ -4208,8 +5843,9 @@ var JsonSubscriptionCredentialStore = class {
4208
5843
  if (!active || !codex?.refreshToken) return false;
4209
5844
  const capturedId = active.id;
4210
5845
  this.materializeMigration(config);
5846
+ const refreshFetch = this.buildRefreshFetch("codex", capturedId);
4211
5847
  try {
4212
- const result = await import_subscriptions3.codexOAuth.refreshAccessToken(codex.refreshToken, this.fetchImpl);
5848
+ const result = await import_subscriptions3.codexOAuth.refreshAccessToken(codex.refreshToken, refreshFetch);
4213
5849
  const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
4214
5850
  const next = {
4215
5851
  ...codex,
@@ -4227,7 +5863,7 @@ var JsonSubscriptionCredentialStore = class {
4227
5863
  return true;
4228
5864
  } catch (error) {
4229
5865
  if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
4230
- const r = await import_subscriptions3.codexOAuth.refreshAccessToken(rt, this.fetchImpl);
5866
+ const r = await import_subscriptions3.codexOAuth.refreshAccessToken(rt, refreshFetch);
4231
5867
  return {
4232
5868
  accessToken: r.accessToken,
4233
5869
  refreshToken: r.refreshToken,
@@ -4257,8 +5893,9 @@ var JsonSubscriptionCredentialStore = class {
4257
5893
  if (!active || !gemini?.refreshToken) return false;
4258
5894
  const capturedId = active.id;
4259
5895
  this.materializeMigration(config);
5896
+ const refreshFetch = this.buildRefreshFetch("gemini", capturedId);
4260
5897
  try {
4261
- const result = await import_subscriptions3.geminiOAuth.refreshAccessToken(gemini.refreshToken, this.fetchImpl);
5898
+ const result = await import_subscriptions3.geminiOAuth.refreshAccessToken(gemini.refreshToken, refreshFetch);
4262
5899
  const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
4263
5900
  const next = {
4264
5901
  ...gemini,
@@ -4292,7 +5929,7 @@ var JsonSubscriptionCredentialStore = class {
4292
5929
  if (!account || !captured?.refreshToken) return false;
4293
5930
  this.materializeMigration(config);
4294
5931
  try {
4295
- const refreshed = await this.refreshUpstream(provider, captured.refreshToken);
5932
+ const refreshed = await this.refreshUpstream(provider, captured.refreshToken, id);
4296
5933
  const next = {
4297
5934
  ...captured,
4298
5935
  accessToken: refreshed.accessToken,
@@ -4314,10 +5951,114 @@ var JsonSubscriptionCredentialStore = class {
4314
5951
  }
4315
5952
  });
4316
5953
  }
5954
+ // ── By-id account-pool surface (subscription-account-scheduling, design D6) ──
5955
+ /**
5956
+ * Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
5957
+ * provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
5958
+ * (refresh is 401-driven, like `getValidClaudeAccessToken`); codex/gemini refresh
5959
+ * a near-expiry token via `refreshAccountById` (like `resolveAccessToken`);
5960
+ * opencodego returns the account's static key. `null` when unknown/expired/
5961
+ * tokenless.
5962
+ */
5963
+ async getAccessTokenForAccount(providerId, accountId) {
5964
+ const account = getAccountById(this.readConfig(), providerId, accountId);
5965
+ if (!account) return null;
5966
+ if (providerId === "opencodego") {
5967
+ return account.tokens.apiKey ?? null;
5968
+ }
5969
+ const oauth = account.tokens;
5970
+ if (!oauth.accessToken) return null;
5971
+ if (providerId === "codex" || providerId === "gemini") {
5972
+ const expiresAtMs = oauth.expiresAt ? Date.parse(oauth.expiresAt) : 0;
5973
+ const expiringSoon = expiresAtMs > 0 && Date.now() >= expiresAtMs - ACCOUNT_REFRESH_LEAD_MS;
5974
+ if (expiringSoon && oauth.refreshToken) {
5975
+ const ok = await this.refreshAccountById(providerId, accountId);
5976
+ if (!ok) return null;
5977
+ const fresh = getAccountById(this.readConfig(), providerId, accountId);
5978
+ return fresh?.tokens?.accessToken ?? null;
5979
+ }
5980
+ }
5981
+ if (oauth.status === "expired") return null;
5982
+ return oauth.accessToken;
5983
+ }
5984
+ /**
5985
+ * Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
5986
+ * `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
5987
+ * → `false` (no refresh affordance).
5988
+ */
5989
+ async refreshAccountToken(providerId, accountId) {
5990
+ if (providerId === "opencodego") return false;
5991
+ return this.refreshAccountById(providerId, accountId);
5992
+ }
5993
+ /**
5994
+ * Best-effort record of a selection time onto the account's `lastUsedAt` by id
5995
+ * (design D4). Entry-metadata only (the token mirror is untouched); a no-op for
5996
+ * an unknown id. The selector throttles the call frequency, so this stays cheap.
5997
+ */
5998
+ async touchAccountLastUsed(providerId, accountId, iso) {
5999
+ const config = this.readConfig();
6000
+ const result = setAccountLastUsed(config, providerId, accountId, iso);
6001
+ if (!result.ok) return;
6002
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6003
+ }
6004
+ /**
6005
+ * Best-effort write-through of a per-account client `identity`
6006
+ * (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
6007
+ * whitelisted fingerprint headers; the token mirror is untouched); a no-op for
6008
+ * an unknown id. Called by the identity store's persistence port on a first-seen
6009
+ * freeze / TTL refresh, so it stays infrequent. Never throws to the caller — the
6010
+ * store's port wrapper swallows a rejection so the relay hot path is unaffected.
6011
+ */
6012
+ async setAccountIdentity(providerId, accountId, identity) {
6013
+ const config = this.readConfig();
6014
+ const result = setAccountIdentity(config, providerId, accountId, identity);
6015
+ if (!result.ok) return;
6016
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6017
+ }
6018
+ /**
6019
+ * DAEMON-ONLY set-priority (subscription-account-scheduling, admin write, NOT on
6020
+ * the port). Set one account's scheduling `priority` by id. Secret-free
6021
+ * (entry-metadata only; the mirror invariant is untouched). Rejects an unknown id.
6022
+ */
6023
+ async setAccountPriority(providerId, accountId, priority) {
6024
+ const config = this.readConfig();
6025
+ const result = setAccountPriority(config, providerId, accountId, priority);
6026
+ if (!result.ok) return result;
6027
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6028
+ return result;
6029
+ }
6030
+ /**
6031
+ * DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
6032
+ * the port). Passing `undefined` clears the override. Write-only password: when
6033
+ * the incoming structured proxy omits the password but the account already had
6034
+ * one, the current (decrypted) password is preserved — editing host/port never
6035
+ * wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
6036
+ */
6037
+ async setAccountProxy(providerId, accountId, proxy) {
6038
+ const config = this.readConfig();
6039
+ const merged = proxy ? preserveProxyConfigSecret(proxy, getAccountProxy(config, providerId, accountId)) : void 0;
6040
+ const result = setAccountProxy(config, providerId, accountId, merged);
6041
+ if (!result.ok) return result;
6042
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6043
+ return result;
6044
+ }
6045
+ /**
6046
+ * DAEMON-ONLY set/clear per-account `supportedModels` (subscription-account-
6047
+ * model-map, admin write, NOT on the port). Passing `undefined` clears it.
6048
+ * Secret-free (model ids only; the mirror invariant is untouched). Rejects an
6049
+ * unknown id.
6050
+ */
6051
+ async setAccountSupportedModels(providerId, accountId, supportedModels) {
6052
+ const config = this.readConfig();
6053
+ const result = setAccountSupportedModels(config, providerId, accountId, supportedModels);
6054
+ if (!result.ok) return result;
6055
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6056
+ return result;
6057
+ }
4317
6058
  /** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
4318
- async refreshUpstream(provider, refreshToken) {
6059
+ async refreshUpstream(provider, refreshToken, accountId) {
4319
6060
  const flow = provider === "claude" ? import_subscriptions3.claudeOAuth : provider === "codex" ? import_subscriptions3.codexOAuth : import_subscriptions3.geminiOAuth;
4320
- const r = await flow.refreshAccessToken(refreshToken, this.fetchImpl);
6061
+ const r = await flow.refreshAccessToken(refreshToken, this.buildRefreshFetch(provider, accountId));
4321
6062
  return {
4322
6063
  accessToken: r.accessToken,
4323
6064
  refreshToken: r.refreshToken,
@@ -4455,119 +6196,907 @@ var JsonSubscriptionCredentialStore = class {
4455
6196
  });
4456
6197
  }
4457
6198
  /**
4458
- * DAEMON-ONLY WRITE (design D1, NOT on the port). Read-merge the given
4459
- * provider's token block into the current `AccountTokensConfig`, stamp a fresh
4460
- * `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
4461
- * OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
4462
- * tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
4463
- * so a first-ever write still produces a valid config. No cache → the next read
4464
- * sees this write.
6199
+ * DAEMON-ONLY WRITE (design D1, NOT on the port). Read-merge the given
6200
+ * provider's token block into the current `AccountTokensConfig`, stamp a fresh
6201
+ * `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
6202
+ * OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
6203
+ * tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
6204
+ * so a first-ever write still produces a valid config. No cache → the next read
6205
+ * sees this write.
6206
+ */
6207
+ async writeProviderTokens(providerId, config) {
6208
+ const current = this.readConfig();
6209
+ writeActiveTokens(current, providerId, config);
6210
+ this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6211
+ }
6212
+ /**
6213
+ * DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
6214
+ * (optional label) and set it active, then re-derive the mirror — used by
6215
+ * `omnicross login <provider> --label` to add an account instead of overwriting.
6216
+ */
6217
+ async appendProviderAccount(providerId, config, label) {
6218
+ const current = this.readConfig();
6219
+ const result = addAccount(current, providerId, config, label);
6220
+ this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6221
+ return result;
6222
+ }
6223
+ /**
6224
+ * DAEMON-ONLY active switch (design D5, NOT on the port). Switch the active
6225
+ * account for a provider; rejects an unknown id. Re-derives the mirror.
6226
+ */
6227
+ async setActiveAccount(providerId, id) {
6228
+ const current = this.readConfig();
6229
+ const result = setActiveAccount(current, providerId, id);
6230
+ if (!result.ok) return result;
6231
+ this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6232
+ return result;
6233
+ }
6234
+ /**
6235
+ * DAEMON-ONLY per-account remove (design D5, NOT on the port). Remove one
6236
+ * account; promote the most-recent remaining on active-removal (or clear the
6237
+ * mirror when none remain). Re-derives the mirror.
6238
+ */
6239
+ async removeAccount(providerId, id) {
6240
+ const current = this.readConfig();
6241
+ const result = removeAccount(current, providerId, id);
6242
+ if (!result.removed) return result;
6243
+ this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6244
+ return result;
6245
+ }
6246
+ /**
6247
+ * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
6248
+ * rejects an unknown id. Label-only — no token material is read or written
6249
+ * (the secret-free invariant holds).
6250
+ */
6251
+ async renameAccount(providerId, id, label) {
6252
+ const current = this.readConfig();
6253
+ const result = renameAccount(current, providerId, id, label);
6254
+ if (!result.ok) return result;
6255
+ this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6256
+ return result;
6257
+ }
6258
+ /**
6259
+ * DAEMON-ONLY CLEAR (design D1/D3, NOT on the port). Remove a single provider's
6260
+ * block from `tokens.json` and re-persist (the strategies already tolerate an
6261
+ * absent block). Stamps a fresh `updatedAt`. A no-op-shaped write when the
6262
+ * provider was already absent (still re-stamps + persists).
6263
+ */
6264
+ async clearProvider(providerId) {
6265
+ const current = this.readConfig();
6266
+ clearProvider(current, providerId);
6267
+ this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6268
+ }
6269
+ /** Write the merged config to disk as pretty JSON (mkdir parent if needed).
6270
+ * Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
6271
+ * → `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
6272
+ * write — incl. child 4's future refresh writes — lands encrypted. */
6273
+ persist(config) {
6274
+ (0, import_node_fs14.mkdirSync)((0, import_node_path7.dirname)(this.tokensPath), { recursive: true });
6275
+ const encrypted = encryptTokens(config, this.box);
6276
+ (0, import_node_fs14.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
6277
+ }
6278
+ /**
6279
+ * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
6280
+ * the token-material fields so every getter returns plaintext (the
6281
+ * subscription bearer path is byte-identical).
6282
+ *
6283
+ * The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
6284
+ * file → empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
6285
+ * wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
6286
+ * box's clear, secret-free error (secrets spec "错误密钥 / 篡改的解密失败 UX":
6287
+ * SHALL fail-fast, SHALL NOT 静默降级 — a swallowed decrypt would report "no
6288
+ * tokens" and silently send the WRONG bearer upstream → 401). Mirrors
6289
+ * `config.ts loadConfig`, which decrypts outside its parse try.
6290
+ */
6291
+ readConfig() {
6292
+ if (!(0, import_node_fs14.existsSync)(this.tokensPath)) return { updatedAt: "" };
6293
+ let parsed;
6294
+ try {
6295
+ const raw = JSON.parse((0, import_node_fs14.readFileSync)(this.tokensPath, "utf8"));
6296
+ parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
6297
+ } catch {
6298
+ parsed = null;
6299
+ }
6300
+ if (!parsed) return { updatedAt: "" };
6301
+ const decrypted = decryptTokens(parsed, this.box);
6302
+ return migrateLazily(decrypted);
6303
+ }
6304
+ };
6305
+
6306
+ // src/AccountHealthProbeScheduler.ts
6307
+ var import_upstreamFetch4 = require("@omnicross/core/pipeline/upstreamFetch");
6308
+
6309
+ // src/probe/ProbeStrategy.ts
6310
+ var PROVIDER_PROBE_PLANS = {
6311
+ claude: {
6312
+ kind: "upstream",
6313
+ // VERIFIED free authed list endpoint (no tokens billed). The anthropic OAuth
6314
+ // bearer is accepted here exactly as on the relay path.
6315
+ url: "https://api.anthropic.com/v1/models",
6316
+ buildInit: (token) => ({
6317
+ method: "GET",
6318
+ headers: {
6319
+ Authorization: `Bearer ${token}`,
6320
+ "anthropic-version": "2023-06-01"
6321
+ }
6322
+ })
6323
+ },
6324
+ // UNVERIFIED cheap authed GET — Phase 1 local-only (LEAD OQ1: do not guess a
6325
+ // billable/wrong endpoint). Upgrade to `{ kind:'upstream' }` once verified.
6326
+ codex: { kind: "local" },
6327
+ gemini: { kind: "local" },
6328
+ opencodego: { kind: "local" }
6329
+ };
6330
+ function probePlanFor(providerId) {
6331
+ return PROVIDER_PROBE_PLANS[providerId] ?? { kind: "local" };
6332
+ }
6333
+
6334
+ // src/AccountHealthProbeScheduler.ts
6335
+ var KEY_SEP = "\0";
6336
+ var MAX_BODY_SNIFF = 2048;
6337
+ var PROBE_PROVIDERS = [
6338
+ "claude",
6339
+ "codex",
6340
+ "gemini",
6341
+ "opencodego"
6342
+ ];
6343
+ var AccountHealthProbeScheduler = class {
6344
+ constructor(store, health2, logger, config, opts = {}) {
6345
+ this.store = store;
6346
+ this.health = health2;
6347
+ this.logger = logger;
6348
+ this.config = config;
6349
+ this.now = opts.now ?? Date.now;
6350
+ this.fetchImpl = opts.fetchImpl ?? import_upstreamFetch4.fetchUpstream;
6351
+ this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6352
+ this.planFor = opts.planFor ?? probePlanFor;
6353
+ }
6354
+ store;
6355
+ health;
6356
+ logger;
6357
+ config;
6358
+ timer = null;
6359
+ sweeping = false;
6360
+ history = /* @__PURE__ */ new Map();
6361
+ now;
6362
+ fetchImpl;
6363
+ sleep;
6364
+ planFor;
6365
+ /** Whether probing is enabled by the current config. */
6366
+ get enabled() {
6367
+ return this.config.enabled;
6368
+ }
6369
+ /**
6370
+ * Re-apply config to the live instance (the async `start.ts` loads the persisted
6371
+ * `accountProbe` segment after `buildDaemon`). Call BEFORE `start()`.
6372
+ */
6373
+ configure(config) {
6374
+ this.config = config;
6375
+ }
6376
+ /** Arm the probe interval. No-op when disabled (zero regression). Idempotent. */
6377
+ start() {
6378
+ if (this.timer || !this.config.enabled) return;
6379
+ this.timer = setInterval(() => void this.sweep(), this.config.intervalMs);
6380
+ this.timer.unref?.();
6381
+ }
6382
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
6383
+ dispose() {
6384
+ if (this.timer) {
6385
+ clearInterval(this.timer);
6386
+ this.timer = null;
6387
+ }
6388
+ }
6389
+ /**
6390
+ * One sweep: probe every ELIGIBLE account SEQUENTIALLY with a `staggerMs` gap.
6391
+ * Disabled ⇒ no-op. `onlyMultiAccount` skips single-account providers. Exposed
6392
+ * for tests; never throws.
6393
+ */
6394
+ async sweep() {
6395
+ if (!this.config.enabled || this.sweeping) return;
6396
+ this.sweeping = true;
6397
+ try {
6398
+ const config = await this.store.getFullConfig();
6399
+ let probed = 0;
6400
+ let marked = 0;
6401
+ for (const providerId of PROBE_PROVIDERS) {
6402
+ const accounts = listAccounts(config, providerId);
6403
+ if (this.config.onlyMultiAccount && accounts.length < 2) continue;
6404
+ for (const account of accounts) {
6405
+ if (probed > 0 && this.config.staggerMs > 0) await this.sleep(this.config.staggerMs);
6406
+ const outcome = await this.probeAccount(providerId, account.id);
6407
+ probed += 1;
6408
+ if (outcome.marked) marked += 1;
6409
+ }
6410
+ }
6411
+ this.logger.debug("account-probe sweep complete", { probed, marked });
6412
+ } catch (error) {
6413
+ this.logger.warn("account-probe sweep failed", {
6414
+ error: error instanceof Error ? error.message : String(error)
6415
+ });
6416
+ } finally {
6417
+ this.sweeping = false;
6418
+ }
6419
+ }
6420
+ /**
6421
+ * Probe ONE account (design D1). Local tier first (dead token → synthesized 401,
6422
+ * no upstream); else the upstream tier when a verified endpoint exists. Records
6423
+ * the rolling history entry either way; returns whether the tracker was MARKED.
6424
+ */
6425
+ async probeAccount(providerId, accountId) {
6426
+ const now = this.now();
6427
+ let token = null;
6428
+ let readThrew = false;
6429
+ try {
6430
+ token = await this.store.getAccessTokenForAccount(providerId, accountId);
6431
+ } catch {
6432
+ readThrew = true;
6433
+ }
6434
+ if (readThrew) {
6435
+ this.record(providerId, accountId, { ts: now, ok: false, status: null, tier: "local" });
6436
+ return { ok: false, marked: false };
6437
+ }
6438
+ if (!token) {
6439
+ this.health.recordUpstreamOutcome(providerId, accountId, { status: 401, now });
6440
+ this.record(providerId, accountId, { ts: now, ok: false, status: 401, tier: "local" });
6441
+ return { ok: false, marked: true };
6442
+ }
6443
+ const plan = this.planFor(providerId);
6444
+ if (plan.kind === "local") {
6445
+ this.record(providerId, accountId, { ts: now, ok: true, tier: "local" });
6446
+ return { ok: true, marked: false };
6447
+ }
6448
+ const start = this.now();
6449
+ let status = null;
6450
+ let bodyText;
6451
+ try {
6452
+ const res = await this.fetchImpl(
6453
+ plan.url,
6454
+ { ...plan.buildInit(token), signal: AbortSignal.timeout(this.config.timeoutMs) },
6455
+ { providerId, accountId }
6456
+ );
6457
+ status = res.status;
6458
+ if (status === 403) bodyText = await this.readBounded(res);
6459
+ } catch {
6460
+ status = null;
6461
+ }
6462
+ const latencyMs = this.now() - start;
6463
+ const marked = this.applyOutcome(providerId, accountId, status, bodyText, now);
6464
+ this.record(providerId, accountId, {
6465
+ ts: now,
6466
+ ok: status !== null && status >= 200 && status < 300,
6467
+ status,
6468
+ latencyMs,
6469
+ tier: "upstream"
6470
+ });
6471
+ return { ok: status !== null && status < 400, marked };
6472
+ }
6473
+ /** Per-account rolling history for the authed admin surface (design D5). */
6474
+ getAllHistory() {
6475
+ const out = [];
6476
+ for (const [key, records] of this.history) {
6477
+ const [providerId, accountId] = this.parseKey(key);
6478
+ out.push({ providerId, accountId, records: records.slice() });
6479
+ }
6480
+ return out;
6481
+ }
6482
+ /**
6483
+ * The coarse, account-ANONYMOUS `/health` signal (design D5): `true` when no
6484
+ * probed account is currently unhealthy (per #2's tracker). No ids, no counts —
6485
+ * safe for the unauthenticated `/health`. Vacuously `true` when nothing probed.
6486
+ */
6487
+ probedAccountsHealthy(now = this.now()) {
6488
+ for (const key of this.history.keys()) {
6489
+ const [providerId, accountId] = this.parseKey(key);
6490
+ if (!this.health.isSchedulable(providerId, accountId, now)) return false;
6491
+ }
6492
+ return true;
6493
+ }
6494
+ /**
6495
+ * Feed ONLY the account/auth-decisive statuses to #2 (LEAD constraint):
6496
+ * 401/403 → mark; 2xx → clear transient; 429 / 5xx / other 4xx / null →
6497
+ * NOT forwarded (never a mark). Returns whether a NEGATIVE mark was applied.
6498
+ */
6499
+ applyOutcome(providerId, accountId, status, bodyText, now) {
6500
+ if (status === null) return false;
6501
+ if (status === 401 || status === 403) {
6502
+ this.health.recordUpstreamOutcome(providerId, accountId, { status, bodyText, now });
6503
+ return true;
6504
+ }
6505
+ if (status >= 200 && status < 300) {
6506
+ this.health.clearTransientMark(providerId, accountId);
6507
+ return false;
6508
+ }
6509
+ return false;
6510
+ }
6511
+ /** Append a record, capping the ring at `historySize` (drop oldest). */
6512
+ record(providerId, accountId, rec) {
6513
+ const key = this.key(providerId, accountId);
6514
+ const list = this.history.get(key) ?? [];
6515
+ list.push(rec);
6516
+ const overflow = list.length - this.config.historySize;
6517
+ if (overflow > 0) list.splice(0, overflow);
6518
+ this.history.set(key, list);
6519
+ }
6520
+ /** Read a bounded slice of the response body for the 403-ban sniff (never throws). */
6521
+ async readBounded(res) {
6522
+ try {
6523
+ return (await res.text()).slice(0, MAX_BODY_SNIFF);
6524
+ } catch {
6525
+ return "";
6526
+ }
6527
+ }
6528
+ key(providerId, accountId) {
6529
+ return `${providerId}${KEY_SEP}${accountId}`;
6530
+ }
6531
+ parseKey(key) {
6532
+ const idx = key.indexOf(KEY_SEP);
6533
+ return [key.slice(0, idx), key.slice(idx + 1)];
6534
+ }
6535
+ };
6536
+
6537
+ // src/AccountHealthSweeper.ts
6538
+ var REFRESH_LEAD_MS = 5 * 6e4;
6539
+ var SWEEP_INTERVAL_MS = 6e4;
6540
+ var OAUTH_PROVIDERS = ["claude", "codex", "gemini"];
6541
+ function isOAuthProvider(providerId) {
6542
+ return OAUTH_PROVIDERS.includes(providerId);
6543
+ }
6544
+ var AccountHealthSweeper = class {
6545
+ constructor(store, health2, logger, intervalMs = SWEEP_INTERVAL_MS, leadMs = REFRESH_LEAD_MS) {
6546
+ this.store = store;
6547
+ this.health = health2;
6548
+ this.logger = logger;
6549
+ this.intervalMs = intervalMs;
6550
+ this.leadMs = leadMs;
6551
+ }
6552
+ store;
6553
+ health;
6554
+ logger;
6555
+ intervalMs;
6556
+ leadMs;
6557
+ timer = null;
6558
+ sweeping = false;
6559
+ /** Arm the sweep interval. Idempotent. The timer never holds the loop open. */
6560
+ start() {
6561
+ if (this.timer) return;
6562
+ this.timer = setInterval(() => void this.sweep(), this.intervalMs);
6563
+ this.timer.unref?.();
6564
+ }
6565
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
6566
+ dispose() {
6567
+ if (this.timer) {
6568
+ clearInterval(this.timer);
6569
+ this.timer = null;
6570
+ }
6571
+ }
6572
+ /**
6573
+ * One sweep: surface accounts that just recovered (emits the recovery signal
6574
+ * through the tracker's hook) and nudge a fresh token for any recovered OAuth
6575
+ * account whose token is near expiry. Exposed for tests. Never throws.
6576
+ */
6577
+ async sweep(now = Date.now()) {
6578
+ if (this.sweeping) return;
6579
+ this.sweeping = true;
6580
+ try {
6581
+ const recovered = this.health.sweepRecoveries(now);
6582
+ if (recovered.length === 0) return;
6583
+ const config = await this.store.getFullConfig();
6584
+ for (const event of recovered) {
6585
+ if (!isOAuthProvider(event.providerId)) continue;
6586
+ const account = getAccountById(config, event.providerId, event.accountId);
6587
+ if (!account || !this.needsRefresh(account.tokens, now)) continue;
6588
+ await this.refreshOne(event.providerId, event.accountId);
6589
+ }
6590
+ } catch (error) {
6591
+ this.logger.warn("account-health sweep failed", {
6592
+ error: error instanceof Error ? error.message : String(error)
6593
+ });
6594
+ } finally {
6595
+ this.sweeping = false;
6596
+ }
6597
+ }
6598
+ /** Expiring within the lead window, refreshable, and not already dead. */
6599
+ needsRefresh(tokens, now) {
6600
+ const t = tokens;
6601
+ if (!t.refreshToken || t.status === "expired" || t.status === "error") return false;
6602
+ if (!t.expiresAt) return false;
6603
+ const expiresAt = Date.parse(t.expiresAt);
6604
+ return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
6605
+ }
6606
+ /** Refresh one recovered account by id; failures are logged, never thrown. */
6607
+ async refreshOne(provider, id) {
6608
+ try {
6609
+ const ok = await this.store.refreshAccountById(provider, id);
6610
+ if (ok) this.logger.info("account-health recovery refresh succeeded", { provider, accountId: id });
6611
+ else this.logger.warn("account-health recovery refresh failed", { provider, accountId: id });
6612
+ } catch (error) {
6613
+ this.logger.warn("account-health recovery refresh threw", {
6614
+ provider,
6615
+ accountId: id,
6616
+ error: error instanceof Error ? error.message : String(error)
6617
+ });
6618
+ }
6619
+ }
6620
+ };
6621
+
6622
+ // src/audit/AuditPruneSweeper.ts
6623
+ var import_node_fs15 = require("fs");
6624
+ var import_node_path8 = require("path");
6625
+
6626
+ // src/audit/auditFiles.ts
6627
+ var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
6628
+ var pad22 = (n) => String(n).padStart(2, "0");
6629
+ function auditFileName(ts) {
6630
+ const d = new Date(ts);
6631
+ return `audit-${d.getFullYear()}-${pad22(d.getMonth() + 1)}-${pad22(d.getDate())}.jsonl`;
6632
+ }
6633
+ function auditFileDateMs(fileName) {
6634
+ const m = AUDIT_FILE_RE.exec(fileName);
6635
+ if (!m) return null;
6636
+ const year = Number(m[1]);
6637
+ const month = Number(m[2]);
6638
+ const day = Number(m[3]);
6639
+ const d = new Date(year, month - 1, day);
6640
+ if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day) {
6641
+ return null;
6642
+ }
6643
+ return d.getTime();
6644
+ }
6645
+
6646
+ // src/audit/AuditPruneSweeper.ts
6647
+ var DAY_MS = 24 * 60 * 6e4;
6648
+ var SWEEP_INTERVAL_MS2 = 60 * 6e4;
6649
+ var AuditPruneSweeper = class {
6650
+ constructor(auditDir, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
6651
+ this.auditDir = auditDir;
6652
+ this.logger = logger;
6653
+ this.config = config;
6654
+ this.intervalMs = intervalMs;
6655
+ this.now = now;
6656
+ }
6657
+ auditDir;
6658
+ logger;
6659
+ config;
6660
+ intervalMs;
6661
+ now;
6662
+ timer = null;
6663
+ sweeping = false;
6664
+ /** Whether pruning is active (audit enabled). */
6665
+ get enabled() {
6666
+ return this.config.enabled;
6667
+ }
6668
+ /** Re-apply config to the live instance (boot + admin PUT hot-reload). */
6669
+ configure(config) {
6670
+ this.config = config;
6671
+ }
6672
+ /**
6673
+ * Arm the prune interval AND run one prune immediately (boot cleanup). No-op
6674
+ * when audit is disabled (zero regression). Idempotent.
6675
+ */
6676
+ start() {
6677
+ if (this.timer || !this.config.enabled) return;
6678
+ void this.sweep();
6679
+ this.timer = setInterval(() => void this.sweep(), this.intervalMs);
6680
+ this.timer.unref?.();
6681
+ }
6682
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
6683
+ dispose() {
6684
+ if (this.timer) {
6685
+ clearInterval(this.timer);
6686
+ this.timer = null;
6687
+ }
6688
+ }
6689
+ /**
6690
+ * One prune: unlink every audit date file strictly OLDER than the retention
6691
+ * cutoff (`now - retentionDays` days, at local-midnight granularity). Exposed
6692
+ * for tests; never throws. Returns the number of files removed.
6693
+ */
6694
+ async sweep() {
6695
+ if (!this.config.enabled || this.sweeping) return 0;
6696
+ this.sweeping = true;
6697
+ try {
6698
+ if (!(0, import_node_fs15.existsSync)(this.auditDir)) return 0;
6699
+ const today = new Date(this.now());
6700
+ const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
6701
+ const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
6702
+ let removed = 0;
6703
+ for (const file of (0, import_node_fs15.readdirSync)(this.auditDir)) {
6704
+ const dateMs = auditFileDateMs(file);
6705
+ if (dateMs === null || dateMs >= cutoff) continue;
6706
+ try {
6707
+ (0, import_node_fs15.unlinkSync)((0, import_node_path8.join)(this.auditDir, file));
6708
+ removed += 1;
6709
+ } catch (error) {
6710
+ this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
6711
+ file,
6712
+ error: error instanceof Error ? error.message : String(error)
6713
+ });
6714
+ }
6715
+ }
6716
+ if (removed > 0) this.logger.debug("audit prune complete", { removed });
6717
+ return removed;
6718
+ } catch (error) {
6719
+ this.logger.warn("audit prune sweep failed", {
6720
+ error: error instanceof Error ? error.message : String(error)
6721
+ });
6722
+ return 0;
6723
+ } finally {
6724
+ this.sweeping = false;
6725
+ }
6726
+ }
6727
+ };
6728
+
6729
+ // src/audit/auditReader.ts
6730
+ var import_node_fs16 = require("fs");
6731
+ var import_node_path9 = require("path");
6732
+ var DEFAULT_LIMIT = 200;
6733
+ var MAX_LIMIT = 2e3;
6734
+ function readAuditRecords(auditDir, query = {}) {
6735
+ if (!(0, import_node_fs16.existsSync)(auditDir)) return [];
6736
+ let files;
6737
+ try {
6738
+ files = (0, import_node_fs16.readdirSync)(auditDir).filter((f) => AUDIT_FILE_RE.test(f));
6739
+ } catch {
6740
+ return [];
6741
+ }
6742
+ const from = typeof query.from === "number" ? query.from : -Infinity;
6743
+ const to = typeof query.to === "number" ? query.to : Infinity;
6744
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit ?? DEFAULT_LIMIT)));
6745
+ const matched = [];
6746
+ for (const file of files.sort().reverse()) {
6747
+ let raw;
6748
+ try {
6749
+ raw = (0, import_node_fs16.readFileSync)((0, import_node_path9.join)(auditDir, file), "utf8");
6750
+ } catch {
6751
+ continue;
6752
+ }
6753
+ for (const line of raw.split("\n")) {
6754
+ const trimmed = line.trim();
6755
+ if (!trimmed) continue;
6756
+ let rec;
6757
+ try {
6758
+ rec = JSON.parse(trimmed);
6759
+ } catch {
6760
+ continue;
6761
+ }
6762
+ if (!isAuditRecord(rec)) continue;
6763
+ if (query.keyId !== void 0 && rec.keyId !== query.keyId) continue;
6764
+ if (rec.ts < from || rec.ts > to) continue;
6765
+ matched.push(rec);
6766
+ }
6767
+ }
6768
+ matched.sort((a, b) => b.ts - a.ts);
6769
+ return matched.slice(0, limit);
6770
+ }
6771
+ function isAuditRecord(value) {
6772
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
6773
+ const r = value;
6774
+ return typeof r["id"] === "string" && typeof r["ts"] === "number" && typeof r["method"] === "string" && typeof r["path"] === "string" && typeof r["status"] === "number";
6775
+ }
6776
+
6777
+ // src/audit/AuditWriter.ts
6778
+ var import_node_fs17 = require("fs");
6779
+ var import_node_path10 = require("path");
6780
+ var AuditWriter = class {
6781
+ constructor(auditDir, logger, defer = (fn) => setTimeout(fn, 0)) {
6782
+ this.auditDir = auditDir;
6783
+ this.logger = logger;
6784
+ this.defer = defer;
6785
+ }
6786
+ auditDir;
6787
+ logger;
6788
+ defer;
6789
+ dirEnsured = false;
6790
+ /**
6791
+ * Enqueue one record for append. Returns IMMEDIATELY (fire-and-forget); the fs
6792
+ * write happens on the deferred tick. A failure is logged, never thrown.
6793
+ */
6794
+ record(record) {
6795
+ this.defer(() => {
6796
+ try {
6797
+ this.appendNow(record);
6798
+ } catch (error) {
6799
+ this.logger.warn("[AuditWriter] failed to append audit record", {
6800
+ error: error instanceof Error ? error.message : String(error)
6801
+ });
6802
+ }
6803
+ });
6804
+ }
6805
+ /**
6806
+ * Append synchronously — the awaitable form tests use to assert the line landed.
6807
+ * Ensures the `audit/` directory exists on first write (lazy, like the usage
6808
+ * store's lazy file creation).
4465
6809
  */
4466
- async writeProviderTokens(providerId, config) {
4467
- const current = this.readConfig();
4468
- writeActiveTokens(current, providerId, config);
4469
- this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6810
+ appendNow(record) {
6811
+ if (!this.dirEnsured) {
6812
+ (0, import_node_fs17.mkdirSync)(this.auditDir, { recursive: true });
6813
+ this.dirEnsured = true;
6814
+ }
6815
+ const file = (0, import_node_path10.join)(this.auditDir, auditFileName(record.ts));
6816
+ (0, import_node_fs17.appendFileSync)(file, JSON.stringify(record) + "\n", "utf8");
6817
+ }
6818
+ };
6819
+
6820
+ // src/billing/BillingPublisher.ts
6821
+ var import_node_fs18 = require("fs");
6822
+ var import_node_crypto10 = require("crypto");
6823
+ var import_node_path11 = require("path");
6824
+ var import_upstreamFetch5 = require("@omnicross/core/pipeline/upstreamFetch");
6825
+
6826
+ // src/billing/billingFiles.ts
6827
+ var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
6828
+ var DELIVERED_FILE_RE = /^delivered-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
6829
+ var pad23 = (n) => String(n).padStart(2, "0");
6830
+ function dateStamp(ts) {
6831
+ const d = new Date(ts);
6832
+ return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}-${pad23(d.getDate())}`;
6833
+ }
6834
+ function billingFileName(ts) {
6835
+ return `billing-${dateStamp(ts)}.jsonl`;
6836
+ }
6837
+ function deliveredFileName(ts) {
6838
+ return `delivered-${dateStamp(ts)}.jsonl`;
6839
+ }
6840
+
6841
+ // src/billing/BillingPublisher.ts
6842
+ var BILLING_POST_TIMEOUT_MS = 1e4;
6843
+ var BillingPublisher = class {
6844
+ constructor(billingDir, logger, opts = {}) {
6845
+ this.billingDir = billingDir;
6846
+ this.logger = logger;
6847
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0, import_upstreamFetch5.fetchUpstream)(url, init));
6848
+ this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
6849
+ this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
6850
+ this.now = opts.now ?? Date.now;
6851
+ }
6852
+ billingDir;
6853
+ logger;
6854
+ config;
6855
+ dirEnsured = false;
6856
+ fetchImpl;
6857
+ defer;
6858
+ timeoutMs;
6859
+ now;
6860
+ /** Install/replace the live billing config (endpoint + secret + retry bound). */
6861
+ setConfig(config) {
6862
+ this.config = config;
4470
6863
  }
4471
6864
  /**
4472
- * DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
4473
- * (optional label) and set it active, then re-derive the mirror used by
4474
- * `omnicross login <provider> --label` to add an account instead of overwriting.
6865
+ * Record one billing event. DURABLE-FIRST: append synchronously (the event is
6866
+ * now on disk, never lost), THEN schedule a best-effort POST off the caller's
6867
+ * stack (non-blocking; ledger-only when no endpoint). Returns IMMEDIATELY and
6868
+ * NEVER throws — a failing append/POST is logged, never propagated.
4475
6869
  */
4476
- async appendProviderAccount(providerId, config, label) {
4477
- const current = this.readConfig();
4478
- const result = addAccount(current, providerId, config, label);
4479
- this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
4480
- return result;
6870
+ record(event) {
6871
+ let appended = false;
6872
+ try {
6873
+ this.appendNow(event);
6874
+ appended = true;
6875
+ } catch (error) {
6876
+ this.logger.warn("[BillingPublisher] failed to append billing event", {
6877
+ error: error instanceof Error ? error.message : String(error)
6878
+ });
6879
+ }
6880
+ if (appended && this.config?.endpoint) {
6881
+ this.defer(() => {
6882
+ void this.deliverNow(event).catch(() => {
6883
+ });
6884
+ });
6885
+ }
4481
6886
  }
4482
6887
  /**
4483
- * DAEMON-ONLY active switch (design D5, NOT on the port). Switch the active
4484
- * account for a provider; rejects an unknown id. Re-derives the mirror.
6888
+ * Append the event as one JSON line to `billing-YYYY-MM-DD.jsonl` (the event's
6889
+ * LOCAL date). Synchronous the awaitable form tests use to assert the ledger
6890
+ * line landed BEFORE any delivery. Ensures the `billing/` directory on first write.
4485
6891
  */
4486
- async setActiveAccount(providerId, id) {
4487
- const current = this.readConfig();
4488
- const result = setActiveAccount(current, providerId, id);
4489
- if (!result.ok) return result;
4490
- this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
4491
- return result;
6892
+ appendNow(event) {
6893
+ this.ensureDir();
6894
+ const file = (0, import_node_path11.join)(this.billingDir, billingFileName(event.ts));
6895
+ (0, import_node_fs18.appendFileSync)(file, JSON.stringify(event) + "\n", "utf8");
4492
6896
  }
4493
6897
  /**
4494
- * DAEMON-ONLY per-account remove (design D5, NOT on the port). Remove one
4495
- * account; promote the most-recent remaining on active-removal (or clear the
4496
- * mirror when none remain). Re-derives the mirror.
6898
+ * One best-effort delivery attempt for an event ALREADY in the ledger. POSTs the
6899
+ * event JSON to the configured endpoint (optionally HMAC-signed); on a 2xx ack
6900
+ * appends a delivery marker and returns `true`. Any non-2xx / thrown / timed-out
6901
+ * attempt returns `false` — the event stays UNdelivered in the ledger (never
6902
+ * lost). NEVER rejects. A no-op `false` when no endpoint is configured.
4497
6903
  */
4498
- async removeAccount(providerId, id) {
4499
- const current = this.readConfig();
4500
- const result = removeAccount(current, providerId, id);
4501
- if (!result.removed) return result;
4502
- this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
4503
- return result;
6904
+ async deliverNow(event) {
6905
+ const endpoint = this.config?.endpoint;
6906
+ if (!endpoint) return false;
6907
+ try {
6908
+ const body = JSON.stringify(event);
6909
+ const headers = { "Content-Type": "application/json" };
6910
+ const secret = this.config?.secret;
6911
+ if (secret) {
6912
+ const hmac = (0, import_node_crypto10.createHmac)("sha256", secret).update(body).digest("hex");
6913
+ headers["X-Omnicross-Billing-Signature"] = `sha256=${hmac}`;
6914
+ }
6915
+ const res = await this.fetchImpl(endpoint, {
6916
+ method: "POST",
6917
+ headers,
6918
+ body,
6919
+ signal: AbortSignal.timeout(this.timeoutMs)
6920
+ });
6921
+ if (!res.ok) {
6922
+ this.logger.debug(`[billing] delivery failed ${event.id} (HTTP ${res.status})`);
6923
+ return false;
6924
+ }
6925
+ this.markDelivered(event);
6926
+ this.logger.debug(`[billing] delivered ${event.id}`);
6927
+ return true;
6928
+ } catch (error) {
6929
+ this.logger.debug(
6930
+ `[billing] delivery error ${event.id}: ${error instanceof Error ? error.message : String(error)}`
6931
+ );
6932
+ return false;
6933
+ }
4504
6934
  }
4505
6935
  /**
4506
- * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
4507
- * rejects an unknown id. Label-only no token material is read or written
4508
- * (the secret-free invariant holds).
6936
+ * Append a delivery marker `{ id, deliveredAt }` to `delivered-YYYY-MM-DD.jsonl`
6937
+ * (keyed by the EVENT's date so the reader finds both together). Idempotent at
6938
+ * the reconciliation layer — the reader unions marker ids into a delivered set,
6939
+ * so a duplicate marker is harmless. A marker-write failure is logged, never thrown.
4509
6940
  */
4510
- async renameAccount(providerId, id, label) {
4511
- const current = this.readConfig();
4512
- const result = renameAccount(current, providerId, id, label);
4513
- if (!result.ok) return result;
4514
- this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
4515
- return result;
6941
+ markDelivered(event) {
6942
+ try {
6943
+ this.ensureDir();
6944
+ const file = (0, import_node_path11.join)(this.billingDir, deliveredFileName(event.ts));
6945
+ (0, import_node_fs18.appendFileSync)(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
6946
+ } catch (error) {
6947
+ this.logger.warn("[BillingPublisher] failed to append delivery marker", {
6948
+ error: error instanceof Error ? error.message : String(error)
6949
+ });
6950
+ }
6951
+ }
6952
+ ensureDir() {
6953
+ if (this.dirEnsured) return;
6954
+ (0, import_node_fs18.mkdirSync)(this.billingDir, { recursive: true });
6955
+ this.dirEnsured = true;
6956
+ }
6957
+ };
6958
+
6959
+ // src/billing/billingReader.ts
6960
+ var import_node_fs19 = require("fs");
6961
+ var import_node_path12 = require("path");
6962
+ function readBillingLedger(billingDir) {
6963
+ const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
6964
+ if (!(0, import_node_fs19.existsSync)(billingDir)) return view;
6965
+ let files;
6966
+ try {
6967
+ files = (0, import_node_fs19.readdirSync)(billingDir);
6968
+ } catch {
6969
+ return view;
6970
+ }
6971
+ for (const file of files.sort()) {
6972
+ if (BILLING_FILE_RE.test(file)) {
6973
+ for (const rec of parseLines(billingDir, file)) {
6974
+ if (isBillingEvent(rec)) view.events.push(rec);
6975
+ }
6976
+ } else if (DELIVERED_FILE_RE.test(file)) {
6977
+ for (const rec of parseLines(billingDir, file)) {
6978
+ const id = rec.id;
6979
+ if (typeof id === "string") view.deliveredIds.add(id);
6980
+ }
6981
+ }
6982
+ }
6983
+ return view;
6984
+ }
6985
+ function readUndeliveredEvents(billingDir) {
6986
+ const { events, deliveredIds } = readBillingLedger(billingDir);
6987
+ return events.filter((e) => !deliveredIds.has(e.id)).sort((a, b) => a.ts - b.ts);
6988
+ }
6989
+ function readBillingStatus(billingDir) {
6990
+ const { events, deliveredIds } = readBillingLedger(billingDir);
6991
+ let delivered = 0;
6992
+ for (const e of events) if (deliveredIds.has(e.id)) delivered += 1;
6993
+ return { total: events.length, delivered, pending: events.length - delivered };
6994
+ }
6995
+ function parseLines(dir, file) {
6996
+ let raw;
6997
+ try {
6998
+ raw = (0, import_node_fs19.readFileSync)((0, import_node_path12.join)(dir, file), "utf8");
6999
+ } catch {
7000
+ return [];
7001
+ }
7002
+ const out = [];
7003
+ for (const line of raw.split("\n")) {
7004
+ const trimmed = line.trim();
7005
+ if (!trimmed) continue;
7006
+ try {
7007
+ out.push(JSON.parse(trimmed));
7008
+ } catch {
7009
+ }
7010
+ }
7011
+ return out;
7012
+ }
7013
+ function isBillingEvent(value) {
7014
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
7015
+ const r = value;
7016
+ return typeof r["id"] === "string" && typeof r["ts"] === "number" && typeof r["model"] === "string" && typeof r["status"] === "number";
7017
+ }
7018
+
7019
+ // src/billing/BillingRetrySweeper.ts
7020
+ var SWEEP_INTERVAL_MS3 = 5 * 6e4;
7021
+ var BillingRetrySweeper = class {
7022
+ constructor(billingDir, publisher2, logger, config, intervalMs = SWEEP_INTERVAL_MS3, now = Date.now) {
7023
+ this.billingDir = billingDir;
7024
+ this.publisher = publisher2;
7025
+ this.logger = logger;
7026
+ this.config = config;
7027
+ this.intervalMs = intervalMs;
7028
+ this.now = now;
7029
+ }
7030
+ billingDir;
7031
+ publisher;
7032
+ logger;
7033
+ config;
7034
+ intervalMs;
7035
+ now;
7036
+ timer = null;
7037
+ sweeping = false;
7038
+ /** Whether retrying is active: billing enabled AND an endpoint is configured. */
7039
+ get enabled() {
7040
+ return this.config.enabled && typeof this.config.endpoint === "string" && this.config.endpoint.length > 0;
7041
+ }
7042
+ /** Re-apply config to the live instance (boot + admin PUT hot-reload). */
7043
+ configure(config) {
7044
+ this.config = config;
4516
7045
  }
4517
7046
  /**
4518
- * DAEMON-ONLY CLEAR (design D1/D3, NOT on the port). Remove a single provider's
4519
- * block from `tokens.json` and re-persist (the strategies already tolerate an
4520
- * absent block). Stamps a fresh `updatedAt`. A no-op-shaped write when the
4521
- * provider was already absent (still re-stamps + persists).
7047
+ * Arm the retry interval AND run one sweep immediately (boot catch-up for events
7048
+ * that failed to deliver while the daemon was down). No-op when disabled or in
7049
+ * ledger-only mode (no endpoint to POST to). Idempotent.
4522
7050
  */
4523
- async clearProvider(providerId) {
4524
- const current = this.readConfig();
4525
- clearProvider(current, providerId);
4526
- this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
7051
+ start() {
7052
+ if (this.timer || !this.enabled) return;
7053
+ void this.sweep();
7054
+ this.timer = setInterval(() => void this.sweep(), this.intervalMs);
7055
+ this.timer.unref?.();
4527
7056
  }
4528
- /** Write the merged config to disk as pretty JSON (mkdir parent if needed).
4529
- * Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
4530
- * → `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
4531
- * write — incl. child 4's future refresh writes — lands encrypted. */
4532
- persist(config) {
4533
- (0, import_node_fs12.mkdirSync)((0, import_node_path7.dirname)(this.tokensPath), { recursive: true });
4534
- const encrypted = encryptTokens(config, this.box);
4535
- (0, import_node_fs12.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
7057
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
7058
+ dispose() {
7059
+ if (this.timer) {
7060
+ clearInterval(this.timer);
7061
+ this.timer = null;
7062
+ }
4536
7063
  }
4537
7064
  /**
4538
- * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
4539
- * the token-material fields so every getter returns plaintext (the
4540
- * subscription bearer path is byte-identical).
4541
- *
4542
- * The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
4543
- * file → empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
4544
- * wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
4545
- * box's clear, secret-free error (secrets spec "错误密钥 / 篡改的解密失败 UX":
4546
- * SHALL fail-fast, SHALL NOT 静默降级 — a swallowed decrypt would report "no
4547
- * tokens" and silently send the WRONG bearer upstream → 401). Mirrors
4548
- * `config.ts loadConfig`, which decrypts outside its parse try.
7065
+ * One sweep: re-POST every UNdelivered ledger event still within
7066
+ * `maxRetryAgeMs`; leave over-age undelivered events for reconciliation (NEVER
7067
+ * deleted). Exposed for tests; never throws. Returns the number of events a
7068
+ * re-POST was attempted for.
4549
7069
  */
4550
- readConfig() {
4551
- if (!(0, import_node_fs12.existsSync)(this.tokensPath)) return { updatedAt: "" };
4552
- let parsed;
7070
+ async sweep() {
7071
+ if (!this.enabled || this.sweeping) return 0;
7072
+ this.sweeping = true;
4553
7073
  try {
4554
- const raw = JSON.parse((0, import_node_fs12.readFileSync)(this.tokensPath, "utf8"));
4555
- parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
4556
- } catch {
4557
- parsed = null;
7074
+ const cutoff = this.now() - this.config.maxRetryAgeMs;
7075
+ let attempted = 0;
7076
+ for (const event of readUndeliveredEvents(this.billingDir)) {
7077
+ if (event.ts < cutoff) continue;
7078
+ attempted += 1;
7079
+ await this.publisher.deliverNow(event);
7080
+ }
7081
+ if (attempted > 0) this.logger.debug("billing retry sweep complete", { attempted });
7082
+ return attempted;
7083
+ } catch (error) {
7084
+ this.logger.warn("billing retry sweep failed", {
7085
+ error: error instanceof Error ? error.message : String(error)
7086
+ });
7087
+ return 0;
7088
+ } finally {
7089
+ this.sweeping = false;
4558
7090
  }
4559
- if (!parsed) return { updatedAt: "" };
4560
- const decrypted = decryptTokens(parsed, this.box);
4561
- return migrateLazily(decrypted);
4562
7091
  }
4563
7092
  };
4564
7093
 
4565
7094
  // src/TokenRefreshScheduler.ts
4566
- var REFRESH_LEAD_MS = 5 * 6e4;
4567
- var SWEEP_INTERVAL_MS = 6e4;
4568
- var OAUTH_PROVIDERS = ["claude", "codex", "gemini"];
7095
+ var REFRESH_LEAD_MS2 = 5 * 6e4;
7096
+ var SWEEP_INTERVAL_MS4 = 6e4;
7097
+ var OAUTH_PROVIDERS2 = ["claude", "codex", "gemini"];
4569
7098
  var TokenRefreshScheduler = class {
4570
- constructor(store, logger, intervalMs = SWEEP_INTERVAL_MS, leadMs = REFRESH_LEAD_MS) {
7099
+ constructor(store, logger, intervalMs = SWEEP_INTERVAL_MS4, leadMs = REFRESH_LEAD_MS2) {
4571
7100
  this.store = store;
4572
7101
  this.logger = logger;
4573
7102
  this.intervalMs = intervalMs;
@@ -4598,7 +7127,7 @@ var TokenRefreshScheduler = class {
4598
7127
  this.sweeping = true;
4599
7128
  try {
4600
7129
  const config = await this.store.getFullConfig();
4601
- for (const provider of OAUTH_PROVIDERS) {
7130
+ for (const provider of OAUTH_PROVIDERS2) {
4602
7131
  const activeId = getActiveAccount(config, provider)?.id;
4603
7132
  for (const account of listAccounts(config, provider)) {
4604
7133
  if (!this.needsRefresh(account.tokens, now)) continue;
@@ -4651,16 +7180,186 @@ var TokenRefreshScheduler = class {
4651
7180
  }
4652
7181
  };
4653
7182
 
7183
+ // src/webhook/WebhookDispatcher.ts
7184
+ var import_node_crypto11 = require("crypto");
7185
+ var import_upstreamFetch6 = require("@omnicross/core/pipeline/upstreamFetch");
7186
+ var WEBHOOK_MAX_ATTEMPTS = 3;
7187
+ var WEBHOOK_QUEUE_MAX = 1e3;
7188
+ var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
7189
+ var WEBHOOK_BASE_BACKOFF_MS = 200;
7190
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
7191
+ var WebhookDispatcher = class {
7192
+ config;
7193
+ queue = [];
7194
+ draining = false;
7195
+ warnedFull = false;
7196
+ fetchImpl;
7197
+ logger;
7198
+ maxAttempts;
7199
+ queueMax;
7200
+ timeoutMs;
7201
+ baseBackoffMs;
7202
+ sleep;
7203
+ now;
7204
+ constructor(opts = {}) {
7205
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0, import_upstreamFetch6.fetchUpstream)(url, init));
7206
+ this.logger = opts.logger;
7207
+ this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
7208
+ this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
7209
+ this.timeoutMs = opts.timeoutMs ?? WEBHOOK_SEND_TIMEOUT_MS;
7210
+ this.baseBackoffMs = opts.baseBackoffMs ?? WEBHOOK_BASE_BACKOFF_MS;
7211
+ this.sleep = opts.sleep ?? defaultSleep;
7212
+ this.now = opts.now ?? Date.now;
7213
+ }
7214
+ /** Install/replace the live webhook config (destinations + master switch). */
7215
+ setConfig(config) {
7216
+ this.config = config;
7217
+ }
7218
+ /**
7219
+ * Enqueue an event and return IMMEDIATELY (fire-and-forget). NEVER awaits a
7220
+ * send, NEVER throws — the drain loop does all sending on a side channel. A
7221
+ * full queue drops the OLDEST event (with a one-shot warn) so a runaway source
7222
+ * can't OOM the process.
7223
+ */
7224
+ emit(event) {
7225
+ if (this.queue.length >= this.queueMax) {
7226
+ this.queue.shift();
7227
+ if (!this.warnedFull) {
7228
+ this.logger?.warn("[webhook] queue full \u2014 dropping oldest events");
7229
+ this.warnedFull = true;
7230
+ }
7231
+ }
7232
+ this.queue.push(event);
7233
+ if (!this.draining) {
7234
+ this.draining = true;
7235
+ queueMicrotask(() => void this.drain());
7236
+ }
7237
+ }
7238
+ /** Drain the queue, sending each event to its matching destinations concurrently. */
7239
+ async drain() {
7240
+ try {
7241
+ while (this.queue.length > 0) {
7242
+ const event = this.queue.shift();
7243
+ const destinations = this.matchingDestinations(event.kind);
7244
+ if (destinations.length === 0) continue;
7245
+ await Promise.all(destinations.map((d) => this.sendWithRetry(event, d)));
7246
+ }
7247
+ } finally {
7248
+ this.draining = false;
7249
+ if (this.queue.length > 0) {
7250
+ this.draining = true;
7251
+ queueMicrotask(() => void this.drain());
7252
+ }
7253
+ }
7254
+ }
7255
+ /** The enabled destinations whose event filter admits this kind (empty ⇒ all). */
7256
+ matchingDestinations(kind) {
7257
+ const cfg = this.config;
7258
+ if (!cfg || !cfg.enabled) return [];
7259
+ return cfg.destinations.filter(
7260
+ (d) => d.enabled && (!d.events || d.events.length === 0 || d.events.includes(kind))
7261
+ );
7262
+ }
7263
+ /** Send with bounded exponential backoff; log-and-drop after `maxAttempts`. */
7264
+ async sendWithRetry(event, dest) {
7265
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
7266
+ const result = await this.sendOnce(event, dest);
7267
+ if (result.ok) {
7268
+ this.logger?.debug(`[webhook] delivered ${event.kind} \u2192 ${dest.id} (${result.status})`);
7269
+ return;
7270
+ }
7271
+ if (attempt < this.maxAttempts) {
7272
+ await this.sleep(this.baseBackoffMs * 2 ** (attempt - 1));
7273
+ } else {
7274
+ this.logger?.warn(
7275
+ `[webhook] dropped ${event.kind} \u2192 ${dest.id} after ${this.maxAttempts} attempts: ${result.error ?? `HTTP ${result.status}`}`
7276
+ );
7277
+ }
7278
+ }
7279
+ }
7280
+ /** One POST attempt. Returns an outcome; a thrown error becomes `{ ok:false }`. */
7281
+ async sendOnce(event, dest) {
7282
+ try {
7283
+ const { body, headers } = buildRequest(event, dest, this.now());
7284
+ const res = await this.fetchImpl(dest.url, {
7285
+ method: "POST",
7286
+ headers: { "Content-Type": "application/json", ...headers },
7287
+ body,
7288
+ signal: AbortSignal.timeout(this.timeoutMs)
7289
+ });
7290
+ return res.ok ? { ok: true, status: res.status } : { ok: false, status: res.status };
7291
+ } catch (err5) {
7292
+ return { ok: false, error: err5 instanceof Error ? err5.message : String(err5) };
7293
+ }
7294
+ }
7295
+ /**
7296
+ * ADMIN test path (design D8): deliver a `test` event to ONE destination and
7297
+ * AWAIT the single-attempt result. This is the ONLY awaited send — it runs on
7298
+ * the admin request path (an operator clicking "Test"), NEVER on a relay path,
7299
+ * so awaiting it is safe. Finds the destination regardless of its `enabled`
7300
+ * flag or the master switch (an explicit operator action).
7301
+ */
7302
+ async deliverTest(destinationId) {
7303
+ const dest = this.config?.destinations.find((d) => d.id === destinationId);
7304
+ if (!dest) return { ok: false, error: "destination not found" };
7305
+ return this.sendOnce({ kind: "test", at: this.now() }, dest);
7306
+ }
7307
+ };
7308
+ function buildRequest(event, dest, nowMs) {
7309
+ if (dest.type === "feishu") return buildFeishu(event, dest, nowMs);
7310
+ return buildCustom(event, dest);
7311
+ }
7312
+ function buildCustom(event, dest) {
7313
+ const body = JSON.stringify(event);
7314
+ const headers = {};
7315
+ if (dest.secret) {
7316
+ const hmac = (0, import_node_crypto11.createHmac)("sha256", dest.secret).update(body).digest("hex");
7317
+ headers["X-Omnicross-Signature"] = `sha256=${hmac}`;
7318
+ }
7319
+ return { body, headers };
7320
+ }
7321
+ function buildFeishu(event, dest, nowMs) {
7322
+ const payload = {
7323
+ msg_type: "text",
7324
+ content: { text: feishuText(event) }
7325
+ };
7326
+ if (dest.secret) {
7327
+ const timestamp = Math.floor(nowMs / 1e3).toString();
7328
+ const stringToSign = `${timestamp}
7329
+ ${dest.secret}`;
7330
+ payload["timestamp"] = timestamp;
7331
+ payload["sign"] = (0, import_node_crypto11.createHmac)("sha256", stringToSign).digest("base64");
7332
+ }
7333
+ return { body: JSON.stringify(payload), headers: {} };
7334
+ }
7335
+ function feishuText(event) {
7336
+ switch (event.kind) {
7337
+ case "account.recovery":
7338
+ return `omnicross: account recovered \u2014 ${event.providerId}/${event.accountId}`;
7339
+ case "account.anomaly":
7340
+ return `omnicross: account anomaly [${event.state}] \u2014 ${event.providerId}/${event.accountId}`;
7341
+ case "key.quotaWarning":
7342
+ return `omnicross: key quota warning (${event.scope}) \u2014 $${event.spentUsd.toFixed(2)} of $${event.limitUsd.toFixed(2)} (key ${event.keyId})`;
7343
+ case "key.quotaExceeded":
7344
+ return `omnicross: key quota EXCEEDED (${event.scope}) \u2014 $${event.spentUsd.toFixed(2)} of $${event.limitUsd.toFixed(2)} (key ${event.keyId})`;
7345
+ case "server.error":
7346
+ return `omnicross: server error \u2014 ${event.message}`;
7347
+ case "test":
7348
+ return "omnicross: webhook test";
7349
+ }
7350
+ }
7351
+
4654
7352
  // src/bootstrap.ts
4655
7353
  function buildDaemon(config, paths) {
4656
- const logger = new ConsoleLogger();
7354
+ const logger = new ConfigurableLogger(config.logging);
4657
7355
  const secretBox3 = new SecretBox(() => resolveMasterKey({ keyFilePath: paths.masterKeyFilePath }));
4658
7356
  setSecretBox(secretBox3);
4659
7357
  setSecretBox2(secretBox3);
4660
7358
  const decryptedConfig = decryptConfigSecrets(config, secretBox3);
4661
7359
  const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
4662
7360
  const keyDb = new JsonOutboundKeyDb(paths.keysPath);
4663
- const settingsStore = new JsonApiServerSettingsStore(paths.configPath);
7361
+ const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
7362
+ const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
4664
7363
  const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
4665
7364
  const subscriptionAccounts = new import_subscriptions4.SubscriptionAccountService(credentialStore);
4666
7365
  (0, import_subscriptions4.setSubscriptionAccountService)(subscriptionAccounts);
@@ -4669,6 +7368,12 @@ function buildDaemon(config, paths) {
4669
7368
  credentialStore
4670
7369
  );
4671
7370
  (0, import_subscriptions4.setSubscriptionProviderRegistry)(subscriptionRegistry);
7371
+ setServerProxyConfig(decryptedConfig.server?.proxy);
7372
+ (0, import_upstreamFetch7.setUpstreamProxyResolver)(
7373
+ createUpstreamProxyResolver({
7374
+ getAccountProxy: (providerId, accountId) => credentialStore.getAccountProxy(providerId, accountId)
7375
+ })
7376
+ );
4672
7377
  (0, import_gemini_code_assist_resolver.setGeminiCodeAssistResolver)((0, import_GeminiCodeAssistProjectResolver.getGeminiCodeAssistProjectResolver)());
4673
7378
  const autoDisableStore = new AutoDisableStore();
4674
7379
  const apiKeyPool = new import_ApiKeyPoolService.ApiKeyPoolService(
@@ -4689,19 +7394,59 @@ function buildDaemon(config, paths) {
4689
7394
  defaultUsageEventsPath(paths.configPath),
4690
7395
  async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
4691
7396
  );
4692
- const usageRecorder = new import_usage.UsageRecorder(usageEventStore, pricingEngine, logger);
7397
+ const keySpendTracker = new import_outbound_api6.KeySpendTracker(usageEventStore);
7398
+ const usageRecorder = new import_usage.UsageRecorder(usageEventStore, pricingEngine, logger, {
7399
+ onRecord: (apiKeyId, costUsd, at) => keySpendTracker.add(apiKeyId, costUsd, at)
7400
+ });
4693
7401
  const providerProxy = (0, import_provider_proxy.getProviderProxy)({ llmConfig, apiKeyPool, usageRecorder });
4694
7402
  llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
4695
- const outboundApiServer = (0, import_outbound_api4.getOutboundApiServer)({
7403
+ const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
7404
+ credentialStore,
7405
+ (0, import_SubscriptionAccountHealth2.getSharedAccountHealth)(),
7406
+ logger,
7407
+ import_outbound_api5.DEFAULT_ACCOUNT_PROBE
7408
+ );
7409
+ const getHealthReport = () => buildHealthReport({
7410
+ version: DAEMON_VERSION,
7411
+ // CRITICAL: the config loaded with a providers array.
7412
+ configPresent: () => Array.isArray(decryptedConfig.providers),
7413
+ // CRITICAL: the credential store's tokens.json is readable WITHOUT
7414
+ // decrypting (a missing file is fine — no accounts yet). A stat/access
7415
+ // only; never reads or decrypts token material.
7416
+ credentialStoreReadable: () => isTokensStoreReadable(paths.tokensPath),
7417
+ outboundServerRunning: () => outboundApiServer.getStatus().running,
7418
+ adminServerRunning: () => adminServer.getStatus().running,
7419
+ // Coarse, account-anonymous probe signal (#8, D5) — added to `checks` ONLY
7420
+ // when probing is ENABLED; disabled ⇒ `undefined` ⇒ key omitted ⇒ the
7421
+ // `/health` body stays byte-identical (zero regression).
7422
+ subscriptionAccountsHealthy: () => accountHealthProbeScheduler.enabled ? accountHealthProbeScheduler.probedAccountsHealthy() : void 0
7423
+ });
7424
+ const outboundApiServer = (0, import_outbound_api5.getOutboundApiServer)({
4696
7425
  db: keyDb,
7426
+ // voucher-redemption #9: the key-authenticated `POST /redeem` endpoint redeems
7427
+ // cards against the presenting key (gated on `voucher.enabled`).
7428
+ voucherDb,
4697
7429
  llmConfig,
4698
7430
  providerProxy,
4699
- proxyDeps: providerProxy.getDeps()
7431
+ proxyDeps: providerProxy.getDeps(),
7432
+ healthReportProvider: getHealthReport,
7433
+ // outbound-key-policy: the wire layer's 402 cost check reads per-key spend.
7434
+ keySpendTracker,
7435
+ // configurable-logging: route the server's OWN lifecycle + relay dispatch-error
7436
+ // lines through the injected logger (honors level/format/file sink).
7437
+ logger
4700
7438
  });
7439
+ const auditDir = defaultAuditDir(paths.configPath);
7440
+ const billingDir = defaultBillingDir(paths.configPath);
4701
7441
  const adminServer = new AdminServer({
4702
7442
  configPath: paths.configPath,
4703
7443
  llmConfig,
4704
7444
  keyDb,
7445
+ // voucher-redemption #9: the admin `/admin/api/voucher` surface generates/
7446
+ // lists/revokes redemption cards (gated on `voucher.enabled`).
7447
+ voucherDb,
7448
+ // outbound-key-policy: the admin key list surfaces each key's OWN spend.
7449
+ keySpendReader: keySpendTracker,
4705
7450
  settingsStore,
4706
7451
  outboundApiServer,
4707
7452
  subscriptionAccounts,
@@ -4723,7 +7468,9 @@ function buildDaemon(config, paths) {
4723
7468
  oauthSessions: new OAuthSessionStore(),
4724
7469
  // Real global fetch by default; a test seam (`paths.oauthExchangeFetch`) can
4725
7470
  // inject a mock so no real token endpoint is hit.
4726
- oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetch(url, init)),
7471
+ // upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
7472
+ // helper so interactive login honors a configured proxy (global/env layers).
7473
+ oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => (0, import_upstreamFetch7.fetchUpstream)(url, init)),
4727
7474
  subscriptionAccountAppender: credentialStore,
4728
7475
  // Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
4729
7476
  // + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
@@ -4750,9 +7497,48 @@ function buildDaemon(config, paths) {
4750
7497
  pricingStore,
4751
7498
  // Use the DECRYPTED config so `admin.token` (if stored as `enc:`) is the
4752
7499
  // plaintext bearer the AdminServer's constant-time compare expects (D4).
4753
- getAdminConfig: () => resolveAdminConfig(decryptedConfig.admin)
7500
+ getAdminConfig: () => resolveAdminConfig(decryptedConfig.admin),
7501
+ // Unauthenticated `/health` probe (daemon-health-endpoint) — the SAME shared
7502
+ // builder the outbound server uses, served before the admin auth gate.
7503
+ getHealthReport,
7504
+ // configurable-logging: the admin listener's lifecycle lines route through
7505
+ // the injected logger.
7506
+ logger,
7507
+ // subscription-account-probe #8: the AUTHED `GET /admin/api/account-probes`
7508
+ // reads per-account probe history from the scheduler (secret-free — ids +
7509
+ // status labels only). Routed in `AdminServer` (not `adminApi.ts`).
7510
+ probeHistoryReader: accountHealthProbeScheduler,
7511
+ // request-audit-log: the AUTHED `GET /admin/api/audit` reads + filters the
7512
+ // date-rotated audit store. Bound to the store dir here so the AdminServer
7513
+ // carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
7514
+ // NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
7515
+ auditReader: (query) => readAuditRecords(auditDir, query),
7516
+ // billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
7517
+ // secret-free total/delivered/pending counts of the durable ledger.
7518
+ billingStatusReader: () => readBillingStatus(billingDir)
7519
+ });
7520
+ const webhookDispatcher = new WebhookDispatcher({
7521
+ logger,
7522
+ fetchImpl: (url, init) => (0, import_upstreamFetch7.fetchUpstream)(url, init)
4754
7523
  });
7524
+ setWebhookRuntime(webhookDispatcher, (0, import_SubscriptionAccountHealth2.getSharedAccountHealth)());
7525
+ const auditWriter = new AuditWriter(auditDir, logger);
7526
+ const auditPruneSweeper = new AuditPruneSweeper(auditDir, logger, import_audit_types.DEFAULT_AUDIT_CONFIG);
7527
+ setAuditRuntime(auditWriter, auditPruneSweeper);
7528
+ const billingPublisher = new BillingPublisher(billingDir, logger);
7529
+ const billingRetrySweeper = new BillingRetrySweeper(
7530
+ billingDir,
7531
+ billingPublisher,
7532
+ logger,
7533
+ import_billing_types.DEFAULT_BILLING_CONFIG
7534
+ );
7535
+ setBillingRuntime(billingPublisher, billingRetrySweeper);
4755
7536
  const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
7537
+ const accountHealthSweeper = new AccountHealthSweeper(
7538
+ credentialStore,
7539
+ (0, import_SubscriptionAccountHealth2.getSharedAccountHealth)(),
7540
+ logger
7541
+ );
4756
7542
  return {
4757
7543
  logger,
4758
7544
  llmConfig,
@@ -4769,9 +7555,25 @@ function buildDaemon(config, paths) {
4769
7555
  pricingEngine,
4770
7556
  usageRecorder,
4771
7557
  adminServer,
4772
- tokenRefreshScheduler
7558
+ tokenRefreshScheduler,
7559
+ accountHealthSweeper,
7560
+ accountHealthProbeScheduler,
7561
+ webhookDispatcher,
7562
+ auditWriter,
7563
+ auditPruneSweeper,
7564
+ billingPublisher,
7565
+ billingRetrySweeper
4773
7566
  };
4774
7567
  }
7568
+ function isTokensStoreReadable(tokensPath) {
7569
+ try {
7570
+ if (!(0, import_node_fs20.existsSync)(tokensPath)) return true;
7571
+ (0, import_node_fs20.accessSync)(tokensPath, import_node_fs20.constants.R_OK);
7572
+ return true;
7573
+ } catch {
7574
+ return false;
7575
+ }
7576
+ }
4775
7577
 
4776
7578
  // src/commands/launch.ts
4777
7579
  var SUPPORTED_LAUNCH_CLIS = [
@@ -4811,10 +7613,10 @@ function buildCliSpawnPlan(opts) {
4811
7613
  };
4812
7614
  }
4813
7615
  function resolveInPathDefault(candidate) {
4814
- const segments = (process.env["PATH"] ?? "").split(import_node_path8.delimiter).filter(Boolean);
7616
+ const segments = (process.env["PATH"] ?? "").split(import_node_path13.delimiter).filter(Boolean);
4815
7617
  for (const seg of segments) {
4816
- const full = (0, import_node_path8.join)(seg, candidate);
4817
- if ((0, import_node_fs13.existsSync)(full)) return full;
7618
+ const full = (0, import_node_path13.join)(seg, candidate);
7619
+ if ((0, import_node_fs21.existsSync)(full)) return full;
4818
7620
  }
4819
7621
  return null;
4820
7622
  }
@@ -4857,6 +7659,10 @@ async function runLaunch(argv, deps) {
4857
7659
  } catch (err5) {
4858
7660
  daemon.apiKeyPool.dispose();
4859
7661
  daemon.tokenRefreshScheduler.dispose();
7662
+ daemon.accountHealthSweeper.dispose();
7663
+ daemon.accountHealthProbeScheduler.dispose();
7664
+ daemon.auditPruneSweeper.dispose();
7665
+ daemon.billingRetrySweeper.dispose();
4860
7666
  throw err5;
4861
7667
  }
4862
7668
  let launch;
@@ -4869,6 +7675,10 @@ async function runLaunch(argv, deps) {
4869
7675
  await daemon.providerProxy.stop();
4870
7676
  daemon.apiKeyPool.dispose();
4871
7677
  daemon.tokenRefreshScheduler.dispose();
7678
+ daemon.accountHealthSweeper.dispose();
7679
+ daemon.accountHealthProbeScheduler.dispose();
7680
+ daemon.auditPruneSweeper.dispose();
7681
+ daemon.billingRetrySweeper.dispose();
4872
7682
  throw err5;
4873
7683
  }
4874
7684
  try {
@@ -4891,6 +7701,10 @@ async function runLaunch(argv, deps) {
4891
7701
  await daemon.providerProxy.stop();
4892
7702
  daemon.apiKeyPool.dispose();
4893
7703
  daemon.tokenRefreshScheduler.dispose();
7704
+ daemon.accountHealthSweeper.dispose();
7705
+ daemon.accountHealthProbeScheduler.dispose();
7706
+ daemon.auditPruneSweeper.dispose();
7707
+ daemon.billingRetrySweeper.dispose();
4894
7708
  }
4895
7709
  }
4896
7710
  async function buildLaunchConfig(cli, llmConfig, opts) {
@@ -4963,6 +7777,7 @@ function spawnCliInherit(plan) {
4963
7777
  var import_node_child_process3 = require("child_process");
4964
7778
  var import_node_readline = require("readline");
4965
7779
  var import_node_util4 = require("util");
7780
+ var import_upstreamFetch8 = require("@omnicross/core/pipeline/upstreamFetch");
4966
7781
  var import_subscriptions5 = require("@omnicross/subscriptions");
4967
7782
  var PROVIDERS = ["claude", "codex", "gemini"];
4968
7783
  async function runLogin(argv, deps) {
@@ -4994,9 +7809,10 @@ async function runLogin(argv, deps) {
4994
7809
  };
4995
7810
  const box = resolveSecretBox(values["master-key-file"]);
4996
7811
  setSecretBox(box);
7812
+ (0, import_upstreamFetch8.setUpstreamProxyResolver)(createUpstreamProxyResolver());
4997
7813
  try {
4998
7814
  const tokensPath = defaultTokensPath(values.config);
4999
- const exchangeFetch = resolved.tokensFetch ?? ((url, init) => fetch(url, init));
7815
+ const exchangeFetch = resolved.tokensFetch ?? ((url, init) => (0, import_upstreamFetch8.fetchUpstream)(url, init, { providerId: provider }));
5000
7816
  const store = new JsonSubscriptionCredentialStore(tokensPath, box, exchangeFetch);
5001
7817
  const expiresAt = await runProviderLogin(
5002
7818
  provider,
@@ -5009,6 +7825,7 @@ async function runLogin(argv, deps) {
5009
7825
  console.info(` token: [stored, encrypted] expiresAt: ${expiresAt ?? "n/a"}`);
5010
7826
  } finally {
5011
7827
  setSecretBox(null);
7828
+ (0, import_upstreamFetch8.setUpstreamProxyResolver)(null);
5012
7829
  }
5013
7830
  }
5014
7831
  async function runProviderLogin(provider, store, deps, exchangeFetch, label) {
@@ -5131,7 +7948,7 @@ function promptPaste(prompt) {
5131
7948
  }
5132
7949
 
5133
7950
  // src/commands/providers.ts
5134
- var import_node_crypto10 = require("crypto");
7951
+ var import_node_crypto12 = require("crypto");
5135
7952
  var import_node_util5 = require("util");
5136
7953
  async function runProviders(argv) {
5137
7954
  const { values, positionals } = (0, import_node_util5.parseArgs)({
@@ -5253,7 +8070,7 @@ function providersAddKey(configPath, providerId, opts) {
5253
8070
  const cfg = loadConfig(configPath);
5254
8071
  const row = cfg.providers.find((p) => p.id === providerId);
5255
8072
  if (!row) throw new Error(`providers add-key: unknown provider '${providerId}'`);
5256
- const entry = { id: (0, import_node_crypto10.randomUUID)(), apiKey: opts.key };
8073
+ const entry = { id: (0, import_node_crypto12.randomUUID)(), apiKey: opts.key };
5257
8074
  if (opts.label) entry.label = opts.label;
5258
8075
  if (opts.weight !== void 0) {
5259
8076
  const w = Number(opts.weight);
@@ -5281,7 +8098,7 @@ function providersRmKey(configPath, providerId, keyId) {
5281
8098
  }
5282
8099
 
5283
8100
  // src/commands/secrets.ts
5284
- var import_node_fs14 = require("fs");
8101
+ var import_node_fs22 = require("fs");
5285
8102
  var import_node_util6 = require("util");
5286
8103
  async function runSecrets(argv) {
5287
8104
  const { values, positionals } = (0, import_node_util6.parseArgs)({
@@ -5353,7 +8170,7 @@ function secretsStatus(args) {
5353
8170
  reportField("admin.token", cfg.admin.token);
5354
8171
  }
5355
8172
  const tokensPath = defaultTokensPath(args.config);
5356
- if ((0, import_node_fs14.existsSync)(tokensPath)) {
8173
+ if ((0, import_node_fs22.existsSync)(tokensPath)) {
5357
8174
  console.info(`Secret status for ${tokensPath}:`);
5358
8175
  reportTokenFields(tokensPath);
5359
8176
  }
@@ -5393,7 +8210,7 @@ function secretsRotate(args) {
5393
8210
  const tokensPath = defaultTokensPath(args.config);
5394
8211
  try {
5395
8212
  cfg = loadConfig(args.config);
5396
- if ((0, import_node_fs14.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, oldBox);
8213
+ if ((0, import_node_fs22.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, oldBox);
5397
8214
  } finally {
5398
8215
  setSecretBox(null);
5399
8216
  }
@@ -5422,20 +8239,20 @@ function secretsDecrypt(args) {
5422
8239
  let tokensPlain = null;
5423
8240
  try {
5424
8241
  cfg = loadConfig(args.config);
5425
- if ((0, import_node_fs14.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
8242
+ if ((0, import_node_fs22.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
5426
8243
  } finally {
5427
8244
  setSecretBox(null);
5428
8245
  }
5429
8246
  saveConfig(args.config, cfg);
5430
8247
  if (tokensPlain) {
5431
- (0, import_node_fs14.writeFileSync)(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
8248
+ (0, import_node_fs22.writeFileSync)(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
5432
8249
  }
5433
8250
  console.info(`Decrypted secrets to plaintext in ${args.config}` + tokensSuffix(args.config));
5434
8251
  }
5435
8252
  function readRawConfig(path2) {
5436
8253
  let parsed;
5437
8254
  try {
5438
- parsed = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
8255
+ parsed = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8"));
5439
8256
  } catch {
5440
8257
  throw new Error(`secrets: cannot read or parse '${path2}'`);
5441
8258
  }
@@ -5443,7 +8260,7 @@ function readRawConfig(path2) {
5443
8260
  }
5444
8261
  function readRawJson(path2) {
5445
8262
  try {
5446
- const parsed = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
8263
+ const parsed = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8"));
5447
8264
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5448
8265
  return parsed;
5449
8266
  }
@@ -5453,7 +8270,7 @@ function readRawJson(path2) {
5453
8270
  }
5454
8271
  function encryptTokensFileInPlace(configPath, box) {
5455
8272
  const tokensPath = defaultTokensPath(configPath);
5456
- if (!(0, import_node_fs14.existsSync)(tokensPath)) return;
8273
+ if (!(0, import_node_fs22.existsSync)(tokensPath)) return;
5457
8274
  const plain = decryptTokensFile(tokensPath, box);
5458
8275
  writeTokensEncrypted(tokensPath, plain, box);
5459
8276
  }
@@ -5466,7 +8283,7 @@ function writeTokensEncrypted(tokensPath, plain, box) {
5466
8283
  { updatedAt: "", ...plain },
5467
8284
  box
5468
8285
  );
5469
- (0, import_node_fs14.writeFileSync)(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
8286
+ (0, import_node_fs22.writeFileSync)(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
5470
8287
  }
5471
8288
  var TOKEN_FIELDS2 = {
5472
8289
  claude: ["accessToken", "refreshToken"],
@@ -5489,12 +8306,47 @@ function walkTokens(raw, fn) {
5489
8306
  return next;
5490
8307
  }
5491
8308
  function tokensSuffix(configPath) {
5492
- return (0, import_node_fs14.existsSync)(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
8309
+ return (0, import_node_fs22.existsSync)(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
5493
8310
  }
5494
8311
 
5495
8312
  // src/commands/start.ts
5496
8313
  var import_node_util7 = require("util");
5497
- var import_outbound_api5 = require("@omnicross/core/outbound-api");
8314
+ var import_outbound_api7 = require("@omnicross/core/outbound-api");
8315
+ var import_SubscriptionAccountHealth3 = require("@omnicross/core/pipeline/SubscriptionAccountHealth");
8316
+
8317
+ // src/identity/identityRuntime.ts
8318
+ var import_SubscriptionIdentityStore3 = require("@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore");
8319
+ async function applyFingerprintConfig(config, credentialStore) {
8320
+ const store = (0, import_SubscriptionIdentityStore3.getSharedIdentityStore)();
8321
+ const enabled = config?.enabled === true;
8322
+ store.configure({ enabled, ua: config?.ua ?? null });
8323
+ if (!enabled) {
8324
+ store.setPersistence(null);
8325
+ return;
8326
+ }
8327
+ await seedIdentities(store, credentialStore);
8328
+ store.setPersistence({
8329
+ persist: (providerId, accountId, identity) => {
8330
+ void credentialStore.setAccountIdentity(providerId, accountId, identity).catch(() => {
8331
+ });
8332
+ }
8333
+ });
8334
+ }
8335
+ async function seedIdentities(store, credentialStore) {
8336
+ let config;
8337
+ try {
8338
+ config = await credentialStore.getFullConfig();
8339
+ } catch {
8340
+ return;
8341
+ }
8342
+ for (const provider of Object.keys(DAEMON_PROVIDER_KEYS)) {
8343
+ for (const account of listAccounts(config, provider)) {
8344
+ if (account.identity) store.seed(provider, account.id, account.identity);
8345
+ }
8346
+ }
8347
+ }
8348
+
8349
+ // src/commands/start.ts
5498
8350
  async function runStart(argv) {
5499
8351
  const { values } = (0, import_node_util7.parseArgs)({
5500
8352
  args: argv,
@@ -5519,19 +8371,45 @@ async function runStart(argv) {
5519
8371
  const daemon = buildDaemon(config, paths);
5520
8372
  await daemon.llmConfig.ready();
5521
8373
  await daemon.providerProxy.start();
5522
- const serverConfig = await (0, import_outbound_api5.loadServerConfig)(daemon.settingsStore);
5523
- await daemon.outboundApiServer.applyConfig({
5524
- enabled: true,
5525
- networkBinding: serverConfig.networkBinding,
5526
- endpoints: serverConfig.endpoints,
5527
- port: serverConfig.port
8374
+ const serverConfig = await (0, import_outbound_api7.loadServerConfig)(daemon.settingsStore);
8375
+ (0, import_SubscriptionAccountHealth3.getSharedAccountHealth)().configure({
8376
+ overloadEnabled: serverConfig.accountHealth?.overloadCooldownEnabled,
8377
+ overloadTtlMs: serverConfig.accountHealth?.overloadCooldownMs
5528
8378
  });
8379
+ try {
8380
+ await daemon.outboundApiServer.applyConfig({
8381
+ enabled: true,
8382
+ networkBinding: serverConfig.networkBinding,
8383
+ endpoints: serverConfig.endpoints,
8384
+ port: serverConfig.port,
8385
+ userMessageQueue: serverConfig.userMessageQueue,
8386
+ concurrencyQueue: serverConfig.concurrencyQueue,
8387
+ // voucher-redemption #9: carry the persisted flag so `POST /redeem` works on
8388
+ // boot when the operator has enabled the product.
8389
+ voucher: serverConfig.voucher
8390
+ });
8391
+ } catch (err5) {
8392
+ if (err5 instanceof import_outbound_api7.OutboundApiConfigError) {
8393
+ console.warn(`[outbound] not started \u2014 incomplete model configuration: ${err5.message}`);
8394
+ } else {
8395
+ throw err5;
8396
+ }
8397
+ }
5529
8398
  let dashboardUrl = null;
5530
8399
  if (!values["no-dashboard"]) {
5531
8400
  await daemon.adminServer.start();
5532
8401
  dashboardUrl = daemon.adminServer.getStatus().url;
5533
8402
  }
5534
8403
  daemon.tokenRefreshScheduler.start();
8404
+ daemon.accountHealthSweeper.start();
8405
+ if (serverConfig.accountProbe) {
8406
+ daemon.accountHealthProbeScheduler.configure(serverConfig.accountProbe);
8407
+ }
8408
+ daemon.accountHealthProbeScheduler.start();
8409
+ applyWebhookConfig(serverConfig.webhook);
8410
+ applyAuditConfig(serverConfig.audit);
8411
+ applyBillingConfig(serverConfig.billing);
8412
+ await applyFingerprintConfig(serverConfig.fingerprint, daemon.credentialStore);
5535
8413
  const status = daemon.outboundApiServer.getStatus();
5536
8414
  console.info("omnicross daemon is running.");
5537
8415
  if (dashboardUrl) console.info(` dashboard : ${dashboardUrl}`);