@omnicross/daemon 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,24 @@
1
1
  // src/bootstrap.ts
2
+ import { accessSync, constants as fsConstants, existsSync as existsSync14 } from "fs";
3
+ import { DEFAULT_AUDIT_CONFIG } from "@omnicross/contracts/audit-types";
4
+ import { DEFAULT_BILLING_CONFIG } from "@omnicross/contracts/billing-types";
2
5
  import { getGeminiCodeAssistProjectResolver } from "@omnicross/core/auth/GeminiCodeAssistProjectResolver";
3
6
  import { ApiKeyPoolService } from "@omnicross/core/completion/ApiKeyPoolService";
4
7
  import {
5
8
  __resetOutboundApiServerForTests,
9
+ DEFAULT_ACCOUNT_PROBE,
6
10
  getOutboundApiServer
7
11
  } from "@omnicross/core/outbound-api";
8
12
  import { setSubscriptionRegistryForOutbound } from "@omnicross/core/outbound-api/subscriptionRegistryPort";
13
+ import { getSharedAccountHealth as getSharedAccountHealth2 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
14
+ import { fetchUpstream as fetchUpstream6, setUpstreamProxyResolver } from "@omnicross/core/pipeline/upstreamFetch";
15
+ import { __resetSharedIdentityStoreForTests } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
9
16
  import { setGeminiCodeAssistResolver } from "@omnicross/core/ports/gemini-code-assist-resolver";
10
17
  import {
11
18
  __resetProviderProxyForTests,
12
19
  getProviderProxy
13
20
  } from "@omnicross/core/provider-proxy";
21
+ import { KeySpendTracker } from "@omnicross/core/outbound-api";
14
22
  import { PricingEngine, UsageRecorder } from "@omnicross/core/usage";
15
23
  import {
16
24
  setSubscriptionAccountService,
@@ -30,6 +38,7 @@ var CodexOAuthSessionStore = class {
30
38
  ttlMs;
31
39
  sessions = /* @__PURE__ */ new Map();
32
40
  activeSessionId = null;
41
+ aborters = /* @__PURE__ */ new Map();
33
42
  /** Whether a codex sign-in is currently in flight (port 1455 held). */
34
43
  isBusy() {
35
44
  this.sweep();
@@ -41,13 +50,22 @@ var CodexOAuthSessionStore = class {
41
50
  const sessionId = crypto.randomBytes(24).toString("base64url");
42
51
  this.sessions.set(sessionId, { status: "pending", createdAt: Date.now() });
43
52
  this.activeSessionId = sessionId;
44
- return sessionId;
53
+ const controller = new AbortController();
54
+ this.aborters.set(sessionId, controller);
55
+ return { sessionId, signal: controller.signal };
45
56
  }
46
57
  /** Settle a flow (done/error) + free the active slot. */
47
58
  settle(sessionId, status, error) {
48
59
  const prior = this.sessions.get(sessionId);
49
60
  this.sessions.set(sessionId, { status, error, createdAt: prior?.createdAt ?? Date.now() });
50
61
  if (this.activeSessionId === sessionId) this.activeSessionId = null;
62
+ this.aborters.delete(sessionId);
63
+ }
64
+ cancel(sessionId) {
65
+ if (!this.sessions.has(sessionId)) return false;
66
+ this.aborters.get(sessionId)?.abort();
67
+ this.settle(sessionId, "error", "login: cancelled");
68
+ return true;
51
69
  }
52
70
  /** Read a flow's status (token-free), or null when unknown/expired. */
53
71
  get(sessionId) {
@@ -76,13 +94,13 @@ function handleCodexOAuthStart(deps) {
76
94
  );
77
95
  }
78
96
  const { authUrl, codeVerifier, state } = codexOAuth.generateAuthParams();
79
- const sessionId = deps.codexSessions.begin();
80
- void runCodexLoopback(sessionId, codeVerifier, state, deps);
97
+ const { sessionId, signal } = deps.codexSessions.begin();
98
+ void runCodexLoopback(sessionId, codeVerifier, state, signal, deps);
81
99
  return { status: 200, body: { authUrl, sessionId } };
82
100
  }
83
- async function runCodexLoopback(sessionId, codeVerifier, state, deps) {
101
+ async function runCodexLoopback(sessionId, codeVerifier, state, signal, deps) {
84
102
  try {
85
- const code = await deps.codexAwaitLoopback(state);
103
+ const code = await deps.codexAwaitLoopback(state, void 0, signal);
86
104
  const result = await codexOAuth.exchangeCodeForTokens(
87
105
  { authorizationCode: code, codeVerifier, state },
88
106
  deps.oauthExchangeFetch
@@ -104,6 +122,10 @@ async function runCodexLoopback(sessionId, codeVerifier, state, deps) {
104
122
  deps.codexSessions.settle(sessionId, "error", reason);
105
123
  }
106
124
  }
125
+ function handleCodexOAuthCancel(sessionId, deps) {
126
+ if (!deps.codexSessions.cancel(sessionId)) return err(404, "unknown or expired codex sign-in session");
127
+ return { status: 200, body: { ok: true } };
128
+ }
107
129
  function handleCodexOAuthStatus(sessionId, deps) {
108
130
  const s = deps.codexSessions.get(sessionId);
109
131
  if (!s) return err(404, "unknown or expired codex sign-in session");
@@ -113,15 +135,146 @@ function handleCodexOAuthStatus(sessionId, deps) {
113
135
  // src/admin/AdminServer.ts
114
136
  import { timingSafeEqual } from "crypto";
115
137
  import http2 from "http";
138
+ import {
139
+ healthHttpStatus
140
+ } from "@omnicross/contracts/health-logging-types";
141
+
142
+ // src/admin/accountProbesApi.ts
143
+ function handleAccountProbes(res, reader) {
144
+ const accounts = reader ? reader.getAllHistory() : [];
145
+ res.writeHead(200, { "Content-Type": "application/json" });
146
+ res.end(JSON.stringify({ accounts }));
147
+ }
148
+
149
+ // src/admin/auditQueryApi.ts
150
+ function intParam(value) {
151
+ if (value === null || value.trim() === "") return void 0;
152
+ const n = Number(value);
153
+ return Number.isFinite(n) ? Math.trunc(n) : void 0;
154
+ }
155
+ function handleAuditQuery(req, res, reader) {
156
+ const url = new URL(req.url ?? "/", "http://localhost");
157
+ const query = {};
158
+ const keyId = url.searchParams.get("keyId");
159
+ if (keyId && keyId.trim()) query.keyId = keyId.trim();
160
+ const from = intParam(url.searchParams.get("from"));
161
+ if (from !== void 0) query.from = from;
162
+ const to = intParam(url.searchParams.get("to"));
163
+ if (to !== void 0) query.to = to;
164
+ const limit = intParam(url.searchParams.get("limit"));
165
+ if (limit !== void 0) query.limit = limit;
166
+ const records = reader ? reader(query) : [];
167
+ res.writeHead(200, { "Content-Type": "application/json" });
168
+ res.end(JSON.stringify({ records }));
169
+ }
170
+
171
+ // src/admin/billingStatusApi.ts
172
+ function handleBillingStatus(res, reader) {
173
+ const status = reader ? reader() : { total: 0, delivered: 0, pending: 0 };
174
+ res.writeHead(200, { "Content-Type": "application/json" });
175
+ res.end(JSON.stringify({ status }));
176
+ }
177
+
178
+ // src/webhook/webhookRuntime.ts
179
+ import { setWebhookSink } from "@omnicross/core/pipeline/webhookEmit";
180
+ var dispatcher = null;
181
+ var health = null;
182
+ var unsubscribers = [];
183
+ var wired = false;
184
+ function setWebhookRuntime(d, h) {
185
+ dispatcher = d;
186
+ health = h;
187
+ }
188
+ function applyWebhookConfig(config) {
189
+ if (!dispatcher) return;
190
+ dispatcher.setConfig(config);
191
+ const shouldWire = config?.enabled === true && (config?.destinations.length ?? 0) > 0;
192
+ if (shouldWire && !wired) {
193
+ const active = dispatcher;
194
+ setWebhookSink((event) => active.emit(event));
195
+ if (health) {
196
+ unsubscribers.push(
197
+ health.onRecovered(
198
+ (e) => active.emit({ kind: "account.recovery", at: e.at, providerId: e.providerId, accountId: e.accountId })
199
+ )
200
+ );
201
+ unsubscribers.push(
202
+ health.onAnomaly(
203
+ (e) => active.emit({
204
+ kind: "account.anomaly",
205
+ at: e.at,
206
+ providerId: e.providerId,
207
+ accountId: e.accountId,
208
+ state: e.state
209
+ })
210
+ )
211
+ );
212
+ }
213
+ wired = true;
214
+ } else if (!shouldWire && wired) {
215
+ teardown();
216
+ }
217
+ }
218
+ async function deliverWebhookTest(destinationId) {
219
+ if (!dispatcher) return { ok: false, error: "webhook dispatcher not wired" };
220
+ return dispatcher.deliverTest(destinationId);
221
+ }
222
+ function teardown() {
223
+ setWebhookSink(null);
224
+ for (const unsub of unsubscribers) unsub();
225
+ unsubscribers = [];
226
+ wired = false;
227
+ }
228
+ function resetWebhookRuntimeForTests() {
229
+ if (wired) teardown();
230
+ dispatcher = null;
231
+ health = null;
232
+ unsubscribers = [];
233
+ wired = false;
234
+ }
235
+
236
+ // src/admin/webhookTestApi.ts
237
+ function readJsonBody(req) {
238
+ return new Promise((resolve) => {
239
+ const chunks = [];
240
+ req.on("data", (c) => chunks.push(c));
241
+ req.on("end", () => {
242
+ try {
243
+ const raw = Buffer.concat(chunks).toString("utf8");
244
+ const parsed = raw ? JSON.parse(raw) : {};
245
+ resolve(parsed && typeof parsed === "object" ? parsed : {});
246
+ } catch {
247
+ resolve({});
248
+ }
249
+ });
250
+ req.on("error", () => resolve({}));
251
+ });
252
+ }
253
+ async function handleWebhookTest(req, res) {
254
+ const body = await readJsonBody(req);
255
+ const destinationId = body["destinationId"];
256
+ if (typeof destinationId !== "string" || !destinationId.trim()) {
257
+ res.writeHead(400, { "Content-Type": "application/json" });
258
+ res.end(JSON.stringify({ error: { type: "bad_request", message: "destinationId is required" } }));
259
+ return;
260
+ }
261
+ const result = await deliverWebhookTest(destinationId.trim());
262
+ res.writeHead(200, { "Content-Type": "application/json" });
263
+ res.end(JSON.stringify({ result }));
264
+ }
116
265
 
117
266
  // src/admin/adminApi.ts
118
267
  import http from "http";
119
268
  import {
120
269
  createNamedKey,
121
- loadServerConfig,
270
+ isKindMappedEndpoint,
271
+ loadServerConfig as loadServerConfig2,
122
272
  mergeServerConfig,
123
- saveServerConfig
273
+ normalizeProxyConfig,
274
+ saveServerConfig,
275
+ validateServerModelConfig
124
276
  } from "@omnicross/core/outbound-api";
277
+ import { fetchUpstream } from "@omnicross/core/pipeline/upstreamFetch";
125
278
 
126
279
  // src/config.ts
127
280
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -317,6 +470,70 @@ var SecretBox = class {
317
470
  };
318
471
 
319
472
  // src/secrets/secretFields.ts
473
+ function urlHasInlineCredential(url) {
474
+ try {
475
+ const u = new URL(url);
476
+ return u.username.length > 0 || u.password.length > 0;
477
+ } catch {
478
+ return false;
479
+ }
480
+ }
481
+ function transformProxyConfig(cfg, fn) {
482
+ if ("url" in cfg) {
483
+ if (isEnvelope(cfg.url) || urlHasInlineCredential(cfg.url)) {
484
+ return { url: fn(cfg.url) };
485
+ }
486
+ return cfg;
487
+ }
488
+ if (typeof cfg.password === "string" && cfg.password.length > 0) {
489
+ return { ...cfg, password: fn(cfg.password) };
490
+ }
491
+ return cfg;
492
+ }
493
+ function transformOutboundProxy(proxy, fn) {
494
+ const next = {};
495
+ if (proxy.global) next.global = transformProxyConfig(proxy.global, fn);
496
+ if (proxy.byProvider) {
497
+ const byProvider = {};
498
+ for (const [key, value] of Object.entries(proxy.byProvider)) {
499
+ byProvider[key] = transformProxyConfig(value, fn);
500
+ }
501
+ next.byProvider = byProvider;
502
+ }
503
+ return next;
504
+ }
505
+ function encryptProxySegment(proxy, box) {
506
+ return transformOutboundProxy(proxy, (v) => box.encryptMaybe(v));
507
+ }
508
+ function decryptProxySegment(proxy, box) {
509
+ return transformOutboundProxy(proxy, (v) => box.decryptMaybe(v));
510
+ }
511
+ function transformWebhookSegment(webhook, fn) {
512
+ return {
513
+ ...webhook,
514
+ destinations: webhook.destinations.map(
515
+ (d) => typeof d.secret === "string" && d.secret.length > 0 ? { ...d, secret: fn(d.secret) } : d
516
+ )
517
+ };
518
+ }
519
+ function encryptWebhookSegment(webhook, box) {
520
+ return transformWebhookSegment(webhook, (v) => box.encryptMaybe(v));
521
+ }
522
+ function decryptWebhookSegment(webhook, box) {
523
+ return transformWebhookSegment(webhook, (v) => box.decryptMaybe(v));
524
+ }
525
+ function transformBillingSegment(billing, fn) {
526
+ if (typeof billing.secret === "string" && billing.secret.length > 0) {
527
+ return { ...billing, secret: fn(billing.secret) };
528
+ }
529
+ return billing;
530
+ }
531
+ function encryptBillingSegment(billing, box) {
532
+ return transformBillingSegment(billing, (v) => box.encryptMaybe(v));
533
+ }
534
+ function decryptBillingSegment(billing, box) {
535
+ return transformBillingSegment(billing, (v) => box.decryptMaybe(v));
536
+ }
320
537
  function transformProvider(provider, fn) {
321
538
  const next = { ...provider, apiKey: fn(provider.apiKey) };
322
539
  if (provider.apiKeys) {
@@ -340,6 +557,17 @@ function transformConfigSecrets(cfg, fn) {
340
557
  if (cfg.admin && typeof cfg.admin.token === "string" && cfg.admin.token.length > 0) {
341
558
  next.admin = { ...cfg.admin, token: fn(cfg.admin.token) };
342
559
  }
560
+ const proxy = cfg.server?.proxy;
561
+ const webhook = cfg.server?.webhook;
562
+ const billing = cfg.server?.billing;
563
+ if (cfg.server && (proxy?.global || proxy?.byProvider || webhook || billing?.secret)) {
564
+ next.server = { ...cfg.server };
565
+ if (proxy && (proxy.global || proxy.byProvider)) {
566
+ next.server.proxy = transformOutboundProxy(proxy, fn);
567
+ }
568
+ if (webhook) next.server.webhook = transformWebhookSegment(webhook, fn);
569
+ if (billing?.secret) next.server.billing = transformBillingSegment(billing, fn);
570
+ }
343
571
  return next;
344
572
  }
345
573
  function encryptConfigSecrets(cfg, box) {
@@ -377,7 +605,7 @@ function transformTokens(tokens, fn) {
377
605
  if (Array.isArray(accounts)) {
378
606
  bag[accountsKey] = accounts.map((entry) => {
379
607
  if (entry && typeof entry === "object" && "tokens" in entry && entry.tokens && typeof entry.tokens === "object") {
380
- return {
608
+ const nextEntry = {
381
609
  ...entry,
382
610
  tokens: transformTokenBlock(
383
611
  entry.tokens,
@@ -385,6 +613,11 @@ function transformTokens(tokens, fn) {
385
613
  fn
386
614
  )
387
615
  };
616
+ const proxy = entry.proxy;
617
+ if (proxy && typeof proxy === "object") {
618
+ nextEntry.proxy = transformProxyConfig(proxy, fn);
619
+ }
620
+ return nextEntry;
388
621
  }
389
622
  return entry;
390
623
  });
@@ -419,6 +652,17 @@ function resolveAdminConfig(admin) {
419
652
  token: typeof admin?.token === "string" && admin.token.length > 0 ? admin.token : void 0
420
653
  };
421
654
  }
655
+ function validateLogging(raw) {
656
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
657
+ const l = raw;
658
+ const out = {};
659
+ if (l["level"] === "error" || l["level"] === "warn" || l["level"] === "info" || l["level"] === "debug") {
660
+ out.level = l["level"];
661
+ }
662
+ if (l["format"] === "text" || l["format"] === "json") out.format = l["format"];
663
+ if (typeof l["file"] === "string" && l["file"].length > 0) out.file = l["file"];
664
+ return out.level !== void 0 || out.format !== void 0 || out.file !== void 0 ? out : void 0;
665
+ }
422
666
  var VALID_FORMATS = ["openai", "anthropic", "gemini"];
423
667
  function validateApiKeys(raw) {
424
668
  if (!Array.isArray(raw)) return void 0;
@@ -593,7 +837,8 @@ function validateConfig(raw) {
593
837
  const providers = providersRaw.map((p, i) => validateProvider(p, i));
594
838
  const server = obj["server"];
595
839
  const admin = validateAdmin(obj["admin"]);
596
- return { providers, server, admin };
840
+ const logging = validateLogging(obj["logging"]);
841
+ return { providers, server, admin, logging };
597
842
  }
598
843
  var secretBox = null;
599
844
  function setSecretBox(box) {
@@ -693,6 +938,163 @@ function listMappablePresets() {
693
938
  return { mappable, excluded };
694
939
  }
695
940
 
941
+ // src/proxy/sanitizeProxy.ts
942
+ function sanitizeProxyConfig(cfg) {
943
+ if ("url" in cfg) {
944
+ let endpoint;
945
+ let username;
946
+ let hasPassword = false;
947
+ try {
948
+ const u = new URL(cfg.url);
949
+ endpoint = u.port ? `${u.hostname}:${u.port}` : u.hostname;
950
+ username = u.username ? decodeURIComponent(u.username) : void 0;
951
+ hasPassword = u.password.length > 0;
952
+ } catch {
953
+ }
954
+ return { kind: "url", endpoint, username, hasPassword };
955
+ }
956
+ return {
957
+ kind: cfg.type,
958
+ endpoint: `${cfg.host}:${cfg.port}`,
959
+ username: cfg.username,
960
+ hasPassword: typeof cfg.password === "string" && cfg.password.length > 0
961
+ };
962
+ }
963
+ function redactProxyConfig(cfg) {
964
+ if ("url" in cfg) {
965
+ try {
966
+ const u = new URL(cfg.url);
967
+ if (u.password) u.password = "";
968
+ return { url: u.toString() };
969
+ } catch {
970
+ return cfg;
971
+ }
972
+ }
973
+ const { password: _password, ...rest } = cfg;
974
+ return rest;
975
+ }
976
+ function redactOutboundProxy(proxy) {
977
+ const out = {};
978
+ if (proxy.global) out.global = redactProxyConfig(proxy.global);
979
+ if (proxy.byProvider) {
980
+ const byProvider = {};
981
+ for (const [key, value] of Object.entries(proxy.byProvider)) {
982
+ byProvider[key] = redactProxyConfig(value);
983
+ }
984
+ out.byProvider = byProvider;
985
+ }
986
+ return out;
987
+ }
988
+ function preserveProxyConfigSecret(incoming, current) {
989
+ if (!current) return incoming;
990
+ if ("url" in incoming) {
991
+ if ("url" in current) {
992
+ try {
993
+ const inU = new URL(incoming.url);
994
+ const curU = new URL(current.url);
995
+ if (!inU.password && curU.password) {
996
+ inU.password = curU.password;
997
+ return { url: inU.toString() };
998
+ }
999
+ } catch {
1000
+ }
1001
+ }
1002
+ return incoming;
1003
+ }
1004
+ if ("url" in current) return incoming;
1005
+ const blank = incoming.password === void 0 || incoming.password === "";
1006
+ if (blank && typeof current.password === "string" && current.password.length > 0) {
1007
+ return { ...incoming, password: current.password };
1008
+ }
1009
+ return incoming;
1010
+ }
1011
+ function preserveOutboundProxySecrets(incoming, current) {
1012
+ const out = {};
1013
+ if (incoming.global) out.global = preserveProxyConfigSecret(incoming.global, current?.global);
1014
+ if (incoming.byProvider) {
1015
+ const byProvider = {};
1016
+ for (const [key, value] of Object.entries(incoming.byProvider)) {
1017
+ byProvider[key] = preserveProxyConfigSecret(value, current?.byProvider?.[key]);
1018
+ }
1019
+ out.byProvider = byProvider;
1020
+ }
1021
+ return out;
1022
+ }
1023
+
1024
+ // src/proxy/upstreamProxyResolver.ts
1025
+ import {
1026
+ bumpUpstreamProxyGeneration
1027
+ } from "@omnicross/core/pipeline/upstreamFetch";
1028
+ var serverProxy;
1029
+ function setServerProxyConfig(proxy) {
1030
+ serverProxy = proxy;
1031
+ bumpUpstreamProxyGeneration();
1032
+ }
1033
+ function getServerProxyConfig() {
1034
+ return serverProxy;
1035
+ }
1036
+ var envProxyLoggedFor;
1037
+ function maskProxyUrl(url) {
1038
+ return url.replace(/\/\/[^/@]*@/, "//***@");
1039
+ }
1040
+ function hostFromCtx(ctx) {
1041
+ if (!ctx.url) return void 0;
1042
+ try {
1043
+ return new URL(ctx.url).hostname.toLowerCase();
1044
+ } catch {
1045
+ return void 0;
1046
+ }
1047
+ }
1048
+ function isLoopbackHost(host) {
1049
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || host.startsWith("127.") || host.endsWith(".localhost");
1050
+ }
1051
+ function noProxyMatches(noProxy, host) {
1052
+ if (!noProxy) return false;
1053
+ for (const raw of noProxy.split(",")) {
1054
+ const entry = raw.trim().toLowerCase();
1055
+ if (!entry) continue;
1056
+ if (entry === "*") return true;
1057
+ const bare = entry.startsWith(".") ? entry.slice(1) : entry;
1058
+ if (host === bare || host.endsWith(`.${bare}`)) return true;
1059
+ }
1060
+ return false;
1061
+ }
1062
+ function resolveEnvProxy(ctx, env = process.env) {
1063
+ const raw = env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? env.HTTP_PROXY ?? env.http_proxy;
1064
+ if (!raw || !raw.trim()) return void 0;
1065
+ const host = hostFromCtx(ctx);
1066
+ if (host && (isLoopbackHost(host) || noProxyMatches(env.NO_PROXY ?? env.no_proxy, host))) {
1067
+ return void 0;
1068
+ }
1069
+ const url = raw.trim();
1070
+ if (envProxyLoggedFor !== url) {
1071
+ envProxyLoggedFor = url;
1072
+ console.info(`[upstream-proxy] routing upstream egress through the environment proxy: ${maskProxyUrl(url)}`);
1073
+ }
1074
+ return { url };
1075
+ }
1076
+ function createUpstreamProxyResolver(src = {}) {
1077
+ const readServer = src.getServerProxy ?? getServerProxyConfig;
1078
+ return (ctx) => {
1079
+ const host = hostFromCtx(ctx);
1080
+ if (host) {
1081
+ if (isLoopbackHost(host)) return void 0;
1082
+ const env = src.env ?? process.env;
1083
+ if (noProxyMatches(env.NO_PROXY ?? env.no_proxy, host)) return void 0;
1084
+ }
1085
+ if (src.getAccountProxy && ctx.providerId && ctx.accountId) {
1086
+ const account = src.getAccountProxy(ctx.providerId, ctx.accountId);
1087
+ if (account) return account;
1088
+ }
1089
+ const server = readServer();
1090
+ if (ctx.providerId && server?.byProvider?.[ctx.providerId]) {
1091
+ return server.byProvider[ctx.providerId];
1092
+ }
1093
+ if (server?.global) return server.global;
1094
+ return resolveEnvProxy(ctx, src.env);
1095
+ };
1096
+ }
1097
+
696
1098
  // src/admin/accountsOAuth.ts
697
1099
  import { claudeOAuth, geminiOAuth } from "@omnicross/subscriptions";
698
1100
 
@@ -815,6 +1217,24 @@ function validateTokenBody(providerId, body) {
815
1217
  return null;
816
1218
  }
817
1219
  }
1220
+ function validateSupportedModelsBody(raw) {
1221
+ if (raw === null || raw === void 0) return { ok: true, value: void 0 };
1222
+ if (Array.isArray(raw)) {
1223
+ if (raw.length === 0) return { ok: false };
1224
+ if (!raw.every((x) => typeof x === "string" && x.trim().length > 0)) return { ok: false };
1225
+ return { ok: true, value: raw };
1226
+ }
1227
+ if (typeof raw === "object") {
1228
+ const entries = Object.entries(raw);
1229
+ if (entries.length === 0) return { ok: false };
1230
+ const valid = entries.every(
1231
+ ([k, v]) => k.trim().length > 0 && typeof v === "string" && v.trim().length > 0
1232
+ );
1233
+ if (!valid) return { ok: false };
1234
+ return { ok: true, value: Object.fromEntries(entries) };
1235
+ }
1236
+ return { ok: false };
1237
+ }
818
1238
  async function statusEntryFor(reader, providerId) {
819
1239
  const all = await reader.listAll();
820
1240
  return all.find((a) => a.providerId === providerId) ?? null;
@@ -1096,6 +1516,448 @@ async function handleCliLaunch(cli, body, ctx) {
1096
1516
  return { status: 200, body: { sessionId: id, providerId: target.providerId, model: target.model } };
1097
1517
  }
1098
1518
 
1519
+ // src/admin/auditConfigBody.ts
1520
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1521
+ function validateAuditSegment(patch) {
1522
+ const errors = [];
1523
+ const audit = patch.audit;
1524
+ if (audit === void 0) return errors;
1525
+ if (!isPlainObject(audit)) {
1526
+ errors.push("audit must be an object");
1527
+ return errors;
1528
+ }
1529
+ for (const flag of ["enabled", "captureBodies", "trustForwardedFor"]) {
1530
+ if (audit[flag] !== void 0 && typeof audit[flag] !== "boolean") {
1531
+ errors.push(`audit.${flag} must be a boolean`);
1532
+ }
1533
+ }
1534
+ const maxBodyBytes = audit["maxBodyBytes"];
1535
+ if (maxBodyBytes !== void 0 && (typeof maxBodyBytes !== "number" || !Number.isFinite(maxBodyBytes) || maxBodyBytes < 0)) {
1536
+ errors.push("audit.maxBodyBytes must be a non-negative number");
1537
+ }
1538
+ const retentionDays = audit["retentionDays"];
1539
+ if (retentionDays !== void 0 && (typeof retentionDays !== "number" || !Number.isFinite(retentionDays) || retentionDays < 0)) {
1540
+ errors.push("audit.retentionDays must be a non-negative number");
1541
+ }
1542
+ return errors;
1543
+ }
1544
+
1545
+ // src/admin/billingConfigBody.ts
1546
+ var BILLING_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
1547
+ var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1548
+ function validateBillingSegment(patch) {
1549
+ const errors = [];
1550
+ const billing = patch.billing;
1551
+ if (billing === void 0) return errors;
1552
+ if (!isPlainObject2(billing)) {
1553
+ errors.push("billing must be an object");
1554
+ return errors;
1555
+ }
1556
+ if (billing["enabled"] !== void 0 && typeof billing["enabled"] !== "boolean") {
1557
+ errors.push("billing.enabled must be a boolean");
1558
+ }
1559
+ if (billing["endpoint"] !== void 0 && typeof billing["endpoint"] !== "string") {
1560
+ errors.push("billing.endpoint must be a string");
1561
+ }
1562
+ if (billing["secret"] !== void 0 && typeof billing["secret"] !== "string") {
1563
+ errors.push("billing.secret must be a string");
1564
+ }
1565
+ const maxRetryAgeMs = billing["maxRetryAgeMs"];
1566
+ if (maxRetryAgeMs !== void 0 && (typeof maxRetryAgeMs !== "number" || !Number.isFinite(maxRetryAgeMs) || maxRetryAgeMs < 0)) {
1567
+ errors.push("billing.maxRetryAgeMs must be a non-negative number");
1568
+ }
1569
+ return errors;
1570
+ }
1571
+ function redactBillingConfig(billing) {
1572
+ if (typeof billing.secret === "string" && billing.secret.length > 0) {
1573
+ return { ...billing, secret: BILLING_SECRET_MASK };
1574
+ }
1575
+ return billing;
1576
+ }
1577
+ function preserveBillingSecret(incoming, current) {
1578
+ const isMaskedOrBlank = incoming.secret === void 0 || incoming.secret === "" || incoming.secret === BILLING_SECRET_MASK;
1579
+ if (isMaskedOrBlank) {
1580
+ if (current?.secret) return { ...incoming, secret: current.secret };
1581
+ const { secret: _secret, ...rest } = incoming;
1582
+ return rest;
1583
+ }
1584
+ return incoming;
1585
+ }
1586
+
1587
+ // src/admin/dashboard.ts
1588
+ function startOfLocalDayMs(ts) {
1589
+ const d = new Date(ts);
1590
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
1591
+ }
1592
+ function accountProviderId(entry) {
1593
+ if (!entry || typeof entry !== "object") return null;
1594
+ const e = entry;
1595
+ if (typeof e["providerId"] === "string" && e["providerId"]) return e["providerId"];
1596
+ if (typeof e["provider"] === "string" && e["provider"]) return e["provider"];
1597
+ return null;
1598
+ }
1599
+ async function handleDashboard(deps) {
1600
+ const now = Date.now();
1601
+ const today = await deps.usageRecorder.getTotals({ startTs: startOfLocalDayMs(now), endTs: now });
1602
+ const total = await deps.usageRecorder.getTotals({ startTs: 0, endTs: now });
1603
+ const providerList = loadConfig(deps.configPath).providers;
1604
+ const providers = {
1605
+ total: providerList.length,
1606
+ enabled: providerList.filter((p) => p.enabled !== false).length
1607
+ };
1608
+ const keys = await deps.keyDb.outboundApiKeysList();
1609
+ const outboundKeys = {
1610
+ total: keys.length,
1611
+ active: keys.filter((k) => k.enabled && k.revokedAt === null).length
1612
+ };
1613
+ const accountsList = await deps.subscriptionAccounts.listAll();
1614
+ const byProvider = {};
1615
+ for (const entry of accountsList) {
1616
+ const providerId = accountProviderId(entry);
1617
+ if (providerId) byProvider[providerId] = (byProvider[providerId] ?? 0) + 1;
1618
+ }
1619
+ const accounts = { total: accountsList.length, byProvider };
1620
+ const status = deps.outboundApiServer.getStatus();
1621
+ const server = {
1622
+ running: status.running,
1623
+ port: status.port,
1624
+ uptimeMs: Math.round(process.uptime() * 1e3)
1625
+ };
1626
+ const summary = { today, total, providers, outboundKeys, accounts, server, generatedAt: now };
1627
+ return { status: 200, body: summary };
1628
+ }
1629
+
1630
+ // src/admin/keyPolicyBody.ts
1631
+ function parseKeyPolicyBody(body) {
1632
+ const policy = {};
1633
+ if ("activationMode" in body) {
1634
+ const m = body["activationMode"];
1635
+ if (m === null) policy.activationMode = null;
1636
+ else if (m === "fixed" || m === "activation") policy.activationMode = m;
1637
+ else return { ok: false, message: "activationMode must be 'fixed', 'activation', or null" };
1638
+ }
1639
+ const numericFields = [
1640
+ { key: "expiresAt", min: 0 },
1641
+ { key: "activationDays", min: 1, integer: true },
1642
+ { key: "dailyCostLimitUsd", min: 0 },
1643
+ { key: "totalCostLimitUsd", min: 0 },
1644
+ { key: "weeklyCostLimitUsd", min: 0 },
1645
+ { key: "rateLimitMaxRequests", min: 0, integer: true },
1646
+ { key: "rateLimitWindowMs", min: 1 }
1647
+ ];
1648
+ for (const { key, min, integer } of numericFields) {
1649
+ if (!(key in body)) continue;
1650
+ const v = body[key];
1651
+ if (v === null) {
1652
+ policy[key] = null;
1653
+ continue;
1654
+ }
1655
+ if (typeof v !== "number" || !Number.isFinite(v) || v < min || integer && !Number.isInteger(v)) {
1656
+ return {
1657
+ ok: false,
1658
+ message: `${key} must be ${integer ? "an integer" : "a number"} >= ${min} or null`
1659
+ };
1660
+ }
1661
+ policy[key] = v;
1662
+ }
1663
+ if ("enableModelRestriction" in body) {
1664
+ const v = body["enableModelRestriction"];
1665
+ if (v === null) policy.enableModelRestriction = null;
1666
+ else if (typeof v === "boolean") policy.enableModelRestriction = v;
1667
+ else return { ok: false, message: "enableModelRestriction must be a boolean or null" };
1668
+ }
1669
+ if ("restrictionMode" in body) {
1670
+ const v = body["restrictionMode"];
1671
+ if (v === null) policy.restrictionMode = null;
1672
+ else if (v === "blacklist" || v === "allowlist") policy.restrictionMode = v;
1673
+ else return { ok: false, message: "restrictionMode must be 'blacklist', 'allowlist', or null" };
1674
+ }
1675
+ if ("restrictedModels" in body) {
1676
+ const v = body["restrictedModels"];
1677
+ if (v === null) {
1678
+ policy.restrictedModels = null;
1679
+ } else if (Array.isArray(v) && v.every((e) => typeof e === "string")) {
1680
+ policy.restrictedModels = v.map((e) => e.trim()).filter((e) => e !== "");
1681
+ } else {
1682
+ return { ok: false, message: "restrictedModels must be an array of strings or null" };
1683
+ }
1684
+ }
1685
+ return { ok: true, policy };
1686
+ }
1687
+
1688
+ // src/admin/voucherAdmin.ts
1689
+ import {
1690
+ generateVoucherCode,
1691
+ hashVoucherCode,
1692
+ loadServerConfig,
1693
+ newVoucherId,
1694
+ toVoucherInfo,
1695
+ voucherCodePrefix
1696
+ } from "@omnicross/core/outbound-api";
1697
+ function writeJson(res, status, body) {
1698
+ res.writeHead(status, { "Content-Type": "application/json" });
1699
+ res.end(JSON.stringify(body));
1700
+ }
1701
+ function writeErr(res, status, message) {
1702
+ writeJson(res, status, { error: { type: "voucher_error", message } });
1703
+ }
1704
+ function readJsonBody2(req) {
1705
+ return new Promise((resolve, reject) => {
1706
+ const chunks = [];
1707
+ req.on("data", (c) => chunks.push(c));
1708
+ req.on("end", () => {
1709
+ const raw = Buffer.concat(chunks).toString("utf8");
1710
+ if (!raw.trim()) return resolve({});
1711
+ try {
1712
+ const parsed = JSON.parse(raw);
1713
+ resolve(parsed && typeof parsed === "object" ? parsed : {});
1714
+ } catch {
1715
+ reject(new Error("invalid-json"));
1716
+ }
1717
+ });
1718
+ req.on("error", reject);
1719
+ });
1720
+ }
1721
+ function optPositive(value, integer) {
1722
+ if (value === void 0 || value === null) return { ok: true, value: void 0 };
1723
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return { ok: false };
1724
+ if (integer && !Number.isInteger(value)) return { ok: false };
1725
+ return { ok: true, value };
1726
+ }
1727
+ function parseVoucherCreateBody(body) {
1728
+ const type = body["type"];
1729
+ if (type !== "credit" && type !== "renewal") {
1730
+ return { ok: false, message: "type must be 'credit' or 'renewal'" };
1731
+ }
1732
+ const maxTotal = optPositive(body["maxTotalCostLimitUsd"], false);
1733
+ if (!maxTotal.ok) return { ok: false, message: "maxTotalCostLimitUsd must be a positive number" };
1734
+ const maxDays = optPositive(body["maxExpiryDays"], true);
1735
+ if (!maxDays.ok) return { ok: false, message: "maxExpiryDays must be a positive integer" };
1736
+ const input = { type };
1737
+ if (maxTotal.value !== void 0) input.maxTotalCostLimitUsd = maxTotal.value;
1738
+ if (maxDays.value !== void 0) input.maxExpiryDays = maxDays.value;
1739
+ if (type === "credit") {
1740
+ const credit = optPositive(body["creditUsd"], false);
1741
+ if (!credit.ok || credit.value === void 0) {
1742
+ return { ok: false, message: "creditUsd must be a positive number for a credit card" };
1743
+ }
1744
+ input.creditUsd = credit.value;
1745
+ } else {
1746
+ const days = optPositive(body["renewalDays"], true);
1747
+ if (!days.ok || days.value === void 0) {
1748
+ return { ok: false, message: "renewalDays must be a positive integer for a renewal card" };
1749
+ }
1750
+ input.renewalDays = days.value;
1751
+ }
1752
+ return { ok: true, input };
1753
+ }
1754
+ async function voucherEnabled(deps) {
1755
+ const config = await loadServerConfig(deps.settingsStore);
1756
+ return config.voucher?.enabled === true;
1757
+ }
1758
+ async function handleVoucher(req, res, method, rest, deps) {
1759
+ const voucherDb = deps.voucherDb;
1760
+ if (!voucherDb) return writeErr(res, 501, "Voucher feature is not available");
1761
+ if (method === "GET" && rest.length === 0) {
1762
+ const rows = await voucherDb.voucherList();
1763
+ return writeJson(res, 200, { vouchers: rows.map(toVoucherInfo) });
1764
+ }
1765
+ if (method === "POST" && rest.length === 0) {
1766
+ if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
1767
+ let body;
1768
+ try {
1769
+ body = await readJsonBody2(req);
1770
+ } catch {
1771
+ return writeErr(res, 400, "Invalid JSON in request body");
1772
+ }
1773
+ const parsed = parseVoucherCreateBody(body);
1774
+ if (!parsed.ok) return writeErr(res, 400, parsed.message);
1775
+ const code = generateVoucherCode();
1776
+ const created = await voucherDb.voucherCreate({
1777
+ id: newVoucherId(),
1778
+ codeHash: hashVoucherCode(code),
1779
+ codePrefix: voucherCodePrefix(code),
1780
+ ...parsed.input
1781
+ });
1782
+ return writeJson(res, 201, {
1783
+ id: created.id,
1784
+ codePrefix: created.codePrefix,
1785
+ type: created.type,
1786
+ createdAt: created.createdAt,
1787
+ // `plaintextOnce` is the ONLY place the full code crosses the wire (D3).
1788
+ plaintextOnce: code
1789
+ });
1790
+ }
1791
+ const id = rest[0];
1792
+ if (method === "POST" && id && rest[1] === "revoke") {
1793
+ if (!await voucherEnabled(deps)) return writeErr(res, 403, "Voucher feature is disabled");
1794
+ const ok = await voucherDb.voucherRevokeCas(id, Date.now());
1795
+ return writeJson(res, ok ? 200 : 409, { ok });
1796
+ }
1797
+ return writeErr(res, 405, `method ${method} not allowed on voucher`);
1798
+ }
1799
+
1800
+ // src/admin/webhookConfigBody.ts
1801
+ import {
1802
+ WEBHOOK_DESTINATION_TYPES,
1803
+ WEBHOOK_EVENT_KINDS
1804
+ } from "@omnicross/contracts/webhook-types";
1805
+ var WEBHOOK_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
1806
+ var isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1807
+ function validateWebhookSegment(patch) {
1808
+ const errors = [];
1809
+ const webhook = patch.webhook;
1810
+ if (webhook === void 0) return errors;
1811
+ if (!isPlainObject3(webhook)) {
1812
+ errors.push("webhook must be an object");
1813
+ return errors;
1814
+ }
1815
+ if (typeof webhook["enabled"] !== "boolean") {
1816
+ errors.push("webhook.enabled must be a boolean");
1817
+ }
1818
+ const destinations = webhook["destinations"];
1819
+ if (destinations !== void 0 && !Array.isArray(destinations)) {
1820
+ errors.push("webhook.destinations must be an array");
1821
+ return errors;
1822
+ }
1823
+ const seenIds = /* @__PURE__ */ new Set();
1824
+ for (const [i, raw] of (Array.isArray(destinations) ? destinations : []).entries()) {
1825
+ if (!isPlainObject3(raw)) {
1826
+ errors.push(`webhook.destinations[${i}] must be an object`);
1827
+ continue;
1828
+ }
1829
+ const id = raw["id"];
1830
+ if (typeof id !== "string" || !id.trim()) {
1831
+ errors.push(`webhook.destinations[${i}].id must be a non-empty string`);
1832
+ } else if (seenIds.has(id.trim())) {
1833
+ errors.push(`webhook.destinations[${i}].id '${id.trim()}' is duplicated`);
1834
+ } else {
1835
+ seenIds.add(id.trim());
1836
+ }
1837
+ if (typeof raw["type"] !== "string" || !WEBHOOK_DESTINATION_TYPES.includes(raw["type"])) {
1838
+ errors.push(`webhook.destinations[${i}].type must be one of ${WEBHOOK_DESTINATION_TYPES.join(", ")}`);
1839
+ }
1840
+ if (typeof raw["url"] !== "string" || !raw["url"].trim()) {
1841
+ errors.push(`webhook.destinations[${i}].url must be a non-empty string`);
1842
+ }
1843
+ if (raw["secret"] !== void 0 && typeof raw["secret"] !== "string") {
1844
+ errors.push(`webhook.destinations[${i}].secret must be a string`);
1845
+ }
1846
+ if (raw["enabled"] !== void 0 && typeof raw["enabled"] !== "boolean") {
1847
+ errors.push(`webhook.destinations[${i}].enabled must be a boolean`);
1848
+ }
1849
+ const events = raw["events"];
1850
+ if (events !== void 0) {
1851
+ if (!Array.isArray(events)) {
1852
+ errors.push(`webhook.destinations[${i}].events must be an array`);
1853
+ } else {
1854
+ for (const e of events) {
1855
+ if (typeof e !== "string" || !WEBHOOK_EVENT_KINDS.includes(e)) {
1856
+ errors.push(`webhook.destinations[${i}].events contains an unknown kind '${String(e)}'`);
1857
+ }
1858
+ }
1859
+ }
1860
+ }
1861
+ }
1862
+ return errors;
1863
+ }
1864
+ function redactWebhookConfig(webhook) {
1865
+ return {
1866
+ ...webhook,
1867
+ destinations: webhook.destinations.map(
1868
+ (d) => typeof d.secret === "string" && d.secret.length > 0 ? { ...d, secret: WEBHOOK_SECRET_MASK } : d
1869
+ )
1870
+ };
1871
+ }
1872
+ function preserveWebhookSecrets(incoming, current) {
1873
+ const currentById = /* @__PURE__ */ new Map();
1874
+ for (const d of current?.destinations ?? []) currentById.set(d.id, d);
1875
+ return {
1876
+ ...incoming,
1877
+ destinations: incoming.destinations.map((d) => {
1878
+ const isMaskedOrBlank = d.secret === void 0 || d.secret === "" || d.secret === WEBHOOK_SECRET_MASK;
1879
+ if (isMaskedOrBlank) {
1880
+ const prev = currentById.get(d.id);
1881
+ if (prev?.secret) return { ...d, secret: prev.secret };
1882
+ const { secret: _secret, ...rest } = d;
1883
+ return rest;
1884
+ }
1885
+ return d;
1886
+ })
1887
+ };
1888
+ }
1889
+
1890
+ // src/audit/auditRuntime.ts
1891
+ import { setAuditCaptureConfig, setAuditSink } from "@omnicross/core/pipeline/auditSink";
1892
+ var writer = null;
1893
+ var sweeper = null;
1894
+ function setAuditRuntime(w, s) {
1895
+ writer = w;
1896
+ sweeper = s;
1897
+ }
1898
+ function applyAuditConfig(config) {
1899
+ const enabled = config?.enabled === true && writer !== null;
1900
+ if (enabled && config) {
1901
+ setAuditCaptureConfig(config);
1902
+ const activeWriter = writer;
1903
+ setAuditSink((record) => activeWriter.record(record));
1904
+ if (sweeper) {
1905
+ sweeper.configure(config);
1906
+ sweeper.start();
1907
+ }
1908
+ } else {
1909
+ setAuditCaptureConfig(null);
1910
+ setAuditSink(null);
1911
+ if (sweeper) {
1912
+ if (config) sweeper.configure(config);
1913
+ sweeper.dispose();
1914
+ }
1915
+ }
1916
+ }
1917
+ function resetAuditRuntimeForTests() {
1918
+ setAuditCaptureConfig(null);
1919
+ setAuditSink(null);
1920
+ if (sweeper) sweeper.dispose();
1921
+ writer = null;
1922
+ sweeper = null;
1923
+ }
1924
+
1925
+ // src/billing/billingRuntime.ts
1926
+ import { setBillingCaptureConfig, setBillingSink } from "@omnicross/core/pipeline/billingEmit";
1927
+ var publisher = null;
1928
+ var sweeper2 = null;
1929
+ function setBillingRuntime(p, s) {
1930
+ publisher = p;
1931
+ sweeper2 = s;
1932
+ }
1933
+ function applyBillingConfig(config) {
1934
+ const enabled = config?.enabled === true && publisher !== null;
1935
+ if (enabled && config) {
1936
+ const activePublisher = publisher;
1937
+ activePublisher.setConfig(config);
1938
+ setBillingCaptureConfig(config);
1939
+ setBillingSink((event) => activePublisher.record(event));
1940
+ if (sweeper2) {
1941
+ sweeper2.configure(config);
1942
+ sweeper2.start();
1943
+ }
1944
+ } else {
1945
+ setBillingCaptureConfig(null);
1946
+ setBillingSink(null);
1947
+ if (sweeper2) {
1948
+ if (config) sweeper2.configure(config);
1949
+ sweeper2.dispose();
1950
+ }
1951
+ }
1952
+ }
1953
+ function resetBillingRuntimeForTests() {
1954
+ setBillingCaptureConfig(null);
1955
+ setBillingSink(null);
1956
+ if (sweeper2) sweeper2.dispose();
1957
+ publisher = null;
1958
+ sweeper2 = null;
1959
+ }
1960
+
1099
1961
  // src/ports/account-multi.ts
1100
1962
  import { randomUUID as randomUUID2 } from "crypto";
1101
1963
  var PROVIDER_KEYS = {
@@ -1207,6 +2069,9 @@ function getAccountById(config, p, id) {
1207
2069
  const account = getAccounts(config, p).find((a) => a.id === id);
1208
2070
  return account ? { id: account.id, tokens: account.tokens } : void 0;
1209
2071
  }
2072
+ function getAccountProxy(config, p, id) {
2073
+ return getAccounts(config, p).find((a) => a.id === id)?.proxy;
2074
+ }
1210
2075
  function getActiveAccount(config, p) {
1211
2076
  const active = getAccounts(config, p).find((a) => a.id === getActiveId(config, p));
1212
2077
  return active ? { id: active.id, tokens: active.tokens } : void 0;
@@ -1247,7 +2112,17 @@ function sanitizeAccounts(config, p) {
1247
2112
  isSetupToken: t.isSetupToken,
1248
2113
  hasAccessToken: !!(t.accessToken || t.apiKey),
1249
2114
  isActive: a.id === activeId,
1250
- syncWarning: t.syncWarning
2115
+ // Scheduling metadata (subscription-account-scheduling): editable priority
2116
+ // (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
2117
+ priority: a.priority,
2118
+ lastUsedAt: a.lastUsedAt,
2119
+ syncWarning: t.syncWarning,
2120
+ // Per-account proxy (upstream-proxy): masked view — password → hasPassword,
2121
+ // userinfo stripped. The plaintext password is NEVER projected.
2122
+ proxy: a.proxy ? sanitizeProxyConfig(a.proxy) : void 0,
2123
+ // Per-account model support / remap (subscription-account-model-map): model
2124
+ // ids are not token material → carried through verbatim for the editor.
2125
+ supportedModels: a.supportedModels
1251
2126
  };
1252
2127
  });
1253
2128
  }
@@ -1261,19 +2136,90 @@ function renameAccount(config, p, id, label) {
1261
2136
  );
1262
2137
  return { ok: true };
1263
2138
  }
1264
- function clearProvider(config, p) {
1265
- setBlock(config, p, void 0);
1266
- setAccounts(config, p, void 0);
1267
- setActiveId(config, p, void 0);
2139
+ function setAccountPriority(config, p, id, priority) {
2140
+ const accounts = getAccounts(config, p);
2141
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2142
+ setAccounts(
2143
+ config,
2144
+ p,
2145
+ accounts.map((a) => a.id === id ? { ...a, priority } : a)
2146
+ );
2147
+ return { ok: true };
1268
2148
  }
1269
- var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
1270
-
1271
- // src/migration/packCodec.ts
1272
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes3, scryptSync } from "crypto";
1273
- var PACK_MAGIC = "OMCXPACK";
1274
- var PACK_VERSION = 1;
1275
- var KDF_ALGORITHM = "scrypt";
1276
- var PACK_PREFIX = `${PACK_MAGIC}${PACK_VERSION}.`;
2149
+ function setAccountProxy(config, p, id, proxy) {
2150
+ const accounts = getAccounts(config, p);
2151
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2152
+ setAccounts(
2153
+ config,
2154
+ p,
2155
+ accounts.map((a) => {
2156
+ if (a.id !== id) return a;
2157
+ if (!proxy) {
2158
+ const { proxy: _drop, ...rest } = a;
2159
+ return rest;
2160
+ }
2161
+ return { ...a, proxy };
2162
+ })
2163
+ );
2164
+ return { ok: true };
2165
+ }
2166
+ function setAccountSupportedModels(config, p, id, supportedModels) {
2167
+ const accounts = getAccounts(config, p);
2168
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2169
+ setAccounts(
2170
+ config,
2171
+ p,
2172
+ accounts.map((a) => {
2173
+ if (a.id !== id) return a;
2174
+ if (supportedModels === void 0) {
2175
+ const { supportedModels: _drop, ...rest } = a;
2176
+ return rest;
2177
+ }
2178
+ return { ...a, supportedModels };
2179
+ })
2180
+ );
2181
+ return { ok: true };
2182
+ }
2183
+ function setAccountLastUsed(config, p, id, iso) {
2184
+ const accounts = getAccounts(config, p);
2185
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2186
+ setAccounts(
2187
+ config,
2188
+ p,
2189
+ accounts.map((a) => a.id === id ? { ...a, lastUsedAt: iso } : a)
2190
+ );
2191
+ return { ok: true };
2192
+ }
2193
+ function setAccountIdentity(config, p, id, identity) {
2194
+ const accounts = getAccounts(config, p);
2195
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
2196
+ setAccounts(
2197
+ config,
2198
+ p,
2199
+ accounts.map((a) => {
2200
+ if (a.id !== id) return a;
2201
+ if (identity === void 0) {
2202
+ const { identity: _drop, ...rest } = a;
2203
+ return rest;
2204
+ }
2205
+ return { ...a, identity };
2206
+ })
2207
+ );
2208
+ return { ok: true };
2209
+ }
2210
+ function clearProvider(config, p) {
2211
+ setBlock(config, p, void 0);
2212
+ setAccounts(config, p, void 0);
2213
+ setActiveId(config, p, void 0);
2214
+ }
2215
+ var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
2216
+
2217
+ // src/migration/packCodec.ts
2218
+ import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes3, scryptSync } from "crypto";
2219
+ var PACK_MAGIC = "OMCXPACK";
2220
+ var PACK_VERSION = 1;
2221
+ var KDF_ALGORITHM = "scrypt";
2222
+ var PACK_PREFIX = `${PACK_MAGIC}${PACK_VERSION}.`;
1277
2223
  var KEY_BYTES3 = 32;
1278
2224
  var IV_BYTES2 = 12;
1279
2225
  var TAG_BYTES2 = 16;
@@ -1526,6 +2472,12 @@ function parseRange(query) {
1526
2472
  return { startTs, endTs };
1527
2473
  }
1528
2474
  var isRange = (v) => v.startTs !== void 0 && !("status" in v);
2475
+ var BUCKET_SPAN_MS = {
2476
+ hour: 36e5,
2477
+ day: 864e5,
2478
+ month: 28 * 864e5
2479
+ };
2480
+ var MAX_TIMESERIES_BUCKETS = 2e3;
1529
2481
  async function handleUsageGet(view, query, deps) {
1530
2482
  const range = parseRange(query);
1531
2483
  if (!isRange(range)) return range;
@@ -1534,6 +2486,24 @@ async function handleUsageGet(view, query, deps) {
1534
2486
  return { status: 200, body: await deps.usageRecorder.getTotals(range) };
1535
2487
  case "by-model":
1536
2488
  return { status: 200, body: await deps.usageRecorder.getByModel(range) };
2489
+ case "timeseries": {
2490
+ const bucket = query.get("bucket");
2491
+ if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
2492
+ return err4(400, "bucket must be one of 'hour', 'day', 'month'");
2493
+ }
2494
+ const now = Date.now();
2495
+ const clamped = { startTs: range.startTs, endTs: Math.min(range.endTs, now) };
2496
+ if (clamped.startTs < clamped.endTs) {
2497
+ const projected = Math.ceil((clamped.endTs - clamped.startTs) / BUCKET_SPAN_MS[bucket]) + 1;
2498
+ if (projected > MAX_TIMESERIES_BUCKETS) {
2499
+ return err4(
2500
+ 400,
2501
+ `requested range projects ~${projected} '${bucket}' buckets (max ${MAX_TIMESERIES_BUCKETS}); narrow the range or use a coarser bucket`
2502
+ );
2503
+ }
2504
+ }
2505
+ return { status: 200, body: await deps.usageRecorder.getTimeSeries(clamped, bucket) };
2506
+ }
1537
2507
  case "by-api-key": {
1538
2508
  const rows = await deps.usageRecorder.getByApiKey(range);
1539
2509
  const labels = poolKeyLabels(loadConfig(deps.configPath));
@@ -1679,7 +2649,7 @@ function readBody(req) {
1679
2649
  req.on("error", reject);
1680
2650
  });
1681
2651
  }
1682
- async function readJsonBody(req) {
2652
+ async function readJsonBody3(req) {
1683
2653
  const raw = await readBody(req);
1684
2654
  if (!raw.trim()) return {};
1685
2655
  try {
@@ -1689,12 +2659,12 @@ async function readJsonBody(req) {
1689
2659
  return {};
1690
2660
  }
1691
2661
  }
1692
- function writeJson(res, status, body) {
2662
+ function writeJson2(res, status, body) {
1693
2663
  res.writeHead(status, { "Content-Type": "application/json" });
1694
2664
  res.end(JSON.stringify(body));
1695
2665
  }
1696
2666
  function writeJsonError(res, status, message) {
1697
- writeJson(res, status, { error: { type: "admin_api_error", message } });
2667
+ writeJson2(res, status, { error: { type: "admin_api_error", message } });
1698
2668
  }
1699
2669
  function maskProviderApiKey(apiKey) {
1700
2670
  if (!apiKey) return "";
@@ -1710,7 +2680,23 @@ function toKeyInfo(row) {
1710
2680
  enabled: row.enabled,
1711
2681
  createdAt: row.createdAt,
1712
2682
  lastUsedAt: row.lastUsedAt,
1713
- revoked: row.revokedAt !== null
2683
+ revoked: row.revokedAt !== null,
2684
+ maxConcurrency: row.maxConcurrency,
2685
+ // Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
2686
+ // the UI reads them to render + pre-fill the policy editor.
2687
+ expiresAt: row.expiresAt,
2688
+ activationMode: row.activationMode,
2689
+ activationDays: row.activationDays,
2690
+ activatedAt: row.activatedAt,
2691
+ dailyCostLimitUsd: row.dailyCostLimitUsd,
2692
+ totalCostLimitUsd: row.totalCostLimitUsd,
2693
+ weeklyCostLimitUsd: row.weeklyCostLimitUsd,
2694
+ rateLimitMaxRequests: row.rateLimitMaxRequests,
2695
+ rateLimitWindowMs: row.rateLimitWindowMs,
2696
+ // Per-key model restriction (#6) — secret-free scalars the UI pre-fills.
2697
+ enableModelRestriction: row.enableModelRestriction,
2698
+ restrictionMode: row.restrictionMode,
2699
+ restrictedModels: row.restrictedModels
1714
2700
  };
1715
2701
  }
1716
2702
  function toProviderView(row) {
@@ -1772,6 +2758,8 @@ async function handleAdminApi(req, res, path2, deps) {
1772
2758
  return handlePresets(res, method);
1773
2759
  case "keys":
1774
2760
  return await handleKeys(req, res, method, rest, deps);
2761
+ case "voucher":
2762
+ return await handleVoucher(req, res, method, rest, deps);
1775
2763
  case "server":
1776
2764
  return await handleServer(req, res, method, deps);
1777
2765
  case "accounts":
@@ -1788,6 +2776,8 @@ async function handleAdminApi(req, res, path2, deps) {
1788
2776
  return await handleMigrationImport(req, res, method, deps);
1789
2777
  case "usage":
1790
2778
  return await handleUsage(req, res, method, rest, deps);
2779
+ case "dashboard":
2780
+ return await handleDashboardRoute(res, method, deps);
1791
2781
  case "pricing":
1792
2782
  return await handlePricing(req, res, method, rest, deps);
1793
2783
  default:
@@ -1803,17 +2793,22 @@ function requestQuery(req) {
1803
2793
  return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
1804
2794
  }
1805
2795
  function writeResult(res, result) {
1806
- writeJson(res, result.status, result.body);
2796
+ writeJson2(res, result.status, result.body);
1807
2797
  }
1808
2798
  async function handleUsage(req, res, method, rest, deps) {
1809
2799
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
1810
2800
  return writeResult(res, await handleUsageGet(rest[0], requestQuery(req), deps));
1811
2801
  }
2802
+ async function handleDashboardRoute(res, method, deps) {
2803
+ if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
2804
+ const result = await handleDashboard(deps);
2805
+ return writeJson2(res, result.status, result.body);
2806
+ }
1812
2807
  async function handlePricing(req, res, method, rest, deps) {
1813
2808
  if (rest.length === 0) {
1814
2809
  if (method === "GET") return writeResult(res, await handlePricingList(deps));
1815
2810
  if (method === "PUT") {
1816
- return writeResult(res, await handlePricingUpsert(await readJsonBody(req), deps));
2811
+ return writeResult(res, await handlePricingUpsert(await readJsonBody3(req), deps));
1817
2812
  }
1818
2813
  if (method === "DELETE") {
1819
2814
  return writeResult(res, await handlePricingDelete(requestQuery(req), deps));
@@ -1824,7 +2819,7 @@ async function handlePricing(req, res, method, rest, deps) {
1824
2819
  return writeResult(res, await handlePricingFetchLatest(deps));
1825
2820
  }
1826
2821
  if (method === "POST" && rest.length === 1 && rest[0] === "resolve-conflicts") {
1827
- return writeResult(res, await handlePricingResolveConflicts(await readJsonBody(req), deps));
2822
+ return writeResult(res, await handlePricingResolveConflicts(await readJsonBody3(req), deps));
1828
2823
  }
1829
2824
  return writeJsonError(res, 404, `unknown pricing route '${rest.join("/")}'`);
1830
2825
  }
@@ -1838,15 +2833,15 @@ function migrationDeps(deps) {
1838
2833
  }
1839
2834
  async function handleMigrationExport(req, res, method, deps) {
1840
2835
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
1841
- const body = await readJsonBody(req);
2836
+ const body = await readJsonBody3(req);
1842
2837
  const result = await handleExport(body, migrationDeps(deps));
1843
- return writeJson(res, result.status, result.body);
2838
+ return writeJson2(res, result.status, result.body);
1844
2839
  }
1845
2840
  async function handleMigrationImport(req, res, method, deps) {
1846
2841
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
1847
- const body = await readJsonBody(req);
2842
+ const body = await readJsonBody3(req);
1848
2843
  const result = await handleImport(body, migrationDeps(deps));
1849
- return writeJson(res, result.status, result.body);
2844
+ return writeJson2(res, result.status, result.body);
1850
2845
  }
1851
2846
  async function handleProviders(req, res, method, rest, deps) {
1852
2847
  const cfg = loadConfig(deps.configPath);
@@ -1877,13 +2872,13 @@ async function handleProviders(req, res, method, rest, deps) {
1877
2872
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
1878
2873
  const row = cfg.providers.find((p) => p.id === rest[0]);
1879
2874
  if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
1880
- return writeJson(res, 200, { apiKey: row.apiKey ?? "" });
2875
+ return writeJson2(res, 200, { apiKey: row.apiKey ?? "" });
1881
2876
  }
1882
2877
  if (method === "GET") {
1883
- return writeJson(res, 200, { providers: cfg.providers.map(toProviderView) });
2878
+ return writeJson2(res, 200, { providers: cfg.providers.map(toProviderView) });
1884
2879
  }
1885
2880
  if (method === "POST") {
1886
- const body = await readJsonBody(req);
2881
+ const body = await readJsonBody3(req);
1887
2882
  const provider = parseProviderInput(body, void 0);
1888
2883
  if (!provider) return writeJsonError(res, 400, "invalid provider (id, apiFormat, baseUrl required)");
1889
2884
  if (cfg.providers.some((p) => p.id === provider.id)) {
@@ -1891,25 +2886,25 @@ async function handleProviders(req, res, method, rest, deps) {
1891
2886
  }
1892
2887
  cfg.providers.push(provider);
1893
2888
  persistProviders(cfg, deps);
1894
- return writeJson(res, 201, { provider: toProviderView(provider) });
2889
+ return writeJson2(res, 201, { provider: toProviderView(provider) });
1895
2890
  }
1896
2891
  const id = rest[0];
1897
2892
  if (!id) return writeJsonError(res, 400, "provider id required in path");
1898
2893
  const idx = cfg.providers.findIndex((p) => p.id === id);
1899
2894
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
1900
2895
  if (method === "PUT") {
1901
- const body = await readJsonBody(req);
2896
+ const body = await readJsonBody3(req);
1902
2897
  const existing = cfg.providers[idx];
1903
2898
  const updated = parseProviderInput(body, existing);
1904
2899
  if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
1905
2900
  cfg.providers[idx] = updated;
1906
2901
  persistProviders(cfg, deps);
1907
- return writeJson(res, 200, { provider: toProviderView(updated) });
2902
+ return writeJson2(res, 200, { provider: toProviderView(updated) });
1908
2903
  }
1909
2904
  if (method === "DELETE") {
1910
2905
  cfg.providers.splice(idx, 1);
1911
2906
  persistProviders(cfg, deps);
1912
- return writeJson(res, 200, { ok: true });
2907
+ return writeJson2(res, 200, { ok: true });
1913
2908
  }
1914
2909
  return writeJsonError(res, 405, `method ${method} not allowed on providers`);
1915
2910
  }
@@ -1918,7 +2913,7 @@ function persistProviders(cfg, deps) {
1918
2913
  deps.llmConfig.reload(cfg);
1919
2914
  }
1920
2915
  async function handleProviderReorder(req, res, cfg, deps) {
1921
- const body = await readJsonBody(req);
2916
+ const body = await readJsonBody3(req);
1922
2917
  const rawOrder = body["order"];
1923
2918
  if (!Array.isArray(rawOrder)) {
1924
2919
  return writeJsonError(res, 400, "reorder requires { order: string[] }");
@@ -1942,14 +2937,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
1942
2937
  }
1943
2938
  cfg.providers = reordered;
1944
2939
  persistProviders(cfg, deps);
1945
- return writeJson(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
2940
+ return writeJson2(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
1946
2941
  }
1947
2942
  async function handleDiscoverModels(res, id, cfg) {
1948
2943
  if (!id) return writeJsonError(res, 400, "provider id required in path");
1949
2944
  const row = cfg.providers.find((p) => p.id === id);
1950
2945
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
1951
2946
  if (row.apiFormat !== "openai") {
1952
- return writeJson(res, 200, { models: [], unsupportedFormat: true });
2947
+ return writeJson2(res, 200, { models: [], unsupportedFormat: true });
1953
2948
  }
1954
2949
  const resolvedKey = resolveEnvKey(row.apiKey);
1955
2950
  const base = row.baseUrl.replace(/\/+$/, "");
@@ -1957,7 +2952,7 @@ async function handleDiscoverModels(res, id, cfg) {
1957
2952
  try {
1958
2953
  const headers = { Accept: "application/json" };
1959
2954
  if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
1960
- const response = await fetch(url, { method: "GET", headers });
2955
+ const response = await fetchUpstream(url, { method: "GET", headers }, { providerId: "byo" });
1961
2956
  if (!response.ok) {
1962
2957
  const text = await response.text().catch(() => "");
1963
2958
  let message = text.slice(0, 300);
@@ -1966,32 +2961,32 @@ async function handleDiscoverModels(res, id, cfg) {
1966
2961
  message = parsed?.error?.message || parsed?.message || message;
1967
2962
  } catch {
1968
2963
  }
1969
- return writeJson(res, 200, {
2964
+ return writeJson2(res, 200, {
1970
2965
  models: [],
1971
2966
  error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
1972
2967
  });
1973
2968
  }
1974
2969
  const data = await response.json();
1975
2970
  const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
1976
- return writeJson(res, 200, { models });
2971
+ return writeJson2(res, 200, { models });
1977
2972
  } catch (err5) {
1978
2973
  const message = err5 instanceof Error ? err5.message : String(err5);
1979
- return writeJson(res, 200, { models: [], error: `discovery failed: ${message}` });
2974
+ return writeJson2(res, 200, { models: [], error: `discovery failed: ${message}` });
1980
2975
  }
1981
2976
  }
1982
2977
  async function handleTestModel(req, res, id, cfg) {
1983
2978
  if (!id) return writeJsonError(res, 400, "provider id required in path");
1984
2979
  const row = cfg.providers.find((p) => p.id === id);
1985
2980
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
1986
- const body = await readJsonBody(req);
2981
+ const body = await readJsonBody3(req);
1987
2982
  const model = typeof body["model"] === "string" ? body["model"].trim() : "";
1988
2983
  if (!model) return writeJsonError(res, 400, "test requires a { model } string");
1989
2984
  if (row.apiFormat === "gemini") {
1990
- return writeJson(res, 200, { ok: false, unsupportedFormat: true });
2985
+ return writeJson2(res, 200, { ok: false, unsupportedFormat: true });
1991
2986
  }
1992
2987
  const resolvedKey = resolveEnvKey(row.apiKey);
1993
2988
  if (!resolvedKey) {
1994
- return writeJson(res, 200, { ok: false, message: "no API key configured for this provider" });
2989
+ return writeJson2(res, 200, { ok: false, message: "no API key configured for this provider" });
1995
2990
  }
1996
2991
  const url = row.baseUrl.replace(/\/+$/, "");
1997
2992
  const prompt = "Reply with the single word: OK.";
@@ -2012,11 +3007,11 @@ async function handleTestModel(req, res, id, cfg) {
2012
3007
  }
2013
3008
  const startedAt = Date.now();
2014
3009
  try {
2015
- const response = await fetch(url, {
2016
- method: "POST",
2017
- headers,
2018
- body: JSON.stringify(payload)
2019
- });
3010
+ const response = await fetchUpstream(
3011
+ url,
3012
+ { method: "POST", headers, body: JSON.stringify(payload) },
3013
+ { providerId: "byo" }
3014
+ );
2020
3015
  const latencyMs = Date.now() - startedAt;
2021
3016
  const text = await response.text().catch(() => "");
2022
3017
  if (!response.ok) {
@@ -2026,9 +3021,9 @@ async function handleTestModel(req, res, id, cfg) {
2026
3021
  message = parsed?.error?.message || parsed?.message || message;
2027
3022
  } catch {
2028
3023
  }
2029
- return writeJson(res, 200, { ok: false, status: response.status, latencyMs, message });
3024
+ return writeJson2(res, 200, { ok: false, status: response.status, latencyMs, message });
2030
3025
  }
2031
- return writeJson(res, 200, {
3026
+ return writeJson2(res, 200, {
2032
3027
  ok: true,
2033
3028
  status: response.status,
2034
3029
  latencyMs,
@@ -2036,7 +3031,7 @@ async function handleTestModel(req, res, id, cfg) {
2036
3031
  });
2037
3032
  } catch (err5) {
2038
3033
  const message = err5 instanceof Error ? err5.message : String(err5);
2039
- return writeJson(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
3034
+ return writeJson2(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
2040
3035
  }
2041
3036
  }
2042
3037
  function extractSampleText(text, apiFormat) {
@@ -2058,9 +3053,9 @@ function toPoolKeyView(row, cooldown, deps) {
2058
3053
  return entries.map((e) => {
2059
3054
  const auto = deps.autoDisableStore.get(e.id);
2060
3055
  const cd = cooldown[e.id];
2061
- const health = {};
2062
- if (cd) health.cooldown = cd;
2063
- if (auto) health.autoDisabled = { status: auto.status, at: auto.at, reason: auto.reason };
3056
+ const health2 = {};
3057
+ if (cd) health2.cooldown = cd;
3058
+ if (auto) health2.autoDisabled = { status: auto.status, at: auto.at, reason: auto.reason };
2064
3059
  return {
2065
3060
  id: e.id,
2066
3061
  label: e.label && e.label.length > 0 ? e.label : e.id,
@@ -2068,7 +3063,7 @@ function toPoolKeyView(row, cooldown, deps) {
2068
3063
  enabled: e.enabled !== false && !deps.autoDisableStore.isDisabled(e.id),
2069
3064
  weight: typeof e.weight === "number" && Number.isFinite(e.weight) ? e.weight : 1,
2070
3065
  apiKeyMasked: maskProviderApiKey(e.apiKey),
2071
- ...Object.keys(health).length > 0 ? { health } : {}
3066
+ ...Object.keys(health2).length > 0 ? { health: health2 } : {}
2072
3067
  };
2073
3068
  });
2074
3069
  }
@@ -2077,7 +3072,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
2077
3072
  const row = cfg.providers.find((p) => p.id === id);
2078
3073
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
2079
3074
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2080
- return writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3075
+ return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
2081
3076
  }
2082
3077
  function parsePoolKeyInput(body, existing) {
2083
3078
  const out = {};
@@ -2096,7 +3091,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
2096
3091
  if (!id) return writeJsonError(res, 400, "provider id required in path");
2097
3092
  const idx = cfg.providers.findIndex((p) => p.id === id);
2098
3093
  if (idx < 0) return writeJsonError(res, 404, `provider '${id}' not found`);
2099
- const body = await readJsonBody(req);
3094
+ const body = await readJsonBody3(req);
2100
3095
  const parsed = parsePoolKeyInput(body);
2101
3096
  if (!parsed.apiKey) return writeJsonError(res, 400, "add requires a non-empty apiKey");
2102
3097
  const keyId = `key-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -2108,7 +3103,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
2108
3103
  row.apiKeys = [...row.apiKeys ?? [], entry];
2109
3104
  persistProviders(cfg, deps);
2110
3105
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2111
- return writeJson(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
3106
+ return writeJson2(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
2112
3107
  }
2113
3108
  async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
2114
3109
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -2118,7 +3113,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
2118
3113
  const row = cfg.providers[idx];
2119
3114
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
2120
3115
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
2121
- const body = await readJsonBody(req);
3116
+ const body = await readJsonBody3(req);
2122
3117
  const existing = row.apiKeys[keyIdx];
2123
3118
  const parsed = parsePoolKeyInput(body, existing);
2124
3119
  const entry = { id: keyId, apiKey: parsed.apiKey || existing.apiKey };
@@ -2128,7 +3123,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
2128
3123
  row.apiKeys[keyIdx] = entry;
2129
3124
  persistProviders(cfg, deps);
2130
3125
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2131
- return writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3126
+ return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
2132
3127
  }
2133
3128
  async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
2134
3129
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -2142,7 +3137,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
2142
3137
  if (row.apiKeys.length === 0) row.apiKeys = void 0;
2143
3138
  persistProviders(cfg, deps);
2144
3139
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2145
- return writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3140
+ return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
2146
3141
  }
2147
3142
  async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
2148
3143
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -2152,11 +3147,11 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
2152
3147
  const row = cfg.providers[idx];
2153
3148
  const keyIdx = (row.apiKeys ?? []).findIndex((k) => k.id === keyId);
2154
3149
  if (keyIdx < 0) return writeJsonError(res, 404, `pool key '${keyId}' not found`);
2155
- const body = await readJsonBody(req);
3150
+ const body = await readJsonBody3(req);
2156
3151
  row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
2157
3152
  persistProviders(cfg, deps);
2158
3153
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
2159
- return writeJson(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3154
+ return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
2160
3155
  }
2161
3156
  function parseApiKeysInput(raw, existing) {
2162
3157
  if (!Array.isArray(raw)) return existing;
@@ -2327,18 +3322,31 @@ function handlePresets(res, method) {
2327
3322
  baseUrl: p.baseUrl,
2328
3323
  models: p.models
2329
3324
  }));
2330
- return writeJson(res, 200, { presets, excluded });
3325
+ return writeJson2(res, 200, { presets, excluded });
2331
3326
  }
2332
3327
  async function handleKeys(req, res, method, rest, deps) {
2333
3328
  if (method === "GET" && rest.length === 0) {
2334
3329
  const rows = await deps.keyDb.outboundApiKeysList();
2335
- return writeJson(res, 200, { keys: rows.map(toKeyInfo) });
3330
+ const reader = deps.keySpendReader;
3331
+ if (!reader) return writeJson2(res, 200, { keys: rows.map(toKeyInfo) });
3332
+ const now = Date.now();
3333
+ const keys = await Promise.all(
3334
+ rows.map(async (row) => {
3335
+ const info = toKeyInfo(row);
3336
+ if (row.revokedAt === null) {
3337
+ const s = await reader.getSpend(row.id, now);
3338
+ info.spend = { dailyUsd: s.dailyUsd, weeklyUsd: s.weeklyUsd, totalUsd: s.totalUsd };
3339
+ }
3340
+ return info;
3341
+ })
3342
+ );
3343
+ return writeJson2(res, 200, { keys });
2336
3344
  }
2337
3345
  if (method === "POST" && rest.length === 0) {
2338
- const body = await readJsonBody(req);
3346
+ const body = await readJsonBody3(req);
2339
3347
  const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
2340
3348
  const created = await createNamedKey(deps.keyDb, name);
2341
- return writeJson(res, 201, {
3349
+ return writeJson2(res, 201, {
2342
3350
  id: created.id,
2343
3351
  name: created.name,
2344
3352
  keyPrefix: created.keyPrefix,
@@ -2350,46 +3358,185 @@ async function handleKeys(req, res, method, rest, deps) {
2350
3358
  const action = rest[1];
2351
3359
  if (method === "POST" && id && action === "revoke") {
2352
3360
  const ok = await deps.keyDb.outboundApiKeysRevoke(id);
2353
- return writeJson(res, ok ? 200 : 404, { ok });
3361
+ return writeJson2(res, ok ? 200 : 404, { ok });
2354
3362
  }
2355
3363
  if (method === "POST" && id && action === "enabled") {
2356
- const body = await readJsonBody(req);
3364
+ const body = await readJsonBody3(req);
2357
3365
  const enabled = body["enabled"] === true;
2358
3366
  const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
2359
- return writeJson(res, ok ? 200 : 404, { ok, enabled });
3367
+ return writeJson2(res, ok ? 200 : 404, { ok, enabled });
3368
+ }
3369
+ if (method === "POST" && id && action === "max-concurrency") {
3370
+ const body = await readJsonBody3(req);
3371
+ const raw = body["maxConcurrency"];
3372
+ let value;
3373
+ if (raw === null) {
3374
+ value = null;
3375
+ } else if (typeof raw === "number" && Number.isInteger(raw) && raw >= 1 && raw <= 1e3) {
3376
+ value = raw;
3377
+ } else {
3378
+ return writeJsonError(
3379
+ res,
3380
+ 400,
3381
+ "maxConcurrency must be an integer 1..1000 or null"
3382
+ );
3383
+ }
3384
+ const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
3385
+ return writeJson2(res, ok ? 200 : 404, { ok, maxConcurrency: value });
3386
+ }
3387
+ if (method === "POST" && id && action === "policy") {
3388
+ const body = await readJsonBody3(req);
3389
+ const parsed = parseKeyPolicyBody(body);
3390
+ if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
3391
+ const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
3392
+ return writeJson2(res, ok ? 200 : 404, { ok });
2360
3393
  }
2361
3394
  return writeJsonError(res, 405, `method ${method} not allowed on keys`);
2362
3395
  }
3396
+ function validateQueueSegments(patch) {
3397
+ const errors = [];
3398
+ const checkNum = (label, value, min, max) => {
3399
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
3400
+ errors.push(`${label} must be a number ${min}..${max}`);
3401
+ }
3402
+ };
3403
+ const isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3404
+ const umq = patch.userMessageQueue;
3405
+ if (umq !== void 0) {
3406
+ if (!isPlainObject4(umq)) {
3407
+ errors.push("userMessageQueue must be an object");
3408
+ } else {
3409
+ if (typeof umq.enabled !== "boolean") {
3410
+ errors.push("userMessageQueue.enabled must be a boolean");
3411
+ }
3412
+ checkNum("userMessageQueue.delayMs", umq.delayMs, 0, 1e4);
3413
+ checkNum("userMessageQueue.waitTimeoutMs", umq.waitTimeoutMs, 1e3, 3e5);
3414
+ }
3415
+ }
3416
+ const cq = patch.concurrencyQueue;
3417
+ if (cq !== void 0) {
3418
+ if (!isPlainObject4(cq)) {
3419
+ errors.push("concurrencyQueue must be an object");
3420
+ } else {
3421
+ checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
3422
+ checkNum("concurrencyQueue.minQueueSize", cq.minQueueSize, 1, 100);
3423
+ checkNum("concurrencyQueue.waitTimeoutMs", cq.waitTimeoutMs, 1e3, 3e5);
3424
+ }
3425
+ }
3426
+ const ah = patch.accountHealth;
3427
+ if (ah !== void 0) {
3428
+ if (!isPlainObject4(ah)) {
3429
+ errors.push("accountHealth must be an object");
3430
+ } else {
3431
+ if (typeof ah.overloadCooldownEnabled !== "boolean") {
3432
+ errors.push("accountHealth.overloadCooldownEnabled must be a boolean");
3433
+ }
3434
+ checkNum("accountHealth.overloadCooldownMs", ah.overloadCooldownMs, 6e4, 36e5);
3435
+ }
3436
+ }
3437
+ return errors;
3438
+ }
2363
3439
  async function handleServer(req, res, method, deps) {
2364
3440
  if (method === "GET") {
2365
- const config = await loadServerConfig(deps.settingsStore);
2366
- return writeJson(res, 200, { server: config });
3441
+ const config = await loadServerConfig2(deps.settingsStore);
3442
+ let server = config;
3443
+ if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
3444
+ if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
3445
+ if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
3446
+ return writeJson2(res, 200, { server });
2367
3447
  }
2368
3448
  if (method === "PUT") {
2369
- const patch = await readJsonBody(req);
2370
- const current = await loadServerConfig(deps.settingsStore);
2371
- const merged = mergeServerConfig(current, patch);
3449
+ const patch = await readJsonBody3(req);
3450
+ const queueErrors = validateQueueSegments(patch);
3451
+ if (queueErrors.length > 0) {
3452
+ return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
3453
+ }
3454
+ const webhookErrors = validateWebhookSegment(patch);
3455
+ if (webhookErrors.length > 0) {
3456
+ return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
3457
+ }
3458
+ const auditErrors = validateAuditSegment(patch);
3459
+ if (auditErrors.length > 0) {
3460
+ return writeJsonError(res, 400, `invalid audit config: ${auditErrors.join("; ")}`);
3461
+ }
3462
+ const billingErrors = validateBillingSegment(patch);
3463
+ if (billingErrors.length > 0) {
3464
+ return writeJsonError(res, 400, `invalid billing config: ${billingErrors.join("; ")}`);
3465
+ }
3466
+ const current = await loadServerConfig2(deps.settingsStore);
3467
+ let effectivePatch = patch;
3468
+ if (patch.proxy) {
3469
+ effectivePatch = { ...effectivePatch, proxy: preserveOutboundProxySecrets(patch.proxy, current.proxy) };
3470
+ }
3471
+ if (patch.webhook) {
3472
+ effectivePatch = { ...effectivePatch, webhook: preserveWebhookSecrets(patch.webhook, current.webhook) };
3473
+ }
3474
+ if (patch.billing) {
3475
+ effectivePatch = { ...effectivePatch, billing: preserveBillingSecret(patch.billing, current.billing) };
3476
+ }
3477
+ const merged = mergeServerConfig(current, effectivePatch);
2372
3478
  await saveServerConfig(deps.settingsStore, merged);
2373
- await deps.outboundApiServer.applyConfig({
2374
- enabled: merged.enabled,
2375
- networkBinding: merged.networkBinding,
2376
- endpoints: merged.endpoints,
2377
- port: merged.port
2378
- });
2379
- return writeJson(res, 200, { server: merged });
3479
+ setServerProxyConfig(merged.proxy);
3480
+ applyWebhookConfig(merged.webhook);
3481
+ applyAuditConfig(merged.audit);
3482
+ applyBillingConfig(merged.billing);
3483
+ if (merged.enabled) {
3484
+ const missing = validateServerModelConfig(merged);
3485
+ if (missing.length > 0) {
3486
+ if (deps.outboundApiServer.getStatus().running) {
3487
+ await deps.outboundApiServer.stop();
3488
+ }
3489
+ return writeJson2(res, 200, {
3490
+ server: merged,
3491
+ error: { code: "incomplete-model-config", missing }
3492
+ });
3493
+ }
3494
+ }
3495
+ try {
3496
+ await deps.outboundApiServer.applyConfig({
3497
+ enabled: merged.enabled,
3498
+ networkBinding: merged.networkBinding,
3499
+ endpoints: merged.endpoints,
3500
+ port: merged.port,
3501
+ userMessageQueue: merged.userMessageQueue,
3502
+ concurrencyQueue: merged.concurrencyQueue,
3503
+ // voucher-redemption #9: hot-apply the voucher flag so enabling the product
3504
+ // takes effect without a restart.
3505
+ voucher: merged.voucher
3506
+ });
3507
+ } catch (err5) {
3508
+ const missing = incompleteConfigMissing(err5);
3509
+ if (missing) {
3510
+ return writeJson2(res, 200, {
3511
+ server: merged,
3512
+ error: { code: "incomplete-model-config", missing }
3513
+ });
3514
+ }
3515
+ throw err5;
3516
+ }
3517
+ return writeJson2(res, 200, { server: merged });
2380
3518
  }
2381
3519
  return writeJsonError(res, 405, `method ${method} not allowed on server`);
2382
3520
  }
3521
+ function incompleteConfigMissing(err5) {
3522
+ if (typeof err5 !== "object" || err5 === null) return null;
3523
+ const missing = err5.missing;
3524
+ return Array.isArray(missing) ? missing : null;
3525
+ }
2383
3526
  async function handleAccounts(req, res, method, rest, deps) {
2384
3527
  if (method === "GET" && rest.length === 0) {
2385
3528
  const accounts = await deps.subscriptionAccounts.listAll();
2386
3529
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
2387
3530
  const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
2388
- return writeJson(res, 200, { accounts, providerAccounts, externalCli });
3531
+ return writeJson2(res, 200, { accounts, providerAccounts, externalCli });
2389
3532
  }
2390
3533
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
2391
3534
  const result = handleCodexOAuthStatus(rest[2], deps);
2392
- return writeJson(res, result.status, result.body);
3535
+ return writeJson2(res, result.status, result.body);
3536
+ }
3537
+ if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
3538
+ const result = handleCodexOAuthCancel(rest[2], deps);
3539
+ return writeJson2(res, result.status, result.body);
2393
3540
  }
2394
3541
  if (method === "PUT" || method === "POST" || method === "DELETE") {
2395
3542
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -2398,15 +3545,15 @@ async function handleAccounts(req, res, method, rest, deps) {
2398
3545
  }
2399
3546
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
2400
3547
  const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
2401
- return writeJson(res, result.status, result.body);
3548
+ return writeJson2(res, result.status, result.body);
2402
3549
  }
2403
3550
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
2404
- const body2 = await readJsonBody(req);
3551
+ const body2 = await readJsonBody3(req);
2405
3552
  const result = await handleOAuthComplete(providerId, body2, deps);
2406
- return writeJson(res, result.status, result.body);
3553
+ return writeJson2(res, result.status, result.body);
2407
3554
  }
2408
3555
  if (method === "POST" && rest[1] === "accounts") {
2409
- const body2 = await readJsonBody(req);
3556
+ const body2 = await readJsonBody3(req);
2410
3557
  const block = validateTokenBody(providerId, body2);
2411
3558
  if (!block) {
2412
3559
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
@@ -2414,79 +3561,113 @@ async function handleAccounts(req, res, method, rest, deps) {
2414
3561
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
2415
3562
  await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
2416
3563
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2417
- return writeJson(res, 200, status2 ? { account: status2 } : { ok: true });
3564
+ return writeJson2(res, 200, status2 ? { account: status2 } : { ok: true });
2418
3565
  }
2419
3566
  if (method === "POST" && rest[1] === "import-external") {
2420
3567
  if (providerId !== "claude" && providerId !== "codex") {
2421
3568
  return writeJsonError(res, 400, `provider '${providerId}' has no external CLI store`);
2422
3569
  }
2423
- const body2 = await readJsonBody(req);
3570
+ const body2 = await readJsonBody3(req);
2424
3571
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
2425
3572
  const result = await deps.subscriptionTokenWriter.importExternalCliAccount(providerId, label);
2426
3573
  if (!result.ok) {
2427
3574
  return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
2428
3575
  }
2429
3576
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2430
- return writeJson(res, 200, { ok: true, account: status2 ?? void 0 });
3577
+ return writeJson2(res, 200, { ok: true, account: status2 ?? void 0 });
2431
3578
  }
2432
3579
  if (method === "POST" && rest[1] === "refresh") {
2433
3580
  if (providerId === "opencodego") {
2434
3581
  return writeJsonError(res, 400, "opencodego credentials are not refreshable");
2435
3582
  }
2436
- const writer = deps.subscriptionTokenWriter;
2437
- const ok = providerId === "claude" ? await writer.refreshClaudeToken() : providerId === "codex" ? await writer.refreshCodexToken() : await writer.refreshGeminiToken();
3583
+ const writer2 = deps.subscriptionTokenWriter;
3584
+ const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
2438
3585
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2439
- return writeJson(res, 200, { ok, account: status2 ?? void 0 });
3586
+ return writeJson2(res, 200, { ok, account: status2 ?? void 0 });
2440
3587
  }
2441
3588
  if (method === "POST" && rest[2] === "label") {
2442
3589
  const accountId = rest[1];
2443
- const body2 = await readJsonBody(req);
3590
+ const body2 = await readJsonBody3(req);
2444
3591
  const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
2445
3592
  const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
2446
3593
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
2447
- return writeJson(res, 200, { ok: true });
3594
+ return writeJson2(res, 200, { ok: true });
3595
+ }
3596
+ if (method === "POST" && rest[2] === "priority") {
3597
+ const accountId = rest[1];
3598
+ const body2 = await readJsonBody3(req);
3599
+ const raw = body2["priority"];
3600
+ const priority = typeof raw === "number" ? raw : Number(raw);
3601
+ if (!Number.isFinite(priority)) {
3602
+ return writeJsonError(res, 400, "priority must be a finite number");
3603
+ }
3604
+ const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
3605
+ if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3606
+ return writeJson2(res, 200, { ok: true });
3607
+ }
3608
+ if (method === "POST" && rest[2] === "proxy") {
3609
+ const accountId = rest[1];
3610
+ const body2 = await readJsonBody3(req);
3611
+ const rawProxy = body2["proxy"];
3612
+ let proxy;
3613
+ if (rawProxy !== null && rawProxy !== void 0) {
3614
+ proxy = normalizeProxyConfig(rawProxy);
3615
+ if (!proxy) return writeJsonError(res, 400, "invalid proxy config");
3616
+ }
3617
+ const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
3618
+ if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3619
+ return writeJson2(res, 200, { ok: true });
3620
+ }
3621
+ if (method === "POST" && rest[2] === "supported-models") {
3622
+ const accountId = rest[1];
3623
+ const body2 = await readJsonBody3(req);
3624
+ const parsed = validateSupportedModelsBody(body2["supportedModels"]);
3625
+ if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
3626
+ const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
3627
+ if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3628
+ return writeJson2(res, 200, { ok: true });
2448
3629
  }
2449
3630
  if (method === "PUT" && rest[1] === "active") {
2450
- const body2 = await readJsonBody(req);
3631
+ const body2 = await readJsonBody3(req);
2451
3632
  const id = typeof body2["id"] === "string" ? body2["id"] : "";
2452
3633
  if (!id) return writeJsonError(res, 400, "active switch requires { id }");
2453
3634
  const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
2454
3635
  if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
2455
- return writeJson(res, 200, { ok: true });
3636
+ return writeJson2(res, 200, { ok: true });
2456
3637
  }
2457
3638
  if (method === "DELETE" && rest.length >= 2) {
2458
3639
  const accountId = rest[1];
2459
3640
  const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
2460
3641
  if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
2461
- return writeJson(res, 200, { ok: true });
3642
+ return writeJson2(res, 200, { ok: true });
2462
3643
  }
2463
3644
  if (method === "DELETE") {
2464
3645
  await deps.subscriptionTokenWriter.clearProvider(providerId);
2465
- return writeJson(res, 200, { ok: true });
3646
+ return writeJson2(res, 200, { ok: true });
2466
3647
  }
2467
- const body = await readJsonBody(req);
3648
+ const body = await readJsonBody3(req);
2468
3649
  const config = validateTokenBody(providerId, body);
2469
3650
  if (!config) {
2470
3651
  return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
2471
3652
  }
2472
3653
  await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
2473
3654
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
2474
- return writeJson(res, 200, status ? { account: status } : { ok: true });
3655
+ return writeJson2(res, 200, status ? { account: status } : { ok: true });
2475
3656
  }
2476
3657
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
2477
3658
  }
2478
3659
  async function handleCli(req, res, method, rest, deps) {
2479
3660
  if (method === "GET" && rest.length === 0) {
2480
3661
  const result = handleCliList(process.platform, deps.cliPathProbe);
2481
- return writeJson(res, result.status, result.body);
3662
+ return writeJson2(res, result.status, result.body);
2482
3663
  }
2483
3664
  if (method === "GET" && rest[0] === "sessions") {
2484
3665
  const result = handleCliSessions();
2485
- return writeJson(res, result.status, result.body);
3666
+ return writeJson2(res, result.status, result.body);
2486
3667
  }
2487
3668
  if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
2488
3669
  const result = handleCliStop(rest[1]);
2489
- return writeJson(res, result.status, result.body);
3670
+ return writeJson2(res, result.status, result.body);
2490
3671
  }
2491
3672
  if (method === "POST" && rest[1] === "install") {
2492
3673
  const cli = rest[0];
@@ -2494,14 +3675,14 @@ async function handleCli(req, res, method, rest, deps) {
2494
3675
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
2495
3676
  }
2496
3677
  const result = await handleCliInstall(cli, deps.cliCommandRunner);
2497
- return writeJson(res, result.status, result.body);
3678
+ return writeJson2(res, result.status, result.body);
2498
3679
  }
2499
3680
  if (method === "POST" && rest[1] === "launch") {
2500
3681
  const cli = rest[0];
2501
3682
  if (!isLaunchCliId(cli)) {
2502
3683
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
2503
3684
  }
2504
- const body = await readJsonBody(req);
3685
+ const body = await readJsonBody3(req);
2505
3686
  const providers = loadConfig(deps.configPath).providers ?? [];
2506
3687
  const result = await handleCliLaunch(cli, body, {
2507
3688
  llmConfig: deps.llmConfig,
@@ -2509,20 +3690,28 @@ async function handleCli(req, res, method, rest, deps) {
2509
3690
  opener: deps.cliTerminalOpener,
2510
3691
  probe: deps.cliPathProbe
2511
3692
  });
2512
- return writeJson(res, result.status, result.body);
3693
+ return writeJson2(res, result.status, result.body);
2513
3694
  }
2514
3695
  return writeJsonError(res, 405, `method ${method} not allowed on cli`);
2515
3696
  }
2516
3697
  async function handleStatus(res, method, deps) {
2517
3698
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
2518
3699
  const status = deps.outboundApiServer.getStatus();
2519
- const serverConfig = await loadServerConfig(deps.settingsStore);
2520
- const endpoints = serverConfig.endpoints.map((e) => ({
2521
- endpoint: e.endpoint,
2522
- model: e.defaultModel,
2523
- useSubscription: e.useSubscription
2524
- }));
2525
- return writeJson(res, 200, { ...status, endpoints });
3700
+ const serverConfig = await loadServerConfig2(deps.settingsStore);
3701
+ const endpoints = serverConfig.endpoints.map((e) => {
3702
+ if (isKindMappedEndpoint(e.endpoint)) {
3703
+ return { endpoint: e.endpoint, kinds: e.modelMap ?? {}, useSubscription: e.useSubscription };
3704
+ }
3705
+ if (e.endpoint === "chat") {
3706
+ return { endpoint: e.endpoint, models: e.models ?? [], useSubscription: e.useSubscription };
3707
+ }
3708
+ return { endpoint: e.endpoint, model: e.defaultModel ?? "", useSubscription: e.useSubscription };
3709
+ });
3710
+ if (status.running) {
3711
+ const queueStatus = deps.outboundApiServer.getQueueStatus();
3712
+ return writeJson2(res, 200, { ...status, endpoints, queueStatus });
3713
+ }
3714
+ return writeJson2(res, 200, { ...status, endpoints });
2526
3715
  }
2527
3716
  function resolvePlaygroundPath(endpoint, body) {
2528
3717
  switch (endpoint) {
@@ -2542,7 +3731,7 @@ function resolvePlaygroundPath(endpoint, body) {
2542
3731
  }
2543
3732
  async function handlePlayground(req, res, method, deps) {
2544
3733
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on playground`);
2545
- const body = await readJsonBody(req);
3734
+ const body = await readJsonBody3(req);
2546
3735
  const endpoint = typeof body["endpoint"] === "string" ? body["endpoint"] : "";
2547
3736
  const key = typeof body["key"] === "string" ? body["key"] : "";
2548
3737
  const payload = body["body"];
@@ -2687,10 +3876,12 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
2687
3876
  return true;
2688
3877
  }
2689
3878
 
3879
+ // src/admin/version.ts
3880
+ var DAEMON_VERSION = true ? "0.1.4" : "0.0.0-dev";
3881
+
2690
3882
  // src/admin/AdminServer.ts
2691
3883
  var LOOPBACK_ADDR = "127.0.0.1";
2692
3884
  var LAN_ADDR = "0.0.0.0";
2693
- var DAEMON_VERSION = true ? "0.1.2" : "0.0.0-dev";
2694
3885
  var AdminServer = class {
2695
3886
  constructor(deps) {
2696
3887
  this.deps = deps;
@@ -2711,7 +3902,7 @@ var AdminServer = class {
2711
3902
  const cfg = this.deps.getAdminConfig();
2712
3903
  if (!cfg.enabled) return 0;
2713
3904
  if (cfg.networkBinding && !cfg.token) {
2714
- console.error(
3905
+ this.deps.logger.error(
2715
3906
  "[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)."
2716
3907
  );
2717
3908
  return 0;
@@ -2720,7 +3911,7 @@ var AdminServer = class {
2720
3911
  const actualPort = await this.listen(bindAddr, cfg.port);
2721
3912
  this.boundAddr = bindAddr;
2722
3913
  this.boundPort = actualPort;
2723
- console.info(`[AdminServer] Dashboard listening on ${bindAddr}:${actualPort}`);
3914
+ this.deps.logger.info(`[AdminServer] Dashboard listening on ${bindAddr}:${actualPort}`);
2724
3915
  return actualPort;
2725
3916
  }
2726
3917
  /** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
@@ -2742,7 +3933,7 @@ var AdminServer = class {
2742
3933
  const addr = server.address();
2743
3934
  if (addr && typeof addr === "object") {
2744
3935
  server.removeListener("error", onError);
2745
- server.on("error", (e) => console.error("[AdminServer] server error", e));
3936
+ server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
2746
3937
  this.server = server;
2747
3938
  resolve(addr.port);
2748
3939
  } else {
@@ -2755,7 +3946,7 @@ var AdminServer = class {
2755
3946
  onRequest(req, res) {
2756
3947
  void this.dispatch(req, res).catch((err5) => {
2757
3948
  const message = err5 instanceof Error ? err5.message : String(err5);
2758
- console.error("[AdminServer] unhandled error:", message);
3949
+ this.deps.logger.error("[AdminServer] unhandled error:", message);
2759
3950
  if (!res.headersSent) {
2760
3951
  res.writeHead(500, { "Content-Type": "application/json" });
2761
3952
  res.end(JSON.stringify({ error: { type: "admin_error", message } }));
@@ -2766,18 +3957,42 @@ var AdminServer = class {
2766
3957
  const cfg = this.deps.getAdminConfig();
2767
3958
  res.setHeader("x-omnicross-daemon", DAEMON_VERSION);
2768
3959
  res.setHeader("x-omnicross-pid", String(process.pid));
3960
+ const url = req.url ?? "/";
3961
+ const path2 = url.split("?")[0];
3962
+ const healthPath = path2.replace(/\/+$/, "") || "/";
3963
+ if ((req.method === "GET" || req.method === "HEAD") && (healthPath === "/health" || healthPath === "/healthz")) {
3964
+ const report = this.deps.getHealthReport();
3965
+ const code = healthHttpStatus(report.status);
3966
+ res.writeHead(code, { "Content-Type": "application/json" });
3967
+ res.end(req.method === "HEAD" ? void 0 : JSON.stringify(report));
3968
+ return;
3969
+ }
2769
3970
  if (cfg.token && !this.isAuthorized(req, cfg.token)) {
2770
3971
  res.writeHead(401, { "Content-Type": "application/json" });
2771
3972
  res.end(JSON.stringify({ error: { type: "unauthorized", message: "admin token required" } }));
2772
3973
  return;
2773
3974
  }
2774
- const url = req.url ?? "/";
2775
- const path2 = url.split("?")[0];
2776
3975
  if ((req.method === "GET" || req.method === "HEAD") && (path2 === "/" || path2 === "/admin")) {
2777
3976
  res.writeHead(302, { Location: "/ui/" });
2778
3977
  res.end();
2779
3978
  return;
2780
3979
  }
3980
+ if (path2 === "/admin/api/account-probes" && (req.method === "GET" || req.method === "HEAD")) {
3981
+ handleAccountProbes(res, this.deps.probeHistoryReader);
3982
+ return;
3983
+ }
3984
+ if (path2 === "/admin/api/audit" && (req.method === "GET" || req.method === "HEAD")) {
3985
+ handleAuditQuery(req, res, this.deps.auditReader);
3986
+ return;
3987
+ }
3988
+ if (path2 === "/admin/api/billing-status" && (req.method === "GET" || req.method === "HEAD")) {
3989
+ handleBillingStatus(res, this.deps.billingStatusReader);
3990
+ return;
3991
+ }
3992
+ if (path2 === "/admin/api/webhook-test" && req.method === "POST") {
3993
+ await handleWebhookTest(req, res);
3994
+ return;
3995
+ }
2781
3996
  if (path2.startsWith("/admin/api/")) {
2782
3997
  await handleAdminApi(req, res, path2, this.deps);
2783
3998
  return;
@@ -2821,6 +4036,51 @@ function constantTimeEquals(a, b) {
2821
4036
  return timingSafeEqual(bufA, bufB);
2822
4037
  }
2823
4038
 
4039
+ // src/admin/health.ts
4040
+ var CRITICAL_CHECKS = ["config", "credentialStore"];
4041
+ var READINESS_CHECKS = ["outboundServer"];
4042
+ function safeBool(fn) {
4043
+ try {
4044
+ return fn() === true;
4045
+ } catch {
4046
+ return false;
4047
+ }
4048
+ }
4049
+ function toMb(bytes) {
4050
+ return Math.round(bytes / (1024 * 1024) * 10) / 10;
4051
+ }
4052
+ function buildHealthReport(deps) {
4053
+ const checks = {
4054
+ config: safeBool(deps.configPresent),
4055
+ credentialStore: safeBool(deps.credentialStoreReadable),
4056
+ outboundServer: safeBool(deps.outboundServerRunning),
4057
+ adminServer: safeBool(deps.adminServerRunning)
4058
+ };
4059
+ if (deps.subscriptionAccountsHealthy) {
4060
+ let probeHealthy;
4061
+ try {
4062
+ probeHealthy = deps.subscriptionAccountsHealthy();
4063
+ } catch {
4064
+ probeHealthy = false;
4065
+ }
4066
+ if (probeHealthy !== void 0) checks.subscriptionAccountsHealthy = probeHealthy;
4067
+ }
4068
+ const criticalOk = CRITICAL_CHECKS.every((k) => checks[k]);
4069
+ const readinessOk = READINESS_CHECKS.every((k) => checks[k]);
4070
+ const status = !criticalOk ? "error" : readinessOk ? "ok" : "degraded";
4071
+ const mem = (deps.memoryUsage ?? process.memoryUsage)();
4072
+ const uptime = (deps.uptimeSeconds ?? process.uptime)();
4073
+ const nowMs = (deps.now ?? Date.now)();
4074
+ return {
4075
+ status,
4076
+ version: deps.version,
4077
+ uptimeSeconds: Math.floor(uptime),
4078
+ timestamp: new Date(nowMs).toISOString(),
4079
+ memory: { rssMb: toMb(mem.rss), heapUsedMb: toMb(mem.heapUsed) },
4080
+ checks
4081
+ };
4082
+ }
4083
+
2824
4084
  // src/admin/oauthSessions.ts
2825
4085
  import crypto2 from "crypto";
2826
4086
  var DEFAULT_OAUTH_SESSION_TTL_MS = 10 * 60 * 1e3;
@@ -2872,7 +4132,7 @@ var DEFAULT_TIMEOUT_MS = 5 * 6e4;
2872
4132
  function pageHtml(message) {
2873
4133
  return `<!doctype html><meta charset="utf-8"><title>omnicross login</title><body style="font-family:sans-serif;padding:2rem"><h2>${message}</h2><p>You can close this window and return to the terminal.</p></body>`;
2874
4134
  }
2875
- function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS) {
4135
+ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
2876
4136
  return new Promise((resolve, reject) => {
2877
4137
  let settled = false;
2878
4138
  const finish = (server2, fn) => {
@@ -2906,6 +4166,12 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS) {
2906
4166
  res.end(pageHtml("Login complete."));
2907
4167
  finish(server, () => resolve(code));
2908
4168
  });
4169
+ const abort = () => finish(server, () => reject(new Error("login: cancelled")));
4170
+ if (signal?.aborted) {
4171
+ abort();
4172
+ return;
4173
+ }
4174
+ signal?.addEventListener("abort", abort, { once: true });
2909
4175
  server.on("error", (err5) => {
2910
4176
  if (settled) return;
2911
4177
  settled = true;
@@ -2994,12 +4260,21 @@ function createPoolKeysLoader(getProviderRow, autoDisabled) {
2994
4260
 
2995
4261
  // src/commands/paths.ts
2996
4262
  import { dirname as dirname2, join as join3 } from "path";
4263
+ function defaultVouchersPath(configPath) {
4264
+ return join3(dirname2(configPath), "vouchers.json");
4265
+ }
2997
4266
  function defaultPricingPath(configPath) {
2998
4267
  return join3(dirname2(configPath), "pricing.json");
2999
4268
  }
3000
4269
  function defaultUsageEventsPath(configPath) {
3001
4270
  return join3(dirname2(configPath), "usage-events.jsonl");
3002
4271
  }
4272
+ function defaultAuditDir(configPath) {
4273
+ return join3(dirname2(configPath), "audit");
4274
+ }
4275
+ function defaultBillingDir(configPath) {
4276
+ return join3(dirname2(configPath), "billing");
4277
+ }
3003
4278
 
3004
4279
  // src/ports/ConfigFileProviderConfigSource.ts
3005
4280
  import {
@@ -3161,46 +4436,199 @@ function toLLMProvider(row) {
3161
4436
  };
3162
4437
  }
3163
4438
 
3164
- // src/ports/ConsoleLogger.ts
3165
- var ConsoleLogger = class {
4439
+ // src/ports/ConfigurableLogger.ts
4440
+ import { createWriteStream } from "fs";
4441
+ var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
4442
+ var RESERVED_JSON_KEYS = /* @__PURE__ */ new Set(["ts", "level", "msg", "error"]);
4443
+ var ConfigurableLogger = class {
4444
+ threshold;
4445
+ format;
4446
+ filePath;
4447
+ fileStream = null;
4448
+ fileDisabled = false;
4449
+ constructor(cfg) {
4450
+ this.threshold = LEVEL_ORDER[cfg?.level ?? "debug"];
4451
+ this.format = cfg?.format ?? "text";
4452
+ this.filePath = cfg?.file && cfg.file.length > 0 ? cfg.file : void 0;
4453
+ }
3166
4454
  info(message, meta) {
3167
- if (meta === void 0) console.info(message);
3168
- else console.info(message, meta);
4455
+ this.emit("info", message, void 0, meta);
3169
4456
  }
3170
4457
  warn(message, meta) {
3171
- if (meta === void 0) console.warn(message);
3172
- else console.warn(message, meta);
4458
+ this.emit("warn", message, void 0, meta);
3173
4459
  }
3174
4460
  error(message, error, meta) {
3175
- if (error === void 0 && meta === void 0) console.error(message);
3176
- else if (meta === void 0) console.error(message, error);
3177
- else console.error(message, error, meta);
4461
+ this.emit("error", message, error, meta);
3178
4462
  }
3179
4463
  debug(message, meta) {
3180
- if (meta === void 0) console.debug(message);
3181
- else console.debug(message, meta);
4464
+ this.emit("debug", message, void 0, meta);
4465
+ }
4466
+ /**
4467
+ * Flush + close the file sink (tests / graceful shutdown). Resolves once the
4468
+ * append stream has finished flushing to disk. No-op when no file sink is open.
4469
+ */
4470
+ close() {
4471
+ const stream = this.fileStream;
4472
+ this.fileStream = null;
4473
+ if (!stream) return Promise.resolve();
4474
+ return new Promise((resolve) => stream.end(() => resolve()));
4475
+ }
4476
+ emit(level, message, error, meta) {
4477
+ if (LEVEL_ORDER[level] > this.threshold) return;
4478
+ this.writeConsole(level, message, error, meta);
4479
+ if (this.filePath) this.writeFile(level, message, error, meta);
4480
+ }
4481
+ /**
4482
+ * Console sink. In `text` format this reproduces the legacy `ConsoleLogger`
4483
+ * EXACTLY (same method + arg shape) so the unconfigured default is a byte-for-
4484
+ * byte drop-in; in `json` format it prints the structured line.
4485
+ */
4486
+ writeConsole(level, message, error, meta) {
4487
+ if (this.format === "json") {
4488
+ this.consoleFn(level)(this.jsonLine(level, message, error, meta));
4489
+ return;
4490
+ }
4491
+ if (level === "error") {
4492
+ if (error === void 0 && meta === void 0) console.error(message);
4493
+ else if (meta === void 0) console.error(message, error);
4494
+ else console.error(message, error, meta);
4495
+ return;
4496
+ }
4497
+ const fn = this.consoleFn(level);
4498
+ if (meta === void 0) fn(message);
4499
+ else fn(message, meta);
4500
+ }
4501
+ /** Append one line to the file sink; a failure disables the sink (swallowed). */
4502
+ writeFile(level, message, error, meta) {
4503
+ const stream = this.getFileStream();
4504
+ if (!stream) return;
4505
+ try {
4506
+ const line = this.format === "json" ? this.jsonLine(level, message, error, meta) : this.textLine(level, message, error, meta);
4507
+ stream.write(line + "\n");
4508
+ } catch {
4509
+ }
4510
+ }
4511
+ /** Lazily open the append-only file stream; disable the sink on any error. */
4512
+ getFileStream() {
4513
+ if (this.fileDisabled || !this.filePath) return null;
4514
+ if (this.fileStream) return this.fileStream;
4515
+ try {
4516
+ const stream = createWriteStream(this.filePath, { flags: "a" });
4517
+ stream.on("error", () => {
4518
+ this.fileDisabled = true;
4519
+ this.fileStream = null;
4520
+ });
4521
+ this.fileStream = stream;
4522
+ return stream;
4523
+ } catch {
4524
+ this.fileDisabled = true;
4525
+ return null;
4526
+ }
4527
+ }
4528
+ consoleFn(level) {
4529
+ switch (level) {
4530
+ case "error":
4531
+ return console.error;
4532
+ case "warn":
4533
+ return console.warn;
4534
+ case "info":
4535
+ return console.info;
4536
+ case "debug":
4537
+ return console.debug;
4538
+ }
4539
+ }
4540
+ /** `{ ts, level, msg, ...meta }` (+ `error` when present) as a single line. */
4541
+ jsonLine(level, message, error, meta) {
4542
+ const obj = {
4543
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4544
+ level,
4545
+ msg: message
4546
+ };
4547
+ if (error !== void 0) obj["error"] = reduceError(error);
4548
+ if (meta !== void 0) {
4549
+ if (meta instanceof Error) obj["meta"] = reduceError(meta);
4550
+ else if (meta && typeof meta === "object") {
4551
+ for (const [k, v] of Object.entries(meta)) {
4552
+ if (!RESERVED_JSON_KEYS.has(k)) obj[k] = v;
4553
+ }
4554
+ } else obj["meta"] = meta;
4555
+ }
4556
+ try {
4557
+ return JSON.stringify(obj);
4558
+ } catch {
4559
+ return JSON.stringify({ ts: obj["ts"], level, msg: message });
4560
+ }
4561
+ }
4562
+ /** Human-readable file line: `ISO [level] message {metaJson}`. */
4563
+ textLine(level, message, error, meta) {
4564
+ const parts = [`${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}`];
4565
+ if (error !== void 0) parts.push(safeStringify(reduceError(error)));
4566
+ if (meta !== void 0) parts.push(safeStringify(meta instanceof Error ? reduceError(meta) : meta));
4567
+ return parts.join(" ");
3182
4568
  }
3183
4569
  };
4570
+ function reduceError(error) {
4571
+ if (error instanceof Error) {
4572
+ return error.stack ? { message: error.message, stack: error.stack } : { message: error.message };
4573
+ }
4574
+ return { value: String(error) };
4575
+ }
4576
+ function safeStringify(value) {
4577
+ try {
4578
+ return typeof value === "string" ? value : JSON.stringify(value);
4579
+ } catch {
4580
+ return "[unserializable]";
4581
+ }
4582
+ }
3184
4583
 
3185
4584
  // src/ports/JsonApiServerSettingsStore.ts
3186
4585
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
3187
4586
  import { OUTBOUND_API_SERVER_CONFIG_KEY } from "@omnicross/core/outbound-api";
3188
4587
  var JsonApiServerSettingsStore = class {
3189
- constructor(configPath) {
4588
+ /**
4589
+ * @param configPath the daemon config.json whose `server` field is backed.
4590
+ * @param box OPTIONAL at-rest `SecretBox` (upstream-proxy). When set, the
4591
+ * `server.proxy.*` passwords are encrypted-on-`set` /
4592
+ * decrypted-on-`get` (the settings-store path is otherwise not
4593
+ * secret-aware — every OTHER server field is non-secret). Null
4594
+ * ⇒ passthrough (legacy/pure tests unchanged).
4595
+ */
4596
+ constructor(configPath, box = null) {
3190
4597
  this.configPath = configPath;
4598
+ this.box = box;
3191
4599
  }
3192
4600
  configPath;
4601
+ box;
3193
4602
  async get(key) {
3194
4603
  if (key !== OUTBOUND_API_SERVER_CONFIG_KEY) return void 0;
3195
4604
  const file = this.readFile();
3196
- return file.server ?? void 0;
4605
+ if (file.server === void 0) return void 0;
4606
+ return this.decryptSecrets(file.server);
3197
4607
  }
3198
4608
  async set(key, value) {
3199
4609
  if (key !== OUTBOUND_API_SERVER_CONFIG_KEY) return;
3200
4610
  const file = this.readFile();
3201
- file.server = value;
4611
+ file.server = this.encryptSecrets(value);
3202
4612
  writeFileSync3(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
3203
4613
  }
4614
+ /** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
4615
+ encryptSecrets(config) {
4616
+ if (!this.box) return config;
4617
+ let out = config;
4618
+ if (out?.proxy) out = { ...out, proxy: encryptProxySegment(out.proxy, this.box) };
4619
+ if (out?.webhook) out = { ...out, webhook: encryptWebhookSegment(out.webhook, this.box) };
4620
+ if (out?.billing) out = { ...out, billing: encryptBillingSegment(out.billing, this.box) };
4621
+ return out;
4622
+ }
4623
+ /** Decrypt the proxy passwords + webhook + billing secrets on read (no-op without a box). */
4624
+ decryptSecrets(config) {
4625
+ if (!this.box) return config;
4626
+ let out = config;
4627
+ if (out?.proxy) out = { ...out, proxy: decryptProxySegment(out.proxy, this.box) };
4628
+ if (out?.webhook) out = { ...out, webhook: decryptWebhookSegment(out.webhook, this.box) };
4629
+ if (out?.billing) out = { ...out, billing: decryptBillingSegment(out.billing, this.box) };
4630
+ return out;
4631
+ }
3204
4632
  /** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
3205
4633
  readFile() {
3206
4634
  try {
@@ -3319,6 +4747,57 @@ var JsonlUsageEventStore = class {
3319
4747
  }
3320
4748
  return Array.from(groups.values());
3321
4749
  }
4750
+ /**
4751
+ * ONE pass over a single key's events (`ts < endTs`) summing its `costUsd` into
4752
+ * `totalUsd` / `dailyUsd` (`ts >= dayStartTs`) / `weeklyUsd` (`ts >= weekStartTs`).
4753
+ * Used to lazily seed the outbound key-policy spend tracker (once per key). A
4754
+ * key with no attributed events yields all zeros.
4755
+ */
4756
+ async getSpendByKey(query) {
4757
+ let totalUsd = 0;
4758
+ let dailyUsd = 0;
4759
+ let weeklyUsd = 0;
4760
+ for (const row of this.readRows({ startTs: 0, endTs: query.endTs })) {
4761
+ if (row.apiKeyId !== query.apiKeyId) continue;
4762
+ totalUsd += row.costUsd;
4763
+ if (row.ts >= query.dayStartTs) dailyUsd += row.costUsd;
4764
+ if (row.ts >= query.weekStartTs) weeklyUsd += row.costUsd;
4765
+ }
4766
+ return { totalUsd, dailyUsd, weeklyUsd };
4767
+ }
4768
+ /**
4769
+ * Time-series aggregation over LOCAL-time bucket boundaries. Every bucket in
4770
+ * `[floor(startTs), endTs)` is present (empty ones zero-filled), ascending by
4771
+ * `bucketStartTs`; an empty range (`startTs >= endTs`) returns `[]`. Reuses
4772
+ * `readRows` so malformed lines are skipped and only in-range rows contribute.
4773
+ */
4774
+ async getTimeSeries(range, bucket) {
4775
+ if (range.startTs >= range.endTs) return [];
4776
+ const buckets = /* @__PURE__ */ new Map();
4777
+ for (let b = floorToBucket(range.startTs, bucket); b < range.endTs; b = nextBoundary(b, bucket)) {
4778
+ buckets.set(b, {
4779
+ bucketStartTs: b,
4780
+ label: bucketLabel(b, bucket),
4781
+ requests: 0,
4782
+ inputTokens: 0,
4783
+ outputTokens: 0,
4784
+ cacheReadTokens: 0,
4785
+ cacheCreationTokens: 0,
4786
+ costUsd: 0
4787
+ });
4788
+ }
4789
+ for (const row of this.readRows(range)) {
4790
+ const g = buckets.get(floorToBucket(row.ts, bucket));
4791
+ if (!g) continue;
4792
+ g.requests += 1;
4793
+ g.inputTokens += row.inputTokens;
4794
+ g.outputTokens += row.outputTokens;
4795
+ g.cacheReadTokens += row.cacheReadTokens;
4796
+ g.cacheCreationTokens += row.cacheCreationTokens;
4797
+ g.costUsd += row.costUsd;
4798
+ }
4799
+ return Array.from(buckets.values());
4800
+ }
3322
4801
  async getMessagesForSession(sessionId) {
3323
4802
  return this.readAllRows().filter((r) => r.sessionId === sessionId).sort((a, b) => a.ts - b.ts).map((r) => ({
3324
4803
  id: r.id,
@@ -3387,6 +4866,43 @@ var JsonlUsageEventStore = class {
3387
4866
  return rows;
3388
4867
  }
3389
4868
  };
4869
+ function floorToBucket(ts, bucket) {
4870
+ const d = new Date(ts);
4871
+ switch (bucket) {
4872
+ case "hour":
4873
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime();
4874
+ case "day":
4875
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
4876
+ case "month":
4877
+ return new Date(d.getFullYear(), d.getMonth(), 1).getTime();
4878
+ }
4879
+ }
4880
+ function nextBoundary(ts, bucket) {
4881
+ const d = new Date(ts);
4882
+ switch (bucket) {
4883
+ case "hour":
4884
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours() + 1).getTime();
4885
+ case "day":
4886
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1).getTime();
4887
+ case "month":
4888
+ return new Date(d.getFullYear(), d.getMonth() + 1, 1).getTime();
4889
+ }
4890
+ }
4891
+ var pad2 = (n) => String(n).padStart(2, "0");
4892
+ function bucketLabel(bucketStartTs, bucket) {
4893
+ const d = new Date(bucketStartTs);
4894
+ const y = d.getFullYear();
4895
+ const mo = pad2(d.getMonth() + 1);
4896
+ const day = pad2(d.getDate());
4897
+ switch (bucket) {
4898
+ case "hour":
4899
+ return `${mo}-${day} ${pad2(d.getHours())}:00`;
4900
+ case "day":
4901
+ return `${y}-${mo}-${day}`;
4902
+ case "month":
4903
+ return `${y}-${mo}`;
4904
+ }
4905
+ }
3390
4906
  var NUMERIC_FIELDS = [
3391
4907
  "ts",
3392
4908
  "inputTokens",
@@ -3470,6 +4986,45 @@ var JsonOutboundKeyDb = class {
3470
4986
  return true;
3471
4987
  });
3472
4988
  }
4989
+ async outboundApiKeysSetMaxConcurrency(id, maxConcurrency) {
4990
+ return this.mutateRow(id, (row) => {
4991
+ if (row.revokedAt !== null) return false;
4992
+ if (maxConcurrency === null) delete row.maxConcurrency;
4993
+ else row.maxConcurrency = maxConcurrency;
4994
+ return true;
4995
+ });
4996
+ }
4997
+ async outboundApiKeysSetPolicy(id, policy) {
4998
+ return this.mutateRow(id, (row) => {
4999
+ if (row.revokedAt !== null) return false;
5000
+ applyPolicyField(row, "expiresAt", policy.expiresAt);
5001
+ applyPolicyField(row, "activationDays", policy.activationDays);
5002
+ applyPolicyField(row, "dailyCostLimitUsd", policy.dailyCostLimitUsd);
5003
+ applyPolicyField(row, "totalCostLimitUsd", policy.totalCostLimitUsd);
5004
+ applyPolicyField(row, "weeklyCostLimitUsd", policy.weeklyCostLimitUsd);
5005
+ applyPolicyField(row, "rateLimitMaxRequests", policy.rateLimitMaxRequests);
5006
+ applyPolicyField(row, "rateLimitWindowMs", policy.rateLimitWindowMs);
5007
+ if (policy.activationMode === null) delete row.activationMode;
5008
+ else if (policy.activationMode !== void 0) row.activationMode = policy.activationMode;
5009
+ if (policy.enableModelRestriction === null) delete row.enableModelRestriction;
5010
+ else if (policy.enableModelRestriction !== void 0) {
5011
+ row.enableModelRestriction = policy.enableModelRestriction;
5012
+ }
5013
+ if (policy.restrictionMode === null) delete row.restrictionMode;
5014
+ else if (policy.restrictionMode !== void 0) row.restrictionMode = policy.restrictionMode;
5015
+ if (policy.restrictedModels === null) delete row.restrictedModels;
5016
+ else if (policy.restrictedModels !== void 0) row.restrictedModels = policy.restrictedModels;
5017
+ return true;
5018
+ });
5019
+ }
5020
+ async outboundApiKeysMarkActivated(id, activatedAt) {
5021
+ return this.mutateRow(id, (row) => {
5022
+ if (row.revokedAt !== null) return false;
5023
+ if (row.activatedAt != null) return false;
5024
+ row.activatedAt = activatedAt;
5025
+ return true;
5026
+ });
5027
+ }
3473
5028
  /** Apply `fn` to the row with `id`, persisting when it returns true. */
3474
5029
  mutateRow(id, fn) {
3475
5030
  const rows = this.readRows();
@@ -3493,6 +5048,11 @@ var JsonOutboundKeyDb = class {
3493
5048
  writeFileSync4(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
3494
5049
  }
3495
5050
  };
5051
+ function applyPolicyField(row, field, value) {
5052
+ if (value === void 0) return;
5053
+ if (value === null) delete row[field];
5054
+ else row[field] = value;
5055
+ }
3496
5056
 
3497
5057
  // src/ports/JsonPricingStore.ts
3498
5058
  import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
@@ -3619,14 +5179,109 @@ var JsonPricingStore = class {
3619
5179
  }
3620
5180
  };
3621
5181
 
3622
- // src/ports/JsonSubscriptionCredentialStore.ts
3623
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
3624
- import { dirname as dirname4 } from "path";
3625
- import {
3626
- claudeOAuth as claudeOAuth2,
3627
- codexOAuth as codexOAuth2,
3628
- geminiOAuth as geminiOAuth2
3629
- } from "@omnicross/subscriptions";
5182
+ // src/ports/JsonVoucherDb.ts
5183
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
5184
+ var JsonVoucherDb = class {
5185
+ constructor(vouchersPath) {
5186
+ this.vouchersPath = vouchersPath;
5187
+ }
5188
+ vouchersPath;
5189
+ async voucherCreate(input) {
5190
+ const rows = this.readRows();
5191
+ const row = {
5192
+ id: input.id,
5193
+ codeHash: input.codeHash,
5194
+ codePrefix: input.codePrefix,
5195
+ type: input.type,
5196
+ status: "unredeemed",
5197
+ createdAt: input.createdAt ?? Date.now()
5198
+ };
5199
+ if (input.creditUsd != null) row.creditUsd = input.creditUsd;
5200
+ if (input.renewalDays != null) row.renewalDays = input.renewalDays;
5201
+ if (input.maxTotalCostLimitUsd != null) row.maxTotalCostLimitUsd = input.maxTotalCostLimitUsd;
5202
+ if (input.maxExpiryDays != null) row.maxExpiryDays = input.maxExpiryDays;
5203
+ rows.push(row);
5204
+ this.writeRows(rows);
5205
+ return row;
5206
+ }
5207
+ async voucherGetByHash(codeHash) {
5208
+ const rows = this.readRows();
5209
+ return rows.find((r) => r.codeHash === codeHash) ?? null;
5210
+ }
5211
+ async voucherRedeemCas(id, keyId, granted, now) {
5212
+ const rows = this.readRows();
5213
+ const row = rows.find((r) => r.id === id);
5214
+ if (!row || row.status !== "unredeemed") return false;
5215
+ row.status = "redeemed";
5216
+ row.redeemedAt = now;
5217
+ row.redeemedByKeyId = keyId;
5218
+ row.grantApplied = false;
5219
+ if (granted.totalCostLimitUsd != null) row.grantedTotalCostLimitUsd = granted.totalCostLimitUsd;
5220
+ if (granted.expiresAt != null) row.grantedExpiresAt = granted.expiresAt;
5221
+ this.writeRows(rows);
5222
+ return true;
5223
+ }
5224
+ async voucherMarkGrantApplied(id) {
5225
+ const rows = this.readRows();
5226
+ const row = rows.find((r) => r.id === id);
5227
+ if (!row || row.status !== "redeemed") return false;
5228
+ if (row.grantApplied === true) return true;
5229
+ row.grantApplied = true;
5230
+ this.writeRows(rows);
5231
+ return true;
5232
+ }
5233
+ async voucherRevertRedeem(id, keyId) {
5234
+ const rows = this.readRows();
5235
+ const row = rows.find((r) => r.id === id);
5236
+ if (!row || row.status !== "redeemed" || row.grantApplied === true) return false;
5237
+ if (row.redeemedByKeyId !== keyId) return false;
5238
+ row.status = "unredeemed";
5239
+ delete row.redeemedAt;
5240
+ delete row.redeemedByKeyId;
5241
+ delete row.grantApplied;
5242
+ delete row.grantedTotalCostLimitUsd;
5243
+ delete row.grantedExpiresAt;
5244
+ this.writeRows(rows);
5245
+ return true;
5246
+ }
5247
+ async voucherRevokeCas(id, now) {
5248
+ const rows = this.readRows();
5249
+ const row = rows.find((r) => r.id === id);
5250
+ if (!row || row.status !== "unredeemed") return false;
5251
+ row.status = "revoked";
5252
+ row.revokedAt = now;
5253
+ this.writeRows(rows);
5254
+ return true;
5255
+ }
5256
+ async voucherList() {
5257
+ return this.readRows();
5258
+ }
5259
+ /** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
5260
+ readRows() {
5261
+ if (!existsSync7(this.vouchersPath)) return [];
5262
+ try {
5263
+ const parsed = JSON.parse(readFileSync7(this.vouchersPath, "utf8"));
5264
+ return Array.isArray(parsed) ? parsed : [];
5265
+ } catch {
5266
+ return [];
5267
+ }
5268
+ }
5269
+ writeRows(rows) {
5270
+ writeFileSync6(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
5271
+ }
5272
+ };
5273
+
5274
+ // src/ports/JsonSubscriptionCredentialStore.ts
5275
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
5276
+ import { dirname as dirname4 } from "path";
5277
+ import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
5278
+ import { fetchUpstream as fetchUpstream2 } from "@omnicross/core/pipeline/upstreamFetch";
5279
+ import { getSharedIdentityStore } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
5280
+ import {
5281
+ claudeOAuth as claudeOAuth2,
5282
+ codexOAuth as codexOAuth2,
5283
+ geminiOAuth as geminiOAuth2
5284
+ } from "@omnicross/subscriptions";
3630
5285
 
3631
5286
  // src/ports/account-sync.ts
3632
5287
  var IMPORT_EXPIRY_MARGIN_MS = 6e4;
@@ -3702,7 +5357,7 @@ function findDuplicateCredentialIds(accounts) {
3702
5357
  }
3703
5358
 
3704
5359
  // src/ports/external-cli-credentials.ts
3705
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
5360
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
3706
5361
  import { homedir as homedir2 } from "os";
3707
5362
  import { join as join4 } from "path";
3708
5363
  function externalStorePath(provider, home = homedir2()) {
@@ -3755,10 +5410,10 @@ function parseCodexTokensEnvelope(raw) {
3755
5410
  }
3756
5411
  function readExternalCliCredentials(provider, home = homedir2()) {
3757
5412
  const path2 = externalStorePath(provider, home);
3758
- if (!existsSync7(path2)) return null;
5413
+ if (!existsSync8(path2)) return null;
3759
5414
  let raw;
3760
5415
  try {
3761
- const parsed = JSON.parse(readFileSync7(path2, "utf8"));
5416
+ const parsed = JSON.parse(readFileSync8(path2, "utf8"));
3762
5417
  raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
3763
5418
  } catch {
3764
5419
  return null;
@@ -3767,7 +5422,7 @@ function readExternalCliCredentials(provider, home = homedir2()) {
3767
5422
  }
3768
5423
 
3769
5424
  // src/ports/external-cli-store.ts
3770
- import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync, writeFileSync as writeFileSync6 } from "fs";
5425
+ import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync7 } from "fs";
3771
5426
  import { homedir as homedir3 } from "os";
3772
5427
  import { dirname as dirname3 } from "path";
3773
5428
  function markerPath(provider, home) {
@@ -3795,9 +5450,9 @@ function buildCodexTokensEnvelope(tokens) {
3795
5450
  return envelope;
3796
5451
  }
3797
5452
  function readExistingObject(path2) {
3798
- if (!existsSync8(path2)) return {};
5453
+ if (!existsSync9(path2)) return {};
3799
5454
  try {
3800
- const parsed = JSON.parse(readFileSync8(path2, "utf8"));
5455
+ const parsed = JSON.parse(readFileSync9(path2, "utf8"));
3801
5456
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3802
5457
  } catch {
3803
5458
  return {};
@@ -3806,16 +5461,16 @@ function readExistingObject(path2) {
3806
5461
  function writeAtomic(path2, content) {
3807
5462
  mkdirSync2(dirname3(path2), { recursive: true });
3808
5463
  const temp = `${path2}.omnicross-tmp`;
3809
- writeFileSync6(temp, content, "utf8");
5464
+ writeFileSync7(temp, content, "utf8");
3810
5465
  renameSync(temp, path2);
3811
5466
  }
3812
5467
  function createExternalCliStore(home = homedir3()) {
3813
5468
  return {
3814
5469
  readMarkerAccountId(provider) {
3815
5470
  const path2 = markerPath(provider, home);
3816
- if (!existsSync8(path2)) return void 0;
5471
+ if (!existsSync9(path2)) return void 0;
3817
5472
  try {
3818
- const parsed = JSON.parse(readFileSync8(path2, "utf8"));
5473
+ const parsed = JSON.parse(readFileSync9(path2, "utf8"));
3819
5474
  return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
3820
5475
  } catch {
3821
5476
  return void 0;
@@ -3833,7 +5488,7 @@ function createExternalCliStore(home = homedir3()) {
3833
5488
  const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
3834
5489
  if (!envelope) return false;
3835
5490
  const storePath = externalStorePath(provider, home);
3836
- if (existsSync8(storePath) && !existsSync8(backupPath(provider, home))) {
5491
+ if (existsSync9(storePath) && !existsSync9(backupPath(provider, home))) {
3837
5492
  copyFileSync(storePath, backupPath(provider, home));
3838
5493
  }
3839
5494
  const existing = readExistingObject(storePath);
@@ -3845,16 +5500,21 @@ function createExternalCliStore(home = homedir3()) {
3845
5500
  }
3846
5501
 
3847
5502
  // src/ports/JsonSubscriptionCredentialStore.ts
5503
+ var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
3848
5504
  var JsonSubscriptionCredentialStore = class {
3849
5505
  /**
3850
5506
  * @param tokensPath on-disk `tokens.json` location.
3851
5507
  * @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
3852
- * @param fetchImpl injectable HTTP port for the OAuth refresh round-trips
3853
- * (oauth design D4). Defaults to the global `fetch` so boot
3854
- * is unchanged; tests inject a mock fetch. NOT used by any
3855
- * read/write path only by `refresh*Token`.
5508
+ * @param fetchImpl OPTIONAL injectable HTTP port for the OAuth refresh
5509
+ * round-trips (oauth design D4). A TEST-injected transport is
5510
+ * used verbatim. When ABSENT (production), each refresh uses a
5511
+ * proxy-aware {@link fetchUpstream} that threads the
5512
+ * `{ providerId, accountId }` ctx (upstream-proxy M1) so a
5513
+ * per-account/per-provider proxy is honored on refresh exactly
5514
+ * as on relay — refresh egresses from the SAME proxy IP as the
5515
+ * account's traffic. NOT used by any read/write path.
3856
5516
  */
3857
- constructor(tokensPath, box, fetchImpl = (url, init) => fetch(url, init), externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
5517
+ constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
3858
5518
  this.tokensPath = tokensPath;
3859
5519
  this.box = box;
3860
5520
  this.fetchImpl = fetchImpl;
@@ -3866,6 +5526,15 @@ var JsonSubscriptionCredentialStore = class {
3866
5526
  fetchImpl;
3867
5527
  externalCliReader;
3868
5528
  externalCliStore;
5529
+ /**
5530
+ * The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
5531
+ * TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
5532
+ * through {@link fetchUpstream} with the account's `{ providerId, accountId }`
5533
+ * ctx so the per-account/provider proxy applies. `@internal` — also a test seam.
5534
+ */
5535
+ buildRefreshFetch(providerId, accountId) {
5536
+ return this.fetchImpl ?? ((url, init) => fetchUpstream2(url, init, { providerId, accountId }));
5537
+ }
3869
5538
  /**
3870
5539
  * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
3871
5540
  * SINGLE-USE: two concurrent refreshes of one account each spend the same
@@ -3897,6 +5566,19 @@ var JsonSubscriptionCredentialStore = class {
3897
5566
  async getValidOpenCodeGoApiKey() {
3898
5567
  return this.readConfig().opencodego?.apiKey ?? null;
3899
5568
  }
5569
+ /**
5570
+ * DAEMON-ONLY per-account proxy lookup by id (upstream-proxy). Returns the
5571
+ * DECRYPTED `ProxyConfig` for the account (`readConfig` decrypts on read), or
5572
+ * `undefined` for an unknown provider/account or no per-account proxy. Feeds the
5573
+ * winning per-account layer of the upstream-proxy resolver. Synchronous like the
5574
+ * other hot reads. Never returns token material.
5575
+ */
5576
+ getAccountProxy(providerId, accountId) {
5577
+ if (providerId !== "claude" && providerId !== "codex" && providerId !== "gemini" && providerId !== "opencodego") {
5578
+ return void 0;
5579
+ }
5580
+ return getAccountProxy(this.readConfig(), providerId, accountId);
5581
+ }
3900
5582
  /**
3901
5583
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
3902
5584
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
@@ -3905,10 +5587,25 @@ var JsonSubscriptionCredentialStore = class {
3905
5587
  */
3906
5588
  async listSanitizedAccounts() {
3907
5589
  const config = this.readConfig();
5590
+ const health2 = getSharedAccountHealth();
5591
+ const identityStore = getSharedIdentityStore();
5592
+ const fingerprintOn = identityStore.isEnabled();
5593
+ const now = Date.now();
3908
5594
  const out = {};
3909
5595
  for (const provider of ["claude", "codex", "gemini", "opencodego"]) {
3910
5596
  const sanitized = sanitizeAccounts(config, provider);
3911
- if (sanitized.length > 0) out[provider] = this.attachSyncWarnings(config, provider, sanitized);
5597
+ if (sanitized.length === 0) continue;
5598
+ for (const account of sanitized) {
5599
+ const status = health2.getStatus(provider, account.id, now);
5600
+ account.health = status.state;
5601
+ account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
5602
+ if (fingerprintOn && provider === "claude") {
5603
+ account.identityCaptured = identityStore.hasIdentity(provider, account.id);
5604
+ const capturedAt = identityStore.capturedAt(provider, account.id);
5605
+ account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
5606
+ }
5607
+ }
5608
+ out[provider] = this.attachSyncWarnings(config, provider, sanitized);
3912
5609
  }
3913
5610
  return out;
3914
5611
  }
@@ -3959,8 +5656,9 @@ var JsonSubscriptionCredentialStore = class {
3959
5656
  if (!active || !claude?.refreshToken) return false;
3960
5657
  const capturedId = active.id;
3961
5658
  this.materializeMigration(config);
5659
+ const refreshFetch = this.buildRefreshFetch("claude", capturedId);
3962
5660
  try {
3963
- const result = await claudeOAuth2.refreshAccessToken(claude.refreshToken, this.fetchImpl);
5661
+ const result = await claudeOAuth2.refreshAccessToken(claude.refreshToken, refreshFetch);
3964
5662
  const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
3965
5663
  const next = {
3966
5664
  ...claude,
@@ -3977,7 +5675,7 @@ var JsonSubscriptionCredentialStore = class {
3977
5675
  return true;
3978
5676
  } catch (error) {
3979
5677
  if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
3980
- const r = await claudeOAuth2.refreshAccessToken(rt, this.fetchImpl);
5678
+ const r = await claudeOAuth2.refreshAccessToken(rt, refreshFetch);
3981
5679
  return {
3982
5680
  accessToken: r.accessToken,
3983
5681
  refreshToken: r.refreshToken,
@@ -4004,8 +5702,9 @@ var JsonSubscriptionCredentialStore = class {
4004
5702
  if (!active || !codex?.refreshToken) return false;
4005
5703
  const capturedId = active.id;
4006
5704
  this.materializeMigration(config);
5705
+ const refreshFetch = this.buildRefreshFetch("codex", capturedId);
4007
5706
  try {
4008
- const result = await codexOAuth2.refreshAccessToken(codex.refreshToken, this.fetchImpl);
5707
+ const result = await codexOAuth2.refreshAccessToken(codex.refreshToken, refreshFetch);
4009
5708
  const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
4010
5709
  const next = {
4011
5710
  ...codex,
@@ -4023,7 +5722,7 @@ var JsonSubscriptionCredentialStore = class {
4023
5722
  return true;
4024
5723
  } catch (error) {
4025
5724
  if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
4026
- const r = await codexOAuth2.refreshAccessToken(rt, this.fetchImpl);
5725
+ const r = await codexOAuth2.refreshAccessToken(rt, refreshFetch);
4027
5726
  return {
4028
5727
  accessToken: r.accessToken,
4029
5728
  refreshToken: r.refreshToken,
@@ -4053,8 +5752,9 @@ var JsonSubscriptionCredentialStore = class {
4053
5752
  if (!active || !gemini?.refreshToken) return false;
4054
5753
  const capturedId = active.id;
4055
5754
  this.materializeMigration(config);
5755
+ const refreshFetch = this.buildRefreshFetch("gemini", capturedId);
4056
5756
  try {
4057
- const result = await geminiOAuth2.refreshAccessToken(gemini.refreshToken, this.fetchImpl);
5757
+ const result = await geminiOAuth2.refreshAccessToken(gemini.refreshToken, refreshFetch);
4058
5758
  const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
4059
5759
  const next = {
4060
5760
  ...gemini,
@@ -4088,7 +5788,7 @@ var JsonSubscriptionCredentialStore = class {
4088
5788
  if (!account || !captured?.refreshToken) return false;
4089
5789
  this.materializeMigration(config);
4090
5790
  try {
4091
- const refreshed = await this.refreshUpstream(provider, captured.refreshToken);
5791
+ const refreshed = await this.refreshUpstream(provider, captured.refreshToken, id);
4092
5792
  const next = {
4093
5793
  ...captured,
4094
5794
  accessToken: refreshed.accessToken,
@@ -4110,10 +5810,114 @@ var JsonSubscriptionCredentialStore = class {
4110
5810
  }
4111
5811
  });
4112
5812
  }
5813
+ // ── By-id account-pool surface (subscription-account-scheduling, design D6) ──
5814
+ /**
5815
+ * Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
5816
+ * provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
5817
+ * (refresh is 401-driven, like `getValidClaudeAccessToken`); codex/gemini refresh
5818
+ * a near-expiry token via `refreshAccountById` (like `resolveAccessToken`);
5819
+ * opencodego returns the account's static key. `null` when unknown/expired/
5820
+ * tokenless.
5821
+ */
5822
+ async getAccessTokenForAccount(providerId, accountId) {
5823
+ const account = getAccountById(this.readConfig(), providerId, accountId);
5824
+ if (!account) return null;
5825
+ if (providerId === "opencodego") {
5826
+ return account.tokens.apiKey ?? null;
5827
+ }
5828
+ const oauth = account.tokens;
5829
+ if (!oauth.accessToken) return null;
5830
+ if (providerId === "codex" || providerId === "gemini") {
5831
+ const expiresAtMs = oauth.expiresAt ? Date.parse(oauth.expiresAt) : 0;
5832
+ const expiringSoon = expiresAtMs > 0 && Date.now() >= expiresAtMs - ACCOUNT_REFRESH_LEAD_MS;
5833
+ if (expiringSoon && oauth.refreshToken) {
5834
+ const ok = await this.refreshAccountById(providerId, accountId);
5835
+ if (!ok) return null;
5836
+ const fresh = getAccountById(this.readConfig(), providerId, accountId);
5837
+ return fresh?.tokens?.accessToken ?? null;
5838
+ }
5839
+ }
5840
+ if (oauth.status === "expired") return null;
5841
+ return oauth.accessToken;
5842
+ }
5843
+ /**
5844
+ * Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
5845
+ * `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
5846
+ * → `false` (no refresh affordance).
5847
+ */
5848
+ async refreshAccountToken(providerId, accountId) {
5849
+ if (providerId === "opencodego") return false;
5850
+ return this.refreshAccountById(providerId, accountId);
5851
+ }
5852
+ /**
5853
+ * Best-effort record of a selection time onto the account's `lastUsedAt` by id
5854
+ * (design D4). Entry-metadata only (the token mirror is untouched); a no-op for
5855
+ * an unknown id. The selector throttles the call frequency, so this stays cheap.
5856
+ */
5857
+ async touchAccountLastUsed(providerId, accountId, iso) {
5858
+ const config = this.readConfig();
5859
+ const result = setAccountLastUsed(config, providerId, accountId, iso);
5860
+ if (!result.ok) return;
5861
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
5862
+ }
5863
+ /**
5864
+ * Best-effort write-through of a per-account client `identity`
5865
+ * (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
5866
+ * whitelisted fingerprint headers; the token mirror is untouched); a no-op for
5867
+ * an unknown id. Called by the identity store's persistence port on a first-seen
5868
+ * freeze / TTL refresh, so it stays infrequent. Never throws to the caller — the
5869
+ * store's port wrapper swallows a rejection so the relay hot path is unaffected.
5870
+ */
5871
+ async setAccountIdentity(providerId, accountId, identity) {
5872
+ const config = this.readConfig();
5873
+ const result = setAccountIdentity(config, providerId, accountId, identity);
5874
+ if (!result.ok) return;
5875
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
5876
+ }
5877
+ /**
5878
+ * DAEMON-ONLY set-priority (subscription-account-scheduling, admin write, NOT on
5879
+ * the port). Set one account's scheduling `priority` by id. Secret-free
5880
+ * (entry-metadata only; the mirror invariant is untouched). Rejects an unknown id.
5881
+ */
5882
+ async setAccountPriority(providerId, accountId, priority) {
5883
+ const config = this.readConfig();
5884
+ const result = setAccountPriority(config, providerId, accountId, priority);
5885
+ if (!result.ok) return result;
5886
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
5887
+ return result;
5888
+ }
5889
+ /**
5890
+ * DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
5891
+ * the port). Passing `undefined` clears the override. Write-only password: when
5892
+ * the incoming structured proxy omits the password but the account already had
5893
+ * one, the current (decrypted) password is preserved — editing host/port never
5894
+ * wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
5895
+ */
5896
+ async setAccountProxy(providerId, accountId, proxy) {
5897
+ const config = this.readConfig();
5898
+ const merged = proxy ? preserveProxyConfigSecret(proxy, getAccountProxy(config, providerId, accountId)) : void 0;
5899
+ const result = setAccountProxy(config, providerId, accountId, merged);
5900
+ if (!result.ok) return result;
5901
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
5902
+ return result;
5903
+ }
5904
+ /**
5905
+ * DAEMON-ONLY set/clear per-account `supportedModels` (subscription-account-
5906
+ * model-map, admin write, NOT on the port). Passing `undefined` clears it.
5907
+ * Secret-free (model ids only; the mirror invariant is untouched). Rejects an
5908
+ * unknown id.
5909
+ */
5910
+ async setAccountSupportedModels(providerId, accountId, supportedModels) {
5911
+ const config = this.readConfig();
5912
+ const result = setAccountSupportedModels(config, providerId, accountId, supportedModels);
5913
+ if (!result.ok) return result;
5914
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
5915
+ return result;
5916
+ }
4113
5917
  /** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
4114
- async refreshUpstream(provider, refreshToken) {
5918
+ async refreshUpstream(provider, refreshToken, accountId) {
4115
5919
  const flow = provider === "claude" ? claudeOAuth2 : provider === "codex" ? codexOAuth2 : geminiOAuth2;
4116
- const r = await flow.refreshAccessToken(refreshToken, this.fetchImpl);
5920
+ const r = await flow.refreshAccessToken(refreshToken, this.buildRefreshFetch(provider, accountId));
4117
5921
  return {
4118
5922
  accessToken: r.accessToken,
4119
5923
  refreshToken: r.refreshToken,
@@ -4328,7 +6132,7 @@ var JsonSubscriptionCredentialStore = class {
4328
6132
  persist(config) {
4329
6133
  mkdirSync3(dirname4(this.tokensPath), { recursive: true });
4330
6134
  const encrypted = encryptTokens(config, this.box);
4331
- writeFileSync7(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
6135
+ writeFileSync8(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
4332
6136
  }
4333
6137
  /**
4334
6138
  * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
@@ -4344,10 +6148,10 @@ var JsonSubscriptionCredentialStore = class {
4344
6148
  * `config.ts loadConfig`, which decrypts outside its parse try.
4345
6149
  */
4346
6150
  readConfig() {
4347
- if (!existsSync9(this.tokensPath)) return { updatedAt: "" };
6151
+ if (!existsSync10(this.tokensPath)) return { updatedAt: "" };
4348
6152
  let parsed;
4349
6153
  try {
4350
- const raw = JSON.parse(readFileSync9(this.tokensPath, "utf8"));
6154
+ const raw = JSON.parse(readFileSync10(this.tokensPath, "utf8"));
4351
6155
  parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
4352
6156
  } catch {
4353
6157
  parsed = null;
@@ -4358,18 +6162,254 @@ var JsonSubscriptionCredentialStore = class {
4358
6162
  }
4359
6163
  };
4360
6164
 
4361
- // src/TokenRefreshScheduler.ts
6165
+ // src/AccountHealthProbeScheduler.ts
6166
+ import { fetchUpstream as fetchUpstream3 } from "@omnicross/core/pipeline/upstreamFetch";
6167
+
6168
+ // src/probe/ProbeStrategy.ts
6169
+ var PROVIDER_PROBE_PLANS = {
6170
+ claude: {
6171
+ kind: "upstream",
6172
+ // VERIFIED free authed list endpoint (no tokens billed). The anthropic OAuth
6173
+ // bearer is accepted here exactly as on the relay path.
6174
+ url: "https://api.anthropic.com/v1/models",
6175
+ buildInit: (token) => ({
6176
+ method: "GET",
6177
+ headers: {
6178
+ Authorization: `Bearer ${token}`,
6179
+ "anthropic-version": "2023-06-01"
6180
+ }
6181
+ })
6182
+ },
6183
+ // UNVERIFIED cheap authed GET — Phase 1 local-only (LEAD OQ1: do not guess a
6184
+ // billable/wrong endpoint). Upgrade to `{ kind:'upstream' }` once verified.
6185
+ codex: { kind: "local" },
6186
+ gemini: { kind: "local" },
6187
+ opencodego: { kind: "local" }
6188
+ };
6189
+ function probePlanFor(providerId) {
6190
+ return PROVIDER_PROBE_PLANS[providerId] ?? { kind: "local" };
6191
+ }
6192
+
6193
+ // src/AccountHealthProbeScheduler.ts
6194
+ var KEY_SEP = "\0";
6195
+ var MAX_BODY_SNIFF = 2048;
6196
+ var PROBE_PROVIDERS = [
6197
+ "claude",
6198
+ "codex",
6199
+ "gemini",
6200
+ "opencodego"
6201
+ ];
6202
+ var AccountHealthProbeScheduler = class {
6203
+ constructor(store, health2, logger, config, opts = {}) {
6204
+ this.store = store;
6205
+ this.health = health2;
6206
+ this.logger = logger;
6207
+ this.config = config;
6208
+ this.now = opts.now ?? Date.now;
6209
+ this.fetchImpl = opts.fetchImpl ?? fetchUpstream3;
6210
+ this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6211
+ this.planFor = opts.planFor ?? probePlanFor;
6212
+ }
6213
+ store;
6214
+ health;
6215
+ logger;
6216
+ config;
6217
+ timer = null;
6218
+ sweeping = false;
6219
+ history = /* @__PURE__ */ new Map();
6220
+ now;
6221
+ fetchImpl;
6222
+ sleep;
6223
+ planFor;
6224
+ /** Whether probing is enabled by the current config. */
6225
+ get enabled() {
6226
+ return this.config.enabled;
6227
+ }
6228
+ /**
6229
+ * Re-apply config to the live instance (the async `start.ts` loads the persisted
6230
+ * `accountProbe` segment after `buildDaemon`). Call BEFORE `start()`.
6231
+ */
6232
+ configure(config) {
6233
+ this.config = config;
6234
+ }
6235
+ /** Arm the probe interval. No-op when disabled (zero regression). Idempotent. */
6236
+ start() {
6237
+ if (this.timer || !this.config.enabled) return;
6238
+ this.timer = setInterval(() => void this.sweep(), this.config.intervalMs);
6239
+ this.timer.unref?.();
6240
+ }
6241
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
6242
+ dispose() {
6243
+ if (this.timer) {
6244
+ clearInterval(this.timer);
6245
+ this.timer = null;
6246
+ }
6247
+ }
6248
+ /**
6249
+ * One sweep: probe every ELIGIBLE account SEQUENTIALLY with a `staggerMs` gap.
6250
+ * Disabled ⇒ no-op. `onlyMultiAccount` skips single-account providers. Exposed
6251
+ * for tests; never throws.
6252
+ */
6253
+ async sweep() {
6254
+ if (!this.config.enabled || this.sweeping) return;
6255
+ this.sweeping = true;
6256
+ try {
6257
+ const config = await this.store.getFullConfig();
6258
+ let probed = 0;
6259
+ let marked = 0;
6260
+ for (const providerId of PROBE_PROVIDERS) {
6261
+ const accounts = listAccounts(config, providerId);
6262
+ if (this.config.onlyMultiAccount && accounts.length < 2) continue;
6263
+ for (const account of accounts) {
6264
+ if (probed > 0 && this.config.staggerMs > 0) await this.sleep(this.config.staggerMs);
6265
+ const outcome = await this.probeAccount(providerId, account.id);
6266
+ probed += 1;
6267
+ if (outcome.marked) marked += 1;
6268
+ }
6269
+ }
6270
+ this.logger.debug("account-probe sweep complete", { probed, marked });
6271
+ } catch (error) {
6272
+ this.logger.warn("account-probe sweep failed", {
6273
+ error: error instanceof Error ? error.message : String(error)
6274
+ });
6275
+ } finally {
6276
+ this.sweeping = false;
6277
+ }
6278
+ }
6279
+ /**
6280
+ * Probe ONE account (design D1). Local tier first (dead token → synthesized 401,
6281
+ * no upstream); else the upstream tier when a verified endpoint exists. Records
6282
+ * the rolling history entry either way; returns whether the tracker was MARKED.
6283
+ */
6284
+ async probeAccount(providerId, accountId) {
6285
+ const now = this.now();
6286
+ let token = null;
6287
+ let readThrew = false;
6288
+ try {
6289
+ token = await this.store.getAccessTokenForAccount(providerId, accountId);
6290
+ } catch {
6291
+ readThrew = true;
6292
+ }
6293
+ if (readThrew) {
6294
+ this.record(providerId, accountId, { ts: now, ok: false, status: null, tier: "local" });
6295
+ return { ok: false, marked: false };
6296
+ }
6297
+ if (!token) {
6298
+ this.health.recordUpstreamOutcome(providerId, accountId, { status: 401, now });
6299
+ this.record(providerId, accountId, { ts: now, ok: false, status: 401, tier: "local" });
6300
+ return { ok: false, marked: true };
6301
+ }
6302
+ const plan = this.planFor(providerId);
6303
+ if (plan.kind === "local") {
6304
+ this.record(providerId, accountId, { ts: now, ok: true, tier: "local" });
6305
+ return { ok: true, marked: false };
6306
+ }
6307
+ const start = this.now();
6308
+ let status = null;
6309
+ let bodyText;
6310
+ try {
6311
+ const res = await this.fetchImpl(
6312
+ plan.url,
6313
+ { ...plan.buildInit(token), signal: AbortSignal.timeout(this.config.timeoutMs) },
6314
+ { providerId, accountId }
6315
+ );
6316
+ status = res.status;
6317
+ if (status === 403) bodyText = await this.readBounded(res);
6318
+ } catch {
6319
+ status = null;
6320
+ }
6321
+ const latencyMs = this.now() - start;
6322
+ const marked = this.applyOutcome(providerId, accountId, status, bodyText, now);
6323
+ this.record(providerId, accountId, {
6324
+ ts: now,
6325
+ ok: status !== null && status >= 200 && status < 300,
6326
+ status,
6327
+ latencyMs,
6328
+ tier: "upstream"
6329
+ });
6330
+ return { ok: status !== null && status < 400, marked };
6331
+ }
6332
+ /** Per-account rolling history for the authed admin surface (design D5). */
6333
+ getAllHistory() {
6334
+ const out = [];
6335
+ for (const [key, records] of this.history) {
6336
+ const [providerId, accountId] = this.parseKey(key);
6337
+ out.push({ providerId, accountId, records: records.slice() });
6338
+ }
6339
+ return out;
6340
+ }
6341
+ /**
6342
+ * The coarse, account-ANONYMOUS `/health` signal (design D5): `true` when no
6343
+ * probed account is currently unhealthy (per #2's tracker). No ids, no counts —
6344
+ * safe for the unauthenticated `/health`. Vacuously `true` when nothing probed.
6345
+ */
6346
+ probedAccountsHealthy(now = this.now()) {
6347
+ for (const key of this.history.keys()) {
6348
+ const [providerId, accountId] = this.parseKey(key);
6349
+ if (!this.health.isSchedulable(providerId, accountId, now)) return false;
6350
+ }
6351
+ return true;
6352
+ }
6353
+ /**
6354
+ * Feed ONLY the account/auth-decisive statuses to #2 (LEAD constraint):
6355
+ * 401/403 → mark; 2xx → clear transient; 429 / 5xx / other 4xx / null →
6356
+ * NOT forwarded (never a mark). Returns whether a NEGATIVE mark was applied.
6357
+ */
6358
+ applyOutcome(providerId, accountId, status, bodyText, now) {
6359
+ if (status === null) return false;
6360
+ if (status === 401 || status === 403) {
6361
+ this.health.recordUpstreamOutcome(providerId, accountId, { status, bodyText, now });
6362
+ return true;
6363
+ }
6364
+ if (status >= 200 && status < 300) {
6365
+ this.health.clearTransientMark(providerId, accountId);
6366
+ return false;
6367
+ }
6368
+ return false;
6369
+ }
6370
+ /** Append a record, capping the ring at `historySize` (drop oldest). */
6371
+ record(providerId, accountId, rec) {
6372
+ const key = this.key(providerId, accountId);
6373
+ const list = this.history.get(key) ?? [];
6374
+ list.push(rec);
6375
+ const overflow = list.length - this.config.historySize;
6376
+ if (overflow > 0) list.splice(0, overflow);
6377
+ this.history.set(key, list);
6378
+ }
6379
+ /** Read a bounded slice of the response body for the 403-ban sniff (never throws). */
6380
+ async readBounded(res) {
6381
+ try {
6382
+ return (await res.text()).slice(0, MAX_BODY_SNIFF);
6383
+ } catch {
6384
+ return "";
6385
+ }
6386
+ }
6387
+ key(providerId, accountId) {
6388
+ return `${providerId}${KEY_SEP}${accountId}`;
6389
+ }
6390
+ parseKey(key) {
6391
+ const idx = key.indexOf(KEY_SEP);
6392
+ return [key.slice(0, idx), key.slice(idx + 1)];
6393
+ }
6394
+ };
6395
+
6396
+ // src/AccountHealthSweeper.ts
4362
6397
  var REFRESH_LEAD_MS = 5 * 6e4;
4363
6398
  var SWEEP_INTERVAL_MS = 6e4;
4364
6399
  var OAUTH_PROVIDERS = ["claude", "codex", "gemini"];
4365
- var TokenRefreshScheduler = class {
4366
- constructor(store, logger, intervalMs = SWEEP_INTERVAL_MS, leadMs = REFRESH_LEAD_MS) {
6400
+ function isOAuthProvider(providerId) {
6401
+ return OAUTH_PROVIDERS.includes(providerId);
6402
+ }
6403
+ var AccountHealthSweeper = class {
6404
+ constructor(store, health2, logger, intervalMs = SWEEP_INTERVAL_MS, leadMs = REFRESH_LEAD_MS) {
4367
6405
  this.store = store;
6406
+ this.health = health2;
4368
6407
  this.logger = logger;
4369
6408
  this.intervalMs = intervalMs;
4370
6409
  this.leadMs = leadMs;
4371
6410
  }
4372
6411
  store;
6412
+ health;
4373
6413
  logger;
4374
6414
  intervalMs;
4375
6415
  leadMs;
@@ -4388,21 +6428,26 @@ var TokenRefreshScheduler = class {
4388
6428
  this.timer = null;
4389
6429
  }
4390
6430
  }
4391
- /** One sweep over every account of every OAuth provider. Exposed for tests. */
6431
+ /**
6432
+ * One sweep: surface accounts that just recovered (emits the recovery signal
6433
+ * through the tracker's hook) and nudge a fresh token for any recovered OAuth
6434
+ * account whose token is near expiry. Exposed for tests. Never throws.
6435
+ */
4392
6436
  async sweep(now = Date.now()) {
4393
6437
  if (this.sweeping) return;
4394
6438
  this.sweeping = true;
4395
6439
  try {
6440
+ const recovered = this.health.sweepRecoveries(now);
6441
+ if (recovered.length === 0) return;
4396
6442
  const config = await this.store.getFullConfig();
4397
- for (const provider of OAUTH_PROVIDERS) {
4398
- const activeId = getActiveAccount(config, provider)?.id;
4399
- for (const account of listAccounts(config, provider)) {
4400
- if (!this.needsRefresh(account.tokens, now)) continue;
4401
- await this.refreshOne(provider, account.id, account.id === activeId);
4402
- }
6443
+ for (const event of recovered) {
6444
+ if (!isOAuthProvider(event.providerId)) continue;
6445
+ const account = getAccountById(config, event.providerId, event.accountId);
6446
+ if (!account || !this.needsRefresh(account.tokens, now)) continue;
6447
+ await this.refreshOne(event.providerId, event.accountId);
4403
6448
  }
4404
6449
  } catch (error) {
4405
- this.logger.warn("token-refresh sweep failed", {
6450
+ this.logger.warn("account-health sweep failed", {
4406
6451
  error: error instanceof Error ? error.message : String(error)
4407
6452
  });
4408
6453
  } finally {
@@ -4417,87 +6462,850 @@ var TokenRefreshScheduler = class {
4417
6462
  const expiresAt = Date.parse(t.expiresAt);
4418
6463
  return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
4419
6464
  }
4420
- /** Refresh one account; failures are logged, never thrown (the store has
4421
- * already flagged the account `expired`). */
4422
- async refreshOne(provider, id, isActive) {
6465
+ /** Refresh one recovered account by id; failures are logged, never thrown. */
6466
+ async refreshOne(provider, id) {
4423
6467
  try {
4424
- const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
4425
- if (!ok) {
4426
- this.logger.warn("background token refresh failed", { provider, accountId: id });
4427
- } else {
4428
- this.logger.info("background token refresh succeeded", { provider, accountId: id });
4429
- }
6468
+ const ok = await this.store.refreshAccountById(provider, id);
6469
+ if (ok) this.logger.info("account-health recovery refresh succeeded", { provider, accountId: id });
6470
+ else this.logger.warn("account-health recovery refresh failed", { provider, accountId: id });
4430
6471
  } catch (error) {
4431
- this.logger.warn("background token refresh threw", {
6472
+ this.logger.warn("account-health recovery refresh threw", {
4432
6473
  provider,
4433
6474
  accountId: id,
4434
6475
  error: error instanceof Error ? error.message : String(error)
4435
6476
  });
4436
6477
  }
4437
6478
  }
4438
- refreshActive(provider) {
4439
- switch (provider) {
4440
- case "claude":
4441
- return this.store.refreshClaudeToken();
4442
- case "codex":
4443
- return this.store.refreshCodexToken();
4444
- case "gemini":
4445
- return this.store.refreshGeminiToken();
4446
- }
4447
- }
4448
6479
  };
4449
6480
 
4450
- // src/bootstrap.ts
4451
- function buildDaemon(config, paths) {
4452
- const logger = new ConsoleLogger();
4453
- const secretBox3 = new SecretBox(() => resolveMasterKey({ keyFilePath: paths.masterKeyFilePath }));
4454
- setSecretBox(secretBox3);
4455
- setSecretBox2(secretBox3);
4456
- const decryptedConfig = decryptConfigSecrets(config, secretBox3);
4457
- const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
4458
- const keyDb = new JsonOutboundKeyDb(paths.keysPath);
4459
- const settingsStore = new JsonApiServerSettingsStore(paths.configPath);
4460
- const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
4461
- const subscriptionAccounts = new SubscriptionAccountService(credentialStore);
4462
- setSubscriptionAccountService(subscriptionAccounts);
4463
- const subscriptionRegistry = new SubscriptionProviderRegistry(
4464
- subscriptionAccounts,
4465
- credentialStore
4466
- );
4467
- setSubscriptionProviderRegistry(subscriptionRegistry);
4468
- setGeminiCodeAssistResolver(getGeminiCodeAssistProjectResolver());
4469
- const autoDisableStore = new AutoDisableStore();
4470
- const apiKeyPool = new ApiKeyPoolService(
4471
- createPoolKeysLoader((id) => llmConfig.getProviderRow(id), autoDisableStore),
4472
- resolveEnvKey,
4473
- logger,
4474
- async (keyId) => {
4475
- autoDisableStore.markAutoDisabled(keyId, 0, Date.now());
4476
- return true;
4477
- },
4478
- async (keyId, status, at) => {
4479
- autoDisableStore.markAutoDisabled(keyId, status, at);
4480
- }
4481
- );
4482
- const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
4483
- const pricingEngine = new PricingEngine(pricingStore, logger);
4484
- const usageEventStore = new JsonlUsageEventStore(
4485
- defaultUsageEventsPath(paths.configPath),
4486
- async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
4487
- );
4488
- const usageRecorder = new UsageRecorder(usageEventStore, pricingEngine, logger);
4489
- const providerProxy = getProviderProxy({ llmConfig, apiKeyPool, usageRecorder });
6481
+ // src/audit/AuditPruneSweeper.ts
6482
+ import { existsSync as existsSync11, readdirSync, unlinkSync } from "fs";
6483
+ import { join as join5 } from "path";
6484
+
6485
+ // src/audit/auditFiles.ts
6486
+ var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
6487
+ var pad22 = (n) => String(n).padStart(2, "0");
6488
+ function auditFileName(ts) {
6489
+ const d = new Date(ts);
6490
+ return `audit-${d.getFullYear()}-${pad22(d.getMonth() + 1)}-${pad22(d.getDate())}.jsonl`;
6491
+ }
6492
+ function auditFileDateMs(fileName) {
6493
+ const m = AUDIT_FILE_RE.exec(fileName);
6494
+ if (!m) return null;
6495
+ const year = Number(m[1]);
6496
+ const month = Number(m[2]);
6497
+ const day = Number(m[3]);
6498
+ const d = new Date(year, month - 1, day);
6499
+ if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day) {
6500
+ return null;
6501
+ }
6502
+ return d.getTime();
6503
+ }
6504
+
6505
+ // src/audit/AuditPruneSweeper.ts
6506
+ var DAY_MS = 24 * 60 * 6e4;
6507
+ var SWEEP_INTERVAL_MS2 = 60 * 6e4;
6508
+ var AuditPruneSweeper = class {
6509
+ constructor(auditDir, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
6510
+ this.auditDir = auditDir;
6511
+ this.logger = logger;
6512
+ this.config = config;
6513
+ this.intervalMs = intervalMs;
6514
+ this.now = now;
6515
+ }
6516
+ auditDir;
6517
+ logger;
6518
+ config;
6519
+ intervalMs;
6520
+ now;
6521
+ timer = null;
6522
+ sweeping = false;
6523
+ /** Whether pruning is active (audit enabled). */
6524
+ get enabled() {
6525
+ return this.config.enabled;
6526
+ }
6527
+ /** Re-apply config to the live instance (boot + admin PUT hot-reload). */
6528
+ configure(config) {
6529
+ this.config = config;
6530
+ }
6531
+ /**
6532
+ * Arm the prune interval AND run one prune immediately (boot cleanup). No-op
6533
+ * when audit is disabled (zero regression). Idempotent.
6534
+ */
6535
+ start() {
6536
+ if (this.timer || !this.config.enabled) return;
6537
+ void this.sweep();
6538
+ this.timer = setInterval(() => void this.sweep(), this.intervalMs);
6539
+ this.timer.unref?.();
6540
+ }
6541
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
6542
+ dispose() {
6543
+ if (this.timer) {
6544
+ clearInterval(this.timer);
6545
+ this.timer = null;
6546
+ }
6547
+ }
6548
+ /**
6549
+ * One prune: unlink every audit date file strictly OLDER than the retention
6550
+ * cutoff (`now - retentionDays` days, at local-midnight granularity). Exposed
6551
+ * for tests; never throws. Returns the number of files removed.
6552
+ */
6553
+ async sweep() {
6554
+ if (!this.config.enabled || this.sweeping) return 0;
6555
+ this.sweeping = true;
6556
+ try {
6557
+ if (!existsSync11(this.auditDir)) return 0;
6558
+ const today = new Date(this.now());
6559
+ const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
6560
+ const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
6561
+ let removed = 0;
6562
+ for (const file of readdirSync(this.auditDir)) {
6563
+ const dateMs = auditFileDateMs(file);
6564
+ if (dateMs === null || dateMs >= cutoff) continue;
6565
+ try {
6566
+ unlinkSync(join5(this.auditDir, file));
6567
+ removed += 1;
6568
+ } catch (error) {
6569
+ this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
6570
+ file,
6571
+ error: error instanceof Error ? error.message : String(error)
6572
+ });
6573
+ }
6574
+ }
6575
+ if (removed > 0) this.logger.debug("audit prune complete", { removed });
6576
+ return removed;
6577
+ } catch (error) {
6578
+ this.logger.warn("audit prune sweep failed", {
6579
+ error: error instanceof Error ? error.message : String(error)
6580
+ });
6581
+ return 0;
6582
+ } finally {
6583
+ this.sweeping = false;
6584
+ }
6585
+ }
6586
+ };
6587
+
6588
+ // src/audit/auditReader.ts
6589
+ import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
6590
+ import { join as join6 } from "path";
6591
+ var DEFAULT_LIMIT = 200;
6592
+ var MAX_LIMIT = 2e3;
6593
+ function readAuditRecords(auditDir, query = {}) {
6594
+ if (!existsSync12(auditDir)) return [];
6595
+ let files;
6596
+ try {
6597
+ files = readdirSync2(auditDir).filter((f) => AUDIT_FILE_RE.test(f));
6598
+ } catch {
6599
+ return [];
6600
+ }
6601
+ const from = typeof query.from === "number" ? query.from : -Infinity;
6602
+ const to = typeof query.to === "number" ? query.to : Infinity;
6603
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit ?? DEFAULT_LIMIT)));
6604
+ const matched = [];
6605
+ for (const file of files.sort().reverse()) {
6606
+ let raw;
6607
+ try {
6608
+ raw = readFileSync11(join6(auditDir, file), "utf8");
6609
+ } catch {
6610
+ continue;
6611
+ }
6612
+ for (const line of raw.split("\n")) {
6613
+ const trimmed = line.trim();
6614
+ if (!trimmed) continue;
6615
+ let rec;
6616
+ try {
6617
+ rec = JSON.parse(trimmed);
6618
+ } catch {
6619
+ continue;
6620
+ }
6621
+ if (!isAuditRecord(rec)) continue;
6622
+ if (query.keyId !== void 0 && rec.keyId !== query.keyId) continue;
6623
+ if (rec.ts < from || rec.ts > to) continue;
6624
+ matched.push(rec);
6625
+ }
6626
+ }
6627
+ matched.sort((a, b) => b.ts - a.ts);
6628
+ return matched.slice(0, limit);
6629
+ }
6630
+ function isAuditRecord(value) {
6631
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
6632
+ const r = value;
6633
+ return typeof r["id"] === "string" && typeof r["ts"] === "number" && typeof r["method"] === "string" && typeof r["path"] === "string" && typeof r["status"] === "number";
6634
+ }
6635
+
6636
+ // src/audit/AuditWriter.ts
6637
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "fs";
6638
+ import { join as join7 } from "path";
6639
+ var AuditWriter = class {
6640
+ constructor(auditDir, logger, defer = (fn) => setTimeout(fn, 0)) {
6641
+ this.auditDir = auditDir;
6642
+ this.logger = logger;
6643
+ this.defer = defer;
6644
+ }
6645
+ auditDir;
6646
+ logger;
6647
+ defer;
6648
+ dirEnsured = false;
6649
+ /**
6650
+ * Enqueue one record for append. Returns IMMEDIATELY (fire-and-forget); the fs
6651
+ * write happens on the deferred tick. A failure is logged, never thrown.
6652
+ */
6653
+ record(record) {
6654
+ this.defer(() => {
6655
+ try {
6656
+ this.appendNow(record);
6657
+ } catch (error) {
6658
+ this.logger.warn("[AuditWriter] failed to append audit record", {
6659
+ error: error instanceof Error ? error.message : String(error)
6660
+ });
6661
+ }
6662
+ });
6663
+ }
6664
+ /**
6665
+ * Append synchronously — the awaitable form tests use to assert the line landed.
6666
+ * Ensures the `audit/` directory exists on first write (lazy, like the usage
6667
+ * store's lazy file creation).
6668
+ */
6669
+ appendNow(record) {
6670
+ if (!this.dirEnsured) {
6671
+ mkdirSync4(this.auditDir, { recursive: true });
6672
+ this.dirEnsured = true;
6673
+ }
6674
+ const file = join7(this.auditDir, auditFileName(record.ts));
6675
+ appendFileSync2(file, JSON.stringify(record) + "\n", "utf8");
6676
+ }
6677
+ };
6678
+
6679
+ // src/billing/BillingPublisher.ts
6680
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync5 } from "fs";
6681
+ import { createHmac } from "crypto";
6682
+ import { join as join8 } from "path";
6683
+ import { fetchUpstream as fetchUpstream4 } from "@omnicross/core/pipeline/upstreamFetch";
6684
+
6685
+ // src/billing/billingFiles.ts
6686
+ var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
6687
+ var DELIVERED_FILE_RE = /^delivered-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
6688
+ var pad23 = (n) => String(n).padStart(2, "0");
6689
+ function dateStamp(ts) {
6690
+ const d = new Date(ts);
6691
+ return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}-${pad23(d.getDate())}`;
6692
+ }
6693
+ function billingFileName(ts) {
6694
+ return `billing-${dateStamp(ts)}.jsonl`;
6695
+ }
6696
+ function deliveredFileName(ts) {
6697
+ return `delivered-${dateStamp(ts)}.jsonl`;
6698
+ }
6699
+
6700
+ // src/billing/BillingPublisher.ts
6701
+ var BILLING_POST_TIMEOUT_MS = 1e4;
6702
+ var BillingPublisher = class {
6703
+ constructor(billingDir, logger, opts = {}) {
6704
+ this.billingDir = billingDir;
6705
+ this.logger = logger;
6706
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream4(url, init));
6707
+ this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
6708
+ this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
6709
+ this.now = opts.now ?? Date.now;
6710
+ }
6711
+ billingDir;
6712
+ logger;
6713
+ config;
6714
+ dirEnsured = false;
6715
+ fetchImpl;
6716
+ defer;
6717
+ timeoutMs;
6718
+ now;
6719
+ /** Install/replace the live billing config (endpoint + secret + retry bound). */
6720
+ setConfig(config) {
6721
+ this.config = config;
6722
+ }
6723
+ /**
6724
+ * Record one billing event. DURABLE-FIRST: append synchronously (the event is
6725
+ * now on disk, never lost), THEN schedule a best-effort POST off the caller's
6726
+ * stack (non-blocking; ledger-only when no endpoint). Returns IMMEDIATELY and
6727
+ * NEVER throws — a failing append/POST is logged, never propagated.
6728
+ */
6729
+ record(event) {
6730
+ let appended = false;
6731
+ try {
6732
+ this.appendNow(event);
6733
+ appended = true;
6734
+ } catch (error) {
6735
+ this.logger.warn("[BillingPublisher] failed to append billing event", {
6736
+ error: error instanceof Error ? error.message : String(error)
6737
+ });
6738
+ }
6739
+ if (appended && this.config?.endpoint) {
6740
+ this.defer(() => {
6741
+ void this.deliverNow(event).catch(() => {
6742
+ });
6743
+ });
6744
+ }
6745
+ }
6746
+ /**
6747
+ * Append the event as one JSON line to `billing-YYYY-MM-DD.jsonl` (the event's
6748
+ * LOCAL date). Synchronous — the awaitable form tests use to assert the ledger
6749
+ * line landed BEFORE any delivery. Ensures the `billing/` directory on first write.
6750
+ */
6751
+ appendNow(event) {
6752
+ this.ensureDir();
6753
+ const file = join8(this.billingDir, billingFileName(event.ts));
6754
+ appendFileSync3(file, JSON.stringify(event) + "\n", "utf8");
6755
+ }
6756
+ /**
6757
+ * One best-effort delivery attempt for an event ALREADY in the ledger. POSTs the
6758
+ * event JSON to the configured endpoint (optionally HMAC-signed); on a 2xx ack
6759
+ * appends a delivery marker and returns `true`. Any non-2xx / thrown / timed-out
6760
+ * attempt returns `false` — the event stays UNdelivered in the ledger (never
6761
+ * lost). NEVER rejects. A no-op `false` when no endpoint is configured.
6762
+ */
6763
+ async deliverNow(event) {
6764
+ const endpoint = this.config?.endpoint;
6765
+ if (!endpoint) return false;
6766
+ try {
6767
+ const body = JSON.stringify(event);
6768
+ const headers = { "Content-Type": "application/json" };
6769
+ const secret = this.config?.secret;
6770
+ if (secret) {
6771
+ const hmac = createHmac("sha256", secret).update(body).digest("hex");
6772
+ headers["X-Omnicross-Billing-Signature"] = `sha256=${hmac}`;
6773
+ }
6774
+ const res = await this.fetchImpl(endpoint, {
6775
+ method: "POST",
6776
+ headers,
6777
+ body,
6778
+ signal: AbortSignal.timeout(this.timeoutMs)
6779
+ });
6780
+ if (!res.ok) {
6781
+ this.logger.debug(`[billing] delivery failed ${event.id} (HTTP ${res.status})`);
6782
+ return false;
6783
+ }
6784
+ this.markDelivered(event);
6785
+ this.logger.debug(`[billing] delivered ${event.id}`);
6786
+ return true;
6787
+ } catch (error) {
6788
+ this.logger.debug(
6789
+ `[billing] delivery error ${event.id}: ${error instanceof Error ? error.message : String(error)}`
6790
+ );
6791
+ return false;
6792
+ }
6793
+ }
6794
+ /**
6795
+ * Append a delivery marker `{ id, deliveredAt }` to `delivered-YYYY-MM-DD.jsonl`
6796
+ * (keyed by the EVENT's date so the reader finds both together). Idempotent at
6797
+ * the reconciliation layer — the reader unions marker ids into a delivered set,
6798
+ * so a duplicate marker is harmless. A marker-write failure is logged, never thrown.
6799
+ */
6800
+ markDelivered(event) {
6801
+ try {
6802
+ this.ensureDir();
6803
+ const file = join8(this.billingDir, deliveredFileName(event.ts));
6804
+ appendFileSync3(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
6805
+ } catch (error) {
6806
+ this.logger.warn("[BillingPublisher] failed to append delivery marker", {
6807
+ error: error instanceof Error ? error.message : String(error)
6808
+ });
6809
+ }
6810
+ }
6811
+ ensureDir() {
6812
+ if (this.dirEnsured) return;
6813
+ mkdirSync5(this.billingDir, { recursive: true });
6814
+ this.dirEnsured = true;
6815
+ }
6816
+ };
6817
+
6818
+ // src/billing/billingReader.ts
6819
+ import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync12 } from "fs";
6820
+ import { join as join9 } from "path";
6821
+ function readBillingLedger(billingDir) {
6822
+ const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
6823
+ if (!existsSync13(billingDir)) return view;
6824
+ let files;
6825
+ try {
6826
+ files = readdirSync3(billingDir);
6827
+ } catch {
6828
+ return view;
6829
+ }
6830
+ for (const file of files.sort()) {
6831
+ if (BILLING_FILE_RE.test(file)) {
6832
+ for (const rec of parseLines(billingDir, file)) {
6833
+ if (isBillingEvent(rec)) view.events.push(rec);
6834
+ }
6835
+ } else if (DELIVERED_FILE_RE.test(file)) {
6836
+ for (const rec of parseLines(billingDir, file)) {
6837
+ const id = rec.id;
6838
+ if (typeof id === "string") view.deliveredIds.add(id);
6839
+ }
6840
+ }
6841
+ }
6842
+ return view;
6843
+ }
6844
+ function readUndeliveredEvents(billingDir) {
6845
+ const { events, deliveredIds } = readBillingLedger(billingDir);
6846
+ return events.filter((e) => !deliveredIds.has(e.id)).sort((a, b) => a.ts - b.ts);
6847
+ }
6848
+ function readBillingStatus(billingDir) {
6849
+ const { events, deliveredIds } = readBillingLedger(billingDir);
6850
+ let delivered = 0;
6851
+ for (const e of events) if (deliveredIds.has(e.id)) delivered += 1;
6852
+ return { total: events.length, delivered, pending: events.length - delivered };
6853
+ }
6854
+ function parseLines(dir, file) {
6855
+ let raw;
6856
+ try {
6857
+ raw = readFileSync12(join9(dir, file), "utf8");
6858
+ } catch {
6859
+ return [];
6860
+ }
6861
+ const out = [];
6862
+ for (const line of raw.split("\n")) {
6863
+ const trimmed = line.trim();
6864
+ if (!trimmed) continue;
6865
+ try {
6866
+ out.push(JSON.parse(trimmed));
6867
+ } catch {
6868
+ }
6869
+ }
6870
+ return out;
6871
+ }
6872
+ function isBillingEvent(value) {
6873
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
6874
+ const r = value;
6875
+ return typeof r["id"] === "string" && typeof r["ts"] === "number" && typeof r["model"] === "string" && typeof r["status"] === "number";
6876
+ }
6877
+
6878
+ // src/billing/BillingRetrySweeper.ts
6879
+ var SWEEP_INTERVAL_MS3 = 5 * 6e4;
6880
+ var BillingRetrySweeper = class {
6881
+ constructor(billingDir, publisher2, logger, config, intervalMs = SWEEP_INTERVAL_MS3, now = Date.now) {
6882
+ this.billingDir = billingDir;
6883
+ this.publisher = publisher2;
6884
+ this.logger = logger;
6885
+ this.config = config;
6886
+ this.intervalMs = intervalMs;
6887
+ this.now = now;
6888
+ }
6889
+ billingDir;
6890
+ publisher;
6891
+ logger;
6892
+ config;
6893
+ intervalMs;
6894
+ now;
6895
+ timer = null;
6896
+ sweeping = false;
6897
+ /** Whether retrying is active: billing enabled AND an endpoint is configured. */
6898
+ get enabled() {
6899
+ return this.config.enabled && typeof this.config.endpoint === "string" && this.config.endpoint.length > 0;
6900
+ }
6901
+ /** Re-apply config to the live instance (boot + admin PUT hot-reload). */
6902
+ configure(config) {
6903
+ this.config = config;
6904
+ }
6905
+ /**
6906
+ * Arm the retry interval AND run one sweep immediately (boot catch-up for events
6907
+ * that failed to deliver while the daemon was down). No-op when disabled or in
6908
+ * ledger-only mode (no endpoint to POST to). Idempotent.
6909
+ */
6910
+ start() {
6911
+ if (this.timer || !this.enabled) return;
6912
+ void this.sweep();
6913
+ this.timer = setInterval(() => void this.sweep(), this.intervalMs);
6914
+ this.timer.unref?.();
6915
+ }
6916
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
6917
+ dispose() {
6918
+ if (this.timer) {
6919
+ clearInterval(this.timer);
6920
+ this.timer = null;
6921
+ }
6922
+ }
6923
+ /**
6924
+ * One sweep: re-POST every UNdelivered ledger event still within
6925
+ * `maxRetryAgeMs`; leave over-age undelivered events for reconciliation (NEVER
6926
+ * deleted). Exposed for tests; never throws. Returns the number of events a
6927
+ * re-POST was attempted for.
6928
+ */
6929
+ async sweep() {
6930
+ if (!this.enabled || this.sweeping) return 0;
6931
+ this.sweeping = true;
6932
+ try {
6933
+ const cutoff = this.now() - this.config.maxRetryAgeMs;
6934
+ let attempted = 0;
6935
+ for (const event of readUndeliveredEvents(this.billingDir)) {
6936
+ if (event.ts < cutoff) continue;
6937
+ attempted += 1;
6938
+ await this.publisher.deliverNow(event);
6939
+ }
6940
+ if (attempted > 0) this.logger.debug("billing retry sweep complete", { attempted });
6941
+ return attempted;
6942
+ } catch (error) {
6943
+ this.logger.warn("billing retry sweep failed", {
6944
+ error: error instanceof Error ? error.message : String(error)
6945
+ });
6946
+ return 0;
6947
+ } finally {
6948
+ this.sweeping = false;
6949
+ }
6950
+ }
6951
+ };
6952
+
6953
+ // src/TokenRefreshScheduler.ts
6954
+ var REFRESH_LEAD_MS2 = 5 * 6e4;
6955
+ var SWEEP_INTERVAL_MS4 = 6e4;
6956
+ var OAUTH_PROVIDERS2 = ["claude", "codex", "gemini"];
6957
+ var TokenRefreshScheduler = class {
6958
+ constructor(store, logger, intervalMs = SWEEP_INTERVAL_MS4, leadMs = REFRESH_LEAD_MS2) {
6959
+ this.store = store;
6960
+ this.logger = logger;
6961
+ this.intervalMs = intervalMs;
6962
+ this.leadMs = leadMs;
6963
+ }
6964
+ store;
6965
+ logger;
6966
+ intervalMs;
6967
+ leadMs;
6968
+ timer = null;
6969
+ sweeping = false;
6970
+ /** Arm the sweep interval. Idempotent. The timer never holds the loop open. */
6971
+ start() {
6972
+ if (this.timer) return;
6973
+ this.timer = setInterval(() => void this.sweep(), this.intervalMs);
6974
+ this.timer.unref?.();
6975
+ }
6976
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
6977
+ dispose() {
6978
+ if (this.timer) {
6979
+ clearInterval(this.timer);
6980
+ this.timer = null;
6981
+ }
6982
+ }
6983
+ /** One sweep over every account of every OAuth provider. Exposed for tests. */
6984
+ async sweep(now = Date.now()) {
6985
+ if (this.sweeping) return;
6986
+ this.sweeping = true;
6987
+ try {
6988
+ const config = await this.store.getFullConfig();
6989
+ for (const provider of OAUTH_PROVIDERS2) {
6990
+ const activeId = getActiveAccount(config, provider)?.id;
6991
+ for (const account of listAccounts(config, provider)) {
6992
+ if (!this.needsRefresh(account.tokens, now)) continue;
6993
+ await this.refreshOne(provider, account.id, account.id === activeId);
6994
+ }
6995
+ }
6996
+ } catch (error) {
6997
+ this.logger.warn("token-refresh sweep failed", {
6998
+ error: error instanceof Error ? error.message : String(error)
6999
+ });
7000
+ } finally {
7001
+ this.sweeping = false;
7002
+ }
7003
+ }
7004
+ /** Expiring within the lead window, refreshable, and not already dead. */
7005
+ needsRefresh(tokens, now) {
7006
+ const t = tokens;
7007
+ if (!t.refreshToken || t.status === "expired" || t.status === "error") return false;
7008
+ if (!t.expiresAt) return false;
7009
+ const expiresAt = Date.parse(t.expiresAt);
7010
+ return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
7011
+ }
7012
+ /** Refresh one account; failures are logged, never thrown (the store has
7013
+ * already flagged the account `expired`). */
7014
+ async refreshOne(provider, id, isActive) {
7015
+ try {
7016
+ const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
7017
+ if (!ok) {
7018
+ this.logger.warn("background token refresh failed", { provider, accountId: id });
7019
+ } else {
7020
+ this.logger.info("background token refresh succeeded", { provider, accountId: id });
7021
+ }
7022
+ } catch (error) {
7023
+ this.logger.warn("background token refresh threw", {
7024
+ provider,
7025
+ accountId: id,
7026
+ error: error instanceof Error ? error.message : String(error)
7027
+ });
7028
+ }
7029
+ }
7030
+ refreshActive(provider) {
7031
+ switch (provider) {
7032
+ case "claude":
7033
+ return this.store.refreshClaudeToken();
7034
+ case "codex":
7035
+ return this.store.refreshCodexToken();
7036
+ case "gemini":
7037
+ return this.store.refreshGeminiToken();
7038
+ }
7039
+ }
7040
+ };
7041
+
7042
+ // src/webhook/WebhookDispatcher.ts
7043
+ import { createHmac as createHmac2 } from "crypto";
7044
+ import { fetchUpstream as fetchUpstream5 } from "@omnicross/core/pipeline/upstreamFetch";
7045
+ var WEBHOOK_MAX_ATTEMPTS = 3;
7046
+ var WEBHOOK_QUEUE_MAX = 1e3;
7047
+ var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
7048
+ var WEBHOOK_BASE_BACKOFF_MS = 200;
7049
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
7050
+ var WebhookDispatcher = class {
7051
+ config;
7052
+ queue = [];
7053
+ draining = false;
7054
+ warnedFull = false;
7055
+ fetchImpl;
7056
+ logger;
7057
+ maxAttempts;
7058
+ queueMax;
7059
+ timeoutMs;
7060
+ baseBackoffMs;
7061
+ sleep;
7062
+ now;
7063
+ constructor(opts = {}) {
7064
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
7065
+ this.logger = opts.logger;
7066
+ this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
7067
+ this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
7068
+ this.timeoutMs = opts.timeoutMs ?? WEBHOOK_SEND_TIMEOUT_MS;
7069
+ this.baseBackoffMs = opts.baseBackoffMs ?? WEBHOOK_BASE_BACKOFF_MS;
7070
+ this.sleep = opts.sleep ?? defaultSleep;
7071
+ this.now = opts.now ?? Date.now;
7072
+ }
7073
+ /** Install/replace the live webhook config (destinations + master switch). */
7074
+ setConfig(config) {
7075
+ this.config = config;
7076
+ }
7077
+ /**
7078
+ * Enqueue an event and return IMMEDIATELY (fire-and-forget). NEVER awaits a
7079
+ * send, NEVER throws — the drain loop does all sending on a side channel. A
7080
+ * full queue drops the OLDEST event (with a one-shot warn) so a runaway source
7081
+ * can't OOM the process.
7082
+ */
7083
+ emit(event) {
7084
+ if (this.queue.length >= this.queueMax) {
7085
+ this.queue.shift();
7086
+ if (!this.warnedFull) {
7087
+ this.logger?.warn("[webhook] queue full \u2014 dropping oldest events");
7088
+ this.warnedFull = true;
7089
+ }
7090
+ }
7091
+ this.queue.push(event);
7092
+ if (!this.draining) {
7093
+ this.draining = true;
7094
+ queueMicrotask(() => void this.drain());
7095
+ }
7096
+ }
7097
+ /** Drain the queue, sending each event to its matching destinations concurrently. */
7098
+ async drain() {
7099
+ try {
7100
+ while (this.queue.length > 0) {
7101
+ const event = this.queue.shift();
7102
+ const destinations = this.matchingDestinations(event.kind);
7103
+ if (destinations.length === 0) continue;
7104
+ await Promise.all(destinations.map((d) => this.sendWithRetry(event, d)));
7105
+ }
7106
+ } finally {
7107
+ this.draining = false;
7108
+ if (this.queue.length > 0) {
7109
+ this.draining = true;
7110
+ queueMicrotask(() => void this.drain());
7111
+ }
7112
+ }
7113
+ }
7114
+ /** The enabled destinations whose event filter admits this kind (empty ⇒ all). */
7115
+ matchingDestinations(kind) {
7116
+ const cfg = this.config;
7117
+ if (!cfg || !cfg.enabled) return [];
7118
+ return cfg.destinations.filter(
7119
+ (d) => d.enabled && (!d.events || d.events.length === 0 || d.events.includes(kind))
7120
+ );
7121
+ }
7122
+ /** Send with bounded exponential backoff; log-and-drop after `maxAttempts`. */
7123
+ async sendWithRetry(event, dest) {
7124
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
7125
+ const result = await this.sendOnce(event, dest);
7126
+ if (result.ok) {
7127
+ this.logger?.debug(`[webhook] delivered ${event.kind} \u2192 ${dest.id} (${result.status})`);
7128
+ return;
7129
+ }
7130
+ if (attempt < this.maxAttempts) {
7131
+ await this.sleep(this.baseBackoffMs * 2 ** (attempt - 1));
7132
+ } else {
7133
+ this.logger?.warn(
7134
+ `[webhook] dropped ${event.kind} \u2192 ${dest.id} after ${this.maxAttempts} attempts: ${result.error ?? `HTTP ${result.status}`}`
7135
+ );
7136
+ }
7137
+ }
7138
+ }
7139
+ /** One POST attempt. Returns an outcome; a thrown error becomes `{ ok:false }`. */
7140
+ async sendOnce(event, dest) {
7141
+ try {
7142
+ const { body, headers } = buildRequest(event, dest, this.now());
7143
+ const res = await this.fetchImpl(dest.url, {
7144
+ method: "POST",
7145
+ headers: { "Content-Type": "application/json", ...headers },
7146
+ body,
7147
+ signal: AbortSignal.timeout(this.timeoutMs)
7148
+ });
7149
+ return res.ok ? { ok: true, status: res.status } : { ok: false, status: res.status };
7150
+ } catch (err5) {
7151
+ return { ok: false, error: err5 instanceof Error ? err5.message : String(err5) };
7152
+ }
7153
+ }
7154
+ /**
7155
+ * ADMIN test path (design D8): deliver a `test` event to ONE destination and
7156
+ * AWAIT the single-attempt result. This is the ONLY awaited send — it runs on
7157
+ * the admin request path (an operator clicking "Test"), NEVER on a relay path,
7158
+ * so awaiting it is safe. Finds the destination regardless of its `enabled`
7159
+ * flag or the master switch (an explicit operator action).
7160
+ */
7161
+ async deliverTest(destinationId) {
7162
+ const dest = this.config?.destinations.find((d) => d.id === destinationId);
7163
+ if (!dest) return { ok: false, error: "destination not found" };
7164
+ return this.sendOnce({ kind: "test", at: this.now() }, dest);
7165
+ }
7166
+ };
7167
+ function buildRequest(event, dest, nowMs) {
7168
+ if (dest.type === "feishu") return buildFeishu(event, dest, nowMs);
7169
+ return buildCustom(event, dest);
7170
+ }
7171
+ function buildCustom(event, dest) {
7172
+ const body = JSON.stringify(event);
7173
+ const headers = {};
7174
+ if (dest.secret) {
7175
+ const hmac = createHmac2("sha256", dest.secret).update(body).digest("hex");
7176
+ headers["X-Omnicross-Signature"] = `sha256=${hmac}`;
7177
+ }
7178
+ return { body, headers };
7179
+ }
7180
+ function buildFeishu(event, dest, nowMs) {
7181
+ const payload = {
7182
+ msg_type: "text",
7183
+ content: { text: feishuText(event) }
7184
+ };
7185
+ if (dest.secret) {
7186
+ const timestamp = Math.floor(nowMs / 1e3).toString();
7187
+ const stringToSign = `${timestamp}
7188
+ ${dest.secret}`;
7189
+ payload["timestamp"] = timestamp;
7190
+ payload["sign"] = createHmac2("sha256", stringToSign).digest("base64");
7191
+ }
7192
+ return { body: JSON.stringify(payload), headers: {} };
7193
+ }
7194
+ function feishuText(event) {
7195
+ switch (event.kind) {
7196
+ case "account.recovery":
7197
+ return `omnicross: account recovered \u2014 ${event.providerId}/${event.accountId}`;
7198
+ case "account.anomaly":
7199
+ return `omnicross: account anomaly [${event.state}] \u2014 ${event.providerId}/${event.accountId}`;
7200
+ case "key.quotaWarning":
7201
+ return `omnicross: key quota warning (${event.scope}) \u2014 $${event.spentUsd.toFixed(2)} of $${event.limitUsd.toFixed(2)} (key ${event.keyId})`;
7202
+ case "key.quotaExceeded":
7203
+ return `omnicross: key quota EXCEEDED (${event.scope}) \u2014 $${event.spentUsd.toFixed(2)} of $${event.limitUsd.toFixed(2)} (key ${event.keyId})`;
7204
+ case "server.error":
7205
+ return `omnicross: server error \u2014 ${event.message}`;
7206
+ case "test":
7207
+ return "omnicross: webhook test";
7208
+ }
7209
+ }
7210
+
7211
+ // src/bootstrap.ts
7212
+ function buildDaemon(config, paths) {
7213
+ const logger = new ConfigurableLogger(config.logging);
7214
+ const secretBox3 = new SecretBox(() => resolveMasterKey({ keyFilePath: paths.masterKeyFilePath }));
7215
+ setSecretBox(secretBox3);
7216
+ setSecretBox2(secretBox3);
7217
+ const decryptedConfig = decryptConfigSecrets(config, secretBox3);
7218
+ const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
7219
+ const keyDb = new JsonOutboundKeyDb(paths.keysPath);
7220
+ const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
7221
+ const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
7222
+ const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
7223
+ const subscriptionAccounts = new SubscriptionAccountService(credentialStore);
7224
+ setSubscriptionAccountService(subscriptionAccounts);
7225
+ const subscriptionRegistry = new SubscriptionProviderRegistry(
7226
+ subscriptionAccounts,
7227
+ credentialStore
7228
+ );
7229
+ setSubscriptionProviderRegistry(subscriptionRegistry);
7230
+ setServerProxyConfig(decryptedConfig.server?.proxy);
7231
+ setUpstreamProxyResolver(
7232
+ createUpstreamProxyResolver({
7233
+ getAccountProxy: (providerId, accountId) => credentialStore.getAccountProxy(providerId, accountId)
7234
+ })
7235
+ );
7236
+ setGeminiCodeAssistResolver(getGeminiCodeAssistProjectResolver());
7237
+ const autoDisableStore = new AutoDisableStore();
7238
+ const apiKeyPool = new ApiKeyPoolService(
7239
+ createPoolKeysLoader((id) => llmConfig.getProviderRow(id), autoDisableStore),
7240
+ resolveEnvKey,
7241
+ logger,
7242
+ async (keyId) => {
7243
+ autoDisableStore.markAutoDisabled(keyId, 0, Date.now());
7244
+ return true;
7245
+ },
7246
+ async (keyId, status, at) => {
7247
+ autoDisableStore.markAutoDisabled(keyId, status, at);
7248
+ }
7249
+ );
7250
+ const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
7251
+ const pricingEngine = new PricingEngine(pricingStore, logger);
7252
+ const usageEventStore = new JsonlUsageEventStore(
7253
+ defaultUsageEventsPath(paths.configPath),
7254
+ async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
7255
+ );
7256
+ const keySpendTracker = new KeySpendTracker(usageEventStore);
7257
+ const usageRecorder = new UsageRecorder(usageEventStore, pricingEngine, logger, {
7258
+ onRecord: (apiKeyId, costUsd, at) => keySpendTracker.add(apiKeyId, costUsd, at)
7259
+ });
7260
+ const providerProxy = getProviderProxy({ llmConfig, apiKeyPool, usageRecorder });
4490
7261
  llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
7262
+ const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
7263
+ credentialStore,
7264
+ getSharedAccountHealth2(),
7265
+ logger,
7266
+ DEFAULT_ACCOUNT_PROBE
7267
+ );
7268
+ const getHealthReport = () => buildHealthReport({
7269
+ version: DAEMON_VERSION,
7270
+ // CRITICAL: the config loaded with a providers array.
7271
+ configPresent: () => Array.isArray(decryptedConfig.providers),
7272
+ // CRITICAL: the credential store's tokens.json is readable WITHOUT
7273
+ // decrypting (a missing file is fine — no accounts yet). A stat/access
7274
+ // only; never reads or decrypts token material.
7275
+ credentialStoreReadable: () => isTokensStoreReadable(paths.tokensPath),
7276
+ outboundServerRunning: () => outboundApiServer.getStatus().running,
7277
+ adminServerRunning: () => adminServer.getStatus().running,
7278
+ // Coarse, account-anonymous probe signal (#8, D5) — added to `checks` ONLY
7279
+ // when probing is ENABLED; disabled ⇒ `undefined` ⇒ key omitted ⇒ the
7280
+ // `/health` body stays byte-identical (zero regression).
7281
+ subscriptionAccountsHealthy: () => accountHealthProbeScheduler.enabled ? accountHealthProbeScheduler.probedAccountsHealthy() : void 0
7282
+ });
4491
7283
  const outboundApiServer = getOutboundApiServer({
4492
7284
  db: keyDb,
7285
+ // voucher-redemption #9: the key-authenticated `POST /redeem` endpoint redeems
7286
+ // cards against the presenting key (gated on `voucher.enabled`).
7287
+ voucherDb,
4493
7288
  llmConfig,
4494
7289
  providerProxy,
4495
- proxyDeps: providerProxy.getDeps()
7290
+ proxyDeps: providerProxy.getDeps(),
7291
+ healthReportProvider: getHealthReport,
7292
+ // outbound-key-policy: the wire layer's 402 cost check reads per-key spend.
7293
+ keySpendTracker,
7294
+ // configurable-logging: route the server's OWN lifecycle + relay dispatch-error
7295
+ // lines through the injected logger (honors level/format/file sink).
7296
+ logger
4496
7297
  });
7298
+ const auditDir = defaultAuditDir(paths.configPath);
7299
+ const billingDir = defaultBillingDir(paths.configPath);
4497
7300
  const adminServer = new AdminServer({
4498
7301
  configPath: paths.configPath,
4499
7302
  llmConfig,
4500
7303
  keyDb,
7304
+ // voucher-redemption #9: the admin `/admin/api/voucher` surface generates/
7305
+ // lists/revokes redemption cards (gated on `voucher.enabled`).
7306
+ voucherDb,
7307
+ // outbound-key-policy: the admin key list surfaces each key's OWN spend.
7308
+ keySpendReader: keySpendTracker,
4501
7309
  settingsStore,
4502
7310
  outboundApiServer,
4503
7311
  subscriptionAccounts,
@@ -4519,14 +7327,16 @@ function buildDaemon(config, paths) {
4519
7327
  oauthSessions: new OAuthSessionStore(),
4520
7328
  // Real global fetch by default; a test seam (`paths.oauthExchangeFetch`) can
4521
7329
  // inject a mock so no real token endpoint is hit.
4522
- oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetch(url, init)),
7330
+ // upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
7331
+ // helper so interactive login honors a configured proxy (global/env layers).
7332
+ oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream6(url, init)),
4523
7333
  subscriptionAccountAppender: credentialStore,
4524
7334
  // Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
4525
7335
  // + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
4526
7336
  // the app polls the token-free status. A test seam (`paths.codexAwaitLoopback`)
4527
7337
  // can inject a mock so no real port is bound.
4528
7338
  codexSessions: new CodexOAuthSessionStore(),
4529
- codexAwaitLoopback: paths.codexAwaitLoopback ?? ((state, timeoutMs) => awaitLoopbackCode(state, timeoutMs)),
7339
+ codexAwaitLoopback: paths.codexAwaitLoopback ?? ((state, timeoutMs, signal) => awaitLoopbackCode(state, timeoutMs, signal)),
4530
7340
  // Migration pack (app-parity child 6, design D2/D3) — the concrete credential
4531
7341
  // store provides BOTH the full DECRYPTED read (`getFullConfig`, export) and
4532
7342
  // the multi-account append (`appendProviderAccount`, import re-encrypts at-
@@ -4546,9 +7356,48 @@ function buildDaemon(config, paths) {
4546
7356
  pricingStore,
4547
7357
  // Use the DECRYPTED config so `admin.token` (if stored as `enc:`) is the
4548
7358
  // plaintext bearer the AdminServer's constant-time compare expects (D4).
4549
- getAdminConfig: () => resolveAdminConfig(decryptedConfig.admin)
7359
+ getAdminConfig: () => resolveAdminConfig(decryptedConfig.admin),
7360
+ // Unauthenticated `/health` probe (daemon-health-endpoint) — the SAME shared
7361
+ // builder the outbound server uses, served before the admin auth gate.
7362
+ getHealthReport,
7363
+ // configurable-logging: the admin listener's lifecycle lines route through
7364
+ // the injected logger.
7365
+ logger,
7366
+ // subscription-account-probe #8: the AUTHED `GET /admin/api/account-probes`
7367
+ // reads per-account probe history from the scheduler (secret-free — ids +
7368
+ // status labels only). Routed in `AdminServer` (not `adminApi.ts`).
7369
+ probeHistoryReader: accountHealthProbeScheduler,
7370
+ // request-audit-log: the AUTHED `GET /admin/api/audit` reads + filters the
7371
+ // date-rotated audit store. Bound to the store dir here so the AdminServer
7372
+ // carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
7373
+ // NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
7374
+ auditReader: (query) => readAuditRecords(auditDir, query),
7375
+ // billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
7376
+ // secret-free total/delivered/pending counts of the durable ledger.
7377
+ billingStatusReader: () => readBillingStatus(billingDir)
4550
7378
  });
7379
+ const webhookDispatcher = new WebhookDispatcher({
7380
+ logger,
7381
+ fetchImpl: (url, init) => fetchUpstream6(url, init)
7382
+ });
7383
+ setWebhookRuntime(webhookDispatcher, getSharedAccountHealth2());
7384
+ const auditWriter = new AuditWriter(auditDir, logger);
7385
+ const auditPruneSweeper = new AuditPruneSweeper(auditDir, logger, DEFAULT_AUDIT_CONFIG);
7386
+ setAuditRuntime(auditWriter, auditPruneSweeper);
7387
+ const billingPublisher = new BillingPublisher(billingDir, logger);
7388
+ const billingRetrySweeper = new BillingRetrySweeper(
7389
+ billingDir,
7390
+ billingPublisher,
7391
+ logger,
7392
+ DEFAULT_BILLING_CONFIG
7393
+ );
7394
+ setBillingRuntime(billingPublisher, billingRetrySweeper);
4551
7395
  const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
7396
+ const accountHealthSweeper = new AccountHealthSweeper(
7397
+ credentialStore,
7398
+ getSharedAccountHealth2(),
7399
+ logger
7400
+ );
4552
7401
  return {
4553
7402
  logger,
4554
7403
  llmConfig,
@@ -4565,7 +7414,14 @@ function buildDaemon(config, paths) {
4565
7414
  pricingEngine,
4566
7415
  usageRecorder,
4567
7416
  adminServer,
4568
- tokenRefreshScheduler
7417
+ tokenRefreshScheduler,
7418
+ accountHealthSweeper,
7419
+ accountHealthProbeScheduler,
7420
+ webhookDispatcher,
7421
+ auditWriter,
7422
+ auditPruneSweeper,
7423
+ billingPublisher,
7424
+ billingRetrySweeper
4569
7425
  };
4570
7426
  }
4571
7427
  function resetDaemonSingletonsForTests() {
@@ -4574,11 +7430,47 @@ function resetDaemonSingletonsForTests() {
4574
7430
  setSubscriptionRegistryForOutbound(null);
4575
7431
  setSubscriptionProviderRegistry(null);
4576
7432
  setSubscriptionAccountService(null);
7433
+ setUpstreamProxyResolver(null);
7434
+ setServerProxyConfig(void 0);
4577
7435
  setGeminiCodeAssistResolver(null);
4578
7436
  setSecretBox(null);
4579
7437
  setSecretBox2(null);
7438
+ resetWebhookRuntimeForTests();
7439
+ resetAuditRuntimeForTests();
7440
+ resetBillingRuntimeForTests();
7441
+ __resetSharedIdentityStoreForTests();
7442
+ }
7443
+ function isTokensStoreReadable(tokensPath) {
7444
+ try {
7445
+ if (!existsSync14(tokensPath)) return true;
7446
+ accessSync(tokensPath, fsConstants.R_OK);
7447
+ return true;
7448
+ } catch {
7449
+ return false;
7450
+ }
4580
7451
  }
4581
7452
 
7453
+ // src/ports/ConsoleLogger.ts
7454
+ var ConsoleLogger = class {
7455
+ info(message, meta) {
7456
+ if (meta === void 0) console.info(message);
7457
+ else console.info(message, meta);
7458
+ }
7459
+ warn(message, meta) {
7460
+ if (meta === void 0) console.warn(message);
7461
+ else console.warn(message, meta);
7462
+ }
7463
+ error(message, error, meta) {
7464
+ if (error === void 0 && meta === void 0) console.error(message);
7465
+ else if (meta === void 0) console.error(message, error);
7466
+ else console.error(message, error, meta);
7467
+ }
7468
+ debug(message, meta) {
7469
+ if (meta === void 0) console.debug(message);
7470
+ else console.debug(message, meta);
7471
+ }
7472
+ };
7473
+
4582
7474
  // src/ccr-import.ts
4583
7475
  function parseCcrConfig(raw) {
4584
7476
  if (!raw || typeof raw !== "object") {
@@ -4653,12 +7545,14 @@ function mapCcrToOmnicross(ccr) {
4653
7545
  export {
4654
7546
  AdminServer,
4655
7547
  ConfigFileProviderConfigSource,
7548
+ ConfigurableLogger,
4656
7549
  ConsoleLogger,
4657
7550
  DEFAULT_ADMIN_PORT,
4658
7551
  JsonApiServerSettingsStore,
4659
7552
  JsonOutboundKeyDb,
4660
7553
  JsonSubscriptionCredentialStore,
4661
7554
  buildDaemon,
7555
+ buildHealthReport,
4662
7556
  handleAdminApi,
4663
7557
  inferApiFormat,
4664
7558
  loadConfig,