@omnicross/daemon 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -22,6 +22,9 @@ function inferApiFormat(provider) {
22
22
  if (hay.includes("generativelanguage") || hay.includes("gemini") || hay.includes("google")) {
23
23
  return { format: "gemini", ambiguous: false };
24
24
  }
25
+ if (hay.includes("/responses")) {
26
+ return { format: "openai-response", ambiguous: false };
27
+ }
25
28
  if (hay.includes("openai") || hay.includes("/v1") || hay.includes("chat/completions")) {
26
29
  return { format: "openai", ambiguous: false };
27
30
  }
@@ -463,7 +466,19 @@ function validateLogging(raw) {
463
466
  if (typeof l["file"] === "string" && l["file"].length > 0) out.file = l["file"];
464
467
  return out.level !== void 0 || out.format !== void 0 || out.file !== void 0 ? out : void 0;
465
468
  }
466
- var VALID_FORMATS = ["openai", "anthropic", "gemini"];
469
+ var VALID_FORMATS = [
470
+ "openai",
471
+ "anthropic",
472
+ "gemini",
473
+ "openai-response"
474
+ ];
475
+ var FORMAT_AXIS_TRANSFORMERS = [
476
+ "openai",
477
+ "anthropic",
478
+ "gemini",
479
+ "openai-response",
480
+ "gemini-code-assist"
481
+ ];
467
482
  function validateApiKeys(raw) {
468
483
  if (!Array.isArray(raw)) return void 0;
469
484
  const out = [];
@@ -568,6 +583,33 @@ function validateApiModes(raw) {
568
583
  }
569
584
  return out.length > 0 ? out : void 0;
570
585
  }
586
+ function transformerEntryName(entry) {
587
+ return typeof entry === "string" ? entry : entry[0];
588
+ }
589
+ function migrateFormatAxis(apiFormat, transformer) {
590
+ const use = transformer?.use;
591
+ if (!use || use.length === 0) return { apiFormat, transformer };
592
+ const hasFormatEntry = use.some((e) => FORMAT_AXIS_TRANSFORMERS.includes(transformerEntryName(e)));
593
+ if (!hasFormatEntry) return { apiFormat, transformer };
594
+ let migratedFormat = apiFormat;
595
+ if (apiFormat === "openai") {
596
+ const promoted = use.map(transformerEntryName).find((n) => VALID_FORMATS.includes(n));
597
+ if (promoted) migratedFormat = promoted;
598
+ }
599
+ const rest = use.filter((e) => !FORMAT_AXIS_TRANSFORMERS.includes(transformerEntryName(e)));
600
+ const next = {};
601
+ let kept = false;
602
+ if (rest.length > 0) {
603
+ next.use = rest;
604
+ kept = true;
605
+ }
606
+ for (const key of Object.keys(transformer)) {
607
+ if (key === "use") continue;
608
+ next[key] = transformer[key];
609
+ kept = true;
610
+ }
611
+ return { apiFormat: migratedFormat, transformer: kept ? next : void 0 };
612
+ }
571
613
  function validateProvider(raw, index) {
572
614
  if (!raw || typeof raw !== "object") {
573
615
  throw new Error(`config: providers[${index}] is not an object`);
@@ -598,10 +640,14 @@ function validateProvider(raw, index) {
598
640
  const apiVersion = typeof p["apiVersion"] === "string" && p["apiVersion"].length > 0 ? p["apiVersion"] : void 0;
599
641
  const maxConcurrency = typeof p["maxConcurrency"] === "number" && Number.isFinite(p["maxConcurrency"]) ? p["maxConcurrency"] : void 0;
600
642
  const modelsEndpoint = typeof p["modelsEndpoint"] === "string" && p["modelsEndpoint"].length > 0 ? p["modelsEndpoint"] : void 0;
643
+ const { apiFormat: migratedFormat, transformer: migratedTransformer } = migrateFormatAxis(
644
+ apiFormat,
645
+ validateTransformer(p["transformer"])
646
+ );
601
647
  return {
602
648
  id,
603
649
  name,
604
- apiFormat,
650
+ apiFormat: migratedFormat,
605
651
  baseUrl,
606
652
  apiKey,
607
653
  models: Array.isArray(models) ? models.filter((m) => typeof m === "string") : void 0,
@@ -615,7 +661,9 @@ function validateProvider(raw, index) {
615
661
  modelsEndpoint,
616
662
  // Provider transformer config (app-parity child 5): load-guard, collapse-to-
617
663
  // undefined; non-secret; ENFORCED via resolveTransformerChain (parity-2 child 2).
618
- transformer: validateTransformer(p["transformer"]),
664
+ // Format-axis entries are stripped by `migrateFormatAxis` — `use[]` is the
665
+ // MODIFIER axis only.
666
+ transformer: migratedTransformer,
619
667
  // Coding-plan endpoint (app-parity-2 child 3): load-guard, collapse-to-undefined.
620
668
  // SECRET-bearing (apiKey encrypted at rest); enforced by core's resolveProviderEndpoint.
621
669
  codingPlan: validateCodingPlan(p["codingPlan"]),
@@ -676,9 +724,18 @@ function defaultVouchersPath(configPath) {
676
724
  function defaultTokensPath(configPath) {
677
725
  return join2(dirname2(configPath), "tokens.json");
678
726
  }
727
+ function defaultIntegrationsPath(configPath) {
728
+ return join2(dirname2(configPath), "integrations.json");
729
+ }
679
730
  function defaultPricingPath(configPath) {
680
731
  return join2(dirname2(configPath), "pricing.json");
681
732
  }
733
+ function defaultPricingRefreshStatePath(configPath) {
734
+ return join2(dirname2(configPath), "pricing-refresh.json");
735
+ }
736
+ function defaultAccountAllowancePath(configPath) {
737
+ return join2(dirname2(configPath), "allowance-cache.json");
738
+ }
682
739
  function defaultUsageEventsPath(configPath) {
683
740
  return join2(dirname2(configPath), "usage-events.jsonl");
684
741
  }
@@ -729,12 +786,712 @@ async function runImportCcr(argv) {
729
786
  }
730
787
  }
731
788
 
732
- // src/commands/keys.ts
789
+ // src/commands/integrations.ts
790
+ import { resolve as resolve2 } from "path";
733
791
  import { parseArgs as parseArgs2 } from "util";
734
- import { createNamedKey } from "@omnicross/core/outbound-api";
792
+
793
+ // src/integrations/IntegrationManager.ts
794
+ import { createHash } from "crypto";
795
+ import { existsSync as existsSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
796
+ import { homedir as homedir2 } from "os";
797
+ import { dirname as dirname4, join as join3, resolve } from "path";
798
+ import { createIntegrationKey } from "@omnicross/core";
799
+
800
+ // src/integrations/IntegrationStateStore.ts
801
+ import {
802
+ chmodSync as chmodSync2,
803
+ existsSync as existsSync2,
804
+ mkdirSync as mkdirSync2,
805
+ readFileSync as readFileSync4,
806
+ renameSync,
807
+ unlinkSync,
808
+ writeFileSync as writeFileSync3
809
+ } from "fs";
810
+ import { dirname as dirname3 } from "path";
811
+ var EMPTY_STATE = { version: 1, clients: {} };
812
+ var IntegrationStateStore = class {
813
+ constructor(path2, box) {
814
+ this.path = path2;
815
+ this.box = box;
816
+ }
817
+ path;
818
+ box;
819
+ load() {
820
+ if (!existsSync2(this.path)) return { ...EMPTY_STATE, clients: {} };
821
+ let raw;
822
+ try {
823
+ raw = JSON.parse(readFileSync4(this.path, "utf8"));
824
+ } catch {
825
+ throw new Error(`integration state '${this.path}' is not valid JSON`);
826
+ }
827
+ if (!isState(raw)) {
828
+ throw new Error(`integration state '${this.path}' has an unsupported shape`);
829
+ }
830
+ return {
831
+ version: 1,
832
+ gatewayKey: raw.gatewayKey ? { ...raw.gatewayKey, secret: this.box.decryptMaybe(raw.gatewayKey.secret) } : void 0,
833
+ clients: decryptClients(raw.clients, this.box)
834
+ };
835
+ }
836
+ save(state) {
837
+ const encrypted = {
838
+ version: 1,
839
+ gatewayKey: state.gatewayKey ? { ...state.gatewayKey, secret: this.box.encrypt(state.gatewayKey.secret) } : void 0,
840
+ clients: encryptClients(state.clients, this.box)
841
+ };
842
+ atomicWrite(this.path, JSON.stringify(encrypted, null, 2) + "\n");
843
+ }
844
+ };
845
+ function transformClients(clients, transform) {
846
+ const out = {};
847
+ for (const client of ["codex", "claude"]) {
848
+ const row = clients[client];
849
+ if (row) {
850
+ out[client] = {
851
+ ...row,
852
+ originalContent: transform(row.originalContent),
853
+ credentialFile: row.credentialFile ? { ...row.credentialFile, originalContent: transform(row.credentialFile.originalContent) } : void 0
854
+ };
855
+ }
856
+ }
857
+ return out;
858
+ }
859
+ function decryptClients(clients, box) {
860
+ return transformClients(clients, (value) => box.decryptMaybe(value));
861
+ }
862
+ function encryptClients(clients, box) {
863
+ return transformClients(clients, (value) => box.encrypt(value));
864
+ }
865
+ function isState(value) {
866
+ if (!value || typeof value !== "object") return false;
867
+ const row = value;
868
+ if (row.version !== 1 || !row.clients || typeof row.clients !== "object") return false;
869
+ if (row.gatewayKey !== void 0) {
870
+ const key = row.gatewayKey;
871
+ if (!key || typeof key !== "object" || typeof key.id !== "string" || typeof key.secret !== "string" || typeof key.createdAt !== "number") return false;
872
+ }
873
+ for (const client of ["codex", "claude"]) {
874
+ const candidate = row.clients[client];
875
+ if (candidate === void 0) continue;
876
+ if (!isInstallRecord(candidate, client)) return false;
877
+ }
878
+ return true;
879
+ }
880
+ function isInstallRecord(value, client) {
881
+ if (!value || typeof value !== "object") return false;
882
+ const row = value;
883
+ return row.client === client && typeof row.configPath === "string" && typeof row.originalExisted === "boolean" && typeof row.originalContent === "string" && typeof row.originalHash === "string" && typeof row.installedHash === "string" && typeof row.installedAt === "number" && typeof row.gatewayBaseUrl === "string" && (row.credentialFile === void 0 || isManagedFileRecord(row.credentialFile));
884
+ }
885
+ function isManagedFileRecord(value) {
886
+ if (!value || typeof value !== "object") return false;
887
+ const row = value;
888
+ return typeof row.path === "string" && typeof row.originalExisted === "boolean" && typeof row.originalContent === "string" && typeof row.originalHash === "string" && typeof row.installedHash === "string";
889
+ }
890
+ function atomicWrite(path2, content) {
891
+ mkdirSync2(dirname3(path2), { recursive: true });
892
+ const temp = `${path2}.tmp-${process.pid}-${Date.now()}`;
893
+ writeFileSync3(temp, content, { encoding: "utf8", mode: 384 });
894
+ try {
895
+ renameSync(temp, path2);
896
+ } catch (error) {
897
+ try {
898
+ unlinkSync(temp);
899
+ } catch {
900
+ }
901
+ throw error;
902
+ } finally {
903
+ if (existsSync2(path2)) {
904
+ try {
905
+ chmodSync2(path2, 384);
906
+ } catch {
907
+ }
908
+ }
909
+ }
910
+ }
911
+
912
+ // src/integrations/configAdapters.ts
913
+ var CODEX_BEGIN = "# >>> omnicross managed provider >>>";
914
+ var CODEX_END = "# <<< omnicross managed provider <<<";
915
+ var CODEX_PROVIDER = "omnicross";
916
+ var CLAUDE_API_KEY_SENTINEL = "omnicross-gateway";
917
+ function renderCodexConfig(input) {
918
+ if (input.existing.includes(CODEX_BEGIN) || input.existing.includes(CODEX_END)) {
919
+ throw new Error("Codex config contains an unmanaged/orphaned Omnicross marker");
920
+ }
921
+ if (/^\s*\[\s*model_providers\s*\.\s*["']?omnicross["']?\s*]/m.test(input.existing)) {
922
+ throw new Error("Codex config already defines model_providers.omnicross");
923
+ }
924
+ const eol = input.existing.includes("\r\n") ? "\r\n" : "\n";
925
+ const lines = input.existing.replace(/\r\n/g, "\n").split("\n");
926
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
927
+ const rootEnd = firstTable < 0 ? lines.length : firstTable;
928
+ const assignments = {
929
+ model_provider: [],
930
+ preferred_auth_method: []
931
+ };
932
+ for (let index = 0; index < rootEnd; index += 1) {
933
+ if (/^\s*#/.test(lines[index])) continue;
934
+ for (const key of Object.keys(assignments)) {
935
+ if (new RegExp(`^\\s*${key}\\s*=`).test(lines[index])) assignments[key].push(index);
936
+ }
937
+ }
938
+ if (assignments.model_provider.length > 1) {
939
+ throw new Error("Codex config has duplicate top-level model_provider keys");
940
+ }
941
+ if (assignments.preferred_auth_method.length > 1) {
942
+ throw new Error("Codex config has duplicate top-level preferred_auth_method keys");
943
+ }
944
+ const managedRoot = {
945
+ model_provider: `model_provider = "${CODEX_PROVIDER}" # managed by Omnicross`,
946
+ preferred_auth_method: 'preferred_auth_method = "apikey" # managed by Omnicross'
947
+ };
948
+ const missing = [];
949
+ for (const key of Object.keys(assignments)) {
950
+ const [index] = assignments[key];
951
+ if (index === void 0) missing.push(managedRoot[key]);
952
+ else lines[index] = managedRoot[key];
953
+ }
954
+ if (missing.length > 0) lines.splice(rootEnd, 0, ...missing, "");
955
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
956
+ const base = lines.length > 0 ? `${lines.join("\n")}
957
+
958
+ ` : "";
959
+ const root = trimTrailingSlash(input.gatewayBaseUrl);
960
+ const block = [
961
+ CODEX_BEGIN,
962
+ `[model_providers.${CODEX_PROVIDER}]`,
963
+ 'name = "Omnicross Local Gateway"',
964
+ `base_url = ${tomlString(`${root}/v1`)}`,
965
+ 'wire_api = "responses"',
966
+ "requires_openai_auth = true",
967
+ "supports_websockets = false",
968
+ CODEX_END,
969
+ ""
970
+ ].join("\n");
971
+ return (base + block).replace(/\n/g, eol);
972
+ }
973
+ function renderCodexAuth(secret) {
974
+ return JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: secret }, null, 2) + "\n";
975
+ }
976
+ function renderClaudeSettings(existing, gatewayBaseUrl, secret) {
977
+ let parsed = {};
978
+ if (existing.trim()) {
979
+ try {
980
+ parsed = JSON.parse(existing);
981
+ } catch {
982
+ throw new Error("Claude settings file is not valid JSON");
983
+ }
984
+ }
985
+ if (!isPlainObject(parsed)) throw new Error("Claude settings root must be a JSON object");
986
+ const settings = { ...parsed };
987
+ const oldEnv = settings.env;
988
+ if (oldEnv !== void 0 && !isPlainObject(oldEnv)) {
989
+ throw new Error("Claude settings env field must be a JSON object");
990
+ }
991
+ settings.env = {
992
+ ...oldEnv,
993
+ ANTHROPIC_BASE_URL: trimTrailingSlash(gatewayBaseUrl),
994
+ ANTHROPIC_AUTH_TOKEN: secret,
995
+ ANTHROPIC_API_KEY: CLAUDE_API_KEY_SENTINEL
996
+ };
997
+ return JSON.stringify(settings, null, 2) + "\n";
998
+ }
999
+ function restoreCodexBase(current, original) {
1000
+ const hasBegin = current.includes(CODEX_BEGIN);
1001
+ const hasEnd = current.includes(CODEX_END);
1002
+ if (hasBegin !== hasEnd) throw new Error("Codex config has an incomplete Omnicross managed block");
1003
+ const eol = current.includes("\r\n") ? "\r\n" : "\n";
1004
+ let normalized = current.replace(/\r\n/g, "\n");
1005
+ if (hasBegin) {
1006
+ const start = normalized.indexOf(CODEX_BEGIN);
1007
+ const endMarker = normalized.indexOf(CODEX_END, start);
1008
+ if (endMarker < 0) throw new Error("Codex config has an incomplete Omnicross managed block");
1009
+ const end = normalized.indexOf("\n", endMarker);
1010
+ normalized = normalized.slice(0, start) + (end < 0 ? "" : normalized.slice(end + 1));
1011
+ }
1012
+ const lines = normalized.split("\n");
1013
+ for (const key of ["model_provider", "preferred_auth_method"]) {
1014
+ const originalAssignment = rootAssignment(original, key);
1015
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
1016
+ const rootEnd = firstTable < 0 ? lines.length : firstTable;
1017
+ const managedIndex = lines.slice(0, rootEnd).findIndex(
1018
+ (line) => new RegExp(`^\\s*${key}\\s*=.*#\\s*managed by Omnicross\\s*$`).test(line)
1019
+ );
1020
+ if (managedIndex >= 0) {
1021
+ if (originalAssignment) lines[managedIndex] = originalAssignment;
1022
+ else lines.splice(managedIndex, 1);
1023
+ }
1024
+ }
1025
+ return lines.join("\n").replace(/\n/g, eol);
1026
+ }
1027
+ function restoreClaudeBase(current, original, gatewayBaseUrl, secret) {
1028
+ const currentRoot = parseSettings(current);
1029
+ const originalRoot = parseSettings(original);
1030
+ const env = isPlainObject(currentRoot.env) ? { ...currentRoot.env } : {};
1031
+ const originalEnv = isPlainObject(originalRoot.env) ? originalRoot.env : {};
1032
+ const expected = {
1033
+ ANTHROPIC_BASE_URL: trimTrailingSlash(gatewayBaseUrl),
1034
+ ANTHROPIC_AUTH_TOKEN: secret,
1035
+ ANTHROPIC_API_KEY: CLAUDE_API_KEY_SENTINEL
1036
+ };
1037
+ for (const [key, value] of Object.entries(expected)) {
1038
+ if (env[key] !== value) continue;
1039
+ if (Object.prototype.hasOwnProperty.call(originalEnv, key)) env[key] = originalEnv[key];
1040
+ else delete env[key];
1041
+ }
1042
+ const next = { ...currentRoot };
1043
+ if (Object.keys(env).length > 0 || Object.prototype.hasOwnProperty.call(originalRoot, "env")) next.env = env;
1044
+ else delete next.env;
1045
+ return JSON.stringify(next, null, 2) + "\n";
1046
+ }
1047
+ function tomlString(value) {
1048
+ return JSON.stringify(value);
1049
+ }
1050
+ function trimTrailingSlash(value) {
1051
+ return value.replace(/\/+$/, "");
1052
+ }
1053
+ function isPlainObject(value) {
1054
+ return !!value && typeof value === "object" && !Array.isArray(value);
1055
+ }
1056
+ function parseSettings(value) {
1057
+ if (!value.trim()) return {};
1058
+ let parsed;
1059
+ try {
1060
+ parsed = JSON.parse(value);
1061
+ } catch {
1062
+ throw new Error("Claude settings file is not valid JSON");
1063
+ }
1064
+ if (!isPlainObject(parsed)) throw new Error("Claude settings root must be a JSON object");
1065
+ return parsed;
1066
+ }
1067
+ function rootAssignment(content, key) {
1068
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
1069
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
1070
+ const root = lines.slice(0, firstTable < 0 ? lines.length : firstTable);
1071
+ return root.find((line) => new RegExp(`^\\s*${key}\\s*=`).test(line) && !/^\s*#/.test(line));
1072
+ }
1073
+
1074
+ // src/integrations/IntegrationManager.ts
1075
+ var IntegrationConflictError = class extends Error {
1076
+ constructor(message) {
1077
+ super(message);
1078
+ this.name = "IntegrationConflictError";
1079
+ }
1080
+ };
1081
+ var IntegrationManager = class {
1082
+ constructor(options) {
1083
+ this.options = options;
1084
+ assertLoopbackGatewayUrl(options.gatewayBaseUrl);
1085
+ this.homeDir = options.homeDir ?? homedir2();
1086
+ }
1087
+ options;
1088
+ homeDir;
1089
+ async listStatus() {
1090
+ const state = this.options.stateStore.load();
1091
+ const keyUsable = await this.isKeyUsable(state);
1092
+ return ["codex", "claude"].map((client) => this.statusFor(client, state, keyUsable));
1093
+ }
1094
+ async plan(client, configPath = this.defaultConfigPath(client)) {
1095
+ const state = this.options.stateStore.load();
1096
+ const record = state.clients[client];
1097
+ const target = record?.configPath ?? resolve(configPath);
1098
+ const status = this.statusFor(client, state, await this.isKeyUsable(state));
1099
+ const changes = client === "codex" ? [
1100
+ "model_provider",
1101
+ "preferred_auth_method",
1102
+ "model_providers.omnicross",
1103
+ "auth.json.auth_mode",
1104
+ "auth.json.OPENAI_API_KEY"
1105
+ ] : ["env.ANTHROPIC_BASE_URL", "env.ANTHROPIC_AUTH_TOKEN", "env.ANTHROPIC_API_KEY"];
1106
+ if (!record) return { client, configPath: target, action: "install", canApply: true, changes, warnings: [] };
1107
+ if (status.status === "enabled") {
1108
+ return { client, configPath: target, action: "none", canApply: true, changes: [], warnings: [] };
1109
+ }
1110
+ return {
1111
+ client,
1112
+ configPath: target,
1113
+ action: "repair",
1114
+ canApply: true,
1115
+ changes,
1116
+ warnings: ["Configuration changed after installation; repair preserves unrelated current settings."]
1117
+ };
1118
+ }
1119
+ async install(client, configPath = this.defaultConfigPath(client)) {
1120
+ const target = resolve(configPath);
1121
+ const state = this.options.stateStore.load();
1122
+ const existingRecord = state.clients[client];
1123
+ if (existingRecord) {
1124
+ const status = this.statusFor(client, state, await this.isKeyUsable(state));
1125
+ if (status.status === "enabled") return status;
1126
+ throw new IntegrationConflictError(
1127
+ `${client} integration configuration has drifted; restore or remove it before reinstalling`
1128
+ );
1129
+ }
1130
+ const key = await this.ensureGatewayKey(state);
1131
+ const original = readOptional(target);
1132
+ const originalContent = original ?? "";
1133
+ const installed = this.renderInstalled(client, originalContent, key.secret);
1134
+ const credentialPath = client === "codex" ? this.codexAuthPathForConfig(target) : void 0;
1135
+ const originalCredential = credentialPath ? readOptional(credentialPath) : null;
1136
+ const installedCredential = credentialPath ? renderCodexAuth(key.secret) : void 0;
1137
+ const record = {
1138
+ client,
1139
+ configPath: target,
1140
+ originalExisted: original !== null,
1141
+ originalContent,
1142
+ originalHash: sha256(originalContent),
1143
+ installedHash: sha256(installed),
1144
+ installedAt: Date.now(),
1145
+ gatewayBaseUrl: this.options.gatewayBaseUrl,
1146
+ credentialFile: credentialPath && installedCredential ? managedFileRecord(credentialPath, originalCredential, installedCredential) : void 0
1147
+ };
1148
+ const prior = state.clients[client];
1149
+ state.clients[client] = record;
1150
+ this.options.stateStore.save(state);
1151
+ try {
1152
+ applyFileChangesWithRollback([
1153
+ { path: target, content: installed },
1154
+ ...credentialPath && installedCredential ? [{ path: credentialPath, content: installedCredential }] : []
1155
+ ]);
1156
+ } catch (error) {
1157
+ if (prior) state.clients[client] = prior;
1158
+ else delete state.clients[client];
1159
+ this.options.stateStore.save(state);
1160
+ throw error;
1161
+ }
1162
+ return this.statusFor(client, state, true);
1163
+ }
1164
+ async repair(client) {
1165
+ const state = this.options.stateStore.load();
1166
+ const record = state.clients[client];
1167
+ if (!record) return this.install(client);
1168
+ const previouslyInstalledSecret = state.gatewayKey?.secret;
1169
+ const currentFile = readOptional(record.configPath);
1170
+ if (client === "claude" && currentFile !== null && !previouslyInstalledSecret) {
1171
+ throw new IntegrationConflictError(
1172
+ "Claude integration key state is missing; refusing to repair an ambiguous settings file"
1173
+ );
1174
+ }
1175
+ const key = await this.ensureGatewayKey(state);
1176
+ const current = currentFile ?? record.originalContent;
1177
+ const base = client === "codex" ? restoreCodexBase(current, record.originalContent) : restoreClaudeBase(
1178
+ current,
1179
+ record.originalContent,
1180
+ record.gatewayBaseUrl,
1181
+ previouslyInstalledSecret ?? key.secret
1182
+ );
1183
+ const installed = this.renderInstalled(client, base, key.secret);
1184
+ const credentialPath = client === "codex" ? record.credentialFile?.path ?? this.codexAuthPathForConfig(record.configPath) : void 0;
1185
+ const currentCredential = credentialPath ? readOptional(credentialPath) : null;
1186
+ const originalCredential = record.credentialFile ? originalSnapshotForRepair(record.credentialFile, currentCredential) : currentCredential;
1187
+ const installedCredential = credentialPath ? renderCodexAuth(key.secret) : void 0;
1188
+ const prior = {
1189
+ ...record,
1190
+ credentialFile: record.credentialFile ? { ...record.credentialFile } : void 0
1191
+ };
1192
+ Object.assign(record, {
1193
+ originalExisted: currentFile !== null || record.originalExisted,
1194
+ originalContent: base,
1195
+ originalHash: sha256(base),
1196
+ installedHash: sha256(installed),
1197
+ installedAt: Date.now(),
1198
+ gatewayBaseUrl: this.options.gatewayBaseUrl,
1199
+ credentialFile: credentialPath && installedCredential ? managedFileRecord(credentialPath, originalCredential, installedCredential) : void 0
1200
+ });
1201
+ this.options.stateStore.save(state);
1202
+ try {
1203
+ applyFileChangesWithRollback([
1204
+ { path: record.configPath, content: installed },
1205
+ ...credentialPath && installedCredential ? [{ path: credentialPath, content: installedCredential }] : []
1206
+ ]);
1207
+ } catch (error) {
1208
+ state.clients[client] = prior;
1209
+ this.options.stateStore.save(state);
1210
+ throw error;
1211
+ }
1212
+ return this.statusFor(client, state, true);
1213
+ }
1214
+ async remove(client) {
1215
+ const state = this.options.stateStore.load();
1216
+ const record = state.clients[client];
1217
+ if (!record) return this.statusFor(client, state, await this.isKeyUsable(state));
1218
+ const files = [primaryManagedFile(record), ...record.credentialFile ? [record.credentialFile] : []];
1219
+ const currentFiles = files.map((file) => ({ file, current: readOptional(file.path) }));
1220
+ const dispositions = currentFiles.map(({ file, current }) => managedFileDisposition(file, current));
1221
+ if (dispositions.some((disposition) => disposition !== "installed" && disposition !== "restored")) {
1222
+ throw new IntegrationConflictError(
1223
+ `${client} configuration changed after Omnicross installed it; refusing to overwrite user edits`
1224
+ );
1225
+ }
1226
+ const changes = currentFiles.flatMap(({ file }, index) => dispositions[index] === "installed" ? [{ path: file.path, content: file.originalExisted ? file.originalContent : null }] : []);
1227
+ applyFileChangesWithRollback(changes);
1228
+ delete state.clients[client];
1229
+ this.options.stateStore.save(state);
1230
+ return this.statusFor(client, state, await this.isKeyUsable(state));
1231
+ }
1232
+ async rotateGatewayKey() {
1233
+ const state = this.options.stateStore.load();
1234
+ const previousGatewayKey = state.gatewayKey;
1235
+ const oldKeyId = state.gatewayKey?.id;
1236
+ const claude = state.clients.claude;
1237
+ const codex = state.clients.codex;
1238
+ let nextClaude;
1239
+ let nextCodexAuth;
1240
+ if (claude) {
1241
+ const current = readOptional(claude.configPath);
1242
+ if (current === null || sha256(current) !== claude.installedHash) {
1243
+ throw new IntegrationConflictError("Claude configuration drift must be resolved before key rotation");
1244
+ }
1245
+ }
1246
+ if (codex) {
1247
+ const current = readOptional(codex.configPath);
1248
+ if (current === null || sha256(current) !== codex.installedHash) {
1249
+ throw new IntegrationConflictError("Codex configuration drift must be resolved before key rotation");
1250
+ }
1251
+ if (!codex.credentialFile) {
1252
+ throw new IntegrationConflictError("Codex integration must be repaired before key rotation");
1253
+ }
1254
+ const currentAuth = readOptional(codex.credentialFile.path);
1255
+ if (currentAuth === null || sha256(currentAuth) !== codex.credentialFile.installedHash) {
1256
+ throw new IntegrationConflictError("Codex credential drift must be resolved before key rotation");
1257
+ }
1258
+ }
1259
+ const created = await createIntegrationKey(this.options.keyDb, "Omnicross native CLI integration");
1260
+ const nextGatewayKey = {
1261
+ id: created.id,
1262
+ secret: created.plaintextOnce,
1263
+ createdAt: created.createdAt
1264
+ };
1265
+ state.gatewayKey = nextGatewayKey;
1266
+ const previousClaudeHash = claude?.installedHash;
1267
+ const previousCodexAuthHash = codex?.credentialFile?.installedHash;
1268
+ if (claude) {
1269
+ const current = readOptional(claude.configPath) ?? "{}";
1270
+ nextClaude = renderClaudeSettings(current, this.options.gatewayBaseUrl, created.plaintextOnce);
1271
+ claude.installedHash = sha256(nextClaude);
1272
+ }
1273
+ if (codex?.credentialFile) {
1274
+ nextCodexAuth = renderCodexAuth(created.plaintextOnce);
1275
+ codex.credentialFile.installedHash = sha256(nextCodexAuth);
1276
+ }
1277
+ try {
1278
+ this.options.stateStore.save(state);
1279
+ applyFileChangesWithRollback([
1280
+ ...codex?.credentialFile && nextCodexAuth !== void 0 ? [{ path: codex.credentialFile.path, content: nextCodexAuth }] : [],
1281
+ ...claude && nextClaude !== void 0 ? [{ path: claude.configPath, content: nextClaude }] : []
1282
+ ]);
1283
+ } catch (error) {
1284
+ state.gatewayKey = previousGatewayKey;
1285
+ if (claude && previousClaudeHash !== void 0) claude.installedHash = previousClaudeHash;
1286
+ if (codex?.credentialFile && previousCodexAuthHash !== void 0) {
1287
+ codex.credentialFile.installedHash = previousCodexAuthHash;
1288
+ }
1289
+ try {
1290
+ this.options.stateStore.save(state);
1291
+ } finally {
1292
+ await this.options.keyDb.outboundApiKeysRevoke(created.id);
1293
+ }
1294
+ throw error;
1295
+ }
1296
+ if (oldKeyId && oldKeyId !== created.id) await this.options.keyDb.outboundApiKeysRevoke(oldKeyId);
1297
+ return { keyId: created.id };
1298
+ }
1299
+ async getGatewayToken() {
1300
+ const state = this.options.stateStore.load();
1301
+ if (!state.gatewayKey || !await this.isKeyUsable(state)) {
1302
+ throw new Error("Omnicross integration key is missing or revoked; reinstall the CLI integration");
1303
+ }
1304
+ return state.gatewayKey.secret;
1305
+ }
1306
+ async ensureGatewayKey(state) {
1307
+ if (state.gatewayKey && await this.isKeyUsable(state)) return state.gatewayKey;
1308
+ const created = await createIntegrationKey(this.options.keyDb, "Omnicross native CLI integration");
1309
+ const previousGatewayKey = state.gatewayKey;
1310
+ const nextGatewayKey = {
1311
+ id: created.id,
1312
+ secret: created.plaintextOnce,
1313
+ createdAt: created.createdAt
1314
+ };
1315
+ state.gatewayKey = nextGatewayKey;
1316
+ try {
1317
+ this.options.stateStore.save(state);
1318
+ return nextGatewayKey;
1319
+ } catch (error) {
1320
+ state.gatewayKey = previousGatewayKey;
1321
+ try {
1322
+ await this.options.keyDb.outboundApiKeysRevoke(created.id);
1323
+ } catch {
1324
+ }
1325
+ throw error;
1326
+ }
1327
+ }
1328
+ async isKeyUsable(state) {
1329
+ if (!state.gatewayKey) return false;
1330
+ const rows = await this.options.keyDb.outboundApiKeysList();
1331
+ return rows.some((row) => row.id === state.gatewayKey?.id && row.enabled && row.revokedAt === null && row.kind === "integration");
1332
+ }
1333
+ statusFor(client, state, keyUsable) {
1334
+ const record = state.clients[client];
1335
+ if (!record) return { client, status: "not-installed", configPath: this.defaultConfigPath(client) };
1336
+ const current = readOptional(record.configPath);
1337
+ if (current === null) {
1338
+ return {
1339
+ client,
1340
+ status: "configuration-missing",
1341
+ configPath: record.configPath,
1342
+ installedAt: record.installedAt,
1343
+ gatewayBaseUrl: record.gatewayBaseUrl
1344
+ };
1345
+ }
1346
+ if (sha256(current) !== record.installedHash) {
1347
+ return {
1348
+ client,
1349
+ status: "configuration-drift",
1350
+ configPath: record.configPath,
1351
+ installedAt: record.installedAt,
1352
+ gatewayBaseUrl: record.gatewayBaseUrl
1353
+ };
1354
+ }
1355
+ if (client === "codex") {
1356
+ if (!record.credentialFile) {
1357
+ return {
1358
+ client,
1359
+ status: "configuration-drift",
1360
+ configPath: record.configPath,
1361
+ installedAt: record.installedAt,
1362
+ gatewayBaseUrl: record.gatewayBaseUrl,
1363
+ message: "Codex integration uses a legacy authentication layout and must be repaired."
1364
+ };
1365
+ }
1366
+ const credential = readOptional(record.credentialFile.path);
1367
+ if (credential === null) {
1368
+ return {
1369
+ client,
1370
+ status: "configuration-missing",
1371
+ configPath: record.configPath,
1372
+ installedAt: record.installedAt,
1373
+ gatewayBaseUrl: record.gatewayBaseUrl,
1374
+ message: "Codex auth.json is missing."
1375
+ };
1376
+ }
1377
+ if (sha256(credential) !== record.credentialFile.installedHash) {
1378
+ return {
1379
+ client,
1380
+ status: "configuration-drift",
1381
+ configPath: record.configPath,
1382
+ installedAt: record.installedAt,
1383
+ gatewayBaseUrl: record.gatewayBaseUrl,
1384
+ message: "Codex auth.json changed after installation."
1385
+ };
1386
+ }
1387
+ }
1388
+ return {
1389
+ client,
1390
+ status: keyUsable ? "enabled" : "key-missing",
1391
+ configPath: record.configPath,
1392
+ installedAt: record.installedAt,
1393
+ gatewayBaseUrl: record.gatewayBaseUrl
1394
+ };
1395
+ }
1396
+ defaultConfigPath(client) {
1397
+ return client === "codex" ? join3(this.homeDir, ".codex", "config.toml") : join3(this.homeDir, ".claude", "settings.json");
1398
+ }
1399
+ codexAuthPathForConfig(configPath) {
1400
+ return join3(dirname4(configPath), "auth.json");
1401
+ }
1402
+ renderInstalled(client, base, secret) {
1403
+ if (client === "claude") {
1404
+ return renderClaudeSettings(base, this.options.gatewayBaseUrl, secret);
1405
+ }
1406
+ return renderCodexConfig({
1407
+ existing: base,
1408
+ gatewayBaseUrl: this.options.gatewayBaseUrl
1409
+ });
1410
+ }
1411
+ };
1412
+ function readOptional(path2) {
1413
+ return existsSync3(path2) ? readFileSync5(path2, "utf8") : null;
1414
+ }
1415
+ function sha256(value) {
1416
+ return createHash("sha256").update(value, "utf8").digest("hex");
1417
+ }
1418
+ function managedFileRecord(path2, original, installed) {
1419
+ const originalContent = original ?? "";
1420
+ return {
1421
+ path: path2,
1422
+ originalExisted: original !== null,
1423
+ originalContent,
1424
+ originalHash: sha256(originalContent),
1425
+ installedHash: sha256(installed)
1426
+ };
1427
+ }
1428
+ function primaryManagedFile(record) {
1429
+ return {
1430
+ path: record.configPath,
1431
+ originalExisted: record.originalExisted,
1432
+ originalContent: record.originalContent,
1433
+ originalHash: record.originalHash,
1434
+ installedHash: record.installedHash
1435
+ };
1436
+ }
1437
+ function managedFileDisposition(record, current) {
1438
+ if (current !== null && sha256(current) === record.installedHash) return "installed";
1439
+ const matchesOriginalExistence = record.originalExisted ? current !== null : current === null;
1440
+ if (matchesOriginalExistence && sha256(current ?? "") === record.originalHash) return "restored";
1441
+ return current === null ? "missing" : "drift";
1442
+ }
1443
+ function originalSnapshotForRepair(record, current) {
1444
+ const disposition = managedFileDisposition(record, current);
1445
+ if (disposition === "installed" || disposition === "restored") {
1446
+ return record.originalExisted ? record.originalContent : null;
1447
+ }
1448
+ return current;
1449
+ }
1450
+ function applyFileChangesWithRollback(changes) {
1451
+ if (changes.length === 0) return;
1452
+ const snapshots = changes.map((change) => ({ path: change.path, content: readOptional(change.path) }));
1453
+ try {
1454
+ for (const change of changes) writeOptional(change.path, change.content);
1455
+ } catch (error) {
1456
+ const rollbackFailures = [];
1457
+ for (const snapshot of [...snapshots].reverse()) {
1458
+ try {
1459
+ writeOptional(snapshot.path, snapshot.content);
1460
+ } catch {
1461
+ rollbackFailures.push(snapshot.path);
1462
+ }
1463
+ }
1464
+ if (rollbackFailures.length > 0) {
1465
+ throw new IntegrationConflictError(
1466
+ `CLI integration update failed and rollback could not restore: ${rollbackFailures.join(", ")}`
1467
+ );
1468
+ }
1469
+ throw error;
1470
+ }
1471
+ }
1472
+ function writeOptional(path2, content) {
1473
+ if (content !== null) {
1474
+ atomicWrite(path2, content);
1475
+ return;
1476
+ }
1477
+ if (existsSync3(path2)) unlinkSync2(path2);
1478
+ }
1479
+ function assertLoopbackGatewayUrl(value) {
1480
+ let url;
1481
+ try {
1482
+ url = new URL(value);
1483
+ } catch {
1484
+ throw new Error("gatewayBaseUrl must be a valid loopback URL");
1485
+ }
1486
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
1487
+ const literalLoopback = host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
1488
+ if (url.protocol !== "http:" || !literalLoopback || url.username || url.password || url.search || url.hash) {
1489
+ throw new Error("native CLI integrations require an unauthenticated literal HTTP loopback gateway URL");
1490
+ }
1491
+ }
735
1492
 
736
1493
  // src/ports/JsonOutboundKeyDb.ts
737
- import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1494
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
738
1495
  var JsonOutboundKeyDb = class {
739
1496
  constructor(keysPath) {
740
1497
  this.keysPath = keysPath;
@@ -760,7 +1517,10 @@ var JsonOutboundKeyDb = class {
760
1517
  enabled: true,
761
1518
  createdAt: input.createdAt ?? Date.now(),
762
1519
  lastUsedAt: null,
763
- revokedAt: null
1520
+ revokedAt: null,
1521
+ kind: input.kind,
1522
+ allowedEndpoints: input.allowedEndpoints,
1523
+ loopbackOnly: input.loopbackOnly
764
1524
  };
765
1525
  rows.push(row);
766
1526
  this.writeRows(rows);
@@ -837,16 +1597,16 @@ var JsonOutboundKeyDb = class {
837
1597
  }
838
1598
  /** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
839
1599
  readRows() {
840
- if (!existsSync2(this.keysPath)) return [];
1600
+ if (!existsSync4(this.keysPath)) return [];
841
1601
  try {
842
- const parsed = JSON.parse(readFileSync4(this.keysPath, "utf8"));
1602
+ const parsed = JSON.parse(readFileSync6(this.keysPath, "utf8"));
843
1603
  return Array.isArray(parsed) ? parsed : [];
844
1604
  } catch {
845
1605
  return [];
846
1606
  }
847
1607
  }
848
1608
  writeRows(rows) {
849
- writeFileSync3(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
1609
+ writeFileSync4(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
850
1610
  }
851
1611
  };
852
1612
  function applyPolicyField(row, field, value) {
@@ -855,9 +1615,62 @@ function applyPolicyField(row, field, value) {
855
1615
  else row[field] = value;
856
1616
  }
857
1617
 
1618
+ // src/commands/integrations.ts
1619
+ async function runIntegrations(argv) {
1620
+ const { values, positionals } = parseArgs2({
1621
+ args: argv,
1622
+ options: {
1623
+ config: { type: "string", short: "c" },
1624
+ "gateway-base-url": { type: "string" },
1625
+ target: { type: "string" },
1626
+ "master-key-file": { type: "string" }
1627
+ },
1628
+ allowPositionals: true
1629
+ });
1630
+ if (!values.config) throw new Error("integrations: --config <path> is required");
1631
+ const action = positionals[0];
1632
+ const client = positionals[1];
1633
+ const store = new IntegrationStateStore(
1634
+ defaultIntegrationsPath(values.config),
1635
+ resolveSecretBox(values["master-key-file"])
1636
+ );
1637
+ const saved = store.load();
1638
+ const savedUrl = saved.clients.codex?.gatewayBaseUrl ?? saved.clients.claude?.gatewayBaseUrl;
1639
+ const gatewayBaseUrl = values["gateway-base-url"] ?? savedUrl ?? "http://127.0.0.1:8765";
1640
+ const manager = new IntegrationManager({
1641
+ configPath: resolve2(values.config),
1642
+ gatewayBaseUrl,
1643
+ keyDb: new JsonOutboundKeyDb(defaultKeysPath(values.config)),
1644
+ stateStore: store
1645
+ });
1646
+ if (action === "status") {
1647
+ console.info(JSON.stringify({ integrations: await manager.listStatus() }, null, 2));
1648
+ return;
1649
+ }
1650
+ if (action === "rotate") {
1651
+ const result2 = await manager.rotateGatewayKey();
1652
+ console.info(`Rotated native CLI integration key (${result2.keyId}).`);
1653
+ return;
1654
+ }
1655
+ if (action !== "install" && action !== "remove" && action !== "plan" && action !== "repair") {
1656
+ throw new Error("integrations: expected status, plan/install/repair/remove <codex|claude>, or rotate");
1657
+ }
1658
+ if (!isClient(client)) throw new Error(`${action}: expected client 'codex' or 'claude'`);
1659
+ if (action === "install" && !values["gateway-base-url"]) {
1660
+ throw new Error("integrations install: --gateway-base-url <loopback-url> is required");
1661
+ }
1662
+ const result = action === "install" ? await manager.install(client, values.target) : action === "remove" ? await manager.remove(client) : action === "repair" ? await manager.repair(client) : await manager.plan(client, values.target);
1663
+ console.info(JSON.stringify(result, null, 2));
1664
+ }
1665
+ function isClient(value) {
1666
+ return value === "codex" || value === "claude";
1667
+ }
1668
+
858
1669
  // src/commands/keys.ts
1670
+ import { parseArgs as parseArgs3 } from "util";
1671
+ import { createNamedKey } from "@omnicross/core/outbound-api";
859
1672
  async function runKeys(argv) {
860
- const { values, positionals } = parseArgs2({
1673
+ const { values, positionals } = parseArgs3({
861
1674
  args: argv,
862
1675
  options: { config: { type: "string", short: "c" } },
863
1676
  allowPositionals: true
@@ -910,9 +1723,9 @@ async function keysRevoke(db, id) {
910
1723
 
911
1724
  // src/commands/launch.ts
912
1725
  import { spawn as spawn2 } from "child_process";
913
- import { existsSync as existsSync15 } from "fs";
914
- import { delimiter as delimiter2, join as join10 } from "path";
915
- import { parseArgs as parseArgs3 } from "util";
1726
+ import { existsSync as existsSync18 } from "fs";
1727
+ import { delimiter as delimiter2, join as join12 } from "path";
1728
+ import { parseArgs as parseArgs4 } from "util";
916
1729
  import {
917
1730
  buildChatCliLaunchConfig as buildChatCliLaunchConfig2,
918
1731
  buildClaudeCliLaunchConfig as buildClaudeCliLaunchConfig2,
@@ -921,7 +1734,7 @@ import {
921
1734
  } from "@omnicross/cli-launcher";
922
1735
 
923
1736
  // src/bootstrap.ts
924
- import { accessSync, constants as fsConstants, existsSync as existsSync14 } from "fs";
1737
+ import { accessSync, constants as fsConstants, existsSync as existsSync17 } from "fs";
925
1738
  import { DEFAULT_AUDIT_CONFIG } from "@omnicross/contracts/audit-types";
926
1739
  import { DEFAULT_BILLING_CONFIG } from "@omnicross/contracts/billing-types";
927
1740
  import { getGeminiCodeAssistProjectResolver } from "@omnicross/core/auth/GeminiCodeAssistProjectResolver";
@@ -929,11 +1742,22 @@ import { ApiKeyPoolService } from "@omnicross/core/completion/ApiKeyPoolService"
929
1742
  import {
930
1743
  __resetOutboundApiServerForTests,
931
1744
  DEFAULT_ACCOUNT_PROBE,
932
- getOutboundApiServer
1745
+ DEFAULT_OUTBOUND_PORT,
1746
+ getOutboundApiServer,
1747
+ normalizeServerConfig
933
1748
  } from "@omnicross/core/outbound-api";
934
1749
  import { setSubscriptionRegistryForOutbound } from "@omnicross/core/outbound-api/subscriptionRegistryPort";
935
- import { getSharedAccountHealth as getSharedAccountHealth2 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
936
- import { fetchUpstream as fetchUpstream6, setUpstreamProxyResolver } from "@omnicross/core/pipeline/upstreamFetch";
1750
+ import { getSharedAccountHealth as getSharedAccountHealth3 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
1751
+ import {
1752
+ __resetSharedAccountAllowanceStoreForTests,
1753
+ AccountAllowanceStore as AccountAllowanceStore3,
1754
+ setSharedAccountAllowanceStore
1755
+ } from "@omnicross/core/pipeline/AccountAllowanceStore";
1756
+ import {
1757
+ __resetSharedAccountAllowanceSchedulingForTests,
1758
+ getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling4
1759
+ } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
1760
+ import { fetchUpstream as fetchUpstream7, setUpstreamProxyResolver } from "@omnicross/core/pipeline/upstreamFetch";
937
1761
  import { __resetSharedIdentityStoreForTests } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
938
1762
  import { setGeminiCodeAssistResolver } from "@omnicross/core/ports/gemini-code-assist-resolver";
939
1763
  import {
@@ -1054,6 +1878,462 @@ function handleCodexOAuthStatus(sessionId, deps) {
1054
1878
  return { status: 200, body: { state: s.status, ...s.error ? { message: s.error } : {} } };
1055
1879
  }
1056
1880
 
1881
+ // src/allowance/AccountAllowanceService.ts
1882
+ import {
1883
+ getSharedAccountAllowanceStore as getSharedAccountAllowanceStore2
1884
+ } from "@omnicross/core/pipeline/AccountAllowanceStore";
1885
+ import {
1886
+ getSharedAccountAllowanceScheduling
1887
+ } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
1888
+
1889
+ // src/allowance/ClaudeAllowanceCollector.ts
1890
+ import {
1891
+ getSharedAccountAllowanceStore
1892
+ } from "@omnicross/core/pipeline/AccountAllowanceStore";
1893
+ import { fetchUpstream } from "@omnicross/core/pipeline/upstreamFetch";
1894
+ import { applyFingerprint } from "@omnicross/core/provider-proxy/identity/fingerprintHeaders";
1895
+ import {
1896
+ getSharedIdentityStore
1897
+ } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
1898
+ var CLAUDE_ALLOWANCE_CACHE_MS = 5 * 6e4;
1899
+ var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
1900
+ function finitePercent(value) {
1901
+ if (value === null || value === void 0 || value === "") return null;
1902
+ const number = typeof value === "number" ? value : Number(value);
1903
+ return Number.isFinite(number) && number >= 0 && number <= 100 ? number : null;
1904
+ }
1905
+ function isoInstant(value) {
1906
+ if (typeof value !== "string" || !value.trim()) return void 0;
1907
+ const time = Date.parse(value);
1908
+ return Number.isFinite(time) ? new Date(time).toISOString() : void 0;
1909
+ }
1910
+ function secondsUntil(instant, now) {
1911
+ if (!instant) return void 0;
1912
+ return Math.max(0, Math.floor((Date.parse(instant) - now) / 1e3));
1913
+ }
1914
+ function windowFromPayload(id, payload, now) {
1915
+ const usedPercent = finitePercent(payload?.utilization);
1916
+ const resetsAt = isoInstant(payload?.resets_at);
1917
+ const isSonnet = id === "seven-day-sonnet";
1918
+ const isFiveHour = id === "five-hour";
1919
+ return {
1920
+ id,
1921
+ label: isFiveHour ? "5 hours" : isSonnet ? "7 days \xB7 Sonnet" : "7 days",
1922
+ scope: isSonnet ? "model-family" : "all",
1923
+ modelFamily: isSonnet ? "sonnet" : void 0,
1924
+ usedPercent,
1925
+ windowMinutes: isFiveHour ? 5 * 60 : 7 * 24 * 60,
1926
+ resetsAt,
1927
+ remainingSeconds: secondsUntil(resetsAt, now),
1928
+ state: usedPercent !== null || resetsAt ? "fresh" : "unavailable"
1929
+ };
1930
+ }
1931
+ function emptyClaudeWindows(state) {
1932
+ return [
1933
+ {
1934
+ id: "five-hour",
1935
+ label: "5 hours",
1936
+ scope: "all",
1937
+ usedPercent: null,
1938
+ windowMinutes: 5 * 60,
1939
+ state
1940
+ },
1941
+ {
1942
+ id: "seven-day",
1943
+ label: "7 days",
1944
+ scope: "all",
1945
+ usedPercent: null,
1946
+ windowMinutes: 7 * 24 * 60,
1947
+ state
1948
+ },
1949
+ {
1950
+ id: "seven-day-sonnet",
1951
+ label: "7 days \xB7 Sonnet",
1952
+ scope: "model-family",
1953
+ modelFamily: "sonnet",
1954
+ usedPercent: null,
1955
+ windowMinutes: 7 * 24 * 60,
1956
+ state
1957
+ }
1958
+ ];
1959
+ }
1960
+ function hasHeader(headers, name) {
1961
+ const wanted = name.toLowerCase();
1962
+ return Object.keys(headers).some((key) => key.toLowerCase() === wanted);
1963
+ }
1964
+ var ClaudeAllowanceCollector = class {
1965
+ constructor(credentials, store = getSharedAccountAllowanceStore(), fetchImpl = (url, init, accountId) => fetchUpstream(url, init, { providerId: "claude", accountId }), identityStore = getSharedIdentityStore(), now = Date.now) {
1966
+ this.credentials = credentials;
1967
+ this.store = store;
1968
+ this.fetchImpl = fetchImpl;
1969
+ this.identityStore = identityStore;
1970
+ this.now = now;
1971
+ }
1972
+ credentials;
1973
+ store;
1974
+ fetchImpl;
1975
+ identityStore;
1976
+ now;
1977
+ inFlight = /* @__PURE__ */ new Map();
1978
+ async collectMany(accounts, options = {}) {
1979
+ const settled = await Promise.allSettled(accounts.map((account) => this.collect(account, options)));
1980
+ return settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
1981
+ }
1982
+ collect(account, options = {}) {
1983
+ const now = this.now();
1984
+ const unsupported = account.tokens.isSetupToken || account.tokens.authMethod !== "oauth";
1985
+ if (unsupported) {
1986
+ const existing = this.store.get("claude", account.id, now);
1987
+ if (existing?.windows.every((window) => window.state === "unsupported")) return Promise.resolve(existing);
1988
+ const snapshot = this.unsupportedSnapshot(account.id, now);
1989
+ this.store.set(snapshot);
1990
+ return Promise.resolve(snapshot);
1991
+ }
1992
+ const cached = this.store.get("claude", account.id, now);
1993
+ if (!options.force && cached && this.isCacheValid(cached, now, options.refreshAheadMs)) return Promise.resolve(cached);
1994
+ const running = this.inFlight.get(account.id);
1995
+ if (running) return running;
1996
+ const promise = this.fetchAccount(account.id).catch(() => this.failureSnapshot(account.id, "claude_usage_request_failed", this.now())).finally(() => this.inFlight.delete(account.id));
1997
+ this.inFlight.set(account.id, promise);
1998
+ return promise;
1999
+ }
2000
+ isCacheValid(snapshot, now, refreshAheadMs) {
2001
+ if (snapshot.source !== "oauth-usage-api") return false;
2002
+ if (snapshot.windows.every((window) => window.state === "unsupported")) return true;
2003
+ const expiresAt = snapshot.expiresAt ? Date.parse(snapshot.expiresAt) : 0;
2004
+ const ahead = typeof refreshAheadMs === "number" && Number.isFinite(refreshAheadMs) ? Math.max(0, refreshAheadMs) : 0;
2005
+ return Number.isFinite(expiresAt) && expiresAt > now + ahead;
2006
+ }
2007
+ async fetchAccount(accountId) {
2008
+ let token = await this.credentials.getAccessTokenForAccount("claude", accountId);
2009
+ if (!token) return this.failureSnapshot(accountId, "claude_usage_token_unavailable", this.now());
2010
+ let response = await this.request(accountId, token);
2011
+ if (response.status === 401) {
2012
+ const refreshed = await this.credentials.refreshAccountToken("claude", accountId);
2013
+ if (!refreshed) return this.failureSnapshot(accountId, "claude_usage_unauthorized", this.now());
2014
+ token = await this.credentials.getAccessTokenForAccount("claude", accountId);
2015
+ if (!token) return this.failureSnapshot(accountId, "claude_usage_token_unavailable", this.now());
2016
+ response = await this.request(accountId, token);
2017
+ }
2018
+ if (response.status === 403) {
2019
+ const snapshot2 = this.unsupportedSnapshot(accountId, this.now(), "claude_usage_unsupported");
2020
+ this.store.set(snapshot2);
2021
+ return snapshot2;
2022
+ }
2023
+ if (!response.ok) {
2024
+ return this.failureSnapshot(accountId, "claude_usage_http_error", this.now());
2025
+ }
2026
+ let payload;
2027
+ try {
2028
+ payload = await response.json();
2029
+ } catch {
2030
+ return this.failureSnapshot(accountId, "claude_usage_invalid_response", this.now());
2031
+ }
2032
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
2033
+ return this.failureSnapshot(accountId, "claude_usage_invalid_response", this.now());
2034
+ }
2035
+ const now = this.now();
2036
+ const usage = payload;
2037
+ const snapshot = {
2038
+ providerId: "claude",
2039
+ accountId,
2040
+ source: "oauth-usage-api",
2041
+ observedAt: new Date(now).toISOString(),
2042
+ expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
2043
+ windows: [
2044
+ windowFromPayload("five-hour", usage.five_hour, now),
2045
+ windowFromPayload("seven-day", usage.seven_day, now),
2046
+ windowFromPayload("seven-day-sonnet", usage.seven_day_sonnet, now)
2047
+ ]
2048
+ };
2049
+ this.store.set(snapshot);
2050
+ return snapshot;
2051
+ }
2052
+ request(accountId, token) {
2053
+ const headers = {
2054
+ Authorization: `Bearer ${token}`,
2055
+ Accept: "application/json",
2056
+ "Content-Type": "application/json",
2057
+ "anthropic-beta": "oauth-2025-04-20",
2058
+ "Accept-Language": "en-US,en;q=0.9"
2059
+ };
2060
+ applyFingerprint(this.identityStore, headers, "claude", accountId, void 0);
2061
+ if (!hasHeader(headers, "user-agent")) {
2062
+ headers["User-Agent"] = "claude-cli/2.0.53 (external, cli)";
2063
+ }
2064
+ return this.fetchImpl(CLAUDE_USAGE_URL, {
2065
+ method: "GET",
2066
+ headers,
2067
+ signal: AbortSignal.timeout(15e3)
2068
+ }, accountId);
2069
+ }
2070
+ failureSnapshot(accountId, code, now) {
2071
+ const existing = this.store.get("claude", accountId, now);
2072
+ const snapshot = existing ? {
2073
+ ...existing,
2074
+ expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
2075
+ windows: existing.windows.map((window) => ({
2076
+ ...window,
2077
+ state: window.state === "unsupported" ? "unsupported" : window.usedPercent !== null || window.resetsAt ? "stale" : "unavailable"
2078
+ })),
2079
+ lastErrorCode: code
2080
+ } : {
2081
+ providerId: "claude",
2082
+ accountId,
2083
+ source: "oauth-usage-api",
2084
+ observedAt: new Date(now).toISOString(),
2085
+ expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
2086
+ windows: emptyClaudeWindows("unavailable"),
2087
+ lastErrorCode: code
2088
+ };
2089
+ this.store.set(snapshot);
2090
+ return snapshot;
2091
+ }
2092
+ unsupportedSnapshot(accountId, now, code = "claude_usage_unsupported_auth") {
2093
+ return {
2094
+ providerId: "claude",
2095
+ accountId,
2096
+ source: "oauth-usage-api",
2097
+ observedAt: new Date(now).toISOString(),
2098
+ windows: emptyClaudeWindows("unsupported"),
2099
+ lastErrorCode: code
2100
+ };
2101
+ }
2102
+ };
2103
+
2104
+ // src/allowance/AccountAllowanceService.ts
2105
+ function codexUnavailable(accountId, now) {
2106
+ return {
2107
+ providerId: "codex",
2108
+ accountId,
2109
+ source: "response-headers",
2110
+ observedAt: new Date(now).toISOString(),
2111
+ windows: [
2112
+ { id: "primary", label: "Primary", scope: "all", usedPercent: null, state: "unavailable" },
2113
+ { id: "secondary", label: "Secondary", scope: "all", usedPercent: null, state: "unavailable" }
2114
+ ],
2115
+ lastErrorCode: "codex_allowance_not_observed"
2116
+ };
2117
+ }
2118
+ var AccountAllowanceService = class {
2119
+ constructor(credentials, store = getSharedAccountAllowanceStore2(), collector, now = Date.now) {
2120
+ this.credentials = credentials;
2121
+ this.store = store;
2122
+ this.now = now;
2123
+ this.claudeCollector = collector ?? new ClaudeAllowanceCollector(credentials, store);
2124
+ }
2125
+ credentials;
2126
+ store;
2127
+ now;
2128
+ claudeCollector;
2129
+ /**
2130
+ * Read all/filtered snapshots. Claude's five-minute cache is refreshed lazily;
2131
+ * Codex remains passive and reports not-observed until a real model response.
2132
+ */
2133
+ async list(filter = {}) {
2134
+ const config = await this.credentials.getFullConfig();
2135
+ this.store.pruneToKnownAccounts([
2136
+ ...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
2137
+ ...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
2138
+ ]);
2139
+ const wantsClaude = !filter.providerId || filter.providerId === "claude";
2140
+ const claudeAccounts = (config.claudeAccounts ?? []).filter(
2141
+ (account) => !filter.accountId || account.id === filter.accountId
2142
+ );
2143
+ if (wantsClaude) await this.claudeCollector.collectMany(claudeAccounts);
2144
+ const wantsCodex = !filter.providerId || filter.providerId === "codex";
2145
+ const codexAccounts = (config.codexAccounts ?? []).filter(
2146
+ (account) => !filter.accountId || account.id === filter.accountId
2147
+ );
2148
+ if (wantsCodex) {
2149
+ for (const account of codexAccounts) {
2150
+ if (!this.store.get("codex", account.id)) this.store.set(codexUnavailable(account.id, this.now()));
2151
+ }
2152
+ }
2153
+ const known = /* @__PURE__ */ new Set();
2154
+ if (wantsClaude) for (const account of claudeAccounts) known.add(`claude\0${account.id}`);
2155
+ if (wantsCodex) for (const account of codexAccounts) known.add(`codex\0${account.id}`);
2156
+ return this.store.list(filter).filter((snapshot) => known.has(`${snapshot.providerId}\0${snapshot.accountId}`));
2157
+ }
2158
+ /** Force-refresh Claude usage for one account or every stored Claude account. */
2159
+ async refreshClaude(accountId) {
2160
+ const config = await this.credentials.getFullConfig();
2161
+ this.store.pruneToKnownAccounts([
2162
+ ...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
2163
+ ...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
2164
+ ]);
2165
+ const accounts = (config.claudeAccounts ?? []).filter(
2166
+ (account) => !accountId || account.id === accountId
2167
+ );
2168
+ return this.claudeCollector.collectMany(accounts, { force: true });
2169
+ }
2170
+ /**
2171
+ * Keep Claude snapshots warm for allowance-aware routing. This deliberately
2172
+ * excludes Codex (whose quota is learned from real response headers) and
2173
+ * preserves the collector's cache + per-account in-flight coalescing.
2174
+ */
2175
+ async maintainClaudeCache(refreshAheadMs) {
2176
+ const config = await this.credentials.getFullConfig();
2177
+ this.store.pruneToKnownAccounts([
2178
+ ...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
2179
+ ...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
2180
+ ]);
2181
+ await this.claudeCollector.collectMany(config.claudeAccounts ?? [], { refreshAheadMs });
2182
+ }
2183
+ /** Remove a cache row as soon as an account is deleted by the admin path. */
2184
+ removeAccountSnapshot(providerId, accountId) {
2185
+ this.store.delete(providerId, accountId);
2186
+ }
2187
+ /** Remove all allowance rows for a provider block that was deleted. */
2188
+ removeProviderSnapshots(providerId) {
2189
+ for (const snapshot of this.store.list({ providerId })) {
2190
+ this.store.delete(snapshot.providerId, snapshot.accountId);
2191
+ }
2192
+ }
2193
+ /** Secret-free policy diagnostics for the settings/accounts UI. */
2194
+ getSchedulingStatus() {
2195
+ const scheduling = getSharedAccountAllowanceScheduling();
2196
+ return { config: scheduling.getConfig(), history: scheduling.getHistory() };
2197
+ }
2198
+ };
2199
+
2200
+ // src/allowance/ClaudeAllowanceRefreshScheduler.ts
2201
+ var CLAUDE_ALLOWANCE_CHECK_INTERVAL_MS = 6e4;
2202
+ var CLAUDE_ALLOWANCE_REFRESH_AHEAD_MS = 9e4;
2203
+ var ClaudeAllowanceRefreshScheduler = class {
2204
+ constructor(service, logger, intervalMs = CLAUDE_ALLOWANCE_CHECK_INTERVAL_MS, refreshAheadMs = CLAUDE_ALLOWANCE_REFRESH_AHEAD_MS) {
2205
+ this.service = service;
2206
+ this.logger = logger;
2207
+ this.intervalMs = intervalMs;
2208
+ this.refreshAheadMs = refreshAheadMs;
2209
+ }
2210
+ service;
2211
+ logger;
2212
+ intervalMs;
2213
+ refreshAheadMs;
2214
+ timer = null;
2215
+ started = false;
2216
+ enabled = false;
2217
+ sweeping = false;
2218
+ /**
2219
+ * Apply live server policy. Once started, enable/disable changes arm or disarm
2220
+ * immediately; the initial enabled sweep is fire-and-forget.
2221
+ */
2222
+ configure(config) {
2223
+ const nextEnabled = config?.enabled === true;
2224
+ if (this.enabled === nextEnabled) return;
2225
+ this.enabled = nextEnabled;
2226
+ if (!this.started) return;
2227
+ if (nextEnabled) {
2228
+ this.arm();
2229
+ void this.sweep();
2230
+ } else {
2231
+ this.disarm();
2232
+ }
2233
+ }
2234
+ /** Start the lifecycle. Disabled policy remains completely inert. */
2235
+ start() {
2236
+ if (this.started) return;
2237
+ this.started = true;
2238
+ if (!this.enabled) return;
2239
+ this.arm();
2240
+ void this.sweep();
2241
+ }
2242
+ /** Stop all future checks. Idempotent and safe during an in-flight refresh. */
2243
+ dispose() {
2244
+ this.started = false;
2245
+ this.disarm();
2246
+ }
2247
+ /** One non-overlapping cache-maintenance pass. Exposed for focused tests. */
2248
+ async sweep() {
2249
+ if (!this.enabled || this.sweeping) return;
2250
+ this.sweeping = true;
2251
+ try {
2252
+ await this.service.maintainClaudeCache(this.refreshAheadMs);
2253
+ } catch (error) {
2254
+ this.logger.warn("Claude allowance background refresh failed", {
2255
+ error: error instanceof Error ? error.message : String(error)
2256
+ });
2257
+ } finally {
2258
+ this.sweeping = false;
2259
+ }
2260
+ }
2261
+ arm() {
2262
+ if (this.timer) return;
2263
+ this.timer = setInterval(() => void this.sweep(), this.intervalMs);
2264
+ this.timer.unref?.();
2265
+ }
2266
+ disarm() {
2267
+ if (this.timer) clearInterval(this.timer);
2268
+ this.timer = null;
2269
+ }
2270
+ };
2271
+
2272
+ // src/allowance/JsonAccountAllowancePersistence.ts
2273
+ import { randomUUID } from "crypto";
2274
+ import {
2275
+ existsSync as existsSync5,
2276
+ mkdirSync as mkdirSync3,
2277
+ readFileSync as readFileSync7,
2278
+ renameSync as renameSync2,
2279
+ rmSync,
2280
+ statSync,
2281
+ writeFileSync as writeFileSync5
2282
+ } from "fs";
2283
+ import { dirname as dirname5 } from "path";
2284
+ import { normalizeAccountAllowanceSnapshot } from "@omnicross/core/pipeline/AccountAllowanceStore";
2285
+ var ACCOUNT_ALLOWANCE_CACHE_VERSION = 1;
2286
+ var MAX_PERSISTED_ALLOWANCE_SNAPSHOTS = 256;
2287
+ var MAX_ALLOWANCE_CACHE_BYTES = 1e6;
2288
+ var JsonAccountAllowancePersistence = class {
2289
+ constructor(cachePath) {
2290
+ this.cachePath = cachePath;
2291
+ }
2292
+ cachePath;
2293
+ /** Read only the `snapshots` payload; all row validation remains defensive. */
2294
+ load() {
2295
+ if (!existsSync5(this.cachePath)) return [];
2296
+ try {
2297
+ if (statSync(this.cachePath).size > MAX_ALLOWANCE_CACHE_BYTES) return [];
2298
+ const raw = readFileSync7(this.cachePath, "utf8");
2299
+ if (!raw.trim()) return [];
2300
+ const parsed = JSON.parse(raw);
2301
+ if (Array.isArray(parsed)) return parsed;
2302
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
2303
+ const file = parsed;
2304
+ return file.version === ACCOUNT_ALLOWANCE_CACHE_VERSION && Array.isArray(file.snapshots) ? file.snapshots : [];
2305
+ } catch {
2306
+ return [];
2307
+ }
2308
+ }
2309
+ /** Replace the file atomically; the target remains intact if replacement fails. */
2310
+ save(snapshots) {
2311
+ const rows = [];
2312
+ for (const snapshot of snapshots) {
2313
+ const normalized = normalizeAccountAllowanceSnapshot(snapshot);
2314
+ if (!normalized) continue;
2315
+ rows.push(normalized);
2316
+ if (rows.length >= MAX_PERSISTED_ALLOWANCE_SNAPSHOTS) break;
2317
+ }
2318
+ const file = {
2319
+ version: ACCOUNT_ALLOWANCE_CACHE_VERSION,
2320
+ snapshots: rows
2321
+ };
2322
+ const serialized = JSON.stringify(file, null, 2) + "\n";
2323
+ if (Buffer.byteLength(serialized, "utf8") > MAX_ALLOWANCE_CACHE_BYTES) {
2324
+ throw new Error("account allowance cache exceeds its size limit");
2325
+ }
2326
+ mkdirSync3(dirname5(this.cachePath), { recursive: true });
2327
+ const temporaryPath = `${this.cachePath}.${process.pid}.${randomUUID()}.tmp`;
2328
+ try {
2329
+ writeFileSync5(temporaryPath, serialized, { encoding: "utf8", flag: "wx" });
2330
+ renameSync2(temporaryPath, this.cachePath);
2331
+ } finally {
2332
+ rmSync(temporaryPath, { force: true });
2333
+ }
2334
+ }
2335
+ };
2336
+
1057
2337
  // src/admin/AdminServer.ts
1058
2338
  import { timingSafeEqual } from "crypto";
1059
2339
  import http2 from "http";
@@ -1076,16 +2356,16 @@ function intParam(value) {
1076
2356
  }
1077
2357
  function handleAuditQuery(req, res, reader) {
1078
2358
  const url = new URL(req.url ?? "/", "http://localhost");
1079
- const query = {};
2359
+ const query2 = {};
1080
2360
  const keyId = url.searchParams.get("keyId");
1081
- if (keyId && keyId.trim()) query.keyId = keyId.trim();
2361
+ if (keyId && keyId.trim()) query2.keyId = keyId.trim();
1082
2362
  const from = intParam(url.searchParams.get("from"));
1083
- if (from !== void 0) query.from = from;
2363
+ if (from !== void 0) query2.from = from;
1084
2364
  const to = intParam(url.searchParams.get("to"));
1085
- if (to !== void 0) query.to = to;
2365
+ if (to !== void 0) query2.to = to;
1086
2366
  const limit = intParam(url.searchParams.get("limit"));
1087
- if (limit !== void 0) query.limit = limit;
1088
- const records = reader ? reader(query) : [];
2367
+ if (limit !== void 0) query2.limit = limit;
2368
+ const records = reader ? reader(query2) : [];
1089
2369
  res.writeHead(200, { "Content-Type": "application/json" });
1090
2370
  res.end(JSON.stringify({ records }));
1091
2371
  }
@@ -1150,19 +2430,19 @@ function teardown() {
1150
2430
 
1151
2431
  // src/admin/webhookTestApi.ts
1152
2432
  function readJsonBody(req) {
1153
- return new Promise((resolve) => {
2433
+ return new Promise((resolve3) => {
1154
2434
  const chunks = [];
1155
2435
  req.on("data", (c) => chunks.push(c));
1156
2436
  req.on("end", () => {
1157
2437
  try {
1158
2438
  const raw = Buffer.concat(chunks).toString("utf8");
1159
2439
  const parsed = raw ? JSON.parse(raw) : {};
1160
- resolve(parsed && typeof parsed === "object" ? parsed : {});
2440
+ resolve3(parsed && typeof parsed === "object" ? parsed : {});
1161
2441
  } catch {
1162
- resolve({});
2442
+ resolve3({});
1163
2443
  }
1164
2444
  });
1165
- req.on("error", () => resolve({}));
2445
+ req.on("error", () => resolve3({}));
1166
2446
  });
1167
2447
  }
1168
2448
  async function handleWebhookTest(req, res) {
@@ -1182,14 +2462,16 @@ async function handleWebhookTest(req, res) {
1182
2462
  import http from "http";
1183
2463
  import {
1184
2464
  createNamedKey as createNamedKey2,
2465
+ gatewayBindingToEndpointConfig,
1185
2466
  isKindMappedEndpoint,
1186
2467
  loadServerConfig as loadServerConfig2,
1187
2468
  mergeServerConfig,
1188
2469
  normalizeProxyConfig,
1189
- saveServerConfig,
1190
- validateServerModelConfig
2470
+ saveServerConfig
1191
2471
  } from "@omnicross/core/outbound-api";
1192
- import { fetchUpstream } from "@omnicross/core/pipeline/upstreamFetch";
2472
+ import { getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling2 } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
2473
+ import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
2474
+ import { fetchUpstream as fetchUpstream2 } from "@omnicross/core/pipeline/upstreamFetch";
1193
2475
 
1194
2476
  // src/pool/resolveEnvKey.ts
1195
2477
  function resolveEnvKey(rawKey) {
@@ -1223,14 +2505,13 @@ function getPresetById(idOrPresetId) {
1223
2505
 
1224
2506
  // src/preset-map.ts
1225
2507
  var EXCLUSION_REASONS = {
1226
- "openai-response": "daemon rows have no openai-response format; the Responses API needs a transformer chain that a BYO daemon provider row cannot express.",
1227
2508
  "azure-openai": "Azure needs an apiVersion + a deployment-name-as-model URL template + an empty baseUrl; a daemon provider row cannot express that shape."
1228
2509
  };
1229
2510
  var FORMAT_MAP = {
1230
2511
  openai: "openai",
1231
2512
  anthropic: "anthropic",
1232
2513
  google: "gemini",
1233
- "openai-response": null,
2514
+ "openai-response": "openai-response",
1234
2515
  "azure-openai": null
1235
2516
  };
1236
2517
  function resolveFormat(raw) {
@@ -1454,6 +2735,67 @@ var VALID_PROVIDER_IDS = [
1454
2735
  function asSubscriptionProviderId(id) {
1455
2736
  return VALID_PROVIDER_IDS.includes(id) ? id : null;
1456
2737
  }
2738
+ var ACCOUNT_PATCH_KEYS = /* @__PURE__ */ new Set(["label", "enabled", "priority", "group", "tags"]);
2739
+ function validateAccountMetadataPatch(body) {
2740
+ const keys = Object.keys(body);
2741
+ if (keys.length === 0 || keys.some((key) => !ACCOUNT_PATCH_KEYS.has(key))) return null;
2742
+ const patch = {};
2743
+ if ("label" in body) {
2744
+ if (typeof body["label"] !== "string" || body["label"].trim().length > 120) return null;
2745
+ patch.label = body["label"].trim();
2746
+ }
2747
+ if ("enabled" in body) {
2748
+ if (typeof body["enabled"] !== "boolean") return null;
2749
+ patch.enabled = body["enabled"];
2750
+ }
2751
+ if ("priority" in body) {
2752
+ const priority = body["priority"];
2753
+ if (typeof priority !== "number" || !Number.isFinite(priority) || priority < -1e4 || priority > 1e4) return null;
2754
+ patch.priority = priority;
2755
+ }
2756
+ if ("group" in body) {
2757
+ const group = body["group"];
2758
+ if (group !== null && typeof group !== "string") return null;
2759
+ const normalized = typeof group === "string" ? group.trim() : null;
2760
+ if (normalized !== null && normalized.length > 80) return null;
2761
+ patch.group = normalized || null;
2762
+ }
2763
+ if ("tags" in body) {
2764
+ const tags = body["tags"];
2765
+ if (!Array.isArray(tags) || tags.length > 20) return null;
2766
+ const normalized = tags.map((tag) => typeof tag === "string" ? tag.trim() : "");
2767
+ if (normalized.some((tag) => !tag || tag.length > 40)) return null;
2768
+ patch.tags = [...new Set(normalized)];
2769
+ }
2770
+ return patch;
2771
+ }
2772
+ function validateAccountBatchBody(body) {
2773
+ const action = body["action"];
2774
+ const rawAccounts = body["accounts"];
2775
+ if (!Array.isArray(rawAccounts) || rawAccounts.length < 1 || rawAccounts.length > 100) return null;
2776
+ if (action !== "enable" && action !== "disable" && action !== "set-group" && action !== "delete") return null;
2777
+ const refs = [];
2778
+ const seen = /* @__PURE__ */ new Set();
2779
+ for (const raw of rawAccounts) {
2780
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
2781
+ const row = raw;
2782
+ const providerId = typeof row["providerId"] === "string" ? asSubscriptionProviderId(row["providerId"]) : null;
2783
+ const accountId = typeof row["accountId"] === "string" ? row["accountId"].trim() : "";
2784
+ if (!providerId || !accountId || accountId.length > 200) return null;
2785
+ const key = `${providerId}\0${accountId}`;
2786
+ if (seen.has(key)) return null;
2787
+ seen.add(key);
2788
+ refs.push({ providerId, accountId });
2789
+ }
2790
+ if (action === "set-group") {
2791
+ const group = body["group"];
2792
+ if (group !== null && typeof group !== "string") return null;
2793
+ const normalized = typeof group === "string" ? group.trim() : null;
2794
+ if (normalized !== null && normalized.length > 80) return null;
2795
+ return { refs, mutation: { action, group: normalized || null } };
2796
+ }
2797
+ return { refs, mutation: { action } };
2798
+ }
1457
2799
  var CLAUDE_AUTH_METHODS = /* @__PURE__ */ new Set(["oauth", "setup_token", "manual"]);
1458
2800
  var OAUTH_AUTH_METHODS = /* @__PURE__ */ new Set(["oauth", "manual"]);
1459
2801
  var TOKEN_STATUSES = /* @__PURE__ */ new Set([
@@ -1665,9 +3007,9 @@ async function exchangeGemini(code, codeVerifier, exchangeFetch) {
1665
3007
 
1666
3008
  // src/admin/cliLaunch.ts
1667
3009
  import { exec, spawn } from "child_process";
1668
- import { randomUUID } from "crypto";
1669
- import { existsSync as existsSync3 } from "fs";
1670
- import { delimiter, join as join3 } from "path";
3010
+ import { randomUUID as randomUUID2 } from "crypto";
3011
+ import { existsSync as existsSync6 } from "fs";
3012
+ import { delimiter, join as join4 } from "path";
1671
3013
  import {
1672
3014
  buildChatCliLaunchConfig,
1673
3015
  buildClaudeCliLaunchConfig,
@@ -1697,8 +3039,8 @@ function isLaunchCliId(id) {
1697
3039
  function probeDefault(candidate) {
1698
3040
  const segments = (process.env["PATH"] ?? "").split(delimiter).filter(Boolean);
1699
3041
  for (const seg of segments) {
1700
- const full = join3(seg, candidate);
1701
- if (existsSync3(full)) return full;
3042
+ const full = join4(seg, candidate);
3043
+ if (existsSync6(full)) return full;
1702
3044
  }
1703
3045
  return null;
1704
3046
  }
@@ -1785,10 +3127,10 @@ var sessions = /* @__PURE__ */ new Map();
1785
3127
  function errBody(message) {
1786
3128
  return { error: { type: "admin_api_error", message } };
1787
3129
  }
1788
- var defaultCommandRunner = (command) => new Promise((resolve) => {
3130
+ var defaultCommandRunner = (command) => new Promise((resolve3) => {
1789
3131
  exec(command, { timeout: 18e4 }, (err5, _stdout, stderr) => {
1790
- if (err5) resolve({ ok: false, error: stderr.trim() || err5.message });
1791
- else resolve({ ok: true });
3132
+ if (err5) resolve3({ ok: false, error: stderr.trim() || err5.message });
3133
+ else resolve3({ ok: true });
1792
3134
  });
1793
3135
  });
1794
3136
  async function handleCliInstall(cli, runner = defaultCommandRunner) {
@@ -1850,7 +3192,7 @@ async function handleCliLaunch(cli, body, ctx) {
1850
3192
  launch.onSessionEnd();
1851
3193
  return { status: 500, body: errBody(err5 instanceof Error ? err5.message : "failed to open terminal") };
1852
3194
  }
1853
- const id = randomUUID();
3195
+ const id = randomUUID2();
1854
3196
  sessions.set(id, {
1855
3197
  id,
1856
3198
  cli,
@@ -1863,12 +3205,12 @@ async function handleCliLaunch(cli, body, ctx) {
1863
3205
  }
1864
3206
 
1865
3207
  // src/admin/auditConfigBody.ts
1866
- var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3208
+ var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1867
3209
  function validateAuditSegment(patch) {
1868
3210
  const errors = [];
1869
3211
  const audit = patch.audit;
1870
3212
  if (audit === void 0) return errors;
1871
- if (!isPlainObject(audit)) {
3213
+ if (!isPlainObject2(audit)) {
1872
3214
  errors.push("audit must be an object");
1873
3215
  return errors;
1874
3216
  }
@@ -1890,12 +3232,12 @@ function validateAuditSegment(patch) {
1890
3232
 
1891
3233
  // src/admin/billingConfigBody.ts
1892
3234
  var BILLING_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
1893
- var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3235
+ var isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1894
3236
  function validateBillingSegment(patch) {
1895
3237
  const errors = [];
1896
3238
  const billing = patch.billing;
1897
3239
  if (billing === void 0) return errors;
1898
- if (!isPlainObject2(billing)) {
3240
+ if (!isPlainObject3(billing)) {
1899
3241
  errors.push("billing must be an object");
1900
3242
  return errors;
1901
3243
  }
@@ -2031,6 +3373,96 @@ function parseKeyPolicyBody(body) {
2031
3373
  return { ok: true, policy };
2032
3374
  }
2033
3375
 
3376
+ // src/admin/gatewayBindingBody.ts
3377
+ var ENDPOINTS = /* @__PURE__ */ new Set(["chat", "responses", "messages", "gemini"]);
3378
+ var TARGET_KINDS = /* @__PURE__ */ new Set(["account", "account-group", "account-pool", "provider"]);
3379
+ var FALLBACKS = /* @__PURE__ */ new Set(["next", "fail", "global"]);
3380
+ function isRecord(value) {
3381
+ return !!value && typeof value === "object" && !Array.isArray(value);
3382
+ }
3383
+ function nonBlank(value) {
3384
+ return typeof value === "string" && value.trim() !== "";
3385
+ }
3386
+ function validateStringArray(value, path2, errors) {
3387
+ if (!Array.isArray(value) || value.some((entry) => !nonBlank(entry))) {
3388
+ errors.push(`${path2} must be an array of non-empty strings`);
3389
+ }
3390
+ }
3391
+ function validateGatewayBindingsSegment(patch) {
3392
+ if (!Object.prototype.hasOwnProperty.call(patch, "bindings")) return [];
3393
+ const raw = patch.bindings;
3394
+ if (!Array.isArray(raw)) return ["bindings must be an array"];
3395
+ if (raw.length > 1e3) return ["bindings cannot contain more than 1000 entries"];
3396
+ const errors = [];
3397
+ const ids = /* @__PURE__ */ new Set();
3398
+ raw.forEach((entry, index) => {
3399
+ const path2 = `bindings[${index}]`;
3400
+ if (!isRecord(entry)) {
3401
+ errors.push(`${path2} must be an object`);
3402
+ return;
3403
+ }
3404
+ if (!nonBlank(entry.id)) errors.push(`${path2}.id is required`);
3405
+ else if (ids.has(entry.id.trim())) errors.push(`${path2}.id must be unique`);
3406
+ else ids.add(entry.id.trim());
3407
+ if (!nonBlank(entry.name)) errors.push(`${path2}.name is required`);
3408
+ if (typeof entry.enabled !== "boolean") errors.push(`${path2}.enabled must be boolean`);
3409
+ if (!ENDPOINTS.has(String(entry.endpoint))) errors.push(`${path2}.endpoint is invalid`);
3410
+ if (!FALLBACKS.has(String(entry.fallback))) {
3411
+ errors.push(`${path2}.fallback must be next or fail`);
3412
+ }
3413
+ if (entry.priority !== void 0 && (typeof entry.priority !== "number" || !Number.isInteger(entry.priority) || entry.priority < 0 || entry.priority > 1e4)) {
3414
+ errors.push(`${path2}.priority must be an integer from 0 to 10000`);
3415
+ }
3416
+ if (entry.apiKeyIds !== void 0) validateStringArray(entry.apiKeyIds, `${path2}.apiKeyIds`, errors);
3417
+ if (entry.keyScope !== void 0 && entry.keyScope !== "all" && entry.keyScope !== "selected") {
3418
+ errors.push(`${path2}.keyScope must be all or selected`);
3419
+ }
3420
+ if (entry.modelMode !== void 0 && entry.modelMode !== "passthrough" && entry.modelMode !== "mapped") {
3421
+ errors.push(`${path2}.modelMode must be passthrough or mapped`);
3422
+ }
3423
+ if (entry.modelMappings !== void 0) {
3424
+ if (!Array.isArray(entry.modelMappings)) {
3425
+ errors.push(`${path2}.modelMappings must be an array`);
3426
+ } else if (entry.modelMappings.length > 100) {
3427
+ errors.push(`${path2}.modelMappings cannot contain more than 100 entries`);
3428
+ } else if (entry.modelMappings.some(
3429
+ (mapping) => !isRecord(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
3430
+ )) {
3431
+ errors.push(`${path2}.modelMappings must contain non-empty source and target strings`);
3432
+ }
3433
+ }
3434
+ if (!isRecord(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
3435
+ errors.push(`${path2}.target is invalid`);
3436
+ } else {
3437
+ if (!nonBlank(entry.target.providerId)) errors.push(`${path2}.target.providerId is required`);
3438
+ if (entry.target.kind === "account" && !nonBlank(entry.target.accountId)) {
3439
+ errors.push(`${path2}.target.accountId is required`);
3440
+ }
3441
+ if (entry.target.kind === "account-group" && !nonBlank(entry.target.group)) {
3442
+ errors.push(`${path2}.target.group is required`);
3443
+ }
3444
+ if (entry.target.kind === "provider" && entry.target.keyId !== void 0 && !nonBlank(entry.target.keyId)) {
3445
+ errors.push(`${path2}.target.keyId must be a non-empty string`);
3446
+ }
3447
+ }
3448
+ if (entry.modelMap !== void 0) {
3449
+ if (!isRecord(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
3450
+ errors.push(`${path2}.modelMap must contain string values`);
3451
+ }
3452
+ }
3453
+ if (entry.models !== void 0) validateStringArray(entry.models, `${path2}.models`, errors);
3454
+ if (entry.backgroundModelIds !== void 0) {
3455
+ validateStringArray(entry.backgroundModelIds, `${path2}.backgroundModelIds`, errors);
3456
+ }
3457
+ for (const field of ["defaultModel", "backgroundModel"]) {
3458
+ if (entry[field] !== void 0 && typeof entry[field] !== "string") {
3459
+ errors.push(`${path2}.${field} must be a string`);
3460
+ }
3461
+ }
3462
+ });
3463
+ return errors;
3464
+ }
3465
+
2034
3466
  // src/admin/voucherAdmin.ts
2035
3467
  import {
2036
3468
  generateVoucherCode,
@@ -2048,15 +3480,15 @@ function writeErr(res, status, message) {
2048
3480
  writeJson(res, status, { error: { type: "voucher_error", message } });
2049
3481
  }
2050
3482
  function readJsonBody2(req) {
2051
- return new Promise((resolve, reject) => {
3483
+ return new Promise((resolve3, reject) => {
2052
3484
  const chunks = [];
2053
3485
  req.on("data", (c) => chunks.push(c));
2054
3486
  req.on("end", () => {
2055
3487
  const raw = Buffer.concat(chunks).toString("utf8");
2056
- if (!raw.trim()) return resolve({});
3488
+ if (!raw.trim()) return resolve3({});
2057
3489
  try {
2058
3490
  const parsed = JSON.parse(raw);
2059
- resolve(parsed && typeof parsed === "object" ? parsed : {});
3491
+ resolve3(parsed && typeof parsed === "object" ? parsed : {});
2060
3492
  } catch {
2061
3493
  reject(new Error("invalid-json"));
2062
3494
  }
@@ -2149,12 +3581,12 @@ import {
2149
3581
  WEBHOOK_EVENT_KINDS
2150
3582
  } from "@omnicross/contracts/webhook-types";
2151
3583
  var WEBHOOK_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
2152
- var isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3584
+ var isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2153
3585
  function validateWebhookSegment(patch) {
2154
3586
  const errors = [];
2155
3587
  const webhook = patch.webhook;
2156
3588
  if (webhook === void 0) return errors;
2157
- if (!isPlainObject3(webhook)) {
3589
+ if (!isPlainObject4(webhook)) {
2158
3590
  errors.push("webhook must be an object");
2159
3591
  return errors;
2160
3592
  }
@@ -2168,7 +3600,7 @@ function validateWebhookSegment(patch) {
2168
3600
  }
2169
3601
  const seenIds = /* @__PURE__ */ new Set();
2170
3602
  for (const [i, raw] of (Array.isArray(destinations) ? destinations : []).entries()) {
2171
- if (!isPlainObject3(raw)) {
3603
+ if (!isPlainObject4(raw)) {
2172
3604
  errors.push(`webhook.destinations[${i}] must be an object`);
2173
3605
  continue;
2174
3606
  }
@@ -2234,12 +3666,16 @@ function preserveWebhookSecrets(incoming, current) {
2234
3666
  }
2235
3667
 
2236
3668
  // src/audit/auditRuntime.ts
3669
+ import { join as join5 } from "path";
2237
3670
  import { setAuditCaptureConfig, setAuditSink } from "@omnicross/core/pipeline/auditSink";
3671
+ import { setUpstreamTracePath } from "@omnicross/core/pipeline/upstreamTrace";
2238
3672
  var writer = null;
2239
3673
  var sweeper = null;
2240
- function setAuditRuntime(w, s) {
3674
+ var auditDir = "";
3675
+ function setAuditRuntime(w, s, dir) {
2241
3676
  writer = w;
2242
3677
  sweeper = s;
3678
+ auditDir = dir;
2243
3679
  }
2244
3680
  function applyAuditConfig(config) {
2245
3681
  const enabled = config?.enabled === true && writer !== null;
@@ -2251,9 +3687,11 @@ function applyAuditConfig(config) {
2251
3687
  sweeper.configure(config);
2252
3688
  sweeper.start();
2253
3689
  }
3690
+ setUpstreamTracePath(config.captureBodies ? join5(auditDir, "upstream-trace.jsonl") : null);
2254
3691
  } else {
2255
3692
  setAuditCaptureConfig(null);
2256
3693
  setAuditSink(null);
3694
+ setUpstreamTracePath(null);
2257
3695
  if (sweeper) {
2258
3696
  if (config) sweeper.configure(config);
2259
3697
  sweeper.dispose();
@@ -2291,7 +3729,7 @@ function applyBillingConfig(config) {
2291
3729
  }
2292
3730
 
2293
3731
  // src/ports/account-multi.ts
2294
- import { randomUUID as randomUUID2 } from "crypto";
3732
+ import { randomUUID as randomUUID3 } from "crypto";
2295
3733
  var PROVIDER_KEYS = {
2296
3734
  claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
2297
3735
  codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
@@ -2357,7 +3795,7 @@ function migrateLazily(config) {
2357
3795
  }
2358
3796
  function addAccount(config, p, tokens, label) {
2359
3797
  const accounts = [...getAccounts(config, p)];
2360
- const id = randomUUID2();
3798
+ const id = randomUUID3();
2361
3799
  accounts.push({
2362
3800
  id,
2363
3801
  label: label ?? `Account ${accounts.length + 1}`,
@@ -2433,9 +3871,13 @@ function sanitizeAccounts(config, p) {
2433
3871
  const activeId = getActiveId(config, p);
2434
3872
  return accounts.map((a) => {
2435
3873
  const t = a.tokens;
3874
+ const enabled = a.enabled !== false;
2436
3875
  return {
2437
3876
  id: a.id,
2438
3877
  label: a.label,
3878
+ enabled,
3879
+ group: a.group?.trim() || p,
3880
+ tags: a.tags ?? [],
2439
3881
  status: t.status ?? "unconfigured",
2440
3882
  authMethod: t.authMethod,
2441
3883
  subscriptionLevel: t.subscriptionLevel,
@@ -2444,6 +3886,8 @@ function sanitizeAccounts(config, p) {
2444
3886
  isSetupToken: t.isSetupToken,
2445
3887
  hasAccessToken: !!(t.accessToken || t.apiKey),
2446
3888
  isActive: a.id === activeId,
3889
+ schedulable: enabled,
3890
+ errorMessage: sanitizeDiagnosticMessage(t.errorMessage),
2447
3891
  // Scheduling metadata (subscription-account-scheduling): editable priority
2448
3892
  // (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
2449
3893
  priority: a.priority,
@@ -2458,6 +3902,54 @@ function sanitizeAccounts(config, p) {
2458
3902
  };
2459
3903
  });
2460
3904
  }
3905
+ function sanitizeDiagnosticMessage(value) {
3906
+ if (!value) return void 0;
3907
+ const lower = value.toLowerCase();
3908
+ if (lower.includes("timeout") || lower.includes("timed out")) return "Credential operation timed out.";
3909
+ if (lower.includes("network") || lower.includes("fetch")) return "Credential network request failed.";
3910
+ if (lower.includes("401") || lower.includes("unauthorized") || lower.includes("revoked")) {
3911
+ return "Credential authorization was rejected.";
3912
+ }
3913
+ return "Credential operation failed.";
3914
+ }
3915
+ function patchAccountMetadata(config, p, id, patch) {
3916
+ const accounts = getAccounts(config, p);
3917
+ if (!accounts.some((account) => account.id === id)) return { ok: false };
3918
+ setAccounts(config, p, accounts.map((account) => {
3919
+ if (account.id !== id) return account;
3920
+ const next = { ...account };
3921
+ if (patch.label !== void 0) next.label = patch.label;
3922
+ if (patch.enabled !== void 0) next.enabled = patch.enabled;
3923
+ if (patch.priority !== void 0) next.priority = patch.priority;
3924
+ if (patch.group !== void 0) {
3925
+ if (patch.group === null || patch.group === "") delete next.group;
3926
+ else next.group = patch.group;
3927
+ }
3928
+ if (patch.tags !== void 0) next.tags = patch.tags;
3929
+ return next;
3930
+ }));
3931
+ return { ok: true };
3932
+ }
3933
+ function batchManageAccounts(config, refs, mutation) {
3934
+ for (const ref of refs) {
3935
+ if (!getAccounts(config, ref.providerId).some((account) => account.id === ref.accountId)) {
3936
+ return { ok: false, missing: ref };
3937
+ }
3938
+ }
3939
+ for (const ref of refs) {
3940
+ if (mutation.action === "delete") {
3941
+ removeAccount(config, ref.providerId, ref.accountId);
3942
+ } else {
3943
+ patchAccountMetadata(
3944
+ config,
3945
+ ref.providerId,
3946
+ ref.accountId,
3947
+ mutation.action === "set-group" ? { group: mutation.group } : { enabled: mutation.action === "enable" }
3948
+ );
3949
+ }
3950
+ }
3951
+ return { ok: true, affected: refs.length };
3952
+ }
2461
3953
  function renameAccount(config, p, id, label) {
2462
3954
  const accounts = getAccounts(config, p);
2463
3955
  if (!accounts.some((a) => a.id === id)) return { ok: false };
@@ -2795,9 +4287,9 @@ function parseFiniteInt(raw) {
2795
4287
  const n = Number(raw);
2796
4288
  return Number.isFinite(n) && Number.isInteger(n) ? n : null;
2797
4289
  }
2798
- function parseRange(query) {
2799
- const startTs = parseFiniteInt(query.get("startTs"));
2800
- const endTs = parseFiniteInt(query.get("endTs"));
4290
+ function parseRange(query2) {
4291
+ const startTs = parseFiniteInt(query2.get("startTs"));
4292
+ const endTs = parseFiniteInt(query2.get("endTs"));
2801
4293
  if (startTs === null || endTs === null) {
2802
4294
  return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
2803
4295
  }
@@ -2810,8 +4302,8 @@ var BUCKET_SPAN_MS = {
2810
4302
  month: 28 * 864e5
2811
4303
  };
2812
4304
  var MAX_TIMESERIES_BUCKETS = 2e3;
2813
- async function handleUsageGet(view, query, deps) {
2814
- const range = parseRange(query);
4305
+ async function handleUsageGet(view, query2, deps) {
4306
+ const range = parseRange(query2);
2815
4307
  if (!isRange(range)) return range;
2816
4308
  switch (view) {
2817
4309
  case "totals":
@@ -2819,7 +4311,7 @@ async function handleUsageGet(view, query, deps) {
2819
4311
  case "by-model":
2820
4312
  return { status: 200, body: await deps.usageRecorder.getByModel(range) };
2821
4313
  case "timeseries": {
2822
- const bucket = query.get("bucket");
4314
+ const bucket = query2.get("bucket");
2823
4315
  if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
2824
4316
  return err4(400, "bucket must be one of 'hour', 'day', 'month'");
2825
4317
  }
@@ -2905,9 +4397,9 @@ async function handlePricingUpsert(body, deps) {
2905
4397
  const entry = await deps.pricingEngine.upsertManual(input);
2906
4398
  return { status: 200, body: { entry } };
2907
4399
  }
2908
- async function handlePricingDelete(query, deps) {
2909
- const providerId = query.get("providerId")?.trim() ?? "";
2910
- const modelId = query.get("modelId")?.trim() ?? "";
4400
+ async function handlePricingDelete(query2, deps) {
4401
+ const providerId = query2.get("providerId")?.trim() ?? "";
4402
+ const modelId = query2.get("modelId")?.trim() ?? "";
2911
4403
  if (!providerId || !modelId) {
2912
4404
  return err4(400, "delete requires providerId and modelId query params");
2913
4405
  }
@@ -2924,7 +4416,8 @@ async function handlePricingFetchLatest(deps) {
2924
4416
  appliedCount: result.applied.length,
2925
4417
  conflicts: result.conflicts,
2926
4418
  fetchedAt: result.fetchedAt,
2927
- sourceUrl: result.sourceUrl
4419
+ sourceUrl: result.sourceUrl,
4420
+ sources: result.sources
2928
4421
  }
2929
4422
  };
2930
4423
  } catch (e) {
@@ -2972,12 +4465,80 @@ async function handlePricingResolveConflicts(body, deps) {
2972
4465
  return { status: 200, body: { ...resolution, staleCount } };
2973
4466
  }
2974
4467
 
4468
+ // src/admin/accountAllowanceApi.ts
4469
+ function writeJson2(res, status, body) {
4470
+ res.writeHead(status, { "Content-Type": "application/json" });
4471
+ res.end(JSON.stringify(body));
4472
+ }
4473
+ function writeError(res, status, message) {
4474
+ writeJson2(res, status, { error: { type: "account_allowance_error", message } });
4475
+ }
4476
+ function readJson(req) {
4477
+ return new Promise((resolve3, reject) => {
4478
+ const chunks = [];
4479
+ req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
4480
+ req.on("end", () => {
4481
+ try {
4482
+ const text = Buffer.concat(chunks).toString("utf8");
4483
+ const parsed = text ? JSON.parse(text) : {};
4484
+ resolve3(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
4485
+ } catch (error) {
4486
+ reject(error);
4487
+ }
4488
+ });
4489
+ req.on("error", reject);
4490
+ });
4491
+ }
4492
+ function query(req) {
4493
+ const raw = req.url ?? "";
4494
+ const index = raw.indexOf("?");
4495
+ return new URLSearchParams(index >= 0 ? raw.slice(index + 1) : "");
4496
+ }
4497
+ function allowanceProvider(value) {
4498
+ if (!value) return void 0;
4499
+ return value === "claude" || value === "codex" ? value : null;
4500
+ }
4501
+ async function handleAccountAllowanceApi(req, res, method, rest, service) {
4502
+ if (!service) return writeError(res, 501, "account allowance service is not available");
4503
+ if (method === "GET" && rest.length === 1 && rest[0] === "scheduling") {
4504
+ if (!service.getSchedulingStatus) {
4505
+ return writeError(res, 501, "allowance scheduling diagnostics are not available");
4506
+ }
4507
+ return writeJson2(res, 200, { scheduling: service.getSchedulingStatus() });
4508
+ }
4509
+ if (method === "GET") {
4510
+ const params = query(req);
4511
+ const pathProvider = rest.length >= 2 ? rest[0] : null;
4512
+ const providerId = allowanceProvider(pathProvider ?? params.get("providerId") ?? params.get("provider"));
4513
+ if (providerId === null) return writeError(res, 400, "providerId must be claude or codex");
4514
+ const accountId = rest.length >= 2 ? rest[1] : params.get("accountId") ?? void 0;
4515
+ const allowances = await service.list({ providerId, accountId });
4516
+ return writeJson2(res, 200, { allowances });
4517
+ }
4518
+ if (method === "POST" && rest[0] === "refresh") {
4519
+ const body = await readJson(req);
4520
+ const requestedProvider = allowanceProvider(
4521
+ typeof body["providerId"] === "string" ? body["providerId"] : "claude"
4522
+ );
4523
+ if (requestedProvider !== "claude") {
4524
+ return writeError(res, 400, "only Claude allowances support explicit refresh");
4525
+ }
4526
+ const accountId = typeof body["accountId"] === "string" && body["accountId"].trim() ? body["accountId"].trim() : void 0;
4527
+ const allowances = await service.refreshClaude(accountId);
4528
+ if (accountId && allowances.length === 0) {
4529
+ return writeError(res, 404, `Claude account '${accountId}' not found`);
4530
+ }
4531
+ return writeJson2(res, 200, { allowances });
4532
+ }
4533
+ return writeError(res, 405, `method ${method} not allowed on account allowances`);
4534
+ }
4535
+
2975
4536
  // src/admin/adminApi.ts
2976
4537
  function readBody(req) {
2977
- return new Promise((resolve, reject) => {
4538
+ return new Promise((resolve3, reject) => {
2978
4539
  const chunks = [];
2979
4540
  req.on("data", (chunk) => chunks.push(chunk));
2980
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
4541
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
2981
4542
  req.on("error", reject);
2982
4543
  });
2983
4544
  }
@@ -2991,12 +4552,12 @@ async function readJsonBody3(req) {
2991
4552
  return {};
2992
4553
  }
2993
4554
  }
2994
- function writeJson2(res, status, body) {
4555
+ function writeJson3(res, status, body) {
2995
4556
  res.writeHead(status, { "Content-Type": "application/json" });
2996
4557
  res.end(JSON.stringify(body));
2997
4558
  }
2998
4559
  function writeJsonError(res, status, message) {
2999
- writeJson2(res, status, { error: { type: "admin_api_error", message } });
4560
+ writeJson3(res, status, { error: { type: "admin_api_error", message } });
3000
4561
  }
3001
4562
  function maskProviderApiKey(apiKey) {
3002
4563
  if (!apiKey) return "";
@@ -3013,6 +4574,9 @@ function toKeyInfo(row) {
3013
4574
  createdAt: row.createdAt,
3014
4575
  lastUsedAt: row.lastUsedAt,
3015
4576
  revoked: row.revokedAt !== null,
4577
+ kind: row.kind,
4578
+ allowedEndpoints: row.allowedEndpoints,
4579
+ loopbackOnly: row.loopbackOnly,
3016
4580
  maxConcurrency: row.maxConcurrency,
3017
4581
  // Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
3018
4582
  // the UI reads them to render + pre-fill the policy editor.
@@ -3098,6 +4662,8 @@ async function handleAdminApi(req, res, path2, deps) {
3098
4662
  return await handleAccounts(req, res, method, rest, deps);
3099
4663
  case "cli":
3100
4664
  return await handleCli(req, res, method, rest, deps);
4665
+ case "integrations":
4666
+ return await handleIntegrations(req, res, method, rest, deps);
3101
4667
  case "status":
3102
4668
  return await handleStatus(res, method, deps);
3103
4669
  case "playground":
@@ -3125,7 +4691,7 @@ function requestQuery(req) {
3125
4691
  return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
3126
4692
  }
3127
4693
  function writeResult(res, result) {
3128
- writeJson2(res, result.status, result.body);
4694
+ writeJson3(res, result.status, result.body);
3129
4695
  }
3130
4696
  async function handleUsage(req, res, method, rest, deps) {
3131
4697
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
@@ -3134,7 +4700,7 @@ async function handleUsage(req, res, method, rest, deps) {
3134
4700
  async function handleDashboardRoute(res, method, deps) {
3135
4701
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
3136
4702
  const result = await handleDashboard(deps);
3137
- return writeJson2(res, result.status, result.body);
4703
+ return writeJson3(res, result.status, result.body);
3138
4704
  }
3139
4705
  async function handlePricing(req, res, method, rest, deps) {
3140
4706
  if (rest.length === 0) {
@@ -3167,13 +4733,13 @@ async function handleMigrationExport(req, res, method, deps) {
3167
4733
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
3168
4734
  const body = await readJsonBody3(req);
3169
4735
  const result = await handleExport(body, migrationDeps(deps));
3170
- return writeJson2(res, result.status, result.body);
4736
+ return writeJson3(res, result.status, result.body);
3171
4737
  }
3172
4738
  async function handleMigrationImport(req, res, method, deps) {
3173
4739
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
3174
4740
  const body = await readJsonBody3(req);
3175
4741
  const result = await handleImport(body, migrationDeps(deps));
3176
- return writeJson2(res, result.status, result.body);
4742
+ return writeJson3(res, result.status, result.body);
3177
4743
  }
3178
4744
  async function handleProviders(req, res, method, rest, deps) {
3179
4745
  const cfg = loadConfig(deps.configPath);
@@ -3204,10 +4770,10 @@ async function handleProviders(req, res, method, rest, deps) {
3204
4770
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
3205
4771
  const row = cfg.providers.find((p) => p.id === rest[0]);
3206
4772
  if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
3207
- return writeJson2(res, 200, { apiKey: row.apiKey ?? "" });
4773
+ return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
3208
4774
  }
3209
4775
  if (method === "GET") {
3210
- return writeJson2(res, 200, { providers: cfg.providers.map(toProviderView) });
4776
+ return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
3211
4777
  }
3212
4778
  if (method === "POST") {
3213
4779
  const body = await readJsonBody3(req);
@@ -3218,7 +4784,7 @@ async function handleProviders(req, res, method, rest, deps) {
3218
4784
  }
3219
4785
  cfg.providers.push(provider);
3220
4786
  persistProviders(cfg, deps);
3221
- return writeJson2(res, 201, { provider: toProviderView(provider) });
4787
+ return writeJson3(res, 201, { provider: toProviderView(provider) });
3222
4788
  }
3223
4789
  const id = rest[0];
3224
4790
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3231,12 +4797,12 @@ async function handleProviders(req, res, method, rest, deps) {
3231
4797
  if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
3232
4798
  cfg.providers[idx] = updated;
3233
4799
  persistProviders(cfg, deps);
3234
- return writeJson2(res, 200, { provider: toProviderView(updated) });
4800
+ return writeJson3(res, 200, { provider: toProviderView(updated) });
3235
4801
  }
3236
4802
  if (method === "DELETE") {
3237
4803
  cfg.providers.splice(idx, 1);
3238
4804
  persistProviders(cfg, deps);
3239
- return writeJson2(res, 200, { ok: true });
4805
+ return writeJson3(res, 200, { ok: true });
3240
4806
  }
3241
4807
  return writeJsonError(res, 405, `method ${method} not allowed on providers`);
3242
4808
  }
@@ -3269,14 +4835,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
3269
4835
  }
3270
4836
  cfg.providers = reordered;
3271
4837
  persistProviders(cfg, deps);
3272
- return writeJson2(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
4838
+ return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
3273
4839
  }
3274
4840
  async function handleDiscoverModels(res, id, cfg) {
3275
4841
  if (!id) return writeJsonError(res, 400, "provider id required in path");
3276
4842
  const row = cfg.providers.find((p) => p.id === id);
3277
4843
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
3278
- if (row.apiFormat !== "openai") {
3279
- return writeJson2(res, 200, { models: [], unsupportedFormat: true });
4844
+ if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
4845
+ return writeJson3(res, 200, { models: [], unsupportedFormat: true });
3280
4846
  }
3281
4847
  const resolvedKey = resolveEnvKey(row.apiKey);
3282
4848
  const base = row.baseUrl.replace(/\/+$/, "");
@@ -3284,7 +4850,7 @@ async function handleDiscoverModels(res, id, cfg) {
3284
4850
  try {
3285
4851
  const headers = { Accept: "application/json" };
3286
4852
  if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
3287
- const response = await fetchUpstream(url, { method: "GET", headers }, { providerId: "byo" });
4853
+ const response = await fetchUpstream2(url, { method: "GET", headers }, { providerId: "byo" });
3288
4854
  if (!response.ok) {
3289
4855
  const text = await response.text().catch(() => "");
3290
4856
  let message = text.slice(0, 300);
@@ -3293,17 +4859,17 @@ async function handleDiscoverModels(res, id, cfg) {
3293
4859
  message = parsed?.error?.message || parsed?.message || message;
3294
4860
  } catch {
3295
4861
  }
3296
- return writeJson2(res, 200, {
4862
+ return writeJson3(res, 200, {
3297
4863
  models: [],
3298
4864
  error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
3299
4865
  });
3300
4866
  }
3301
4867
  const data = await response.json();
3302
4868
  const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
3303
- return writeJson2(res, 200, { models });
4869
+ return writeJson3(res, 200, { models });
3304
4870
  } catch (err5) {
3305
4871
  const message = err5 instanceof Error ? err5.message : String(err5);
3306
- return writeJson2(res, 200, { models: [], error: `discovery failed: ${message}` });
4872
+ return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
3307
4873
  }
3308
4874
  }
3309
4875
  async function handleTestModel(req, res, id, cfg) {
@@ -3314,13 +4880,13 @@ async function handleTestModel(req, res, id, cfg) {
3314
4880
  const model = typeof body["model"] === "string" ? body["model"].trim() : "";
3315
4881
  if (!model) return writeJsonError(res, 400, "test requires a { model } string");
3316
4882
  if (row.apiFormat === "gemini") {
3317
- return writeJson2(res, 200, { ok: false, unsupportedFormat: true });
4883
+ return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
3318
4884
  }
3319
4885
  const resolvedKey = resolveEnvKey(row.apiKey);
3320
4886
  if (!resolvedKey) {
3321
- return writeJson2(res, 200, { ok: false, message: "no API key configured for this provider" });
4887
+ return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
3322
4888
  }
3323
- const url = row.baseUrl.replace(/\/+$/, "");
4889
+ let url = row.baseUrl.replace(/\/+$/, "");
3324
4890
  const prompt = "Reply with the single word: OK.";
3325
4891
  const headers = { "Content-Type": "application/json" };
3326
4892
  let payload;
@@ -3328,6 +4894,10 @@ async function handleTestModel(req, res, id, cfg) {
3328
4894
  headers["x-api-key"] = resolvedKey;
3329
4895
  headers["anthropic-version"] = "2023-06-01";
3330
4896
  payload = { model, max_tokens: 16, messages: [{ role: "user", content: prompt }] };
4897
+ } else if (row.apiFormat === "openai-response") {
4898
+ headers["Authorization"] = `Bearer ${resolvedKey}`;
4899
+ if (!/\/responses$/.test(url)) url = `${url}/v1/responses`;
4900
+ payload = { model, max_output_tokens: 16, stream: false, input: prompt };
3331
4901
  } else {
3332
4902
  headers["Authorization"] = `Bearer ${resolvedKey}`;
3333
4903
  payload = {
@@ -3339,7 +4909,7 @@ async function handleTestModel(req, res, id, cfg) {
3339
4909
  }
3340
4910
  const startedAt = Date.now();
3341
4911
  try {
3342
- const response = await fetchUpstream(
4912
+ const response = await fetchUpstream2(
3343
4913
  url,
3344
4914
  { method: "POST", headers, body: JSON.stringify(payload) },
3345
4915
  { providerId: "byo" }
@@ -3353,9 +4923,9 @@ async function handleTestModel(req, res, id, cfg) {
3353
4923
  message = parsed?.error?.message || parsed?.message || message;
3354
4924
  } catch {
3355
4925
  }
3356
- return writeJson2(res, 200, { ok: false, status: response.status, latencyMs, message });
4926
+ return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
3357
4927
  }
3358
- return writeJson2(res, 200, {
4928
+ return writeJson3(res, 200, {
3359
4929
  ok: true,
3360
4930
  status: response.status,
3361
4931
  latencyMs,
@@ -3363,7 +4933,7 @@ async function handleTestModel(req, res, id, cfg) {
3363
4933
  });
3364
4934
  } catch (err5) {
3365
4935
  const message = err5 instanceof Error ? err5.message : String(err5);
3366
- return writeJson2(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
4936
+ return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
3367
4937
  }
3368
4938
  }
3369
4939
  function extractSampleText(text, apiFormat) {
@@ -3404,7 +4974,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
3404
4974
  const row = cfg.providers.find((p) => p.id === id);
3405
4975
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
3406
4976
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3407
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
4977
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3408
4978
  }
3409
4979
  function parsePoolKeyInput(body, existing) {
3410
4980
  const out = {};
@@ -3435,7 +5005,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
3435
5005
  row.apiKeys = [...row.apiKeys ?? [], entry];
3436
5006
  persistProviders(cfg, deps);
3437
5007
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3438
- return writeJson2(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
5008
+ return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
3439
5009
  }
3440
5010
  async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
3441
5011
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3455,7 +5025,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
3455
5025
  row.apiKeys[keyIdx] = entry;
3456
5026
  persistProviders(cfg, deps);
3457
5027
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3458
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5028
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3459
5029
  }
3460
5030
  async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
3461
5031
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3469,7 +5039,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
3469
5039
  if (row.apiKeys.length === 0) row.apiKeys = void 0;
3470
5040
  persistProviders(cfg, deps);
3471
5041
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3472
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5042
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3473
5043
  }
3474
5044
  async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
3475
5045
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3483,7 +5053,7 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
3483
5053
  row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
3484
5054
  persistProviders(cfg, deps);
3485
5055
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3486
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5056
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3487
5057
  }
3488
5058
  function parseApiKeysInput(raw, existing) {
3489
5059
  if (!Array.isArray(raw)) return existing;
@@ -3601,7 +5171,9 @@ function parseProviderInput(body, existing) {
3601
5171
  const baseUrl = body["baseUrl"];
3602
5172
  if (!id) return null;
3603
5173
  const name = typeof body["name"] === "string" && body["name"].length > 0 ? body["name"] : body["name"] === null ? void 0 : existing?.name;
3604
- if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini") return null;
5174
+ if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini" && apiFormat !== "openai-response") {
5175
+ return null;
5176
+ }
3605
5177
  if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
3606
5178
  const rawKey = body["apiKey"];
3607
5179
  let apiKey = typeof rawKey === "string" && rawKey.length > 0 ? rawKey : existing?.apiKey ?? "";
@@ -3623,10 +5195,11 @@ function parseProviderInput(body, existing) {
3623
5195
  apiKey = mode.apiKey;
3624
5196
  }
3625
5197
  }
5198
+ const migrated = migrateFormatAxis(apiFormat, transformer);
3626
5199
  return {
3627
5200
  id,
3628
5201
  name,
3629
- apiFormat,
5202
+ apiFormat: migrated.apiFormat,
3630
5203
  baseUrl: baseUrl.trim(),
3631
5204
  apiKey,
3632
5205
  models,
@@ -3637,7 +5210,7 @@ function parseProviderInput(body, existing) {
3637
5210
  apiVersion,
3638
5211
  maxConcurrency,
3639
5212
  modelsEndpoint,
3640
- transformer,
5213
+ transformer: migrated.transformer,
3641
5214
  codingPlan,
3642
5215
  apiModes,
3643
5216
  selectedApiModeId
@@ -3654,13 +5227,13 @@ function handlePresets(res, method) {
3654
5227
  baseUrl: p.baseUrl,
3655
5228
  models: p.models
3656
5229
  }));
3657
- return writeJson2(res, 200, { presets, excluded });
5230
+ return writeJson3(res, 200, { presets, excluded });
3658
5231
  }
3659
5232
  async function handleKeys(req, res, method, rest, deps) {
3660
5233
  if (method === "GET" && rest.length === 0) {
3661
5234
  const rows = await deps.keyDb.outboundApiKeysList();
3662
5235
  const reader = deps.keySpendReader;
3663
- if (!reader) return writeJson2(res, 200, { keys: rows.map(toKeyInfo) });
5236
+ if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
3664
5237
  const now = Date.now();
3665
5238
  const keys = await Promise.all(
3666
5239
  rows.map(async (row) => {
@@ -3672,13 +5245,13 @@ async function handleKeys(req, res, method, rest, deps) {
3672
5245
  return info;
3673
5246
  })
3674
5247
  );
3675
- return writeJson2(res, 200, { keys });
5248
+ return writeJson3(res, 200, { keys });
3676
5249
  }
3677
5250
  if (method === "POST" && rest.length === 0) {
3678
5251
  const body = await readJsonBody3(req);
3679
5252
  const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
3680
5253
  const created = await createNamedKey2(deps.keyDb, name);
3681
- return writeJson2(res, 201, {
5254
+ return writeJson3(res, 201, {
3682
5255
  id: created.id,
3683
5256
  name: created.name,
3684
5257
  keyPrefix: created.keyPrefix,
@@ -3690,13 +5263,13 @@ async function handleKeys(req, res, method, rest, deps) {
3690
5263
  const action = rest[1];
3691
5264
  if (method === "POST" && id && action === "revoke") {
3692
5265
  const ok = await deps.keyDb.outboundApiKeysRevoke(id);
3693
- return writeJson2(res, ok ? 200 : 404, { ok });
5266
+ return writeJson3(res, ok ? 200 : 404, { ok });
3694
5267
  }
3695
5268
  if (method === "POST" && id && action === "enabled") {
3696
5269
  const body = await readJsonBody3(req);
3697
5270
  const enabled = body["enabled"] === true;
3698
5271
  const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
3699
- return writeJson2(res, ok ? 200 : 404, { ok, enabled });
5272
+ return writeJson3(res, ok ? 200 : 404, { ok, enabled });
3700
5273
  }
3701
5274
  if (method === "POST" && id && action === "max-concurrency") {
3702
5275
  const body = await readJsonBody3(req);
@@ -3714,14 +5287,14 @@ async function handleKeys(req, res, method, rest, deps) {
3714
5287
  );
3715
5288
  }
3716
5289
  const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
3717
- return writeJson2(res, ok ? 200 : 404, { ok, maxConcurrency: value });
5290
+ return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
3718
5291
  }
3719
5292
  if (method === "POST" && id && action === "policy") {
3720
5293
  const body = await readJsonBody3(req);
3721
5294
  const parsed = parseKeyPolicyBody(body);
3722
5295
  if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
3723
5296
  const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
3724
- return writeJson2(res, ok ? 200 : 404, { ok });
5297
+ return writeJson3(res, ok ? 200 : 404, { ok });
3725
5298
  }
3726
5299
  return writeJsonError(res, 405, `method ${method} not allowed on keys`);
3727
5300
  }
@@ -3732,10 +5305,10 @@ function validateQueueSegments(patch) {
3732
5305
  errors.push(`${label} must be a number ${min}..${max}`);
3733
5306
  }
3734
5307
  };
3735
- const isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5308
+ const isPlainObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3736
5309
  const umq = patch.userMessageQueue;
3737
5310
  if (umq !== void 0) {
3738
- if (!isPlainObject4(umq)) {
5311
+ if (!isPlainObject5(umq)) {
3739
5312
  errors.push("userMessageQueue must be an object");
3740
5313
  } else {
3741
5314
  if (typeof umq.enabled !== "boolean") {
@@ -3747,7 +5320,7 @@ function validateQueueSegments(patch) {
3747
5320
  }
3748
5321
  const cq = patch.concurrencyQueue;
3749
5322
  if (cq !== void 0) {
3750
- if (!isPlainObject4(cq)) {
5323
+ if (!isPlainObject5(cq)) {
3751
5324
  errors.push("concurrencyQueue must be an object");
3752
5325
  } else {
3753
5326
  checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
@@ -3757,7 +5330,7 @@ function validateQueueSegments(patch) {
3757
5330
  }
3758
5331
  const ah = patch.accountHealth;
3759
5332
  if (ah !== void 0) {
3760
- if (!isPlainObject4(ah)) {
5333
+ if (!isPlainObject5(ah)) {
3761
5334
  errors.push("accountHealth must be an object");
3762
5335
  } else {
3763
5336
  if (typeof ah.overloadCooldownEnabled !== "boolean") {
@@ -3768,6 +5341,31 @@ function validateQueueSegments(patch) {
3768
5341
  }
3769
5342
  return errors;
3770
5343
  }
5344
+ function validateAllowanceSchedulingSegment(patch) {
5345
+ const value = patch.allowanceScheduling;
5346
+ if (value === void 0) return [];
5347
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5348
+ return ["allowanceScheduling must be an object"];
5349
+ }
5350
+ const allowance = value;
5351
+ const errors = [];
5352
+ const checkNumber = (field, min, max) => {
5353
+ const candidate = allowance[field];
5354
+ if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate < min || candidate > max) {
5355
+ errors.push(`allowanceScheduling.${field} must be a number ${min}..${max}`);
5356
+ }
5357
+ };
5358
+ if (typeof allowance.enabled !== "boolean") {
5359
+ errors.push("allowanceScheduling.enabled must be a boolean");
5360
+ }
5361
+ checkNumber("demoteAtPercent", 0, 100);
5362
+ checkNumber("pauseAtPercent", 0, 100);
5363
+ checkNumber("priorityPenalty", 1, 1e3);
5364
+ if (typeof allowance.demoteAtPercent === "number" && typeof allowance.pauseAtPercent === "number" && allowance.pauseAtPercent < allowance.demoteAtPercent) {
5365
+ errors.push("allowanceScheduling.pauseAtPercent must be >= demoteAtPercent");
5366
+ }
5367
+ return errors;
5368
+ }
3771
5369
  async function handleServer(req, res, method, deps) {
3772
5370
  if (method === "GET") {
3773
5371
  const config = await loadServerConfig2(deps.settingsStore);
@@ -3775,7 +5373,7 @@ async function handleServer(req, res, method, deps) {
3775
5373
  if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
3776
5374
  if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
3777
5375
  if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
3778
- return writeJson2(res, 200, { server });
5376
+ return writeJson3(res, 200, { server });
3779
5377
  }
3780
5378
  if (method === "PUT") {
3781
5379
  const patch = await readJsonBody3(req);
@@ -3783,6 +5381,18 @@ async function handleServer(req, res, method, deps) {
3783
5381
  if (queueErrors.length > 0) {
3784
5382
  return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
3785
5383
  }
5384
+ const allowanceErrors = validateAllowanceSchedulingSegment(patch);
5385
+ if (allowanceErrors.length > 0) {
5386
+ return writeJsonError(
5387
+ res,
5388
+ 400,
5389
+ `invalid allowance scheduling config: ${allowanceErrors.join("; ")}`
5390
+ );
5391
+ }
5392
+ const bindingErrors = validateGatewayBindingsSegment(patch);
5393
+ if (bindingErrors.length > 0) {
5394
+ return writeJsonError(res, 400, `invalid gateway bindings: ${bindingErrors.join("; ")}`);
5395
+ }
3786
5396
  const webhookErrors = validateWebhookSegment(patch);
3787
5397
  if (webhookErrors.length > 0) {
3788
5398
  return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
@@ -3809,66 +5419,113 @@ async function handleServer(req, res, method, deps) {
3809
5419
  const merged = mergeServerConfig(current, effectivePatch);
3810
5420
  await saveServerConfig(deps.settingsStore, merged);
3811
5421
  setServerProxyConfig(merged.proxy);
5422
+ getSharedAccountAllowanceScheduling2().configure(merged.allowanceScheduling);
5423
+ deps.allowanceRefreshScheduler?.configure(merged.allowanceScheduling);
3812
5424
  applyWebhookConfig(merged.webhook);
3813
5425
  applyAuditConfig(merged.audit);
3814
5426
  applyBillingConfig(merged.billing);
3815
- if (merged.enabled) {
3816
- const missing = validateServerModelConfig(merged);
3817
- if (missing.length > 0) {
3818
- if (deps.outboundApiServer.getStatus().running) {
3819
- await deps.outboundApiServer.stop();
3820
- }
3821
- return writeJson2(res, 200, {
3822
- server: merged,
3823
- error: { code: "incomplete-model-config", missing }
3824
- });
3825
- }
3826
- }
3827
- try {
3828
- await deps.outboundApiServer.applyConfig({
3829
- enabled: merged.enabled,
3830
- networkBinding: merged.networkBinding,
3831
- endpoints: merged.endpoints,
3832
- port: merged.port,
3833
- userMessageQueue: merged.userMessageQueue,
3834
- concurrencyQueue: merged.concurrencyQueue,
3835
- // voucher-redemption #9: hot-apply the voucher flag so enabling the product
3836
- // takes effect without a restart.
3837
- voucher: merged.voucher
3838
- });
3839
- } catch (err5) {
3840
- const missing = incompleteConfigMissing(err5);
3841
- if (missing) {
3842
- return writeJson2(res, 200, {
3843
- server: merged,
3844
- error: { code: "incomplete-model-config", missing }
3845
- });
3846
- }
3847
- throw err5;
3848
- }
3849
- return writeJson2(res, 200, { server: merged });
5427
+ await deps.outboundApiServer.applyConfig({
5428
+ enabled: merged.enabled,
5429
+ networkBinding: merged.networkBinding,
5430
+ endpoints: merged.endpoints,
5431
+ bindings: merged.bindings,
5432
+ port: merged.port,
5433
+ userMessageQueue: merged.userMessageQueue,
5434
+ concurrencyQueue: merged.concurrencyQueue,
5435
+ // voucher-redemption #9: hot-apply the voucher flag so enabling the product
5436
+ // takes effect without a restart.
5437
+ voucher: merged.voucher
5438
+ });
5439
+ return writeJson3(res, 200, { server: merged });
3850
5440
  }
3851
5441
  return writeJsonError(res, 405, `method ${method} not allowed on server`);
3852
5442
  }
3853
- function incompleteConfigMissing(err5) {
3854
- if (typeof err5 !== "object" || err5 === null) return null;
3855
- const missing = err5.missing;
3856
- return Array.isArray(missing) ? missing : null;
3857
- }
3858
5443
  async function handleAccounts(req, res, method, rest, deps) {
5444
+ if (rest[0] === "allowances") {
5445
+ return handleAccountAllowanceApi(
5446
+ req,
5447
+ res,
5448
+ method,
5449
+ rest.slice(1),
5450
+ deps.accountAllowanceService
5451
+ );
5452
+ }
3859
5453
  if (method === "GET" && rest.length === 0) {
3860
5454
  const accounts = await deps.subscriptionAccounts.listAll();
3861
5455
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
3862
5456
  const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
3863
- return writeJson2(res, 200, { accounts, providerAccounts, externalCli });
5457
+ return writeJson3(res, 200, { accounts, providerAccounts, externalCli });
5458
+ }
5459
+ if (method === "POST" && rest[0] === "batch" && rest.length === 1) {
5460
+ const body = await readJsonBody3(req);
5461
+ const parsed = validateAccountBatchBody(body);
5462
+ if (!parsed) return writeJsonError(res, 400, "invalid account batch request");
5463
+ const result = await deps.subscriptionTokenWriter.batchManageAccounts(parsed.refs, parsed.mutation);
5464
+ if (!result.ok) {
5465
+ return writeJsonError(
5466
+ res,
5467
+ 404,
5468
+ `account '${result.missing.accountId}' not found for provider '${result.missing.providerId}'`
5469
+ );
5470
+ }
5471
+ if (parsed.mutation.action === "delete") {
5472
+ for (const ref of parsed.refs) {
5473
+ deps.accountAllowanceService?.removeAccountSnapshot?.(ref.providerId, ref.accountId);
5474
+ }
5475
+ }
5476
+ return writeJson3(res, 200, { ok: true, affected: result.affected });
3864
5477
  }
3865
5478
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
3866
5479
  const result = handleCodexOAuthStatus(rest[2], deps);
3867
- return writeJson2(res, result.status, result.body);
5480
+ return writeJson3(res, result.status, result.body);
3868
5481
  }
3869
5482
  if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
3870
5483
  const result = handleCodexOAuthCancel(rest[2], deps);
3871
- return writeJson2(res, result.status, result.body);
5484
+ return writeJson3(res, result.status, result.body);
5485
+ }
5486
+ if (method === "GET" && rest.length === 3 && rest[2] === "diagnostics") {
5487
+ const providerId = asSubscriptionProviderId(rest[0]);
5488
+ if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
5489
+ const accountId = rest[1];
5490
+ const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
5491
+ if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
5492
+ return writeJsonError(res, 404, `account '${accountId}' not found`);
5493
+ }
5494
+ const health2 = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
5495
+ const allowance = deps.accountAllowanceService?.getSchedulingStatus?.()?.history.filter((entry) => entry.providerId === providerId && entry.accountId === accountId).map((entry) => ({
5496
+ kind: "allowance-policy",
5497
+ at: Date.parse(entry.decidedAt),
5498
+ providerId: entry.providerId,
5499
+ accountId: entry.accountId,
5500
+ action: entry.action,
5501
+ reason: entry.reason,
5502
+ usedPercent: entry.usedPercent,
5503
+ resumeAt: entry.resumeAt
5504
+ })) ?? [];
5505
+ const diagnostics = [...health2, ...allowance].sort((left, right) => right.at - left.at).slice(0, 200);
5506
+ return writeJson3(res, 200, { diagnostics });
5507
+ }
5508
+ if (method === "GET" && rest.length === 3 && rest[2] === "events") {
5509
+ const providerId = asSubscriptionProviderId(rest[0]);
5510
+ if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
5511
+ const accountId = rest[1];
5512
+ const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
5513
+ if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
5514
+ return writeJsonError(res, 404, `account '${accountId}' not found`);
5515
+ }
5516
+ const snapshot = deps.accountProbeService?.getAllHistory().find((entry) => entry.providerId === providerId && entry.accountId === accountId);
5517
+ const diagnostics = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
5518
+ return writeJson3(res, 200, { events: snapshot?.records ?? [], diagnostics });
5519
+ }
5520
+ if (method === "PATCH" && rest.length === 2) {
5521
+ const providerId = asSubscriptionProviderId(rest[0]);
5522
+ if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
5523
+ const body = await readJsonBody3(req);
5524
+ const patch = validateAccountMetadataPatch(body);
5525
+ if (!patch) return writeJsonError(res, 400, "invalid account metadata patch");
5526
+ const result = await deps.subscriptionTokenWriter.patchAccountMetadata(providerId, rest[1], patch);
5527
+ if (!result.ok) return writeJsonError(res, 404, `account '${rest[1]}' not found`);
5528
+ return writeJson3(res, 200, { ok: true });
3872
5529
  }
3873
5530
  if (method === "PUT" || method === "POST" || method === "DELETE") {
3874
5531
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -3877,12 +5534,12 @@ async function handleAccounts(req, res, method, rest, deps) {
3877
5534
  }
3878
5535
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
3879
5536
  const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
3880
- return writeJson2(res, result.status, result.body);
5537
+ return writeJson3(res, result.status, result.body);
3881
5538
  }
3882
5539
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
3883
5540
  const body2 = await readJsonBody3(req);
3884
5541
  const result = await handleOAuthComplete(providerId, body2, deps);
3885
- return writeJson2(res, result.status, result.body);
5542
+ return writeJson3(res, result.status, result.body);
3886
5543
  }
3887
5544
  if (method === "POST" && rest[1] === "accounts") {
3888
5545
  const body2 = await readJsonBody3(req);
@@ -3893,7 +5550,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3893
5550
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
3894
5551
  await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
3895
5552
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3896
- return writeJson2(res, 200, status2 ? { account: status2 } : { ok: true });
5553
+ return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
3897
5554
  }
3898
5555
  if (method === "POST" && rest[1] === "import-external") {
3899
5556
  if (providerId !== "claude" && providerId !== "codex") {
@@ -3906,7 +5563,13 @@ async function handleAccounts(req, res, method, rest, deps) {
3906
5563
  return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
3907
5564
  }
3908
5565
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3909
- return writeJson2(res, 200, { ok: true, account: status2 ?? void 0 });
5566
+ return writeJson3(res, 200, {
5567
+ ok: true,
5568
+ account: status2 ?? void 0,
5569
+ nativeCredentialMode: result.nativeCredentialMode,
5570
+ refreshWritesNativeCredentials: result.refreshWritesNativeCredentials,
5571
+ message: "Imported a read-only copy. Omnicross does not manage the native CLI credential file and future refreshes do not write it."
5572
+ });
3910
5573
  }
3911
5574
  if (method === "POST" && rest[1] === "refresh") {
3912
5575
  if (providerId === "opencodego") {
@@ -3915,7 +5578,17 @@ async function handleAccounts(req, res, method, rest, deps) {
3915
5578
  const writer2 = deps.subscriptionTokenWriter;
3916
5579
  const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
3917
5580
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3918
- return writeJson2(res, 200, { ok, account: status2 ?? void 0 });
5581
+ return writeJson3(res, 200, { ok, account: status2 ?? void 0 });
5582
+ }
5583
+ if (method === "POST" && rest.length === 3 && rest[2] === "test") {
5584
+ const accountId = rest[1];
5585
+ if (!deps.accountProbeService) return writeJsonError(res, 501, "account probe service unavailable");
5586
+ const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
5587
+ if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
5588
+ return writeJsonError(res, 404, `account '${accountId}' not found`);
5589
+ }
5590
+ const result = await deps.accountProbeService.probeAccount(providerId, accountId);
5591
+ return writeJson3(res, 200, { ok: result.ok, marked: result.marked });
3919
5592
  }
3920
5593
  if (method === "POST" && rest[2] === "label") {
3921
5594
  const accountId = rest[1];
@@ -3923,7 +5596,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3923
5596
  const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
3924
5597
  const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
3925
5598
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3926
- return writeJson2(res, 200, { ok: true });
5599
+ return writeJson3(res, 200, { ok: true });
3927
5600
  }
3928
5601
  if (method === "POST" && rest[2] === "priority") {
3929
5602
  const accountId = rest[1];
@@ -3935,7 +5608,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3935
5608
  }
3936
5609
  const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
3937
5610
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3938
- return writeJson2(res, 200, { ok: true });
5611
+ return writeJson3(res, 200, { ok: true });
3939
5612
  }
3940
5613
  if (method === "POST" && rest[2] === "proxy") {
3941
5614
  const accountId = rest[1];
@@ -3948,7 +5621,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3948
5621
  }
3949
5622
  const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
3950
5623
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3951
- return writeJson2(res, 200, { ok: true });
5624
+ return writeJson3(res, 200, { ok: true });
3952
5625
  }
3953
5626
  if (method === "POST" && rest[2] === "supported-models") {
3954
5627
  const accountId = rest[1];
@@ -3957,7 +5630,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3957
5630
  if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
3958
5631
  const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
3959
5632
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3960
- return writeJson2(res, 200, { ok: true });
5633
+ return writeJson3(res, 200, { ok: true });
3961
5634
  }
3962
5635
  if (method === "PUT" && rest[1] === "active") {
3963
5636
  const body2 = await readJsonBody3(req);
@@ -3965,17 +5638,22 @@ async function handleAccounts(req, res, method, rest, deps) {
3965
5638
  if (!id) return writeJsonError(res, 400, "active switch requires { id }");
3966
5639
  const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
3967
5640
  if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
3968
- return writeJson2(res, 200, { ok: true });
5641
+ return writeJson3(res, 200, { ok: true });
3969
5642
  }
3970
- if (method === "DELETE" && rest.length >= 2) {
5643
+ if (method === "DELETE" && rest.length === 2) {
3971
5644
  const accountId = rest[1];
3972
5645
  const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
3973
5646
  if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
3974
- return writeJson2(res, 200, { ok: true });
5647
+ deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
5648
+ return writeJson3(res, 200, { ok: true });
3975
5649
  }
3976
- if (method === "DELETE") {
5650
+ if (method === "DELETE" && rest.length === 1) {
3977
5651
  await deps.subscriptionTokenWriter.clearProvider(providerId);
3978
- return writeJson2(res, 200, { ok: true });
5652
+ deps.accountAllowanceService?.removeProviderSnapshots?.(providerId);
5653
+ return writeJson3(res, 200, { ok: true });
5654
+ }
5655
+ if (method === "DELETE") {
5656
+ return writeJsonError(res, 405, "method DELETE not allowed on this accounts path");
3979
5657
  }
3980
5658
  const body = await readJsonBody3(req);
3981
5659
  const config = validateTokenBody(providerId, body);
@@ -3984,22 +5662,22 @@ async function handleAccounts(req, res, method, rest, deps) {
3984
5662
  }
3985
5663
  await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
3986
5664
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
3987
- return writeJson2(res, 200, status ? { account: status } : { ok: true });
5665
+ return writeJson3(res, 200, status ? { account: status } : { ok: true });
3988
5666
  }
3989
5667
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
3990
5668
  }
3991
5669
  async function handleCli(req, res, method, rest, deps) {
3992
5670
  if (method === "GET" && rest.length === 0) {
3993
5671
  const result = handleCliList(process.platform, deps.cliPathProbe);
3994
- return writeJson2(res, result.status, result.body);
5672
+ return writeJson3(res, result.status, result.body);
3995
5673
  }
3996
5674
  if (method === "GET" && rest[0] === "sessions") {
3997
5675
  const result = handleCliSessions();
3998
- return writeJson2(res, result.status, result.body);
5676
+ return writeJson3(res, result.status, result.body);
3999
5677
  }
4000
5678
  if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
4001
5679
  const result = handleCliStop(rest[1]);
4002
- return writeJson2(res, result.status, result.body);
5680
+ return writeJson3(res, result.status, result.body);
4003
5681
  }
4004
5682
  if (method === "POST" && rest[1] === "install") {
4005
5683
  const cli = rest[0];
@@ -4007,7 +5685,7 @@ async function handleCli(req, res, method, rest, deps) {
4007
5685
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
4008
5686
  }
4009
5687
  const result = await handleCliInstall(cli, deps.cliCommandRunner);
4010
- return writeJson2(res, result.status, result.body);
5688
+ return writeJson3(res, result.status, result.body);
4011
5689
  }
4012
5690
  if (method === "POST" && rest[1] === "launch") {
4013
5691
  const cli = rest[0];
@@ -4022,28 +5700,100 @@ async function handleCli(req, res, method, rest, deps) {
4022
5700
  opener: deps.cliTerminalOpener,
4023
5701
  probe: deps.cliPathProbe
4024
5702
  });
4025
- return writeJson2(res, result.status, result.body);
5703
+ return writeJson3(res, result.status, result.body);
4026
5704
  }
4027
5705
  return writeJsonError(res, 405, `method ${method} not allowed on cli`);
4028
5706
  }
5707
+ async function handleIntegrations(req, res, method, rest, deps) {
5708
+ const factory = deps.integrationManagerFactory;
5709
+ if (!factory) return writeJsonError(res, 501, "native CLI integration is not available");
5710
+ const manager = factory();
5711
+ try {
5712
+ if (method === "GET" && rest.length === 0) {
5713
+ return writeJson3(res, 200, {
5714
+ integrations: await manager.listStatus(),
5715
+ gateway: deps.outboundApiServer.getStatus()
5716
+ });
5717
+ }
5718
+ if (method === "POST" && rest.length === 1 && rest[0] === "rotate") {
5719
+ await manager.rotateGatewayKey();
5720
+ return writeJson3(res, 200, { ok: true, integrations: await manager.listStatus() });
5721
+ }
5722
+ const client = rest[0];
5723
+ if (!isIntegrationClient(client)) {
5724
+ return writeJsonError(res, 400, `unknown integration client '${client ?? ""}'`);
5725
+ }
5726
+ if (method === "POST" && rest[1] === "plan") {
5727
+ const body = await readJsonBody3(req);
5728
+ const configPath = body.configPath;
5729
+ if (configPath !== void 0 && typeof configPath !== "string") {
5730
+ return writeJsonError(res, 400, "configPath must be a string");
5731
+ }
5732
+ const plan = await manager.plan(client, configPath);
5733
+ return writeJson3(res, 200, { plan });
5734
+ }
5735
+ if (method === "POST" && (rest[1] === "install" || rest[1] === "apply")) {
5736
+ const body = await readJsonBody3(req);
5737
+ const configPath = body.configPath;
5738
+ if (configPath !== void 0 && typeof configPath !== "string") {
5739
+ return writeJsonError(res, 400, "configPath must be a string");
5740
+ }
5741
+ const status = await manager.install(client, configPath);
5742
+ return writeJson3(res, 200, { integration: status });
5743
+ }
5744
+ if (method === "POST" && rest[1] === "repair") {
5745
+ const status = await manager.repair(client);
5746
+ return writeJson3(res, 200, { integration: status });
5747
+ }
5748
+ if (method === "DELETE" && rest.length === 1 || method === "POST" && rest[1] === "remove") {
5749
+ const status = await manager.remove(client);
5750
+ return writeJson3(res, 200, { integration: status });
5751
+ }
5752
+ return writeJsonError(res, 405, `method ${method} not allowed on integrations`);
5753
+ } catch (error) {
5754
+ if (error instanceof IntegrationConflictError) {
5755
+ return writeJsonError(res, 409, error.message);
5756
+ }
5757
+ throw error;
5758
+ }
5759
+ }
5760
+ function isIntegrationClient(value) {
5761
+ return value === "codex" || value === "claude";
5762
+ }
4029
5763
  async function handleStatus(res, method, deps) {
4030
5764
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
4031
5765
  const status = deps.outboundApiServer.getStatus();
4032
5766
  const serverConfig = await loadServerConfig2(deps.settingsStore);
4033
- const endpoints = serverConfig.endpoints.map((e) => {
4034
- if (isKindMappedEndpoint(e.endpoint)) {
4035
- return { endpoint: e.endpoint, kinds: e.modelMap ?? {}, useSubscription: e.useSubscription };
5767
+ const endpoints = ["chat", "responses", "messages", "gemini"].map((endpoint) => {
5768
+ const routes = (serverConfig.bindings ?? []).filter((binding) => binding.enabled && binding.endpoint === endpoint).map((binding) => gatewayBindingToEndpointConfig(binding));
5769
+ const useSubscription = routes.some((route) => route.useSubscription);
5770
+ if (isKindMappedEndpoint(endpoint)) {
5771
+ const kinds = {};
5772
+ for (const route of routes) {
5773
+ for (const [kind, ref] of Object.entries(route.modelMap ?? {})) {
5774
+ if (ref?.trim() && !kinds[kind]) kinds[kind] = ref;
5775
+ }
5776
+ }
5777
+ return { endpoint, kinds, useSubscription };
4036
5778
  }
4037
- if (e.endpoint === "chat") {
4038
- return { endpoint: e.endpoint, models: e.models ?? [], useSubscription: e.useSubscription };
5779
+ if (endpoint === "chat") {
5780
+ return {
5781
+ endpoint,
5782
+ models: [...new Set(routes.flatMap((route) => route.models ?? []))],
5783
+ useSubscription
5784
+ };
4039
5785
  }
4040
- return { endpoint: e.endpoint, model: e.defaultModel ?? "", useSubscription: e.useSubscription };
5786
+ return {
5787
+ endpoint,
5788
+ model: routes.find((route) => route.defaultModel?.trim())?.defaultModel ?? "",
5789
+ useSubscription
5790
+ };
4041
5791
  });
4042
5792
  if (status.running) {
4043
5793
  const queueStatus = deps.outboundApiServer.getQueueStatus();
4044
- return writeJson2(res, 200, { ...status, endpoints, queueStatus });
5794
+ return writeJson3(res, 200, { ...status, endpoints, queueStatus });
4045
5795
  }
4046
- return writeJson2(res, 200, { ...status, endpoints });
5796
+ return writeJson3(res, 200, { ...status, endpoints });
4047
5797
  }
4048
5798
  function resolvePlaygroundPath(endpoint, body) {
4049
5799
  switch (endpoint) {
@@ -4069,16 +5819,16 @@ async function handlePlayground(req, res, method, deps) {
4069
5819
  const payload = body["body"];
4070
5820
  const status = deps.outboundApiServer.getStatus();
4071
5821
  if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
4072
- const path2 = resolvePlaygroundPath(endpoint, isRecord(payload) ? payload : {});
5822
+ const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
4073
5823
  if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
4074
5824
  const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
4075
5825
  await proxyToOutbound(res, status.port, path2, key, upstreamBody);
4076
5826
  }
4077
- function isRecord(v) {
5827
+ function isRecord2(v) {
4078
5828
  return !!v && typeof v === "object" && !Array.isArray(v);
4079
5829
  }
4080
5830
  function proxyToOutbound(res, outboundPort, path2, key, body) {
4081
- return new Promise((resolve) => {
5831
+ return new Promise((resolve3) => {
4082
5832
  const upstream = http.request(
4083
5833
  {
4084
5834
  host: "127.0.0.1",
@@ -4099,14 +5849,14 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
4099
5849
  proxRes.on("data", (chunk) => res.write(chunk));
4100
5850
  proxRes.on("end", () => {
4101
5851
  res.end();
4102
- resolve();
5852
+ resolve3();
4103
5853
  });
4104
5854
  }
4105
5855
  );
4106
5856
  upstream.on("error", (err5) => {
4107
5857
  if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
4108
5858
  else res.end();
4109
- resolve();
5859
+ resolve3();
4110
5860
  });
4111
5861
  upstream.write(body);
4112
5862
  upstream.end();
@@ -4114,7 +5864,7 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
4114
5864
  }
4115
5865
 
4116
5866
  // src/admin/uiStatic.ts
4117
- import { existsSync as existsSync4, statSync } from "fs";
5867
+ import { existsSync as existsSync7, statSync as statSync2 } from "fs";
4118
5868
  import { readFile } from "fs/promises";
4119
5869
  import { createRequire } from "module";
4120
5870
  import path from "path";
@@ -4137,13 +5887,13 @@ var CONTENT_TYPES = {
4137
5887
  function resolveUiDist() {
4138
5888
  const fromEnv = process.env["OMNICROSS_UI_DIST"];
4139
5889
  if (fromEnv) {
4140
- return existsSync4(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
5890
+ return existsSync7(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
4141
5891
  }
4142
5892
  try {
4143
5893
  const req = createRequire(typeof __filename !== "undefined" ? __filename : import.meta.url);
4144
5894
  const pkgJson = req.resolve("@omnicross/ui/package.json");
4145
5895
  const dist = path.join(path.dirname(pkgJson), "dist");
4146
- return existsSync4(path.join(dist, "index.html")) ? dist : null;
5896
+ return existsSync7(path.join(dist, "index.html")) ? dist : null;
4147
5897
  } catch {
4148
5898
  return null;
4149
5899
  }
@@ -4192,7 +5942,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
4192
5942
  return true;
4193
5943
  }
4194
5944
  let target = filePath;
4195
- if (!existsSync4(target) || statSync(target).isDirectory()) {
5945
+ if (!existsSync7(target) || statSync2(target).isDirectory()) {
4196
5946
  if (path.extname(rel) === "") {
4197
5947
  target = path.join(uiDist, "index.html");
4198
5948
  } else {
@@ -4209,7 +5959,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
4209
5959
  }
4210
5960
 
4211
5961
  // src/admin/version.ts
4212
- var DAEMON_VERSION = true ? "0.1.4" : "0.0.0-dev";
5962
+ var DAEMON_VERSION = true ? "0.1.6" : "0.0.0-dev";
4213
5963
 
4214
5964
  // src/admin/AdminServer.ts
4215
5965
  var LOOPBACK_ADDR = "127.0.0.1";
@@ -4248,14 +5998,14 @@ var AdminServer = class {
4248
5998
  }
4249
5999
  /** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
4250
6000
  listen(bindAddr, port) {
4251
- return new Promise((resolve, reject) => {
6001
+ return new Promise((resolve3, reject) => {
4252
6002
  const server = http2.createServer((req, res) => {
4253
6003
  this.onRequest(req, res);
4254
6004
  });
4255
6005
  const onError = (err5) => {
4256
6006
  if (err5.code === "EADDRINUSE" && port !== 0) {
4257
6007
  server.removeListener("error", onError);
4258
- this.listen(bindAddr, 0).then(resolve, reject);
6008
+ this.listen(bindAddr, 0).then(resolve3, reject);
4259
6009
  return;
4260
6010
  }
4261
6011
  reject(err5);
@@ -4267,7 +6017,7 @@ var AdminServer = class {
4267
6017
  server.removeListener("error", onError);
4268
6018
  server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
4269
6019
  this.server = server;
4270
- resolve(addr.port);
6020
+ resolve3(addr.port);
4271
6021
  } else {
4272
6022
  reject(new Error("Failed to get admin server address"));
4273
6023
  }
@@ -4348,8 +6098,8 @@ var AdminServer = class {
4348
6098
  if (!server) return;
4349
6099
  this.server = null;
4350
6100
  this.boundPort = 0;
4351
- return new Promise((resolve) => {
4352
- server.close(() => resolve());
6101
+ return new Promise((resolve3) => {
6102
+ server.close(() => resolve3());
4353
6103
  });
4354
6104
  }
4355
6105
  /** A live status snapshot. */
@@ -4465,7 +6215,7 @@ function pageHtml(message) {
4465
6215
  return `<!doctype html><meta charset="utf-8"><title>omnicross login</title><body style="font-family:sans-serif;padding:2rem"><h2>${message}</h2><p>You can close this window and return to the terminal.</p></body>`;
4466
6216
  }
4467
6217
  function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
4468
- return new Promise((resolve, reject) => {
6218
+ return new Promise((resolve3, reject) => {
4469
6219
  let settled = false;
4470
6220
  const finish = (server2, fn) => {
4471
6221
  if (settled) return;
@@ -4496,7 +6246,7 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal
4496
6246
  }
4497
6247
  res.writeHead(200, { "Content-Type": "text/html" });
4498
6248
  res.end(pageHtml("Login complete."));
4499
- finish(server, () => resolve(code));
6249
+ finish(server, () => resolve3(code));
4500
6250
  });
4501
6251
  const abort = () => finish(server, () => reject(new Error("login: cancelled")));
4502
6252
  if (signal?.aborted) {
@@ -4600,8 +6350,10 @@ var EMPTY_CHAIN = {
4600
6350
  modelTransformers: []
4601
6351
  };
4602
6352
  var FORMAT_TRANSFORMER = {
6353
+ openai: "openai",
4603
6354
  anthropic: "anthropic",
4604
- gemini: "gemini"
6355
+ gemini: "gemini",
6356
+ "openai-response": "openai-response"
4605
6357
  };
4606
6358
  var ConfigFileProviderConfigSource = class {
4607
6359
  providers = /* @__PURE__ */ new Map();
@@ -4670,7 +6422,7 @@ var ConfigFileProviderConfigSource = class {
4670
6422
  }
4671
6423
  async getMainTransformer(providerId) {
4672
6424
  const row = this.providers.get(providerId);
4673
- if (!row || row.apiFormat === "openai") return null;
6425
+ if (!row) return null;
4674
6426
  const name = FORMAT_TRANSFORMER[row.apiFormat];
4675
6427
  const instances = this.transformerService.resolveTransformerReferences([name]);
4676
6428
  return instances[0] ?? null;
@@ -4680,11 +6432,8 @@ var ConfigFileProviderConfigSource = class {
4680
6432
  if (!row) return EMPTY_CHAIN;
4681
6433
  const customRefs = row.transformer?.use ?? [];
4682
6434
  if (customRefs.length === 0) return EMPTY_CHAIN;
4683
- const formatName = row.apiFormat === "openai" ? void 0 : FORMAT_TRANSFORMER[row.apiFormat];
4684
- const effectiveRefs = formatName ? customRefs.filter((ref) => (typeof ref === "string" ? ref : ref[0]) !== formatName) : customRefs;
4685
- if (effectiveRefs.length === 0) return EMPTY_CHAIN;
4686
6435
  return {
4687
- providerTransformers: this.transformerService.resolveTransformerReferences(effectiveRefs),
6436
+ providerTransformers: this.transformerService.resolveTransformerReferences(customRefs),
4688
6437
  modelTransformers: []
4689
6438
  };
4690
6439
  }
@@ -4715,7 +6464,7 @@ function resolvePreferredApiKey(row) {
4715
6464
  }
4716
6465
  function toLLMProvider(row) {
4717
6466
  const apiFormat = row.apiFormat === "gemini" ? "google" : row.apiFormat;
4718
- const transformer = row.apiFormat === "openai" ? void 0 : { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
6467
+ const transformer = { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
4719
6468
  const allModels = row.models ?? [];
4720
6469
  const models = row.modelConfigs ? allModels.filter((id) => row.modelConfigs.find((c) => c.id === id)?.enabled !== false) : allModels;
4721
6470
  return {
@@ -4785,7 +6534,7 @@ var ConfigurableLogger = class {
4785
6534
  const stream = this.fileStream;
4786
6535
  this.fileStream = null;
4787
6536
  if (!stream) return Promise.resolve();
4788
- return new Promise((resolve) => stream.end(() => resolve()));
6537
+ return new Promise((resolve3) => stream.end(() => resolve3()));
4789
6538
  }
4790
6539
  emit(level, message, error, meta) {
4791
6540
  if (LEVEL_ORDER[level] > this.threshold) return;
@@ -4896,7 +6645,7 @@ function safeStringify(value) {
4896
6645
  }
4897
6646
 
4898
6647
  // src/ports/JsonApiServerSettingsStore.ts
4899
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
6648
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
4900
6649
  import { OUTBOUND_API_SERVER_CONFIG_KEY } from "@omnicross/core/outbound-api";
4901
6650
  var JsonApiServerSettingsStore = class {
4902
6651
  /**
@@ -4923,7 +6672,7 @@ var JsonApiServerSettingsStore = class {
4923
6672
  if (key !== OUTBOUND_API_SERVER_CONFIG_KEY) return;
4924
6673
  const file = this.readFile();
4925
6674
  file.server = this.encryptSecrets(value);
4926
- writeFileSync4(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
6675
+ writeFileSync6(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
4927
6676
  }
4928
6677
  /** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
4929
6678
  encryptSecrets(config) {
@@ -4946,7 +6695,7 @@ var JsonApiServerSettingsStore = class {
4946
6695
  /** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
4947
6696
  readFile() {
4948
6697
  try {
4949
- const raw = readFileSync5(this.configPath, "utf8");
6698
+ const raw = readFileSync8(this.configPath, "utf8");
4950
6699
  const parsed = JSON.parse(raw);
4951
6700
  if (parsed && typeof parsed === "object") return parsed;
4952
6701
  } catch {
@@ -4956,8 +6705,8 @@ var JsonApiServerSettingsStore = class {
4956
6705
  };
4957
6706
 
4958
6707
  // src/ports/JsonlUsageEventStore.ts
4959
- import { randomUUID as randomUUID3 } from "crypto";
4960
- import { appendFileSync, existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
6708
+ import { randomUUID as randomUUID4 } from "crypto";
6709
+ import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
4961
6710
  var JsonlUsageEventStore = class {
4962
6711
  constructor(eventsPath, isPriced) {
4963
6712
  this.eventsPath = eventsPath;
@@ -4969,7 +6718,7 @@ var JsonlUsageEventStore = class {
4969
6718
  async insert(input) {
4970
6719
  const row = {
4971
6720
  ...input,
4972
- id: randomUUID3(),
6721
+ id: randomUUID4(),
4973
6722
  ts: input.ts ?? Date.now()
4974
6723
  };
4975
6724
  appendFileSync(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
@@ -5067,15 +6816,15 @@ var JsonlUsageEventStore = class {
5067
6816
  * Used to lazily seed the outbound key-policy spend tracker (once per key). A
5068
6817
  * key with no attributed events yields all zeros.
5069
6818
  */
5070
- async getSpendByKey(query) {
6819
+ async getSpendByKey(query2) {
5071
6820
  let totalUsd = 0;
5072
6821
  let dailyUsd = 0;
5073
6822
  let weeklyUsd = 0;
5074
- for (const row of this.readRows({ startTs: 0, endTs: query.endTs })) {
5075
- if (row.apiKeyId !== query.apiKeyId) continue;
6823
+ for (const row of this.readRows({ startTs: 0, endTs: query2.endTs })) {
6824
+ if (row.apiKeyId !== query2.apiKeyId) continue;
5076
6825
  totalUsd += row.costUsd;
5077
- if (row.ts >= query.dayStartTs) dailyUsd += row.costUsd;
5078
- if (row.ts >= query.weekStartTs) weeklyUsd += row.costUsd;
6826
+ if (row.ts >= query2.dayStartTs) dailyUsd += row.costUsd;
6827
+ if (row.ts >= query2.weekStartTs) weeklyUsd += row.costUsd;
5079
6828
  }
5080
6829
  return { totalUsd, dailyUsd, weeklyUsd };
5081
6830
  }
@@ -5160,10 +6909,10 @@ var JsonlUsageEventStore = class {
5160
6909
  }
5161
6910
  /** Parse every line, skipping malformed/torn lines defensively. */
5162
6911
  readAllRows() {
5163
- if (!existsSync5(this.eventsPath)) return [];
6912
+ if (!existsSync8(this.eventsPath)) return [];
5164
6913
  let raw;
5165
6914
  try {
5166
- raw = readFileSync6(this.eventsPath, "utf8");
6915
+ raw = readFileSync9(this.eventsPath, "utf8");
5167
6916
  } catch {
5168
6917
  return [];
5169
6918
  }
@@ -5247,12 +6996,29 @@ function isUsageEventRecord(parsed) {
5247
6996
  }
5248
6997
 
5249
6998
  // src/ports/JsonPricingStore.ts
5250
- import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
6999
+ import { existsSync as existsSync9, readFileSync as readFileSync10, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
7000
+ import { randomUUID as randomUUID5 } from "crypto";
5251
7001
  var JsonPricingStore = class {
5252
7002
  constructor(pricingPath) {
5253
7003
  this.pricingPath = pricingPath;
5254
7004
  }
5255
7005
  pricingPath;
7006
+ /**
7007
+ * Return whether the durable snapshot can actually serve at least one price.
7008
+ *
7009
+ * This intentionally checks the file itself instead of relying on refresh
7010
+ * metadata: a recent `lastSuccessAt` must not hide a deleted, truncated, or
7011
+ * otherwise unusable pricing table after a crash or manual file edit.
7012
+ */
7013
+ hasUsableSnapshot() {
7014
+ if (!existsSync9(this.pricingPath)) return false;
7015
+ try {
7016
+ const parsed = JSON.parse(readFileSync10(this.pricingPath, "utf8"));
7017
+ return Array.isArray(parsed) && parsed.some(isUsablePricingRow);
7018
+ } catch {
7019
+ return false;
7020
+ }
7021
+ }
5256
7022
  async getAll() {
5257
7023
  return this.readRows();
5258
7024
  }
@@ -5265,17 +7031,17 @@ var JsonPricingStore = class {
5265
7031
  */
5266
7032
  async upsert(input, asUserEdit) {
5267
7033
  const rows = this.readRows();
5268
- const entry = this.applyUpsert(rows, input, asUserEdit);
7034
+ const entry = this.applyUpsert(rows, input, asUserEdit, "litellm");
5269
7035
  this.writeRows(rows);
5270
7036
  return entry;
5271
7037
  }
5272
7038
  /**
5273
7039
  * Apply a batch fetched from a pricing source. Rows whose local copy is
5274
7040
  * user-edited are NOT applied — they come back as `{ current, incoming }`
5275
- * conflicts; everything else is upserted (source 'litellm'). ONE file write
5276
- * for the whole batch.
7041
+ * conflicts; everything else is upserted with the supplied automatic source.
7042
+ * ONE file write for the whole batch.
5277
7043
  */
5278
- async bulkApplyFromSource(entries) {
7044
+ async bulkApplyFromSource(entries, source = "litellm") {
5279
7045
  const rows = this.readRows();
5280
7046
  const applied = [];
5281
7047
  const conflicts = [];
@@ -5291,7 +7057,8 @@ var JsonPricingStore = class {
5291
7057
  rows,
5292
7058
  incoming,
5293
7059
  /* asUserEdit */
5294
- false
7060
+ false,
7061
+ source
5295
7062
  ));
5296
7063
  }
5297
7064
  if (applied.length > 0) this.writeRows(rows);
@@ -5314,7 +7081,8 @@ var JsonPricingStore = class {
5314
7081
  rows,
5315
7082
  r.incoming,
5316
7083
  /* asUserEdit */
5317
- false
7084
+ false,
7085
+ "litellm"
5318
7086
  );
5319
7087
  overwrittenCount += 1;
5320
7088
  }
@@ -5335,7 +7103,7 @@ var JsonPricingStore = class {
5335
7103
  return true;
5336
7104
  }
5337
7105
  /** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
5338
- applyUpsert(rows, input, asUserEdit) {
7106
+ applyUpsert(rows, input, asUserEdit, automaticSource) {
5339
7107
  const now = Date.now();
5340
7108
  const entry = {
5341
7109
  providerId: input.providerId,
@@ -5344,7 +7112,7 @@ var JsonPricingStore = class {
5344
7112
  outputPricePer1m: input.outputPricePer1m,
5345
7113
  cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
5346
7114
  cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
5347
- source: asUserEdit ? "user" : "litellm",
7115
+ source: asUserEdit ? "user" : automaticSource,
5348
7116
  userEdited: asUserEdit,
5349
7117
  editedAt: asUserEdit ? now : null,
5350
7118
  updatedAt: now
@@ -5358,21 +7126,142 @@ var JsonPricingStore = class {
5358
7126
  }
5359
7127
  /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
5360
7128
  readRows() {
5361
- if (!existsSync6(this.pricingPath)) return [];
7129
+ if (!existsSync9(this.pricingPath)) return [];
5362
7130
  try {
5363
- const parsed = JSON.parse(readFileSync7(this.pricingPath, "utf8"));
7131
+ const parsed = JSON.parse(readFileSync10(this.pricingPath, "utf8"));
5364
7132
  return Array.isArray(parsed) ? parsed : [];
5365
7133
  } catch {
5366
7134
  return [];
5367
7135
  }
5368
7136
  }
5369
7137
  writeRows(rows) {
5370
- writeFileSync5(this.pricingPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
7138
+ const temporaryPath = `${this.pricingPath}.${process.pid}.${randomUUID5()}.tmp`;
7139
+ try {
7140
+ writeFileSync7(temporaryPath, JSON.stringify(rows, null, 2) + "\n", {
7141
+ encoding: "utf8",
7142
+ flag: "wx"
7143
+ });
7144
+ this.replaceFile(temporaryPath);
7145
+ } finally {
7146
+ rmSync2(temporaryPath, { force: true });
7147
+ }
7148
+ }
7149
+ /** Isolated for deterministic failure testing; never removes the target. */
7150
+ replaceFile(temporaryPath) {
7151
+ renameSync3(temporaryPath, this.pricingPath);
7152
+ }
7153
+ };
7154
+ function isUsablePricingRow(value) {
7155
+ if (!value || typeof value !== "object") return false;
7156
+ const row = value;
7157
+ return typeof row.providerId === "string" && row.providerId.length > 0 && typeof row.modelId === "string" && row.modelId.length > 0 && typeof row.inputPricePer1m === "number" && Number.isFinite(row.inputPricePer1m) && typeof row.outputPricePer1m === "number" && Number.isFinite(row.outputPricePer1m);
7158
+ }
7159
+
7160
+ // src/pricing/PricingRefreshScheduler.ts
7161
+ import { existsSync as existsSync10, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "fs";
7162
+ var EMPTY_STATE2 = {
7163
+ lastAttemptAt: null,
7164
+ lastSuccessAt: null,
7165
+ lastError: null,
7166
+ sources: []
7167
+ };
7168
+ var PricingRefreshScheduler = class {
7169
+ constructor(engine, catalog2, statePath, logger, options = {}) {
7170
+ this.engine = engine;
7171
+ this.catalog = catalog2;
7172
+ this.statePath = statePath;
7173
+ this.logger = logger;
7174
+ this.staleAfterMs = options.staleAfterMs ?? 24 * 60 * 60 * 1e3;
7175
+ this.intervalMs = options.intervalMs ?? 60 * 60 * 1e3;
7176
+ this.now = options.now ?? Date.now;
7177
+ }
7178
+ engine;
7179
+ catalog;
7180
+ statePath;
7181
+ logger;
7182
+ staleAfterMs;
7183
+ intervalMs;
7184
+ now;
7185
+ timer = null;
7186
+ inFlight = null;
7187
+ /** Fire one stale check immediately and arm an unref'ed periodic check. */
7188
+ start() {
7189
+ if (this.timer) return;
7190
+ void this.refreshIfStale();
7191
+ this.timer = setInterval(() => void this.refreshIfStale(), this.intervalMs);
7192
+ this.timer.unref?.();
7193
+ }
7194
+ dispose() {
7195
+ if (this.timer) clearInterval(this.timer);
7196
+ this.timer = null;
7197
+ }
7198
+ getState() {
7199
+ if (!existsSync10(this.statePath)) return { ...EMPTY_STATE2, sources: [] };
7200
+ try {
7201
+ const value = JSON.parse(readFileSync11(this.statePath, "utf8"));
7202
+ return {
7203
+ lastAttemptAt: finiteOrNull(value.lastAttemptAt),
7204
+ lastSuccessAt: finiteOrNull(value.lastSuccessAt),
7205
+ lastError: typeof value.lastError === "string" ? value.lastError : null,
7206
+ sources: Array.isArray(value.sources) ? value.sources : []
7207
+ };
7208
+ } catch {
7209
+ return { ...EMPTY_STATE2, sources: [] };
7210
+ }
7211
+ }
7212
+ /** Public for admin/manual tests; concurrent checks share one promise. */
7213
+ refreshIfStale(force = false) {
7214
+ if (this.inFlight) return this.inFlight;
7215
+ const state = this.getState();
7216
+ if (!force && this.catalog.hasUsableSnapshot() && state.lastSuccessAt !== null && this.now() - state.lastSuccessAt < this.staleAfterMs) {
7217
+ return Promise.resolve();
7218
+ }
7219
+ const task = this.runRefresh(state);
7220
+ this.inFlight = task;
7221
+ return task.finally(() => {
7222
+ if (this.inFlight === task) this.inFlight = null;
7223
+ });
7224
+ }
7225
+ async runRefresh(previous) {
7226
+ const lastAttemptAt = this.now();
7227
+ try {
7228
+ const result = await this.engine.fetchLatestFromSource();
7229
+ const failed = result.sources.filter((source) => source.status === "failed");
7230
+ const complete = failed.length === 0;
7231
+ this.writeState({
7232
+ lastAttemptAt,
7233
+ // A partial refresh keeps useful rows, but remains stale so the failed
7234
+ // source is retried on the next hourly check instead of 24 hours later.
7235
+ lastSuccessAt: complete ? this.now() : previous.lastSuccessAt,
7236
+ lastError: complete ? null : failed.map((source) => `${source.source}: ${source.error ?? "failed"}`).join("; "),
7237
+ sources: result.sources
7238
+ });
7239
+ } catch (error) {
7240
+ const message = error instanceof Error ? error.message : String(error);
7241
+ this.writeState({
7242
+ lastAttemptAt,
7243
+ lastSuccessAt: previous.lastSuccessAt,
7244
+ lastError: message,
7245
+ sources: previous.sources
7246
+ });
7247
+ this.logger.warn("[PricingRefreshScheduler] background refresh failed; cached prices retained", {
7248
+ error: message
7249
+ });
7250
+ }
7251
+ }
7252
+ writeState(state) {
7253
+ const temporaryPath = `${this.statePath}.tmp`;
7254
+ writeFileSync8(temporaryPath, `${JSON.stringify(state, null, 2)}
7255
+ `, "utf8");
7256
+ renameSync4(temporaryPath, this.statePath);
5371
7257
  }
5372
7258
  };
7259
+ function finiteOrNull(value) {
7260
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
7261
+ }
5373
7262
 
5374
7263
  // src/ports/JsonVoucherDb.ts
5375
- import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
7264
+ import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
5376
7265
  var JsonVoucherDb = class {
5377
7266
  constructor(vouchersPath) {
5378
7267
  this.vouchersPath = vouchersPath;
@@ -5450,25 +7339,26 @@ var JsonVoucherDb = class {
5450
7339
  }
5451
7340
  /** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
5452
7341
  readRows() {
5453
- if (!existsSync7(this.vouchersPath)) return [];
7342
+ if (!existsSync11(this.vouchersPath)) return [];
5454
7343
  try {
5455
- const parsed = JSON.parse(readFileSync8(this.vouchersPath, "utf8"));
7344
+ const parsed = JSON.parse(readFileSync12(this.vouchersPath, "utf8"));
5456
7345
  return Array.isArray(parsed) ? parsed : [];
5457
7346
  } catch {
5458
7347
  return [];
5459
7348
  }
5460
7349
  }
5461
7350
  writeRows(rows) {
5462
- writeFileSync6(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
7351
+ writeFileSync9(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
5463
7352
  }
5464
7353
  };
5465
7354
 
5466
7355
  // src/ports/JsonSubscriptionCredentialStore.ts
5467
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
5468
- import { dirname as dirname4 } from "path";
5469
- import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
5470
- import { fetchUpstream as fetchUpstream2 } from "@omnicross/core/pipeline/upstreamFetch";
5471
- import { getSharedIdentityStore } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
7356
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
7357
+ import { dirname as dirname6 } from "path";
7358
+ import { getSharedAccountHealth as getSharedAccountHealth2 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
7359
+ import { getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling3 } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
7360
+ import { fetchUpstream as fetchUpstream3 } from "@omnicross/core/pipeline/upstreamFetch";
7361
+ import { getSharedIdentityStore as getSharedIdentityStore2 } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
5472
7362
  import {
5473
7363
  claudeOAuth as claudeOAuth2,
5474
7364
  codexOAuth as codexOAuth2,
@@ -5476,33 +7366,9 @@ import {
5476
7366
  } from "@omnicross/subscriptions";
5477
7367
 
5478
7368
  // src/ports/account-sync.ts
5479
- var IMPORT_EXPIRY_MARGIN_MS = 6e4;
5480
7369
  function viewOf(tokens) {
5481
7370
  return tokens;
5482
7371
  }
5483
- function decideExternalImport(captured, external, now = Date.now()) {
5484
- if (!external?.accessToken) return "no-credential";
5485
- const capturedRt = viewOf(captured).refreshToken;
5486
- const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
5487
- const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
5488
- return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
5489
- }
5490
- function buildImportedTokens(captured, external) {
5491
- const imported = {
5492
- ...captured,
5493
- accessToken: external.accessToken,
5494
- status: "authorized",
5495
- errorMessage: void 0,
5496
- syncWarning: void 0,
5497
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
5498
- };
5499
- if (external.refreshToken) imported.refreshToken = external.refreshToken;
5500
- if (external.expiresAt) imported.expiresAt = external.expiresAt;
5501
- else delete imported.expiresAt;
5502
- if (external.idToken) imported.idToken = external.idToken;
5503
- if (external.scopes) imported.scopes = external.scopes;
5504
- return imported;
5505
- }
5506
7372
  function buildTokensFromExternal(provider, external) {
5507
7373
  const base = {
5508
7374
  authMethod: "oauth",
@@ -5523,14 +7389,6 @@ function buildTokensFromExternal(provider, external) {
5523
7389
  if (external.idToken) tokens.idToken = external.idToken;
5524
7390
  return tokens;
5525
7391
  }
5526
- function isExternalDivergent(stored, external) {
5527
- if (!external?.accessToken || !external.refreshToken) return false;
5528
- const view = viewOf(stored);
5529
- if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
5530
- const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
5531
- const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
5532
- return !Number.isFinite(storedExp) || externalExp > storedExp;
5533
- }
5534
7392
  function findDuplicateCredentialIds(accounts) {
5535
7393
  const byCredential = /* @__PURE__ */ new Map();
5536
7394
  for (const account of accounts) {
@@ -5549,11 +7407,11 @@ function findDuplicateCredentialIds(accounts) {
5549
7407
  }
5550
7408
 
5551
7409
  // src/ports/external-cli-credentials.ts
5552
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
5553
- import { homedir as homedir2 } from "os";
5554
- import { join as join4 } from "path";
5555
- function externalStorePath(provider, home = homedir2()) {
5556
- return provider === "claude" ? join4(home, ".claude", ".credentials.json") : join4(home, ".codex", "auth.json");
7410
+ import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
7411
+ import { homedir as homedir3 } from "os";
7412
+ import { join as join6 } from "path";
7413
+ function externalStorePath(provider, home = homedir3()) {
7414
+ return provider === "claude" ? join6(home, ".claude", ".credentials.json") : join6(home, ".codex", "auth.json");
5557
7415
  }
5558
7416
  function decodeJwtExpiryMs(token) {
5559
7417
  try {
@@ -5600,12 +7458,12 @@ function parseCodexTokensEnvelope(raw) {
5600
7458
  }
5601
7459
  return parsed;
5602
7460
  }
5603
- function readExternalCliCredentials(provider, home = homedir2()) {
7461
+ function readExternalCliCredentials(provider, home = homedir3()) {
5604
7462
  const path2 = externalStorePath(provider, home);
5605
- if (!existsSync8(path2)) return null;
7463
+ if (!existsSync12(path2)) return null;
5606
7464
  let raw;
5607
7465
  try {
5608
- const parsed = JSON.parse(readFileSync9(path2, "utf8"));
7466
+ const parsed = JSON.parse(readFileSync13(path2, "utf8"));
5609
7467
  raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
5610
7468
  } catch {
5611
7469
  return null;
@@ -5613,84 +7471,6 @@ function readExternalCliCredentials(provider, home = homedir2()) {
5613
7471
  return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
5614
7472
  }
5615
7473
 
5616
- // src/ports/external-cli-store.ts
5617
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync10, renameSync, writeFileSync as writeFileSync7 } from "fs";
5618
- import { homedir as homedir3 } from "os";
5619
- import { dirname as dirname3 } from "path";
5620
- function markerPath(provider, home) {
5621
- return `${externalStorePath(provider, home)}.omnicross-managed`;
5622
- }
5623
- function backupPath(provider, home) {
5624
- return `${externalStorePath(provider, home)}.omnicross-backup`;
5625
- }
5626
- function buildClaudeOAuthEnvelope(tokens) {
5627
- if (!tokens.accessToken) return null;
5628
- const envelope = { accessToken: tokens.accessToken };
5629
- if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
5630
- if (tokens.expiresAt) {
5631
- const ms = Date.parse(tokens.expiresAt);
5632
- if (Number.isFinite(ms)) envelope.expiresAt = ms;
5633
- }
5634
- if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
5635
- return envelope;
5636
- }
5637
- function buildCodexTokensEnvelope(tokens) {
5638
- if (!tokens.accessToken && !tokens.idToken) return null;
5639
- const envelope = { access_token: tokens.accessToken ?? "" };
5640
- if (tokens.idToken) envelope.id_token = tokens.idToken;
5641
- if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
5642
- return envelope;
5643
- }
5644
- function readExistingObject(path2) {
5645
- if (!existsSync9(path2)) return {};
5646
- try {
5647
- const parsed = JSON.parse(readFileSync10(path2, "utf8"));
5648
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5649
- } catch {
5650
- return {};
5651
- }
5652
- }
5653
- function writeAtomic(path2, content) {
5654
- mkdirSync2(dirname3(path2), { recursive: true });
5655
- const temp = `${path2}.omnicross-tmp`;
5656
- writeFileSync7(temp, content, "utf8");
5657
- renameSync(temp, path2);
5658
- }
5659
- function createExternalCliStore(home = homedir3()) {
5660
- return {
5661
- readMarkerAccountId(provider) {
5662
- const path2 = markerPath(provider, home);
5663
- if (!existsSync9(path2)) return void 0;
5664
- try {
5665
- const parsed = JSON.parse(readFileSync10(path2, "utf8"));
5666
- return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
5667
- } catch {
5668
- return void 0;
5669
- }
5670
- },
5671
- writeMarker(provider, accountId) {
5672
- writeAtomic(
5673
- markerPath(provider, home),
5674
- JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
5675
- );
5676
- },
5677
- writeBack(provider, accountId, tokens) {
5678
- const owner = this.readMarkerAccountId(provider);
5679
- if (owner !== accountId) return false;
5680
- const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
5681
- if (!envelope) return false;
5682
- const storePath = externalStorePath(provider, home);
5683
- if (existsSync9(storePath) && !existsSync9(backupPath(provider, home))) {
5684
- copyFileSync(storePath, backupPath(provider, home));
5685
- }
5686
- const existing = readExistingObject(storePath);
5687
- const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
5688
- writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
5689
- return true;
5690
- }
5691
- };
5692
- }
5693
-
5694
7474
  // src/ports/JsonSubscriptionCredentialStore.ts
5695
7475
  var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
5696
7476
  var JsonSubscriptionCredentialStore = class {
@@ -5703,32 +7483,30 @@ var JsonSubscriptionCredentialStore = class {
5703
7483
  * proxy-aware {@link fetchUpstream} that threads the
5704
7484
  * `{ providerId, accountId }` ctx (upstream-proxy M1) so a
5705
7485
  * per-account/per-provider proxy is honored on refresh exactly
5706
- * as on relay refresh egresses from the SAME proxy IP as the
7486
+ * as on relay refresh egresses from the SAME proxy IP as the
5707
7487
  * account's traffic. NOT used by any read/write path.
5708
7488
  */
5709
- constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
7489
+ constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials) {
5710
7490
  this.tokensPath = tokensPath;
5711
7491
  this.box = box;
5712
7492
  this.fetchImpl = fetchImpl;
5713
7493
  this.externalCliReader = externalCliReader;
5714
- this.externalCliStore = externalCliStore;
5715
7494
  }
5716
7495
  tokensPath;
5717
7496
  box;
5718
7497
  fetchImpl;
5719
7498
  externalCliReader;
5720
- externalCliStore;
5721
7499
  /**
5722
7500
  * The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
5723
7501
  * TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
5724
7502
  * through {@link fetchUpstream} with the account's `{ providerId, accountId }`
5725
- * ctx so the per-account/provider proxy applies. `@internal` also a test seam.
7503
+ * ctx so the per-account/provider proxy applies. `@internal` also a test seam.
5726
7504
  */
5727
7505
  buildRefreshFetch(providerId, accountId) {
5728
- return this.fetchImpl ?? ((url, init) => fetchUpstream2(url, init, { providerId, accountId }));
7506
+ return this.fetchImpl ?? ((url, init) => fetchUpstream3(url, init, { providerId, accountId }));
5729
7507
  }
5730
7508
  /**
5731
- * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
7509
+ * In-flight refresh coalescing. OAuth refresh tokens are
5732
7510
  * SINGLE-USE: two concurrent refreshes of one account each spend the same
5733
7511
  * token and the loser bricks a healthy account. Every refresh entry point
5734
7512
  * (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
@@ -5743,13 +7521,13 @@ var JsonSubscriptionCredentialStore = class {
5743
7521
  return run;
5744
7522
  }
5745
7523
  /** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
5746
- * file is absent/corrupt). This is the hot read the codex / gemini auth
7524
+ * file is absent/corrupt). This is the hot read the codex / gemini auth
5747
7525
  * strategies pull `accessToken` / `expiresAt` / `status` from it. */
5748
7526
  async getFullConfig() {
5749
7527
  return this.readConfig();
5750
7528
  }
5751
7529
  /** Current Claude OAuth access token, or `null` when none is stored. No inline
5752
- * refresh here the lead-window / 401-retry refresh is driven by the
7530
+ * refresh here the lead-window / 401-retry refresh is driven by the
5753
7531
  * subscription auth strategy, which calls `refreshClaudeToken` (now real). */
5754
7532
  async getValidClaudeAccessToken() {
5755
7533
  return this.readConfig().claude?.accessToken ?? null;
@@ -5774,13 +7552,14 @@ var JsonSubscriptionCredentialStore = class {
5774
7552
  /**
5775
7553
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
5776
7554
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
5777
- * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
7555
+ * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
5778
7556
  * Used by the admin accounts GET (secret-IN-never-OUT).
5779
7557
  */
5780
7558
  async listSanitizedAccounts() {
5781
7559
  const config = this.readConfig();
5782
- const health2 = getSharedAccountHealth();
5783
- const identityStore = getSharedIdentityStore();
7560
+ const health2 = getSharedAccountHealth2();
7561
+ const allowanceScheduling = getSharedAccountAllowanceScheduling3();
7562
+ const identityStore = getSharedIdentityStore2();
5784
7563
  const fingerprintOn = identityStore.isEnabled();
5785
7564
  const now = Date.now();
5786
7565
  const out = {};
@@ -5789,7 +7568,13 @@ var JsonSubscriptionCredentialStore = class {
5789
7568
  if (sanitized.length === 0) continue;
5790
7569
  for (const account of sanitized) {
5791
7570
  const status = health2.getStatus(provider, account.id, now);
7571
+ const allowance = allowanceScheduling.preview(provider, account.id, account.priority ?? 50, now);
5792
7572
  account.health = status.state;
7573
+ account.schedulable = account.enabled && status.state === "healthy" && allowance.schedulable;
7574
+ account.allowanceAction = allowance.action;
7575
+ account.allowanceEffectivePriority = allowance.effectivePriority;
7576
+ account.allowanceUsedPercent = allowance.usedPercent;
7577
+ account.allowanceResumeAt = allowance.resumeAt;
5793
7578
  account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
5794
7579
  if (fingerprintOn && provider === "claude") {
5795
7580
  account.identityCaptured = identityStore.hasIdentity(provider, account.id);
@@ -5797,31 +7582,25 @@ var JsonSubscriptionCredentialStore = class {
5797
7582
  account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
5798
7583
  }
5799
7584
  }
5800
- out[provider] = this.attachSyncWarnings(config, provider, sanitized);
7585
+ out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
5801
7586
  }
5802
7587
  return out;
5803
7588
  }
5804
7589
  /**
5805
- * List-time credential-conflict warnings (external-cli-sync). Computed, not
5806
- * persisted: (a) `duplicate-token` when two accounts of one provider share a
5807
- * credential, (b) `external-divergent` when the external CLI native store has
5808
- * rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
5809
- * a failed refresh (`external-not-rotated`) takes precedence — it is the most
5810
- * actionable state.
7590
+ * List-time managed-credential conflict warnings. Computed, not persisted:
7591
+ * `duplicate-token` is projected when two accounts of one provider share a
7592
+ * credential. This deliberately does not inspect either native CLI file.
5811
7593
  */
5812
- attachSyncWarnings(config, provider, sanitized) {
7594
+ attachDuplicateWarnings(config, provider, sanitized) {
5813
7595
  const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
5814
- let divergentId;
5815
- if (provider === "claude" || provider === "codex") {
5816
- const active = getActiveAccount(config, provider);
5817
- if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
5818
- divergentId = active.id;
5819
- }
5820
- }
5821
- if (duplicates.size === 0 && !divergentId) return sanitized;
5822
7596
  return sanitized.map((account) => {
5823
- const computed = account.id === divergentId ? "external-divergent" : duplicates.has(account.id) ? "duplicate-token" : void 0;
5824
- return { ...account, syncWarning: account.syncWarning ?? computed };
7597
+ const computed = duplicates.has(account.id) ? "duplicate-token" : void 0;
7598
+ const persisted = account.syncWarning === "duplicate-token" ? account.syncWarning : void 0;
7599
+ if (!persisted && !computed) {
7600
+ const { syncWarning: _obsoleteWarning, ...withoutWarning } = account;
7601
+ return withoutWarning;
7602
+ }
7603
+ return { ...account, syncWarning: persisted ?? computed };
5825
7604
  });
5826
7605
  }
5827
7606
  /** Read the external CLI store, never letting an fs/parse error escape. */
@@ -5834,11 +7613,11 @@ var JsonSubscriptionCredentialStore = class {
5834
7613
  }
5835
7614
  /**
5836
7615
  * Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
5837
- * the block has no refresh_token (setup-token / manual) no upstream call, the
7616
+ * the block has no refresh_token (setup-token / manual) no upstream call, the
5838
7617
  * block is untouched. Otherwise mint via the shared claude refresh flow and
5839
7618
  * write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
5840
- * On failure status:expired +
5841
- * errorMessage `false`.
7619
+ * On failure status:expired +
7620
+ * errorMessage `false`.
5842
7621
  */
5843
7622
  async refreshClaudeToken() {
5844
7623
  return this.coalesce("claude:active", async () => {
@@ -5863,19 +7642,8 @@ var JsonSubscriptionCredentialStore = class {
5863
7642
  syncWarning: void 0
5864
7643
  };
5865
7644
  this.writeBackById("claude", capturedId, next);
5866
- this.resyncExternal("claude", capturedId, next);
5867
7645
  return true;
5868
7646
  } catch (error) {
5869
- if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
5870
- const r = await claudeOAuth2.refreshAccessToken(rt, refreshFetch);
5871
- return {
5872
- accessToken: r.accessToken,
5873
- refreshToken: r.refreshToken,
5874
- expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
5875
- };
5876
- })) {
5877
- return true;
5878
- }
5879
7647
  this.markExpiredById("claude", capturedId, claude, error);
5880
7648
  return false;
5881
7649
  }
@@ -5910,20 +7678,8 @@ var JsonSubscriptionCredentialStore = class {
5910
7678
  syncWarning: void 0
5911
7679
  };
5912
7680
  this.writeBackById("codex", capturedId, next);
5913
- this.resyncExternal("codex", capturedId, next);
5914
7681
  return true;
5915
7682
  } catch (error) {
5916
- if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
5917
- const r = await codexOAuth2.refreshAccessToken(rt, refreshFetch);
5918
- return {
5919
- accessToken: r.accessToken,
5920
- refreshToken: r.refreshToken,
5921
- idToken: r.idToken,
5922
- expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
5923
- };
5924
- })) {
5925
- return true;
5926
- }
5927
7683
  this.markExpiredById("codex", capturedId, codex, error);
5928
7684
  return false;
5929
7685
  }
@@ -5966,11 +7722,10 @@ var JsonSubscriptionCredentialStore = class {
5966
7722
  });
5967
7723
  }
5968
7724
  /**
5969
- * Refresh a SPECIFIC account by id (background scheduler sweep,
5970
- * external-cli-sync). Unlike the active-account refreshers it does NOT
5971
- * attempt the external-import fallback the external CLI file's lineage can
5972
- * only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
5973
- * failure flags ONLY that account `expired`.
7725
+ * Refresh a SPECIFIC managed account by id (background scheduler sweep and
7726
+ * account-pool resolution). It uses only that account's stored refresh
7727
+ * token. Coalesced per `provider:id`; on failure flags ONLY that account
7728
+ * `expired`.
5974
7729
  */
5975
7730
  async refreshAccountById(provider, id) {
5976
7731
  return this.coalesce(`${provider}:${id}`, async () => {
@@ -5984,7 +7739,7 @@ var JsonSubscriptionCredentialStore = class {
5984
7739
  const next = {
5985
7740
  ...captured,
5986
7741
  accessToken: refreshed.accessToken,
5987
- // Gemini's refresh response omits a new refresh token keep the captured.
7742
+ // Gemini's refresh response omits a new refresh token keep the captured.
5988
7743
  refreshToken: refreshed.refreshToken ?? captured.refreshToken,
5989
7744
  expiresAt: refreshed.expiresAt,
5990
7745
  status: "authorized",
@@ -5994,7 +7749,6 @@ var JsonSubscriptionCredentialStore = class {
5994
7749
  };
5995
7750
  if (refreshed.idToken) next.idToken = refreshed.idToken;
5996
7751
  this.writeBackById(provider, id, next);
5997
- if (provider !== "gemini") this.resyncExternal(provider, id, next);
5998
7752
  return true;
5999
7753
  } catch (error) {
6000
7754
  this.markExpiredById(provider, id, captured, error);
@@ -6002,7 +7756,7 @@ var JsonSubscriptionCredentialStore = class {
6002
7756
  }
6003
7757
  });
6004
7758
  }
6005
- // ── By-id account-pool surface (subscription-account-scheduling, design D6) ──
7759
+ // By-id account-pool surface (subscription-account-scheduling, design D6)
6006
7760
  /**
6007
7761
  * Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
6008
7762
  * provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
@@ -6035,7 +7789,7 @@ var JsonSubscriptionCredentialStore = class {
6035
7789
  /**
6036
7790
  * Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
6037
7791
  * `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
6038
- * `false` (no refresh affordance).
7792
+ * `false` (no refresh affordance).
6039
7793
  */
6040
7794
  async refreshAccountToken(providerId, accountId) {
6041
7795
  if (providerId === "opencodego") return false;
@@ -6057,7 +7811,7 @@ var JsonSubscriptionCredentialStore = class {
6057
7811
  * (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
6058
7812
  * whitelisted fingerprint headers; the token mirror is untouched); a no-op for
6059
7813
  * an unknown id. Called by the identity store's persistence port on a first-seen
6060
- * freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
7814
+ * freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
6061
7815
  * store's port wrapper swallows a rejection so the relay hot path is unaffected.
6062
7816
  */
6063
7817
  async setAccountIdentity(providerId, accountId, identity) {
@@ -6082,7 +7836,7 @@ var JsonSubscriptionCredentialStore = class {
6082
7836
  * DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
6083
7837
  * the port). Passing `undefined` clears the override. Write-only password: when
6084
7838
  * the incoming structured proxy omits the password but the account already had
6085
- * one, the current (decrypted) password is preserved editing host/port never
7839
+ * one, the current (decrypted) password is preserved editing host/port never
6086
7840
  * wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
6087
7841
  */
6088
7842
  async setAccountProxy(providerId, accountId, proxy) {
@@ -6117,75 +7871,25 @@ var JsonSubscriptionCredentialStore = class {
6117
7871
  expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
6118
7872
  };
6119
7873
  }
6120
- /**
6121
- * External-import fallback for a FAILED active-account refresh
6122
- * (external-cli-sync). Reads the CLI native store; imports when the external
6123
- * lineage ROTATED (different refresh token) or its access token is still
6124
- * valid. When the imported access token is already expired it refreshes once
6125
- * with the rotated refresh token. A `not-rotated` outcome persists the
6126
- * `external-not-rotated` warning on the (about-to-be-expired) account so the
6127
- * UI can tell "genuine revocation" apart from a plain refresh failure.
6128
- */
6129
- async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
6130
- const markerOwner = this.safeReadMarker(provider);
6131
- if (markerOwner && markerOwner !== capturedId) return false;
6132
- const external = this.safeReadExternal(provider);
6133
- const decision = decideExternalImport(captured, external);
6134
- if (decision === "not-rotated") {
6135
- captured.syncWarning = "external-not-rotated";
6136
- return false;
6137
- }
6138
- if (decision !== "import" || !external) return false;
6139
- let imported = buildImportedTokens(
6140
- captured,
6141
- external
6142
- );
6143
- const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
6144
- if (!accessStillValid) {
6145
- try {
6146
- const refreshed = await refreshWithToken(external.refreshToken);
6147
- imported = {
6148
- ...imported,
6149
- accessToken: refreshed.accessToken,
6150
- refreshToken: refreshed.refreshToken ?? imported.refreshToken,
6151
- expiresAt: refreshed.expiresAt,
6152
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
6153
- };
6154
- if (refreshed.idToken) imported.idToken = refreshed.idToken;
6155
- } catch {
6156
- return false;
6157
- }
6158
- }
6159
- this.writeBackById(provider, capturedId, imported);
6160
- this.resyncExternal(provider, capturedId, imported);
6161
- return true;
6162
- }
6163
- /**
6164
- * Marker-gated external write-back (external-cli-sync). After a successful
6165
- * refresh of the account that OWNS the provider's native CLI store (imported
6166
- * via `importExternalCliAccount`), push the rotated credential back into the
6167
- * file — otherwise the daemon's refresh invalidates the single-use refresh
6168
- * token and silently logs the bare CLI out. NON-FATAL: the internal store is
6169
- * already persisted; a failed external write only leaves the file stale,
6170
- * which the `external-divergent` warning surfaces.
6171
- */
6172
- resyncExternal(provider, accountId, tokens) {
6173
- try {
6174
- this.externalCliStore.writeBack(provider, accountId, tokens);
6175
- } catch {
6176
- }
7874
+ /** Atomically patch one account's non-secret management metadata. */
7875
+ async patchAccountMetadata(providerId, accountId, patch) {
7876
+ const config = this.readConfig();
7877
+ const result = patchAccountMetadata(config, providerId, accountId, patch);
7878
+ if (!result.ok) return result;
7879
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
7880
+ return result;
6177
7881
  }
6178
- /** Read the marker's owning account id, never letting an fs error escape. */
6179
- safeReadMarker(provider) {
6180
- try {
6181
- return this.externalCliStore.readMarkerAccountId(provider);
6182
- } catch {
6183
- return void 0;
6184
- }
7882
+ /** Validate every target, then persist one all-or-nothing batch mutation. */
7883
+ async batchManageAccounts(refs, mutation) {
7884
+ const config = this.readConfig();
7885
+ const result = batchManageAccounts(config, refs, mutation);
7886
+ if (!result.ok) return result;
7887
+ this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
7888
+ return result;
6185
7889
  }
6186
7890
  /**
6187
7891
  * DAEMON-ONLY (admin import button): which providers have a usable external
6188
- * CLI credential on THIS machine. Pure detection reads the native files,
7892
+ * CLI credential on THIS machine. Pure detection reads the native files,
6189
7893
  * never mutates anything, never returns a token.
6190
7894
  */
6191
7895
  async listExternalCliAvailability() {
@@ -6196,21 +7900,22 @@ var JsonSubscriptionCredentialStore = class {
6196
7900
  }
6197
7901
  /**
6198
7902
  * DAEMON-ONLY (admin import button): import the external CLI's current login
6199
- * as a NEW account (+ activate), and take MANAGED ownership of the native
6200
- * store (marker) so subsequent refreshes write back keeping the bare CLI
6201
- * and the daemon on the same live credential instead of silently killing one
6202
- * side's single-use refresh token.
7903
+ * as a NEW account (+ activate). This is a COPY-ONLY import: Omnicross never
7904
+ * claims, writes, moves, restores, or deletes the native CLI credential file
7905
+ * or any legacy `.omnicross-managed` marker/backup beside it. Subsequent
7906
+ * refreshes persist only Omnicross's encrypted token store.
6203
7907
  */
6204
7908
  async importExternalCliAccount(provider, label) {
6205
7909
  const external = this.safeReadExternal(provider);
6206
7910
  if (!external?.accessToken) return { ok: false, reason: "no-credential" };
6207
7911
  const tokens = buildTokensFromExternal(provider, external);
6208
7912
  const result = await this.appendProviderAccount(provider, tokens, label);
6209
- try {
6210
- this.externalCliStore.writeMarker(provider, result.id);
6211
- } catch {
6212
- }
6213
- return { ok: true, id: result.id };
7913
+ return {
7914
+ ok: true,
7915
+ id: result.id,
7916
+ nativeCredentialMode: "read-only",
7917
+ refreshWritesNativeCredentials: false
7918
+ };
6214
7919
  }
6215
7920
  /**
6216
7921
  * Materialize a lazily-synthesized account id to disk (design D3). On a legacy
@@ -6243,7 +7948,8 @@ var JsonSubscriptionCredentialStore = class {
6243
7948
  this.writeBackById(providerId, capturedId, {
6244
7949
  ...block,
6245
7950
  status: "expired",
6246
- errorMessage
7951
+ errorMessage,
7952
+ syncWarning: "syncWarning" in block && block.syncWarning === "duplicate-token" ? "duplicate-token" : void 0
6247
7953
  });
6248
7954
  }
6249
7955
  /**
@@ -6252,7 +7958,7 @@ var JsonSubscriptionCredentialStore = class {
6252
7958
  * `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
6253
7959
  * OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
6254
7960
  * tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
6255
- * so a first-ever write still produces a valid config. No cache the next read
7961
+ * so a first-ever write still produces a valid config. No cache the next read
6256
7962
  * sees this write.
6257
7963
  */
6258
7964
  async writeProviderTokens(providerId, config) {
@@ -6262,7 +7968,7 @@ var JsonSubscriptionCredentialStore = class {
6262
7968
  }
6263
7969
  /**
6264
7970
  * DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
6265
- * (optional label) and set it active, then re-derive the mirror used by
7971
+ * (optional label) and set it active, then re-derive the mirror used by
6266
7972
  * `omnicross login <provider> --label` to add an account instead of overwriting.
6267
7973
  */
6268
7974
  async appendProviderAccount(providerId, config, label) {
@@ -6296,7 +8002,7 @@ var JsonSubscriptionCredentialStore = class {
6296
8002
  }
6297
8003
  /**
6298
8004
  * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
6299
- * rejects an unknown id. Label-only no token material is read or written
8005
+ * rejects an unknown id. Label-only no token material is read or written
6300
8006
  * (the secret-free invariant holds).
6301
8007
  */
6302
8008
  async renameAccount(providerId, id, label) {
@@ -6319,12 +8025,12 @@ var JsonSubscriptionCredentialStore = class {
6319
8025
  }
6320
8026
  /** Write the merged config to disk as pretty JSON (mkdir parent if needed).
6321
8027
  * Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
6322
- * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
6323
- * write incl. child 4's future refresh writes lands encrypted. */
8028
+ * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
8029
+ * write incl. child 4's future refresh writes lands encrypted. */
6324
8030
  persist(config) {
6325
- mkdirSync3(dirname4(this.tokensPath), { recursive: true });
8031
+ mkdirSync4(dirname6(this.tokensPath), { recursive: true });
6326
8032
  const encrypted = encryptTokens(config, this.box);
6327
- writeFileSync8(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
8033
+ writeFileSync10(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
6328
8034
  }
6329
8035
  /**
6330
8036
  * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
@@ -6332,18 +8038,18 @@ var JsonSubscriptionCredentialStore = class {
6332
8038
  * subscription bearer path is byte-identical).
6333
8039
  *
6334
8040
  * The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
6335
- * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
8041
+ * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
6336
8042
  * wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
6337
- * box's clear, secret-free error (secrets spec "错误密钥 / 篡改的解密失败 UX":
6338
- * SHALL fail-fast, SHALL NOT 静默降级 — a swallowed decrypt would report "no
6339
- * tokens" and silently send the WRONG bearer upstream 401). Mirrors
8043
+ * box's clear, secret-free error (secrets spec "/ UX":
8044
+ * SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
8045
+ * tokens" and silently send the WRONG bearer upstream 401). Mirrors
6340
8046
  * `config.ts loadConfig`, which decrypts outside its parse try.
6341
8047
  */
6342
8048
  readConfig() {
6343
- if (!existsSync10(this.tokensPath)) return { updatedAt: "" };
8049
+ if (!existsSync13(this.tokensPath)) return { updatedAt: "" };
6344
8050
  let parsed;
6345
8051
  try {
6346
- const raw = JSON.parse(readFileSync11(this.tokensPath, "utf8"));
8052
+ const raw = JSON.parse(readFileSync14(this.tokensPath, "utf8"));
6347
8053
  parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
6348
8054
  } catch {
6349
8055
  parsed = null;
@@ -6355,7 +8061,7 @@ var JsonSubscriptionCredentialStore = class {
6355
8061
  };
6356
8062
 
6357
8063
  // src/AccountHealthProbeScheduler.ts
6358
- import { fetchUpstream as fetchUpstream3 } from "@omnicross/core/pipeline/upstreamFetch";
8064
+ import { fetchUpstream as fetchUpstream4 } from "@omnicross/core/pipeline/upstreamFetch";
6359
8065
 
6360
8066
  // src/probe/ProbeStrategy.ts
6361
8067
  var PROVIDER_PROBE_PLANS = {
@@ -6398,7 +8104,7 @@ var AccountHealthProbeScheduler = class {
6398
8104
  this.logger = logger;
6399
8105
  this.config = config;
6400
8106
  this.now = opts.now ?? Date.now;
6401
- this.fetchImpl = opts.fetchImpl ?? fetchUpstream3;
8107
+ this.fetchImpl = opts.fetchImpl ?? fetchUpstream4;
6402
8108
  this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6403
8109
  this.planFor = opts.planFor ?? probePlanFor;
6404
8110
  }
@@ -6671,8 +8377,8 @@ var AccountHealthSweeper = class {
6671
8377
  };
6672
8378
 
6673
8379
  // src/audit/AuditPruneSweeper.ts
6674
- import { existsSync as existsSync11, readdirSync, unlinkSync } from "fs";
6675
- import { join as join5 } from "path";
8380
+ import { existsSync as existsSync14, readdirSync, unlinkSync as unlinkSync3 } from "fs";
8381
+ import { join as join7 } from "path";
6676
8382
 
6677
8383
  // src/audit/auditFiles.ts
6678
8384
  var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
@@ -6698,8 +8404,8 @@ function auditFileDateMs(fileName) {
6698
8404
  var DAY_MS = 24 * 60 * 6e4;
6699
8405
  var SWEEP_INTERVAL_MS2 = 60 * 6e4;
6700
8406
  var AuditPruneSweeper = class {
6701
- constructor(auditDir, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
6702
- this.auditDir = auditDir;
8407
+ constructor(auditDir2, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
8408
+ this.auditDir = auditDir2;
6703
8409
  this.logger = logger;
6704
8410
  this.config = config;
6705
8411
  this.intervalMs = intervalMs;
@@ -6746,7 +8452,7 @@ var AuditPruneSweeper = class {
6746
8452
  if (!this.config.enabled || this.sweeping) return 0;
6747
8453
  this.sweeping = true;
6748
8454
  try {
6749
- if (!existsSync11(this.auditDir)) return 0;
8455
+ if (!existsSync14(this.auditDir)) return 0;
6750
8456
  const today = new Date(this.now());
6751
8457
  const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
6752
8458
  const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
@@ -6755,7 +8461,7 @@ var AuditPruneSweeper = class {
6755
8461
  const dateMs = auditFileDateMs(file);
6756
8462
  if (dateMs === null || dateMs >= cutoff) continue;
6757
8463
  try {
6758
- unlinkSync(join5(this.auditDir, file));
8464
+ unlinkSync3(join7(this.auditDir, file));
6759
8465
  removed += 1;
6760
8466
  } catch (error) {
6761
8467
  this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
@@ -6778,26 +8484,26 @@ var AuditPruneSweeper = class {
6778
8484
  };
6779
8485
 
6780
8486
  // src/audit/auditReader.ts
6781
- import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync12 } from "fs";
6782
- import { join as join6 } from "path";
8487
+ import { existsSync as existsSync15, readdirSync as readdirSync2, readFileSync as readFileSync15 } from "fs";
8488
+ import { join as join8 } from "path";
6783
8489
  var DEFAULT_LIMIT = 200;
6784
8490
  var MAX_LIMIT = 2e3;
6785
- function readAuditRecords(auditDir, query = {}) {
6786
- if (!existsSync12(auditDir)) return [];
8491
+ function readAuditRecords(auditDir2, query2 = {}) {
8492
+ if (!existsSync15(auditDir2)) return [];
6787
8493
  let files;
6788
8494
  try {
6789
- files = readdirSync2(auditDir).filter((f) => AUDIT_FILE_RE.test(f));
8495
+ files = readdirSync2(auditDir2).filter((f) => AUDIT_FILE_RE.test(f));
6790
8496
  } catch {
6791
8497
  return [];
6792
8498
  }
6793
- const from = typeof query.from === "number" ? query.from : -Infinity;
6794
- const to = typeof query.to === "number" ? query.to : Infinity;
6795
- const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit ?? DEFAULT_LIMIT)));
8499
+ const from = typeof query2.from === "number" ? query2.from : -Infinity;
8500
+ const to = typeof query2.to === "number" ? query2.to : Infinity;
8501
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query2.limit ?? DEFAULT_LIMIT)));
6796
8502
  const matched = [];
6797
8503
  for (const file of files.sort().reverse()) {
6798
8504
  let raw;
6799
8505
  try {
6800
- raw = readFileSync12(join6(auditDir, file), "utf8");
8506
+ raw = readFileSync15(join8(auditDir2, file), "utf8");
6801
8507
  } catch {
6802
8508
  continue;
6803
8509
  }
@@ -6811,7 +8517,7 @@ function readAuditRecords(auditDir, query = {}) {
6811
8517
  continue;
6812
8518
  }
6813
8519
  if (!isAuditRecord(rec)) continue;
6814
- if (query.keyId !== void 0 && rec.keyId !== query.keyId) continue;
8520
+ if (query2.keyId !== void 0 && rec.keyId !== query2.keyId) continue;
6815
8521
  if (rec.ts < from || rec.ts > to) continue;
6816
8522
  matched.push(rec);
6817
8523
  }
@@ -6826,11 +8532,11 @@ function isAuditRecord(value) {
6826
8532
  }
6827
8533
 
6828
8534
  // src/audit/AuditWriter.ts
6829
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "fs";
6830
- import { join as join7 } from "path";
8535
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
8536
+ import { join as join9 } from "path";
6831
8537
  var AuditWriter = class {
6832
- constructor(auditDir, logger, defer = (fn) => setTimeout(fn, 0)) {
6833
- this.auditDir = auditDir;
8538
+ constructor(auditDir2, logger, defer = (fn) => setTimeout(fn, 0)) {
8539
+ this.auditDir = auditDir2;
6834
8540
  this.logger = logger;
6835
8541
  this.defer = defer;
6836
8542
  }
@@ -6860,19 +8566,19 @@ var AuditWriter = class {
6860
8566
  */
6861
8567
  appendNow(record) {
6862
8568
  if (!this.dirEnsured) {
6863
- mkdirSync4(this.auditDir, { recursive: true });
8569
+ mkdirSync5(this.auditDir, { recursive: true });
6864
8570
  this.dirEnsured = true;
6865
8571
  }
6866
- const file = join7(this.auditDir, auditFileName(record.ts));
8572
+ const file = join9(this.auditDir, auditFileName(record.ts));
6867
8573
  appendFileSync2(file, JSON.stringify(record) + "\n", "utf8");
6868
8574
  }
6869
8575
  };
6870
8576
 
6871
8577
  // src/billing/BillingPublisher.ts
6872
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync5 } from "fs";
8578
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync6 } from "fs";
6873
8579
  import { createHmac } from "crypto";
6874
- import { join as join8 } from "path";
6875
- import { fetchUpstream as fetchUpstream4 } from "@omnicross/core/pipeline/upstreamFetch";
8580
+ import { join as join10 } from "path";
8581
+ import { fetchUpstream as fetchUpstream5 } from "@omnicross/core/pipeline/upstreamFetch";
6876
8582
 
6877
8583
  // src/billing/billingFiles.ts
6878
8584
  var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
@@ -6895,7 +8601,7 @@ var BillingPublisher = class {
6895
8601
  constructor(billingDir, logger, opts = {}) {
6896
8602
  this.billingDir = billingDir;
6897
8603
  this.logger = logger;
6898
- this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream4(url, init));
8604
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
6899
8605
  this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
6900
8606
  this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
6901
8607
  this.now = opts.now ?? Date.now;
@@ -6942,7 +8648,7 @@ var BillingPublisher = class {
6942
8648
  */
6943
8649
  appendNow(event) {
6944
8650
  this.ensureDir();
6945
- const file = join8(this.billingDir, billingFileName(event.ts));
8651
+ const file = join10(this.billingDir, billingFileName(event.ts));
6946
8652
  appendFileSync3(file, JSON.stringify(event) + "\n", "utf8");
6947
8653
  }
6948
8654
  /**
@@ -6992,7 +8698,7 @@ var BillingPublisher = class {
6992
8698
  markDelivered(event) {
6993
8699
  try {
6994
8700
  this.ensureDir();
6995
- const file = join8(this.billingDir, deliveredFileName(event.ts));
8701
+ const file = join10(this.billingDir, deliveredFileName(event.ts));
6996
8702
  appendFileSync3(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
6997
8703
  } catch (error) {
6998
8704
  this.logger.warn("[BillingPublisher] failed to append delivery marker", {
@@ -7002,17 +8708,17 @@ var BillingPublisher = class {
7002
8708
  }
7003
8709
  ensureDir() {
7004
8710
  if (this.dirEnsured) return;
7005
- mkdirSync5(this.billingDir, { recursive: true });
8711
+ mkdirSync6(this.billingDir, { recursive: true });
7006
8712
  this.dirEnsured = true;
7007
8713
  }
7008
8714
  };
7009
8715
 
7010
8716
  // src/billing/billingReader.ts
7011
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync13 } from "fs";
7012
- import { join as join9 } from "path";
8717
+ import { existsSync as existsSync16, readdirSync as readdirSync3, readFileSync as readFileSync16 } from "fs";
8718
+ import { join as join11 } from "path";
7013
8719
  function readBillingLedger(billingDir) {
7014
8720
  const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
7015
- if (!existsSync13(billingDir)) return view;
8721
+ if (!existsSync16(billingDir)) return view;
7016
8722
  let files;
7017
8723
  try {
7018
8724
  files = readdirSync3(billingDir);
@@ -7046,7 +8752,7 @@ function readBillingStatus(billingDir) {
7046
8752
  function parseLines(dir, file) {
7047
8753
  let raw;
7048
8754
  try {
7049
- raw = readFileSync13(join9(dir, file), "utf8");
8755
+ raw = readFileSync16(join11(dir, file), "utf8");
7050
8756
  } catch {
7051
8757
  return [];
7052
8758
  }
@@ -7201,8 +8907,9 @@ var TokenRefreshScheduler = class {
7201
8907
  const expiresAt = Date.parse(t.expiresAt);
7202
8908
  return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
7203
8909
  }
7204
- /** Refresh one account; failures are logged, never thrown (the store has
7205
- * already flagged the account `expired`). */
8910
+ /** Refresh one managed account; failures are logged, never thrown. The
8911
+ * store marks only the targeted account `expired` on a failed refresh.
8912
+ */
7206
8913
  async refreshOne(provider, id, isActive) {
7207
8914
  try {
7208
8915
  const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
@@ -7233,7 +8940,7 @@ var TokenRefreshScheduler = class {
7233
8940
 
7234
8941
  // src/webhook/WebhookDispatcher.ts
7235
8942
  import { createHmac as createHmac2 } from "crypto";
7236
- import { fetchUpstream as fetchUpstream5 } from "@omnicross/core/pipeline/upstreamFetch";
8943
+ import { fetchUpstream as fetchUpstream6 } from "@omnicross/core/pipeline/upstreamFetch";
7237
8944
  var WEBHOOK_MAX_ATTEMPTS = 3;
7238
8945
  var WEBHOOK_QUEUE_MAX = 1e3;
7239
8946
  var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
@@ -7253,7 +8960,7 @@ var WebhookDispatcher = class {
7253
8960
  sleep;
7254
8961
  now;
7255
8962
  constructor(opts = {}) {
7256
- this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
8963
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream6(url, init));
7257
8964
  this.logger = opts.logger;
7258
8965
  this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
7259
8966
  this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
@@ -7407,11 +9114,32 @@ function buildDaemon(config, paths) {
7407
9114
  setSecretBox(secretBox3);
7408
9115
  setSecretBox2(secretBox3);
7409
9116
  const decryptedConfig = decryptConfigSecrets(config, secretBox3);
9117
+ const accountAllowanceStore = new AccountAllowanceStore3(
9118
+ Date.now,
9119
+ void 0,
9120
+ new JsonAccountAllowancePersistence(defaultAccountAllowancePath(paths.configPath))
9121
+ );
9122
+ setSharedAccountAllowanceStore(accountAllowanceStore);
9123
+ getSharedAccountAllowanceScheduling4().configure(
9124
+ normalizeServerConfig(decryptedConfig.server).allowanceScheduling
9125
+ );
7410
9126
  const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
7411
9127
  const keyDb = new JsonOutboundKeyDb(paths.keysPath);
7412
9128
  const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
7413
9129
  const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
9130
+ const integrationStateStore = new IntegrationStateStore(
9131
+ defaultIntegrationsPath(paths.configPath),
9132
+ secretBox3
9133
+ );
7414
9134
  const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
9135
+ const accountAllowanceService = new AccountAllowanceService(credentialStore, accountAllowanceStore);
9136
+ const claudeAllowanceRefreshScheduler = new ClaudeAllowanceRefreshScheduler(
9137
+ accountAllowanceService,
9138
+ logger
9139
+ );
9140
+ claudeAllowanceRefreshScheduler.configure(
9141
+ normalizeServerConfig(decryptedConfig.server).allowanceScheduling
9142
+ );
7415
9143
  const subscriptionAccounts = new SubscriptionAccountService(credentialStore);
7416
9144
  setSubscriptionAccountService(subscriptionAccounts);
7417
9145
  const subscriptionRegistry = new SubscriptionProviderRegistry(
@@ -7440,7 +9168,17 @@ function buildDaemon(config, paths) {
7440
9168
  }
7441
9169
  );
7442
9170
  const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
7443
- const pricingEngine = new PricingEngine(pricingStore, logger);
9171
+ const pricingEngine = new PricingEngine(pricingStore, logger, {
9172
+ // Catalog egress follows the same global/env proxy policy as every other
9173
+ // daemon upstream call; no provider/account override applies here.
9174
+ fetchImpl: ((input, init) => fetchUpstream7(String(input), init ?? {}))
9175
+ });
9176
+ const pricingRefreshScheduler = new PricingRefreshScheduler(
9177
+ pricingEngine,
9178
+ pricingStore,
9179
+ defaultPricingRefreshStatePath(paths.configPath),
9180
+ logger
9181
+ );
7444
9182
  const usageEventStore = new JsonlUsageEventStore(
7445
9183
  defaultUsageEventsPath(paths.configPath),
7446
9184
  async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
@@ -7453,7 +9191,7 @@ function buildDaemon(config, paths) {
7453
9191
  llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
7454
9192
  const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
7455
9193
  credentialStore,
7456
- getSharedAccountHealth2(),
9194
+ getSharedAccountHealth3(),
7457
9195
  logger,
7458
9196
  DEFAULT_ACCOUNT_PROBE
7459
9197
  );
@@ -7487,7 +9225,7 @@ function buildDaemon(config, paths) {
7487
9225
  // lines through the injected logger (honors level/format/file sink).
7488
9226
  logger
7489
9227
  });
7490
- const auditDir = defaultAuditDir(paths.configPath);
9228
+ const auditDir2 = defaultAuditDir(paths.configPath);
7491
9229
  const billingDir = defaultBillingDir(paths.configPath);
7492
9230
  const adminServer = new AdminServer({
7493
9231
  configPath: paths.configPath,
@@ -7501,6 +9239,9 @@ function buildDaemon(config, paths) {
7501
9239
  settingsStore,
7502
9240
  outboundApiServer,
7503
9241
  subscriptionAccounts,
9242
+ accountAllowanceService,
9243
+ allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
9244
+ accountProbeService: accountHealthProbeScheduler,
7504
9245
  // Least-authority token WRITER (design D4) — the concrete credential store
7505
9246
  // exposes `writeProviderTokens` / `clearProvider` as daemon-only methods (NOT
7506
9247
  // on the `SubscriptionCredentialStore` port). The admin API sees ONLY these two
@@ -7521,7 +9262,7 @@ function buildDaemon(config, paths) {
7521
9262
  // inject a mock so no real token endpoint is hit.
7522
9263
  // upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
7523
9264
  // helper so interactive login honors a configured proxy (global/env layers).
7524
- oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream6(url, init)),
9265
+ oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream7(url, init)),
7525
9266
  subscriptionAccountAppender: credentialStore,
7526
9267
  // Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
7527
9268
  // + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
@@ -7539,6 +9280,16 @@ function buildDaemon(config, paths) {
7539
9280
  cliTerminalOpener: paths.cliTerminalOpener,
7540
9281
  cliPathProbe: paths.cliPathProbe,
7541
9282
  cliCommandRunner: paths.cliCommandRunner,
9283
+ integrationManagerFactory: () => {
9284
+ const live = outboundApiServer.getStatus();
9285
+ const port = live.port || decryptedConfig.server?.port || DEFAULT_OUTBOUND_PORT;
9286
+ return new IntegrationManager({
9287
+ configPath: paths.configPath,
9288
+ gatewayBaseUrl: live.loopbackUrl ?? `http://127.0.0.1:${port}`,
9289
+ keyDb,
9290
+ stateStore: integrationStateStore
9291
+ });
9292
+ },
7542
9293
  // Usage/pricing admin surface (usage-pricing child): stats queries go
7543
9294
  // through the recorder facade, pricing mutations through the engine, and
7544
9295
  // the row DELETE through the concrete store (delete is store-local — the
@@ -7563,19 +9314,19 @@ function buildDaemon(config, paths) {
7563
9314
  // date-rotated audit store. Bound to the store dir here so the AdminServer
7564
9315
  // carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
7565
9316
  // NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
7566
- auditReader: (query) => readAuditRecords(auditDir, query),
9317
+ auditReader: (query2) => readAuditRecords(auditDir2, query2),
7567
9318
  // billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
7568
9319
  // secret-free total/delivered/pending counts of the durable ledger.
7569
9320
  billingStatusReader: () => readBillingStatus(billingDir)
7570
9321
  });
7571
9322
  const webhookDispatcher = new WebhookDispatcher({
7572
9323
  logger,
7573
- fetchImpl: (url, init) => fetchUpstream6(url, init)
9324
+ fetchImpl: (url, init) => fetchUpstream7(url, init)
7574
9325
  });
7575
- setWebhookRuntime(webhookDispatcher, getSharedAccountHealth2());
7576
- const auditWriter = new AuditWriter(auditDir, logger);
7577
- const auditPruneSweeper = new AuditPruneSweeper(auditDir, logger, DEFAULT_AUDIT_CONFIG);
7578
- setAuditRuntime(auditWriter, auditPruneSweeper);
9326
+ setWebhookRuntime(webhookDispatcher, getSharedAccountHealth3());
9327
+ const auditWriter = new AuditWriter(auditDir2, logger);
9328
+ const auditPruneSweeper = new AuditPruneSweeper(auditDir2, logger, DEFAULT_AUDIT_CONFIG);
9329
+ setAuditRuntime(auditWriter, auditPruneSweeper, auditDir2);
7579
9330
  const billingPublisher = new BillingPublisher(billingDir, logger);
7580
9331
  const billingRetrySweeper = new BillingRetrySweeper(
7581
9332
  billingDir,
@@ -7587,7 +9338,7 @@ function buildDaemon(config, paths) {
7587
9338
  const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
7588
9339
  const accountHealthSweeper = new AccountHealthSweeper(
7589
9340
  credentialStore,
7590
- getSharedAccountHealth2(),
9341
+ getSharedAccountHealth3(),
7591
9342
  logger
7592
9343
  );
7593
9344
  return {
@@ -7602,8 +9353,11 @@ function buildDaemon(config, paths) {
7602
9353
  credentialStore,
7603
9354
  subscriptionRegistry,
7604
9355
  subscriptionAccounts,
9356
+ accountAllowanceService,
9357
+ claudeAllowanceRefreshScheduler,
7605
9358
  pricingStore,
7606
9359
  pricingEngine,
9360
+ pricingRefreshScheduler,
7607
9361
  usageRecorder,
7608
9362
  adminServer,
7609
9363
  tokenRefreshScheduler,
@@ -7618,7 +9372,7 @@ function buildDaemon(config, paths) {
7618
9372
  }
7619
9373
  function isTokensStoreReadable(tokensPath) {
7620
9374
  try {
7621
- if (!existsSync14(tokensPath)) return true;
9375
+ if (!existsSync17(tokensPath)) return true;
7622
9376
  accessSync(tokensPath, fsConstants.R_OK);
7623
9377
  return true;
7624
9378
  } catch {
@@ -7666,8 +9420,8 @@ function buildCliSpawnPlan(opts) {
7666
9420
  function resolveInPathDefault(candidate) {
7667
9421
  const segments = (process.env["PATH"] ?? "").split(delimiter2).filter(Boolean);
7668
9422
  for (const seg of segments) {
7669
- const full = join10(seg, candidate);
7670
- if (existsSync15(full)) return full;
9423
+ const full = join12(seg, candidate);
9424
+ if (existsSync18(full)) return full;
7671
9425
  }
7672
9426
  return null;
7673
9427
  }
@@ -7675,7 +9429,7 @@ async function runLaunch(argv, deps) {
7675
9429
  const sep = argv.indexOf("--");
7676
9430
  const own = sep === -1 ? argv : argv.slice(0, sep);
7677
9431
  const passthrough = sep === -1 ? [] : argv.slice(sep + 1);
7678
- const { values, positionals } = parseArgs3({
9432
+ const { values, positionals } = parseArgs4({
7679
9433
  args: own,
7680
9434
  options: {
7681
9435
  provider: { type: "string", short: "p" },
@@ -7710,10 +9464,12 @@ async function runLaunch(argv, deps) {
7710
9464
  } catch (err5) {
7711
9465
  daemon.apiKeyPool.dispose();
7712
9466
  daemon.tokenRefreshScheduler.dispose();
9467
+ daemon.claudeAllowanceRefreshScheduler.dispose();
7713
9468
  daemon.accountHealthSweeper.dispose();
7714
9469
  daemon.accountHealthProbeScheduler.dispose();
7715
9470
  daemon.auditPruneSweeper.dispose();
7716
9471
  daemon.billingRetrySweeper.dispose();
9472
+ daemon.pricingRefreshScheduler.dispose();
7717
9473
  throw err5;
7718
9474
  }
7719
9475
  let launch;
@@ -7726,10 +9482,12 @@ async function runLaunch(argv, deps) {
7726
9482
  await daemon.providerProxy.stop();
7727
9483
  daemon.apiKeyPool.dispose();
7728
9484
  daemon.tokenRefreshScheduler.dispose();
9485
+ daemon.claudeAllowanceRefreshScheduler.dispose();
7729
9486
  daemon.accountHealthSweeper.dispose();
7730
9487
  daemon.accountHealthProbeScheduler.dispose();
7731
9488
  daemon.auditPruneSweeper.dispose();
7732
9489
  daemon.billingRetrySweeper.dispose();
9490
+ daemon.pricingRefreshScheduler.dispose();
7733
9491
  throw err5;
7734
9492
  }
7735
9493
  try {
@@ -7752,10 +9510,12 @@ async function runLaunch(argv, deps) {
7752
9510
  await daemon.providerProxy.stop();
7753
9511
  daemon.apiKeyPool.dispose();
7754
9512
  daemon.tokenRefreshScheduler.dispose();
9513
+ daemon.claudeAllowanceRefreshScheduler.dispose();
7755
9514
  daemon.accountHealthSweeper.dispose();
7756
9515
  daemon.accountHealthProbeScheduler.dispose();
7757
9516
  daemon.auditPruneSweeper.dispose();
7758
9517
  daemon.billingRetrySweeper.dispose();
9518
+ daemon.pricingRefreshScheduler.dispose();
7759
9519
  }
7760
9520
  }
7761
9521
  async function buildLaunchConfig(cli, llmConfig, opts) {
@@ -7785,7 +9545,7 @@ async function buildLaunchConfig(cli, llmConfig, opts) {
7785
9545
  }
7786
9546
  }
7787
9547
  function spawnCliInherit(plan) {
7788
- return new Promise((resolve, reject) => {
9548
+ return new Promise((resolve3, reject) => {
7789
9549
  const child = spawn2(plan.command, plan.args, {
7790
9550
  stdio: "inherit",
7791
9551
  env: plan.env,
@@ -7819,7 +9579,7 @@ function spawnCliInherit(plan) {
7819
9579
  });
7820
9580
  child.on("exit", (code, signal) => {
7821
9581
  detach();
7822
- resolve(code ?? (signal ? 1 : 0));
9582
+ resolve3(code ?? (signal ? 1 : 0));
7823
9583
  });
7824
9584
  });
7825
9585
  }
@@ -7827,12 +9587,12 @@ function spawnCliInherit(plan) {
7827
9587
  // src/commands/login.ts
7828
9588
  import { spawn as spawn3 } from "child_process";
7829
9589
  import { createInterface } from "readline";
7830
- import { parseArgs as parseArgs4 } from "util";
7831
- import { fetchUpstream as fetchUpstream7, setUpstreamProxyResolver as setUpstreamProxyResolver2 } from "@omnicross/core/pipeline/upstreamFetch";
9590
+ import { parseArgs as parseArgs5 } from "util";
9591
+ import { fetchUpstream as fetchUpstream8, setUpstreamProxyResolver as setUpstreamProxyResolver2 } from "@omnicross/core/pipeline/upstreamFetch";
7832
9592
  import { claudeOAuth as claudeOAuth3, codexOAuth as codexOAuth3, geminiOAuth as geminiOAuth3 } from "@omnicross/subscriptions";
7833
9593
  var PROVIDERS = ["claude", "codex", "gemini"];
7834
9594
  async function runLogin(argv, deps) {
7835
- const { values, positionals } = parseArgs4({
9595
+ const { values, positionals } = parseArgs5({
7836
9596
  args: argv,
7837
9597
  options: {
7838
9598
  config: { type: "string", short: "c" },
@@ -7863,7 +9623,7 @@ async function runLogin(argv, deps) {
7863
9623
  setUpstreamProxyResolver2(createUpstreamProxyResolver());
7864
9624
  try {
7865
9625
  const tokensPath = defaultTokensPath(values.config);
7866
- const exchangeFetch = resolved.tokensFetch ?? ((url, init) => fetchUpstream7(url, init, { providerId: provider }));
9626
+ const exchangeFetch = resolved.tokensFetch ?? ((url, init) => fetchUpstream8(url, init, { providerId: provider }));
7867
9627
  const store = new JsonSubscriptionCredentialStore(tokensPath, box, exchangeFetch);
7868
9628
  const expiresAt = await runProviderLogin(
7869
9629
  provider,
@@ -7976,33 +9736,33 @@ function buildOpenBrowserCommand(platform, url) {
7976
9736
  return { command: "xdg-open", args: [url] };
7977
9737
  }
7978
9738
  function openBrowser(url) {
7979
- return new Promise((resolve) => {
9739
+ return new Promise((resolve3) => {
7980
9740
  try {
7981
9741
  const { command, args } = buildOpenBrowserCommand(process.platform, url);
7982
9742
  const child = spawn3(command, args, { stdio: "ignore", detached: true });
7983
- child.on("error", () => resolve(false));
9743
+ child.on("error", () => resolve3(false));
7984
9744
  child.unref();
7985
- resolve(true);
9745
+ resolve3(true);
7986
9746
  } catch {
7987
- resolve(false);
9747
+ resolve3(false);
7988
9748
  }
7989
9749
  });
7990
9750
  }
7991
9751
  function promptPaste(prompt) {
7992
9752
  const rl = createInterface({ input: process.stdin, output: process.stdout });
7993
- return new Promise((resolve) => {
9753
+ return new Promise((resolve3) => {
7994
9754
  rl.question(prompt, (answer) => {
7995
9755
  rl.close();
7996
- resolve(answer);
9756
+ resolve3(answer);
7997
9757
  });
7998
9758
  });
7999
9759
  }
8000
9760
 
8001
9761
  // src/commands/providers.ts
8002
- import { randomUUID as randomUUID4 } from "crypto";
8003
- import { parseArgs as parseArgs5 } from "util";
9762
+ import { randomUUID as randomUUID6 } from "crypto";
9763
+ import { parseArgs as parseArgs6 } from "util";
8004
9764
  async function runProviders(argv) {
8005
- const { values, positionals } = parseArgs5({
9765
+ const { values, positionals } = parseArgs6({
8006
9766
  args: argv,
8007
9767
  options: {
8008
9768
  config: { type: "string", short: "c" },
@@ -8121,7 +9881,7 @@ function providersAddKey(configPath, providerId, opts) {
8121
9881
  const cfg = loadConfig(configPath);
8122
9882
  const row = cfg.providers.find((p) => p.id === providerId);
8123
9883
  if (!row) throw new Error(`providers add-key: unknown provider '${providerId}'`);
8124
- const entry = { id: randomUUID4(), apiKey: opts.key };
9884
+ const entry = { id: randomUUID6(), apiKey: opts.key };
8125
9885
  if (opts.label) entry.label = opts.label;
8126
9886
  if (opts.weight !== void 0) {
8127
9887
  const w = Number(opts.weight);
@@ -8149,10 +9909,10 @@ function providersRmKey(configPath, providerId, keyId) {
8149
9909
  }
8150
9910
 
8151
9911
  // src/commands/secrets.ts
8152
- import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
8153
- import { parseArgs as parseArgs6 } from "util";
9912
+ import { existsSync as existsSync19, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
9913
+ import { parseArgs as parseArgs7 } from "util";
8154
9914
  async function runSecrets(argv) {
8155
- const { values, positionals } = parseArgs6({
9915
+ const { values, positionals } = parseArgs7({
8156
9916
  args: argv,
8157
9917
  options: {
8158
9918
  config: { type: "string", short: "c" },
@@ -8178,7 +9938,7 @@ async function runSecrets(argv) {
8178
9938
  case "status":
8179
9939
  return secretsStatus(args);
8180
9940
  case "rotate":
8181
- return secretsRotate(args);
9941
+ return await secretsRotate(args);
8182
9942
  case "decrypt":
8183
9943
  return secretsDecrypt(args);
8184
9944
  default:
@@ -8194,6 +9954,7 @@ function secretsEncrypt(args) {
8194
9954
  const cfg = loadConfig(args.config);
8195
9955
  saveConfig(args.config, cfg);
8196
9956
  encryptTokensFileInPlace(args.config, box);
9957
+ rewriteIntegrationState(args.config, box, box);
8197
9958
  } finally {
8198
9959
  setSecretBox(null);
8199
9960
  }
@@ -8221,10 +9982,27 @@ function secretsStatus(args) {
8221
9982
  reportField("admin.token", cfg.admin.token);
8222
9983
  }
8223
9984
  const tokensPath = defaultTokensPath(args.config);
8224
- if (existsSync16(tokensPath)) {
9985
+ if (existsSync19(tokensPath)) {
8225
9986
  console.info(`Secret status for ${tokensPath}:`);
8226
9987
  reportTokenFields(tokensPath);
8227
9988
  }
9989
+ const integrationsPath = defaultIntegrationsPath(args.config);
9990
+ if (existsSync19(integrationsPath)) {
9991
+ const state = readRawJson(integrationsPath);
9992
+ const key = state.gatewayKey;
9993
+ if (key && typeof key === "object" && !Array.isArray(key)) {
9994
+ const secret = key.secret;
9995
+ if (typeof secret === "string") reportField("integrations.gatewayKey", secret);
9996
+ }
9997
+ const clients = state.clients;
9998
+ if (clients && typeof clients === "object" && !Array.isArray(clients)) {
9999
+ for (const client of ["codex", "claude"]) {
10000
+ const record = clients[client];
10001
+ const snapshot = record && typeof record === "object" && !Array.isArray(record) ? record.originalContent : void 0;
10002
+ if (typeof snapshot === "string") reportField(`integrations.${client}.snapshot`, snapshot);
10003
+ }
10004
+ }
10005
+ }
8228
10006
  }
8229
10007
  function reportField(name, raw) {
8230
10008
  const cls = classify(raw);
@@ -8249,7 +10027,7 @@ function reportTokenFields(tokensPath) {
8249
10027
  }
8250
10028
  }
8251
10029
  }
8252
- function secretsRotate(args) {
10030
+ async function secretsRotate(args) {
8253
10031
  if (!args.newMasterKeyFile) {
8254
10032
  throw new Error("secrets rotate: --new-master-key-file <path> is required");
8255
10033
  }
@@ -8258,10 +10036,15 @@ function secretsRotate(args) {
8258
10036
  setSecretBox(oldBox);
8259
10037
  let cfg;
8260
10038
  let tokensPlain = null;
10039
+ let integrationsPlain = null;
8261
10040
  const tokensPath = defaultTokensPath(args.config);
10041
+ const integrationsPath = defaultIntegrationsPath(args.config);
8262
10042
  try {
8263
10043
  cfg = loadConfig(args.config);
8264
- if (existsSync16(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, oldBox);
10044
+ if (existsSync19(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, oldBox);
10045
+ if (existsSync19(integrationsPath)) {
10046
+ integrationsPlain = new IntegrationStateStore(integrationsPath, oldBox).load();
10047
+ }
8265
10048
  } finally {
8266
10049
  setSecretBox(null);
8267
10050
  }
@@ -8269,6 +10052,10 @@ function secretsRotate(args) {
8269
10052
  try {
8270
10053
  saveConfig(args.config, cfg);
8271
10054
  if (tokensPlain) writeTokensEncrypted(tokensPath, tokensPlain, newBox);
10055
+ if (integrationsPlain) {
10056
+ const newStore = new IntegrationStateStore(integrationsPath, newBox);
10057
+ newStore.save(integrationsPlain);
10058
+ }
8272
10059
  } finally {
8273
10060
  setSecretBox(null);
8274
10061
  }
@@ -8290,20 +10077,20 @@ function secretsDecrypt(args) {
8290
10077
  let tokensPlain = null;
8291
10078
  try {
8292
10079
  cfg = loadConfig(args.config);
8293
- if (existsSync16(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
10080
+ if (existsSync19(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
8294
10081
  } finally {
8295
10082
  setSecretBox(null);
8296
10083
  }
8297
10084
  saveConfig(args.config, cfg);
8298
10085
  if (tokensPlain) {
8299
- writeFileSync9(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
10086
+ writeFileSync11(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
8300
10087
  }
8301
10088
  console.info(`Decrypted secrets to plaintext in ${args.config}` + tokensSuffix(args.config));
8302
10089
  }
8303
10090
  function readRawConfig(path2) {
8304
10091
  let parsed;
8305
10092
  try {
8306
- parsed = JSON.parse(readFileSync14(path2, "utf8"));
10093
+ parsed = JSON.parse(readFileSync17(path2, "utf8"));
8307
10094
  } catch {
8308
10095
  throw new Error(`secrets: cannot read or parse '${path2}'`);
8309
10096
  }
@@ -8311,7 +10098,7 @@ function readRawConfig(path2) {
8311
10098
  }
8312
10099
  function readRawJson(path2) {
8313
10100
  try {
8314
- const parsed = JSON.parse(readFileSync14(path2, "utf8"));
10101
+ const parsed = JSON.parse(readFileSync17(path2, "utf8"));
8315
10102
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
8316
10103
  return parsed;
8317
10104
  }
@@ -8321,10 +10108,16 @@ function readRawJson(path2) {
8321
10108
  }
8322
10109
  function encryptTokensFileInPlace(configPath, box) {
8323
10110
  const tokensPath = defaultTokensPath(configPath);
8324
- if (!existsSync16(tokensPath)) return;
10111
+ if (!existsSync19(tokensPath)) return;
8325
10112
  const plain = decryptTokensFile(tokensPath, box);
8326
10113
  writeTokensEncrypted(tokensPath, plain, box);
8327
10114
  }
10115
+ function rewriteIntegrationState(configPath, readBox, writeBox) {
10116
+ const path2 = defaultIntegrationsPath(configPath);
10117
+ if (!existsSync19(path2)) return;
10118
+ const state = new IntegrationStateStore(path2, readBox).load();
10119
+ new IntegrationStateStore(path2, writeBox).save(state);
10120
+ }
8328
10121
  function decryptTokensFile(tokensPath, box) {
8329
10122
  const raw = readRawJson(tokensPath);
8330
10123
  return walkTokens(raw, (v) => box.decryptMaybe(v));
@@ -8334,7 +10127,7 @@ function writeTokensEncrypted(tokensPath, plain, box) {
8334
10127
  { updatedAt: "", ...plain },
8335
10128
  box
8336
10129
  );
8337
- writeFileSync9(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
10130
+ writeFileSync11(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
8338
10131
  }
8339
10132
  var TOKEN_FIELDS2 = {
8340
10133
  claude: ["accessToken", "refreshToken"],
@@ -8357,18 +10150,19 @@ function walkTokens(raw, fn) {
8357
10150
  return next;
8358
10151
  }
8359
10152
  function tokensSuffix(configPath) {
8360
- return existsSync16(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
10153
+ return existsSync19(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
8361
10154
  }
8362
10155
 
8363
10156
  // src/commands/start.ts
8364
- import { parseArgs as parseArgs7 } from "util";
8365
- import { loadServerConfig as loadServerConfig3, OutboundApiConfigError } from "@omnicross/core/outbound-api";
8366
- import { getSharedAccountHealth as getSharedAccountHealth3 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
10157
+ import { parseArgs as parseArgs8 } from "util";
10158
+ import { loadServerConfig as loadServerConfig3 } from "@omnicross/core/outbound-api";
10159
+ import { getSharedAccountHealth as getSharedAccountHealth4 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
10160
+ import { getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling5 } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
8367
10161
 
8368
10162
  // src/identity/identityRuntime.ts
8369
- import { getSharedIdentityStore as getSharedIdentityStore2 } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
10163
+ import { getSharedIdentityStore as getSharedIdentityStore3 } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
8370
10164
  async function applyFingerprintConfig(config, credentialStore) {
8371
- const store = getSharedIdentityStore2();
10165
+ const store = getSharedIdentityStore3();
8372
10166
  const enabled = config?.enabled === true;
8373
10167
  store.configure({ enabled, ua: config?.ua ?? null });
8374
10168
  if (!enabled) {
@@ -8399,7 +10193,7 @@ async function seedIdentities(store, credentialStore) {
8399
10193
 
8400
10194
  // src/commands/start.ts
8401
10195
  async function runStart(argv) {
8402
- const { values } = parseArgs7({
10196
+ const { values } = parseArgs8({
8403
10197
  args: argv,
8404
10198
  options: {
8405
10199
  config: { type: "string", short: "c" },
@@ -8423,35 +10217,32 @@ async function runStart(argv) {
8423
10217
  await daemon.llmConfig.ready();
8424
10218
  await daemon.providerProxy.start();
8425
10219
  const serverConfig = await loadServerConfig3(daemon.settingsStore);
8426
- getSharedAccountHealth3().configure({
10220
+ getSharedAccountHealth4().configure({
8427
10221
  overloadEnabled: serverConfig.accountHealth?.overloadCooldownEnabled,
8428
10222
  overloadTtlMs: serverConfig.accountHealth?.overloadCooldownMs
8429
10223
  });
8430
- try {
8431
- await daemon.outboundApiServer.applyConfig({
8432
- enabled: true,
8433
- networkBinding: serverConfig.networkBinding,
8434
- endpoints: serverConfig.endpoints,
8435
- port: serverConfig.port,
8436
- userMessageQueue: serverConfig.userMessageQueue,
8437
- concurrencyQueue: serverConfig.concurrencyQueue,
8438
- // voucher-redemption #9: carry the persisted flag so `POST /redeem` works on
8439
- // boot when the operator has enabled the product.
8440
- voucher: serverConfig.voucher
8441
- });
8442
- } catch (err5) {
8443
- if (err5 instanceof OutboundApiConfigError) {
8444
- console.warn(`[outbound] not started \u2014 incomplete model configuration: ${err5.message}`);
8445
- } else {
8446
- throw err5;
8447
- }
8448
- }
10224
+ getSharedAccountAllowanceScheduling5().configure(serverConfig.allowanceScheduling);
10225
+ daemon.claudeAllowanceRefreshScheduler.configure(serverConfig.allowanceScheduling);
10226
+ await daemon.outboundApiServer.applyConfig({
10227
+ enabled: true,
10228
+ networkBinding: serverConfig.networkBinding,
10229
+ endpoints: serverConfig.endpoints,
10230
+ bindings: serverConfig.bindings,
10231
+ port: serverConfig.port,
10232
+ userMessageQueue: serverConfig.userMessageQueue,
10233
+ concurrencyQueue: serverConfig.concurrencyQueue,
10234
+ // voucher-redemption #9: carry the persisted flag so `POST /redeem` works on
10235
+ // boot when the operator has enabled the product.
10236
+ voucher: serverConfig.voucher
10237
+ });
8449
10238
  let dashboardUrl = null;
8450
10239
  if (!values["no-dashboard"]) {
8451
10240
  await daemon.adminServer.start();
8452
10241
  dashboardUrl = daemon.adminServer.getStatus().url;
8453
10242
  }
8454
10243
  daemon.tokenRefreshScheduler.start();
10244
+ daemon.claudeAllowanceRefreshScheduler.start();
10245
+ daemon.pricingRefreshScheduler.start();
8455
10246
  daemon.accountHealthSweeper.start();
8456
10247
  if (serverConfig.accountProbe) {
8457
10248
  daemon.accountHealthProbeScheduler.configure(serverConfig.accountProbe);
@@ -8528,6 +10319,16 @@ Usage:
8528
10319
  omnicross launch <cli> --provider <id> --model <m> --config <p> [--cwd <dir>] [-- <cli-args\u2026>]
8529
10320
  Launch a Code CLI (claude|codex|gemini|qwen|copilot|opencode)
8530
10321
  against an in-process proxy (route-token auth; BYO).
10322
+ omnicross integrations status --config <p> Inspect native CLI gateway integration.
10323
+ omnicross integrations plan <codex|claude> --config <p> [--target <path>]
10324
+ Preview redacted configuration changes.
10325
+ omnicross integrations install <codex|claude> --config <p> --gateway-base-url <url>
10326
+ Reversibly configure a native CLI for Omnicross.
10327
+ omnicross integrations repair <codex|claude> --config <p>
10328
+ Repair managed fields while preserving unrelated edits.
10329
+ omnicross integrations remove <codex|claude> --config <p>
10330
+ Restore the exact pre-install configuration.
10331
+ omnicross integrations rotate --config <p> Rotate the shared local integration key.
8531
10332
  omnicross import-ccr <ccr.json> [--out <p>] Translate a CCR config.
8532
10333
  omnicross secrets encrypt --config <p> Encrypt all at-rest secrets in place.
8533
10334
  omnicross secrets status --config <p> Report each secret field (no values shown).
@@ -8554,6 +10355,9 @@ async function main() {
8554
10355
  case "launch":
8555
10356
  process.exitCode = await runLaunch(rest);
8556
10357
  return;
10358
+ case "integrations":
10359
+ await runIntegrations(rest);
10360
+ return;
8557
10361
  case "import-ccr":
8558
10362
  await runImportCcr(rest);
8559
10363
  return;