@omnicross/daemon 0.1.2 → 0.1.3

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