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