@omnicross/daemon 0.1.5 → 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 join11 } 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,7 +3666,7 @@ function preserveWebhookSecrets(incoming, current) {
2234
3666
  }
2235
3667
 
2236
3668
  // src/audit/auditRuntime.ts
2237
- import { join as join4 } from "path";
3669
+ import { join as join5 } from "path";
2238
3670
  import { setAuditCaptureConfig, setAuditSink } from "@omnicross/core/pipeline/auditSink";
2239
3671
  import { setUpstreamTracePath } from "@omnicross/core/pipeline/upstreamTrace";
2240
3672
  var writer = null;
@@ -2255,7 +3687,7 @@ function applyAuditConfig(config) {
2255
3687
  sweeper.configure(config);
2256
3688
  sweeper.start();
2257
3689
  }
2258
- setUpstreamTracePath(config.captureBodies ? join4(auditDir, "upstream-trace.jsonl") : null);
3690
+ setUpstreamTracePath(config.captureBodies ? join5(auditDir, "upstream-trace.jsonl") : null);
2259
3691
  } else {
2260
3692
  setAuditCaptureConfig(null);
2261
3693
  setAuditSink(null);
@@ -2297,7 +3729,7 @@ function applyBillingConfig(config) {
2297
3729
  }
2298
3730
 
2299
3731
  // src/ports/account-multi.ts
2300
- import { randomUUID as randomUUID2 } from "crypto";
3732
+ import { randomUUID as randomUUID3 } from "crypto";
2301
3733
  var PROVIDER_KEYS = {
2302
3734
  claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
2303
3735
  codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
@@ -2363,7 +3795,7 @@ function migrateLazily(config) {
2363
3795
  }
2364
3796
  function addAccount(config, p, tokens, label) {
2365
3797
  const accounts = [...getAccounts(config, p)];
2366
- const id = randomUUID2();
3798
+ const id = randomUUID3();
2367
3799
  accounts.push({
2368
3800
  id,
2369
3801
  label: label ?? `Account ${accounts.length + 1}`,
@@ -2439,9 +3871,13 @@ function sanitizeAccounts(config, p) {
2439
3871
  const activeId = getActiveId(config, p);
2440
3872
  return accounts.map((a) => {
2441
3873
  const t = a.tokens;
3874
+ const enabled = a.enabled !== false;
2442
3875
  return {
2443
3876
  id: a.id,
2444
3877
  label: a.label,
3878
+ enabled,
3879
+ group: a.group?.trim() || p,
3880
+ tags: a.tags ?? [],
2445
3881
  status: t.status ?? "unconfigured",
2446
3882
  authMethod: t.authMethod,
2447
3883
  subscriptionLevel: t.subscriptionLevel,
@@ -2450,6 +3886,8 @@ function sanitizeAccounts(config, p) {
2450
3886
  isSetupToken: t.isSetupToken,
2451
3887
  hasAccessToken: !!(t.accessToken || t.apiKey),
2452
3888
  isActive: a.id === activeId,
3889
+ schedulable: enabled,
3890
+ errorMessage: sanitizeDiagnosticMessage(t.errorMessage),
2453
3891
  // Scheduling metadata (subscription-account-scheduling): editable priority
2454
3892
  // (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
2455
3893
  priority: a.priority,
@@ -2464,6 +3902,54 @@ function sanitizeAccounts(config, p) {
2464
3902
  };
2465
3903
  });
2466
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
+ }
2467
3953
  function renameAccount(config, p, id, label) {
2468
3954
  const accounts = getAccounts(config, p);
2469
3955
  if (!accounts.some((a) => a.id === id)) return { ok: false };
@@ -2801,9 +4287,9 @@ function parseFiniteInt(raw) {
2801
4287
  const n = Number(raw);
2802
4288
  return Number.isFinite(n) && Number.isInteger(n) ? n : null;
2803
4289
  }
2804
- function parseRange(query) {
2805
- const startTs = parseFiniteInt(query.get("startTs"));
2806
- const endTs = parseFiniteInt(query.get("endTs"));
4290
+ function parseRange(query2) {
4291
+ const startTs = parseFiniteInt(query2.get("startTs"));
4292
+ const endTs = parseFiniteInt(query2.get("endTs"));
2807
4293
  if (startTs === null || endTs === null) {
2808
4294
  return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
2809
4295
  }
@@ -2816,8 +4302,8 @@ var BUCKET_SPAN_MS = {
2816
4302
  month: 28 * 864e5
2817
4303
  };
2818
4304
  var MAX_TIMESERIES_BUCKETS = 2e3;
2819
- async function handleUsageGet(view, query, deps) {
2820
- const range = parseRange(query);
4305
+ async function handleUsageGet(view, query2, deps) {
4306
+ const range = parseRange(query2);
2821
4307
  if (!isRange(range)) return range;
2822
4308
  switch (view) {
2823
4309
  case "totals":
@@ -2825,7 +4311,7 @@ async function handleUsageGet(view, query, deps) {
2825
4311
  case "by-model":
2826
4312
  return { status: 200, body: await deps.usageRecorder.getByModel(range) };
2827
4313
  case "timeseries": {
2828
- const bucket = query.get("bucket");
4314
+ const bucket = query2.get("bucket");
2829
4315
  if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
2830
4316
  return err4(400, "bucket must be one of 'hour', 'day', 'month'");
2831
4317
  }
@@ -2911,9 +4397,9 @@ async function handlePricingUpsert(body, deps) {
2911
4397
  const entry = await deps.pricingEngine.upsertManual(input);
2912
4398
  return { status: 200, body: { entry } };
2913
4399
  }
2914
- async function handlePricingDelete(query, deps) {
2915
- const providerId = query.get("providerId")?.trim() ?? "";
2916
- 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() ?? "";
2917
4403
  if (!providerId || !modelId) {
2918
4404
  return err4(400, "delete requires providerId and modelId query params");
2919
4405
  }
@@ -2930,7 +4416,8 @@ async function handlePricingFetchLatest(deps) {
2930
4416
  appliedCount: result.applied.length,
2931
4417
  conflicts: result.conflicts,
2932
4418
  fetchedAt: result.fetchedAt,
2933
- sourceUrl: result.sourceUrl
4419
+ sourceUrl: result.sourceUrl,
4420
+ sources: result.sources
2934
4421
  }
2935
4422
  };
2936
4423
  } catch (e) {
@@ -2978,12 +4465,80 @@ async function handlePricingResolveConflicts(body, deps) {
2978
4465
  return { status: 200, body: { ...resolution, staleCount } };
2979
4466
  }
2980
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
+
2981
4536
  // src/admin/adminApi.ts
2982
4537
  function readBody(req) {
2983
- return new Promise((resolve, reject) => {
4538
+ return new Promise((resolve3, reject) => {
2984
4539
  const chunks = [];
2985
4540
  req.on("data", (chunk) => chunks.push(chunk));
2986
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
4541
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
2987
4542
  req.on("error", reject);
2988
4543
  });
2989
4544
  }
@@ -2997,12 +4552,12 @@ async function readJsonBody3(req) {
2997
4552
  return {};
2998
4553
  }
2999
4554
  }
3000
- function writeJson2(res, status, body) {
4555
+ function writeJson3(res, status, body) {
3001
4556
  res.writeHead(status, { "Content-Type": "application/json" });
3002
4557
  res.end(JSON.stringify(body));
3003
4558
  }
3004
4559
  function writeJsonError(res, status, message) {
3005
- writeJson2(res, status, { error: { type: "admin_api_error", message } });
4560
+ writeJson3(res, status, { error: { type: "admin_api_error", message } });
3006
4561
  }
3007
4562
  function maskProviderApiKey(apiKey) {
3008
4563
  if (!apiKey) return "";
@@ -3019,6 +4574,9 @@ function toKeyInfo(row) {
3019
4574
  createdAt: row.createdAt,
3020
4575
  lastUsedAt: row.lastUsedAt,
3021
4576
  revoked: row.revokedAt !== null,
4577
+ kind: row.kind,
4578
+ allowedEndpoints: row.allowedEndpoints,
4579
+ loopbackOnly: row.loopbackOnly,
3022
4580
  maxConcurrency: row.maxConcurrency,
3023
4581
  // Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
3024
4582
  // the UI reads them to render + pre-fill the policy editor.
@@ -3104,6 +4662,8 @@ async function handleAdminApi(req, res, path2, deps) {
3104
4662
  return await handleAccounts(req, res, method, rest, deps);
3105
4663
  case "cli":
3106
4664
  return await handleCli(req, res, method, rest, deps);
4665
+ case "integrations":
4666
+ return await handleIntegrations(req, res, method, rest, deps);
3107
4667
  case "status":
3108
4668
  return await handleStatus(res, method, deps);
3109
4669
  case "playground":
@@ -3131,7 +4691,7 @@ function requestQuery(req) {
3131
4691
  return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
3132
4692
  }
3133
4693
  function writeResult(res, result) {
3134
- writeJson2(res, result.status, result.body);
4694
+ writeJson3(res, result.status, result.body);
3135
4695
  }
3136
4696
  async function handleUsage(req, res, method, rest, deps) {
3137
4697
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
@@ -3140,7 +4700,7 @@ async function handleUsage(req, res, method, rest, deps) {
3140
4700
  async function handleDashboardRoute(res, method, deps) {
3141
4701
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
3142
4702
  const result = await handleDashboard(deps);
3143
- return writeJson2(res, result.status, result.body);
4703
+ return writeJson3(res, result.status, result.body);
3144
4704
  }
3145
4705
  async function handlePricing(req, res, method, rest, deps) {
3146
4706
  if (rest.length === 0) {
@@ -3173,13 +4733,13 @@ async function handleMigrationExport(req, res, method, deps) {
3173
4733
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
3174
4734
  const body = await readJsonBody3(req);
3175
4735
  const result = await handleExport(body, migrationDeps(deps));
3176
- return writeJson2(res, result.status, result.body);
4736
+ return writeJson3(res, result.status, result.body);
3177
4737
  }
3178
4738
  async function handleMigrationImport(req, res, method, deps) {
3179
4739
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
3180
4740
  const body = await readJsonBody3(req);
3181
4741
  const result = await handleImport(body, migrationDeps(deps));
3182
- return writeJson2(res, result.status, result.body);
4742
+ return writeJson3(res, result.status, result.body);
3183
4743
  }
3184
4744
  async function handleProviders(req, res, method, rest, deps) {
3185
4745
  const cfg = loadConfig(deps.configPath);
@@ -3210,10 +4770,10 @@ async function handleProviders(req, res, method, rest, deps) {
3210
4770
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
3211
4771
  const row = cfg.providers.find((p) => p.id === rest[0]);
3212
4772
  if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
3213
- return writeJson2(res, 200, { apiKey: row.apiKey ?? "" });
4773
+ return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
3214
4774
  }
3215
4775
  if (method === "GET") {
3216
- return writeJson2(res, 200, { providers: cfg.providers.map(toProviderView) });
4776
+ return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
3217
4777
  }
3218
4778
  if (method === "POST") {
3219
4779
  const body = await readJsonBody3(req);
@@ -3224,7 +4784,7 @@ async function handleProviders(req, res, method, rest, deps) {
3224
4784
  }
3225
4785
  cfg.providers.push(provider);
3226
4786
  persistProviders(cfg, deps);
3227
- return writeJson2(res, 201, { provider: toProviderView(provider) });
4787
+ return writeJson3(res, 201, { provider: toProviderView(provider) });
3228
4788
  }
3229
4789
  const id = rest[0];
3230
4790
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3237,12 +4797,12 @@ async function handleProviders(req, res, method, rest, deps) {
3237
4797
  if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
3238
4798
  cfg.providers[idx] = updated;
3239
4799
  persistProviders(cfg, deps);
3240
- return writeJson2(res, 200, { provider: toProviderView(updated) });
4800
+ return writeJson3(res, 200, { provider: toProviderView(updated) });
3241
4801
  }
3242
4802
  if (method === "DELETE") {
3243
4803
  cfg.providers.splice(idx, 1);
3244
4804
  persistProviders(cfg, deps);
3245
- return writeJson2(res, 200, { ok: true });
4805
+ return writeJson3(res, 200, { ok: true });
3246
4806
  }
3247
4807
  return writeJsonError(res, 405, `method ${method} not allowed on providers`);
3248
4808
  }
@@ -3275,14 +4835,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
3275
4835
  }
3276
4836
  cfg.providers = reordered;
3277
4837
  persistProviders(cfg, deps);
3278
- return writeJson2(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
4838
+ return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
3279
4839
  }
3280
4840
  async function handleDiscoverModels(res, id, cfg) {
3281
4841
  if (!id) return writeJsonError(res, 400, "provider id required in path");
3282
4842
  const row = cfg.providers.find((p) => p.id === id);
3283
4843
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
3284
- if (row.apiFormat !== "openai") {
3285
- return writeJson2(res, 200, { models: [], unsupportedFormat: true });
4844
+ if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
4845
+ return writeJson3(res, 200, { models: [], unsupportedFormat: true });
3286
4846
  }
3287
4847
  const resolvedKey = resolveEnvKey(row.apiKey);
3288
4848
  const base = row.baseUrl.replace(/\/+$/, "");
@@ -3290,7 +4850,7 @@ async function handleDiscoverModels(res, id, cfg) {
3290
4850
  try {
3291
4851
  const headers = { Accept: "application/json" };
3292
4852
  if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
3293
- const response = await fetchUpstream(url, { method: "GET", headers }, { providerId: "byo" });
4853
+ const response = await fetchUpstream2(url, { method: "GET", headers }, { providerId: "byo" });
3294
4854
  if (!response.ok) {
3295
4855
  const text = await response.text().catch(() => "");
3296
4856
  let message = text.slice(0, 300);
@@ -3299,17 +4859,17 @@ async function handleDiscoverModels(res, id, cfg) {
3299
4859
  message = parsed?.error?.message || parsed?.message || message;
3300
4860
  } catch {
3301
4861
  }
3302
- return writeJson2(res, 200, {
4862
+ return writeJson3(res, 200, {
3303
4863
  models: [],
3304
4864
  error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
3305
4865
  });
3306
4866
  }
3307
4867
  const data = await response.json();
3308
4868
  const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
3309
- return writeJson2(res, 200, { models });
4869
+ return writeJson3(res, 200, { models });
3310
4870
  } catch (err5) {
3311
4871
  const message = err5 instanceof Error ? err5.message : String(err5);
3312
- return writeJson2(res, 200, { models: [], error: `discovery failed: ${message}` });
4872
+ return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
3313
4873
  }
3314
4874
  }
3315
4875
  async function handleTestModel(req, res, id, cfg) {
@@ -3320,13 +4880,13 @@ async function handleTestModel(req, res, id, cfg) {
3320
4880
  const model = typeof body["model"] === "string" ? body["model"].trim() : "";
3321
4881
  if (!model) return writeJsonError(res, 400, "test requires a { model } string");
3322
4882
  if (row.apiFormat === "gemini") {
3323
- return writeJson2(res, 200, { ok: false, unsupportedFormat: true });
4883
+ return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
3324
4884
  }
3325
4885
  const resolvedKey = resolveEnvKey(row.apiKey);
3326
4886
  if (!resolvedKey) {
3327
- 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" });
3328
4888
  }
3329
- const url = row.baseUrl.replace(/\/+$/, "");
4889
+ let url = row.baseUrl.replace(/\/+$/, "");
3330
4890
  const prompt = "Reply with the single word: OK.";
3331
4891
  const headers = { "Content-Type": "application/json" };
3332
4892
  let payload;
@@ -3334,6 +4894,10 @@ async function handleTestModel(req, res, id, cfg) {
3334
4894
  headers["x-api-key"] = resolvedKey;
3335
4895
  headers["anthropic-version"] = "2023-06-01";
3336
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 };
3337
4901
  } else {
3338
4902
  headers["Authorization"] = `Bearer ${resolvedKey}`;
3339
4903
  payload = {
@@ -3345,7 +4909,7 @@ async function handleTestModel(req, res, id, cfg) {
3345
4909
  }
3346
4910
  const startedAt = Date.now();
3347
4911
  try {
3348
- const response = await fetchUpstream(
4912
+ const response = await fetchUpstream2(
3349
4913
  url,
3350
4914
  { method: "POST", headers, body: JSON.stringify(payload) },
3351
4915
  { providerId: "byo" }
@@ -3359,9 +4923,9 @@ async function handleTestModel(req, res, id, cfg) {
3359
4923
  message = parsed?.error?.message || parsed?.message || message;
3360
4924
  } catch {
3361
4925
  }
3362
- return writeJson2(res, 200, { ok: false, status: response.status, latencyMs, message });
4926
+ return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
3363
4927
  }
3364
- return writeJson2(res, 200, {
4928
+ return writeJson3(res, 200, {
3365
4929
  ok: true,
3366
4930
  status: response.status,
3367
4931
  latencyMs,
@@ -3369,7 +4933,7 @@ async function handleTestModel(req, res, id, cfg) {
3369
4933
  });
3370
4934
  } catch (err5) {
3371
4935
  const message = err5 instanceof Error ? err5.message : String(err5);
3372
- return writeJson2(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
4936
+ return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
3373
4937
  }
3374
4938
  }
3375
4939
  function extractSampleText(text, apiFormat) {
@@ -3410,7 +4974,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
3410
4974
  const row = cfg.providers.find((p) => p.id === id);
3411
4975
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
3412
4976
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3413
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
4977
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3414
4978
  }
3415
4979
  function parsePoolKeyInput(body, existing) {
3416
4980
  const out = {};
@@ -3441,7 +5005,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
3441
5005
  row.apiKeys = [...row.apiKeys ?? [], entry];
3442
5006
  persistProviders(cfg, deps);
3443
5007
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3444
- return writeJson2(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
5008
+ return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
3445
5009
  }
3446
5010
  async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
3447
5011
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3461,7 +5025,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
3461
5025
  row.apiKeys[keyIdx] = entry;
3462
5026
  persistProviders(cfg, deps);
3463
5027
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3464
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5028
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3465
5029
  }
3466
5030
  async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
3467
5031
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3475,7 +5039,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
3475
5039
  if (row.apiKeys.length === 0) row.apiKeys = void 0;
3476
5040
  persistProviders(cfg, deps);
3477
5041
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3478
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5042
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3479
5043
  }
3480
5044
  async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
3481
5045
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3489,7 +5053,7 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
3489
5053
  row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
3490
5054
  persistProviders(cfg, deps);
3491
5055
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3492
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
5056
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3493
5057
  }
3494
5058
  function parseApiKeysInput(raw, existing) {
3495
5059
  if (!Array.isArray(raw)) return existing;
@@ -3607,7 +5171,9 @@ function parseProviderInput(body, existing) {
3607
5171
  const baseUrl = body["baseUrl"];
3608
5172
  if (!id) return null;
3609
5173
  const name = typeof body["name"] === "string" && body["name"].length > 0 ? body["name"] : body["name"] === null ? void 0 : existing?.name;
3610
- if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini") return null;
5174
+ if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini" && apiFormat !== "openai-response") {
5175
+ return null;
5176
+ }
3611
5177
  if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
3612
5178
  const rawKey = body["apiKey"];
3613
5179
  let apiKey = typeof rawKey === "string" && rawKey.length > 0 ? rawKey : existing?.apiKey ?? "";
@@ -3629,10 +5195,11 @@ function parseProviderInput(body, existing) {
3629
5195
  apiKey = mode.apiKey;
3630
5196
  }
3631
5197
  }
5198
+ const migrated = migrateFormatAxis(apiFormat, transformer);
3632
5199
  return {
3633
5200
  id,
3634
5201
  name,
3635
- apiFormat,
5202
+ apiFormat: migrated.apiFormat,
3636
5203
  baseUrl: baseUrl.trim(),
3637
5204
  apiKey,
3638
5205
  models,
@@ -3643,7 +5210,7 @@ function parseProviderInput(body, existing) {
3643
5210
  apiVersion,
3644
5211
  maxConcurrency,
3645
5212
  modelsEndpoint,
3646
- transformer,
5213
+ transformer: migrated.transformer,
3647
5214
  codingPlan,
3648
5215
  apiModes,
3649
5216
  selectedApiModeId
@@ -3660,13 +5227,13 @@ function handlePresets(res, method) {
3660
5227
  baseUrl: p.baseUrl,
3661
5228
  models: p.models
3662
5229
  }));
3663
- return writeJson2(res, 200, { presets, excluded });
5230
+ return writeJson3(res, 200, { presets, excluded });
3664
5231
  }
3665
5232
  async function handleKeys(req, res, method, rest, deps) {
3666
5233
  if (method === "GET" && rest.length === 0) {
3667
5234
  const rows = await deps.keyDb.outboundApiKeysList();
3668
5235
  const reader = deps.keySpendReader;
3669
- if (!reader) return writeJson2(res, 200, { keys: rows.map(toKeyInfo) });
5236
+ if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
3670
5237
  const now = Date.now();
3671
5238
  const keys = await Promise.all(
3672
5239
  rows.map(async (row) => {
@@ -3678,13 +5245,13 @@ async function handleKeys(req, res, method, rest, deps) {
3678
5245
  return info;
3679
5246
  })
3680
5247
  );
3681
- return writeJson2(res, 200, { keys });
5248
+ return writeJson3(res, 200, { keys });
3682
5249
  }
3683
5250
  if (method === "POST" && rest.length === 0) {
3684
5251
  const body = await readJsonBody3(req);
3685
5252
  const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
3686
5253
  const created = await createNamedKey2(deps.keyDb, name);
3687
- return writeJson2(res, 201, {
5254
+ return writeJson3(res, 201, {
3688
5255
  id: created.id,
3689
5256
  name: created.name,
3690
5257
  keyPrefix: created.keyPrefix,
@@ -3696,13 +5263,13 @@ async function handleKeys(req, res, method, rest, deps) {
3696
5263
  const action = rest[1];
3697
5264
  if (method === "POST" && id && action === "revoke") {
3698
5265
  const ok = await deps.keyDb.outboundApiKeysRevoke(id);
3699
- return writeJson2(res, ok ? 200 : 404, { ok });
5266
+ return writeJson3(res, ok ? 200 : 404, { ok });
3700
5267
  }
3701
5268
  if (method === "POST" && id && action === "enabled") {
3702
5269
  const body = await readJsonBody3(req);
3703
5270
  const enabled = body["enabled"] === true;
3704
5271
  const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
3705
- return writeJson2(res, ok ? 200 : 404, { ok, enabled });
5272
+ return writeJson3(res, ok ? 200 : 404, { ok, enabled });
3706
5273
  }
3707
5274
  if (method === "POST" && id && action === "max-concurrency") {
3708
5275
  const body = await readJsonBody3(req);
@@ -3720,14 +5287,14 @@ async function handleKeys(req, res, method, rest, deps) {
3720
5287
  );
3721
5288
  }
3722
5289
  const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
3723
- return writeJson2(res, ok ? 200 : 404, { ok, maxConcurrency: value });
5290
+ return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
3724
5291
  }
3725
5292
  if (method === "POST" && id && action === "policy") {
3726
5293
  const body = await readJsonBody3(req);
3727
5294
  const parsed = parseKeyPolicyBody(body);
3728
5295
  if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
3729
5296
  const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
3730
- return writeJson2(res, ok ? 200 : 404, { ok });
5297
+ return writeJson3(res, ok ? 200 : 404, { ok });
3731
5298
  }
3732
5299
  return writeJsonError(res, 405, `method ${method} not allowed on keys`);
3733
5300
  }
@@ -3738,10 +5305,10 @@ function validateQueueSegments(patch) {
3738
5305
  errors.push(`${label} must be a number ${min}..${max}`);
3739
5306
  }
3740
5307
  };
3741
- const isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5308
+ const isPlainObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3742
5309
  const umq = patch.userMessageQueue;
3743
5310
  if (umq !== void 0) {
3744
- if (!isPlainObject4(umq)) {
5311
+ if (!isPlainObject5(umq)) {
3745
5312
  errors.push("userMessageQueue must be an object");
3746
5313
  } else {
3747
5314
  if (typeof umq.enabled !== "boolean") {
@@ -3753,7 +5320,7 @@ function validateQueueSegments(patch) {
3753
5320
  }
3754
5321
  const cq = patch.concurrencyQueue;
3755
5322
  if (cq !== void 0) {
3756
- if (!isPlainObject4(cq)) {
5323
+ if (!isPlainObject5(cq)) {
3757
5324
  errors.push("concurrencyQueue must be an object");
3758
5325
  } else {
3759
5326
  checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
@@ -3763,7 +5330,7 @@ function validateQueueSegments(patch) {
3763
5330
  }
3764
5331
  const ah = patch.accountHealth;
3765
5332
  if (ah !== void 0) {
3766
- if (!isPlainObject4(ah)) {
5333
+ if (!isPlainObject5(ah)) {
3767
5334
  errors.push("accountHealth must be an object");
3768
5335
  } else {
3769
5336
  if (typeof ah.overloadCooldownEnabled !== "boolean") {
@@ -3774,6 +5341,31 @@ function validateQueueSegments(patch) {
3774
5341
  }
3775
5342
  return errors;
3776
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
+ }
3777
5369
  async function handleServer(req, res, method, deps) {
3778
5370
  if (method === "GET") {
3779
5371
  const config = await loadServerConfig2(deps.settingsStore);
@@ -3781,7 +5373,7 @@ async function handleServer(req, res, method, deps) {
3781
5373
  if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
3782
5374
  if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
3783
5375
  if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
3784
- return writeJson2(res, 200, { server });
5376
+ return writeJson3(res, 200, { server });
3785
5377
  }
3786
5378
  if (method === "PUT") {
3787
5379
  const patch = await readJsonBody3(req);
@@ -3789,6 +5381,18 @@ async function handleServer(req, res, method, deps) {
3789
5381
  if (queueErrors.length > 0) {
3790
5382
  return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
3791
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
+ }
3792
5396
  const webhookErrors = validateWebhookSegment(patch);
3793
5397
  if (webhookErrors.length > 0) {
3794
5398
  return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
@@ -3815,66 +5419,113 @@ async function handleServer(req, res, method, deps) {
3815
5419
  const merged = mergeServerConfig(current, effectivePatch);
3816
5420
  await saveServerConfig(deps.settingsStore, merged);
3817
5421
  setServerProxyConfig(merged.proxy);
5422
+ getSharedAccountAllowanceScheduling2().configure(merged.allowanceScheduling);
5423
+ deps.allowanceRefreshScheduler?.configure(merged.allowanceScheduling);
3818
5424
  applyWebhookConfig(merged.webhook);
3819
5425
  applyAuditConfig(merged.audit);
3820
5426
  applyBillingConfig(merged.billing);
3821
- if (merged.enabled) {
3822
- const missing = validateServerModelConfig(merged);
3823
- if (missing.length > 0) {
3824
- if (deps.outboundApiServer.getStatus().running) {
3825
- await deps.outboundApiServer.stop();
3826
- }
3827
- return writeJson2(res, 200, {
3828
- server: merged,
3829
- error: { code: "incomplete-model-config", missing }
3830
- });
3831
- }
3832
- }
3833
- try {
3834
- await deps.outboundApiServer.applyConfig({
3835
- enabled: merged.enabled,
3836
- networkBinding: merged.networkBinding,
3837
- endpoints: merged.endpoints,
3838
- port: merged.port,
3839
- userMessageQueue: merged.userMessageQueue,
3840
- concurrencyQueue: merged.concurrencyQueue,
3841
- // voucher-redemption #9: hot-apply the voucher flag so enabling the product
3842
- // takes effect without a restart.
3843
- voucher: merged.voucher
3844
- });
3845
- } catch (err5) {
3846
- const missing = incompleteConfigMissing(err5);
3847
- if (missing) {
3848
- return writeJson2(res, 200, {
3849
- server: merged,
3850
- error: { code: "incomplete-model-config", missing }
3851
- });
3852
- }
3853
- throw err5;
3854
- }
3855
- 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 });
3856
5440
  }
3857
5441
  return writeJsonError(res, 405, `method ${method} not allowed on server`);
3858
5442
  }
3859
- function incompleteConfigMissing(err5) {
3860
- if (typeof err5 !== "object" || err5 === null) return null;
3861
- const missing = err5.missing;
3862
- return Array.isArray(missing) ? missing : null;
3863
- }
3864
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
+ }
3865
5453
  if (method === "GET" && rest.length === 0) {
3866
5454
  const accounts = await deps.subscriptionAccounts.listAll();
3867
5455
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
3868
5456
  const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
3869
- 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 });
3870
5477
  }
3871
5478
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
3872
5479
  const result = handleCodexOAuthStatus(rest[2], deps);
3873
- return writeJson2(res, result.status, result.body);
5480
+ return writeJson3(res, result.status, result.body);
3874
5481
  }
3875
5482
  if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
3876
5483
  const result = handleCodexOAuthCancel(rest[2], deps);
3877
- 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 });
3878
5529
  }
3879
5530
  if (method === "PUT" || method === "POST" || method === "DELETE") {
3880
5531
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -3883,12 +5534,12 @@ async function handleAccounts(req, res, method, rest, deps) {
3883
5534
  }
3884
5535
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
3885
5536
  const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
3886
- return writeJson2(res, result.status, result.body);
5537
+ return writeJson3(res, result.status, result.body);
3887
5538
  }
3888
5539
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
3889
5540
  const body2 = await readJsonBody3(req);
3890
5541
  const result = await handleOAuthComplete(providerId, body2, deps);
3891
- return writeJson2(res, result.status, result.body);
5542
+ return writeJson3(res, result.status, result.body);
3892
5543
  }
3893
5544
  if (method === "POST" && rest[1] === "accounts") {
3894
5545
  const body2 = await readJsonBody3(req);
@@ -3899,7 +5550,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3899
5550
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
3900
5551
  await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
3901
5552
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3902
- return writeJson2(res, 200, status2 ? { account: status2 } : { ok: true });
5553
+ return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
3903
5554
  }
3904
5555
  if (method === "POST" && rest[1] === "import-external") {
3905
5556
  if (providerId !== "claude" && providerId !== "codex") {
@@ -3912,7 +5563,13 @@ async function handleAccounts(req, res, method, rest, deps) {
3912
5563
  return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
3913
5564
  }
3914
5565
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3915
- 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
+ });
3916
5573
  }
3917
5574
  if (method === "POST" && rest[1] === "refresh") {
3918
5575
  if (providerId === "opencodego") {
@@ -3921,7 +5578,17 @@ async function handleAccounts(req, res, method, rest, deps) {
3921
5578
  const writer2 = deps.subscriptionTokenWriter;
3922
5579
  const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
3923
5580
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3924
- 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 });
3925
5592
  }
3926
5593
  if (method === "POST" && rest[2] === "label") {
3927
5594
  const accountId = rest[1];
@@ -3929,7 +5596,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3929
5596
  const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
3930
5597
  const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
3931
5598
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3932
- return writeJson2(res, 200, { ok: true });
5599
+ return writeJson3(res, 200, { ok: true });
3933
5600
  }
3934
5601
  if (method === "POST" && rest[2] === "priority") {
3935
5602
  const accountId = rest[1];
@@ -3941,7 +5608,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3941
5608
  }
3942
5609
  const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
3943
5610
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3944
- return writeJson2(res, 200, { ok: true });
5611
+ return writeJson3(res, 200, { ok: true });
3945
5612
  }
3946
5613
  if (method === "POST" && rest[2] === "proxy") {
3947
5614
  const accountId = rest[1];
@@ -3954,7 +5621,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3954
5621
  }
3955
5622
  const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
3956
5623
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3957
- return writeJson2(res, 200, { ok: true });
5624
+ return writeJson3(res, 200, { ok: true });
3958
5625
  }
3959
5626
  if (method === "POST" && rest[2] === "supported-models") {
3960
5627
  const accountId = rest[1];
@@ -3963,7 +5630,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3963
5630
  if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
3964
5631
  const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
3965
5632
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3966
- return writeJson2(res, 200, { ok: true });
5633
+ return writeJson3(res, 200, { ok: true });
3967
5634
  }
3968
5635
  if (method === "PUT" && rest[1] === "active") {
3969
5636
  const body2 = await readJsonBody3(req);
@@ -3971,17 +5638,22 @@ async function handleAccounts(req, res, method, rest, deps) {
3971
5638
  if (!id) return writeJsonError(res, 400, "active switch requires { id }");
3972
5639
  const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
3973
5640
  if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
3974
- return writeJson2(res, 200, { ok: true });
5641
+ return writeJson3(res, 200, { ok: true });
3975
5642
  }
3976
- if (method === "DELETE" && rest.length >= 2) {
5643
+ if (method === "DELETE" && rest.length === 2) {
3977
5644
  const accountId = rest[1];
3978
5645
  const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
3979
5646
  if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
3980
- return writeJson2(res, 200, { ok: true });
5647
+ deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
5648
+ return writeJson3(res, 200, { ok: true });
3981
5649
  }
3982
- if (method === "DELETE") {
5650
+ if (method === "DELETE" && rest.length === 1) {
3983
5651
  await deps.subscriptionTokenWriter.clearProvider(providerId);
3984
- 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");
3985
5657
  }
3986
5658
  const body = await readJsonBody3(req);
3987
5659
  const config = validateTokenBody(providerId, body);
@@ -3990,22 +5662,22 @@ async function handleAccounts(req, res, method, rest, deps) {
3990
5662
  }
3991
5663
  await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
3992
5664
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
3993
- return writeJson2(res, 200, status ? { account: status } : { ok: true });
5665
+ return writeJson3(res, 200, status ? { account: status } : { ok: true });
3994
5666
  }
3995
5667
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
3996
5668
  }
3997
5669
  async function handleCli(req, res, method, rest, deps) {
3998
5670
  if (method === "GET" && rest.length === 0) {
3999
5671
  const result = handleCliList(process.platform, deps.cliPathProbe);
4000
- return writeJson2(res, result.status, result.body);
5672
+ return writeJson3(res, result.status, result.body);
4001
5673
  }
4002
5674
  if (method === "GET" && rest[0] === "sessions") {
4003
5675
  const result = handleCliSessions();
4004
- return writeJson2(res, result.status, result.body);
5676
+ return writeJson3(res, result.status, result.body);
4005
5677
  }
4006
5678
  if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
4007
5679
  const result = handleCliStop(rest[1]);
4008
- return writeJson2(res, result.status, result.body);
5680
+ return writeJson3(res, result.status, result.body);
4009
5681
  }
4010
5682
  if (method === "POST" && rest[1] === "install") {
4011
5683
  const cli = rest[0];
@@ -4013,7 +5685,7 @@ async function handleCli(req, res, method, rest, deps) {
4013
5685
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
4014
5686
  }
4015
5687
  const result = await handleCliInstall(cli, deps.cliCommandRunner);
4016
- return writeJson2(res, result.status, result.body);
5688
+ return writeJson3(res, result.status, result.body);
4017
5689
  }
4018
5690
  if (method === "POST" && rest[1] === "launch") {
4019
5691
  const cli = rest[0];
@@ -4028,28 +5700,100 @@ async function handleCli(req, res, method, rest, deps) {
4028
5700
  opener: deps.cliTerminalOpener,
4029
5701
  probe: deps.cliPathProbe
4030
5702
  });
4031
- return writeJson2(res, result.status, result.body);
5703
+ return writeJson3(res, result.status, result.body);
4032
5704
  }
4033
5705
  return writeJsonError(res, 405, `method ${method} not allowed on cli`);
4034
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
+ }
4035
5763
  async function handleStatus(res, method, deps) {
4036
5764
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
4037
5765
  const status = deps.outboundApiServer.getStatus();
4038
5766
  const serverConfig = await loadServerConfig2(deps.settingsStore);
4039
- const endpoints = serverConfig.endpoints.map((e) => {
4040
- if (isKindMappedEndpoint(e.endpoint)) {
4041
- 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 };
4042
5778
  }
4043
- if (e.endpoint === "chat") {
4044
- 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
+ };
4045
5785
  }
4046
- 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
+ };
4047
5791
  });
4048
5792
  if (status.running) {
4049
5793
  const queueStatus = deps.outboundApiServer.getQueueStatus();
4050
- return writeJson2(res, 200, { ...status, endpoints, queueStatus });
5794
+ return writeJson3(res, 200, { ...status, endpoints, queueStatus });
4051
5795
  }
4052
- return writeJson2(res, 200, { ...status, endpoints });
5796
+ return writeJson3(res, 200, { ...status, endpoints });
4053
5797
  }
4054
5798
  function resolvePlaygroundPath(endpoint, body) {
4055
5799
  switch (endpoint) {
@@ -4075,16 +5819,16 @@ async function handlePlayground(req, res, method, deps) {
4075
5819
  const payload = body["body"];
4076
5820
  const status = deps.outboundApiServer.getStatus();
4077
5821
  if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
4078
- const path2 = resolvePlaygroundPath(endpoint, isRecord(payload) ? payload : {});
5822
+ const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
4079
5823
  if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
4080
5824
  const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
4081
5825
  await proxyToOutbound(res, status.port, path2, key, upstreamBody);
4082
5826
  }
4083
- function isRecord(v) {
5827
+ function isRecord2(v) {
4084
5828
  return !!v && typeof v === "object" && !Array.isArray(v);
4085
5829
  }
4086
5830
  function proxyToOutbound(res, outboundPort, path2, key, body) {
4087
- return new Promise((resolve) => {
5831
+ return new Promise((resolve3) => {
4088
5832
  const upstream = http.request(
4089
5833
  {
4090
5834
  host: "127.0.0.1",
@@ -4105,14 +5849,14 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
4105
5849
  proxRes.on("data", (chunk) => res.write(chunk));
4106
5850
  proxRes.on("end", () => {
4107
5851
  res.end();
4108
- resolve();
5852
+ resolve3();
4109
5853
  });
4110
5854
  }
4111
5855
  );
4112
5856
  upstream.on("error", (err5) => {
4113
5857
  if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
4114
5858
  else res.end();
4115
- resolve();
5859
+ resolve3();
4116
5860
  });
4117
5861
  upstream.write(body);
4118
5862
  upstream.end();
@@ -4120,7 +5864,7 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
4120
5864
  }
4121
5865
 
4122
5866
  // src/admin/uiStatic.ts
4123
- import { existsSync as existsSync4, statSync } from "fs";
5867
+ import { existsSync as existsSync7, statSync as statSync2 } from "fs";
4124
5868
  import { readFile } from "fs/promises";
4125
5869
  import { createRequire } from "module";
4126
5870
  import path from "path";
@@ -4143,13 +5887,13 @@ var CONTENT_TYPES = {
4143
5887
  function resolveUiDist() {
4144
5888
  const fromEnv = process.env["OMNICROSS_UI_DIST"];
4145
5889
  if (fromEnv) {
4146
- return existsSync4(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
5890
+ return existsSync7(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
4147
5891
  }
4148
5892
  try {
4149
5893
  const req = createRequire(typeof __filename !== "undefined" ? __filename : import.meta.url);
4150
5894
  const pkgJson = req.resolve("@omnicross/ui/package.json");
4151
5895
  const dist = path.join(path.dirname(pkgJson), "dist");
4152
- return existsSync4(path.join(dist, "index.html")) ? dist : null;
5896
+ return existsSync7(path.join(dist, "index.html")) ? dist : null;
4153
5897
  } catch {
4154
5898
  return null;
4155
5899
  }
@@ -4198,7 +5942,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
4198
5942
  return true;
4199
5943
  }
4200
5944
  let target = filePath;
4201
- if (!existsSync4(target) || statSync(target).isDirectory()) {
5945
+ if (!existsSync7(target) || statSync2(target).isDirectory()) {
4202
5946
  if (path.extname(rel) === "") {
4203
5947
  target = path.join(uiDist, "index.html");
4204
5948
  } else {
@@ -4215,7 +5959,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
4215
5959
  }
4216
5960
 
4217
5961
  // src/admin/version.ts
4218
- var DAEMON_VERSION = true ? "0.1.5" : "0.0.0-dev";
5962
+ var DAEMON_VERSION = true ? "0.1.6" : "0.0.0-dev";
4219
5963
 
4220
5964
  // src/admin/AdminServer.ts
4221
5965
  var LOOPBACK_ADDR = "127.0.0.1";
@@ -4254,14 +5998,14 @@ var AdminServer = class {
4254
5998
  }
4255
5999
  /** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
4256
6000
  listen(bindAddr, port) {
4257
- return new Promise((resolve, reject) => {
6001
+ return new Promise((resolve3, reject) => {
4258
6002
  const server = http2.createServer((req, res) => {
4259
6003
  this.onRequest(req, res);
4260
6004
  });
4261
6005
  const onError = (err5) => {
4262
6006
  if (err5.code === "EADDRINUSE" && port !== 0) {
4263
6007
  server.removeListener("error", onError);
4264
- this.listen(bindAddr, 0).then(resolve, reject);
6008
+ this.listen(bindAddr, 0).then(resolve3, reject);
4265
6009
  return;
4266
6010
  }
4267
6011
  reject(err5);
@@ -4273,7 +6017,7 @@ var AdminServer = class {
4273
6017
  server.removeListener("error", onError);
4274
6018
  server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
4275
6019
  this.server = server;
4276
- resolve(addr.port);
6020
+ resolve3(addr.port);
4277
6021
  } else {
4278
6022
  reject(new Error("Failed to get admin server address"));
4279
6023
  }
@@ -4354,8 +6098,8 @@ var AdminServer = class {
4354
6098
  if (!server) return;
4355
6099
  this.server = null;
4356
6100
  this.boundPort = 0;
4357
- return new Promise((resolve) => {
4358
- server.close(() => resolve());
6101
+ return new Promise((resolve3) => {
6102
+ server.close(() => resolve3());
4359
6103
  });
4360
6104
  }
4361
6105
  /** A live status snapshot. */
@@ -4471,7 +6215,7 @@ function pageHtml(message) {
4471
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>`;
4472
6216
  }
4473
6217
  function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
4474
- return new Promise((resolve, reject) => {
6218
+ return new Promise((resolve3, reject) => {
4475
6219
  let settled = false;
4476
6220
  const finish = (server2, fn) => {
4477
6221
  if (settled) return;
@@ -4502,7 +6246,7 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal
4502
6246
  }
4503
6247
  res.writeHead(200, { "Content-Type": "text/html" });
4504
6248
  res.end(pageHtml("Login complete."));
4505
- finish(server, () => resolve(code));
6249
+ finish(server, () => resolve3(code));
4506
6250
  });
4507
6251
  const abort = () => finish(server, () => reject(new Error("login: cancelled")));
4508
6252
  if (signal?.aborted) {
@@ -4606,8 +6350,10 @@ var EMPTY_CHAIN = {
4606
6350
  modelTransformers: []
4607
6351
  };
4608
6352
  var FORMAT_TRANSFORMER = {
6353
+ openai: "openai",
4609
6354
  anthropic: "anthropic",
4610
- gemini: "gemini"
6355
+ gemini: "gemini",
6356
+ "openai-response": "openai-response"
4611
6357
  };
4612
6358
  var ConfigFileProviderConfigSource = class {
4613
6359
  providers = /* @__PURE__ */ new Map();
@@ -4676,7 +6422,7 @@ var ConfigFileProviderConfigSource = class {
4676
6422
  }
4677
6423
  async getMainTransformer(providerId) {
4678
6424
  const row = this.providers.get(providerId);
4679
- if (!row || row.apiFormat === "openai") return null;
6425
+ if (!row) return null;
4680
6426
  const name = FORMAT_TRANSFORMER[row.apiFormat];
4681
6427
  const instances = this.transformerService.resolveTransformerReferences([name]);
4682
6428
  return instances[0] ?? null;
@@ -4686,11 +6432,8 @@ var ConfigFileProviderConfigSource = class {
4686
6432
  if (!row) return EMPTY_CHAIN;
4687
6433
  const customRefs = row.transformer?.use ?? [];
4688
6434
  if (customRefs.length === 0) return EMPTY_CHAIN;
4689
- const formatName = row.apiFormat === "openai" ? void 0 : FORMAT_TRANSFORMER[row.apiFormat];
4690
- const effectiveRefs = formatName ? customRefs.filter((ref) => (typeof ref === "string" ? ref : ref[0]) !== formatName) : customRefs;
4691
- if (effectiveRefs.length === 0) return EMPTY_CHAIN;
4692
6435
  return {
4693
- providerTransformers: this.transformerService.resolveTransformerReferences(effectiveRefs),
6436
+ providerTransformers: this.transformerService.resolveTransformerReferences(customRefs),
4694
6437
  modelTransformers: []
4695
6438
  };
4696
6439
  }
@@ -4721,7 +6464,7 @@ function resolvePreferredApiKey(row) {
4721
6464
  }
4722
6465
  function toLLMProvider(row) {
4723
6466
  const apiFormat = row.apiFormat === "gemini" ? "google" : row.apiFormat;
4724
- const transformer = row.apiFormat === "openai" ? void 0 : { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
6467
+ const transformer = { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
4725
6468
  const allModels = row.models ?? [];
4726
6469
  const models = row.modelConfigs ? allModels.filter((id) => row.modelConfigs.find((c) => c.id === id)?.enabled !== false) : allModels;
4727
6470
  return {
@@ -4791,7 +6534,7 @@ var ConfigurableLogger = class {
4791
6534
  const stream = this.fileStream;
4792
6535
  this.fileStream = null;
4793
6536
  if (!stream) return Promise.resolve();
4794
- return new Promise((resolve) => stream.end(() => resolve()));
6537
+ return new Promise((resolve3) => stream.end(() => resolve3()));
4795
6538
  }
4796
6539
  emit(level, message, error, meta) {
4797
6540
  if (LEVEL_ORDER[level] > this.threshold) return;
@@ -4902,7 +6645,7 @@ function safeStringify(value) {
4902
6645
  }
4903
6646
 
4904
6647
  // src/ports/JsonApiServerSettingsStore.ts
4905
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
6648
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
4906
6649
  import { OUTBOUND_API_SERVER_CONFIG_KEY } from "@omnicross/core/outbound-api";
4907
6650
  var JsonApiServerSettingsStore = class {
4908
6651
  /**
@@ -4929,7 +6672,7 @@ var JsonApiServerSettingsStore = class {
4929
6672
  if (key !== OUTBOUND_API_SERVER_CONFIG_KEY) return;
4930
6673
  const file = this.readFile();
4931
6674
  file.server = this.encryptSecrets(value);
4932
- writeFileSync4(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
6675
+ writeFileSync6(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
4933
6676
  }
4934
6677
  /** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
4935
6678
  encryptSecrets(config) {
@@ -4952,7 +6695,7 @@ var JsonApiServerSettingsStore = class {
4952
6695
  /** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
4953
6696
  readFile() {
4954
6697
  try {
4955
- const raw = readFileSync5(this.configPath, "utf8");
6698
+ const raw = readFileSync8(this.configPath, "utf8");
4956
6699
  const parsed = JSON.parse(raw);
4957
6700
  if (parsed && typeof parsed === "object") return parsed;
4958
6701
  } catch {
@@ -4962,8 +6705,8 @@ var JsonApiServerSettingsStore = class {
4962
6705
  };
4963
6706
 
4964
6707
  // src/ports/JsonlUsageEventStore.ts
4965
- import { randomUUID as randomUUID3 } from "crypto";
4966
- 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";
4967
6710
  var JsonlUsageEventStore = class {
4968
6711
  constructor(eventsPath, isPriced) {
4969
6712
  this.eventsPath = eventsPath;
@@ -4975,7 +6718,7 @@ var JsonlUsageEventStore = class {
4975
6718
  async insert(input) {
4976
6719
  const row = {
4977
6720
  ...input,
4978
- id: randomUUID3(),
6721
+ id: randomUUID4(),
4979
6722
  ts: input.ts ?? Date.now()
4980
6723
  };
4981
6724
  appendFileSync(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
@@ -5073,15 +6816,15 @@ var JsonlUsageEventStore = class {
5073
6816
  * Used to lazily seed the outbound key-policy spend tracker (once per key). A
5074
6817
  * key with no attributed events yields all zeros.
5075
6818
  */
5076
- async getSpendByKey(query) {
6819
+ async getSpendByKey(query2) {
5077
6820
  let totalUsd = 0;
5078
6821
  let dailyUsd = 0;
5079
6822
  let weeklyUsd = 0;
5080
- for (const row of this.readRows({ startTs: 0, endTs: query.endTs })) {
5081
- 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;
5082
6825
  totalUsd += row.costUsd;
5083
- if (row.ts >= query.dayStartTs) dailyUsd += row.costUsd;
5084
- 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;
5085
6828
  }
5086
6829
  return { totalUsd, dailyUsd, weeklyUsd };
5087
6830
  }
@@ -5166,10 +6909,10 @@ var JsonlUsageEventStore = class {
5166
6909
  }
5167
6910
  /** Parse every line, skipping malformed/torn lines defensively. */
5168
6911
  readAllRows() {
5169
- if (!existsSync5(this.eventsPath)) return [];
6912
+ if (!existsSync8(this.eventsPath)) return [];
5170
6913
  let raw;
5171
6914
  try {
5172
- raw = readFileSync6(this.eventsPath, "utf8");
6915
+ raw = readFileSync9(this.eventsPath, "utf8");
5173
6916
  } catch {
5174
6917
  return [];
5175
6918
  }
@@ -5253,12 +6996,29 @@ function isUsageEventRecord(parsed) {
5253
6996
  }
5254
6997
 
5255
6998
  // src/ports/JsonPricingStore.ts
5256
- 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";
5257
7001
  var JsonPricingStore = class {
5258
7002
  constructor(pricingPath) {
5259
7003
  this.pricingPath = pricingPath;
5260
7004
  }
5261
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
+ }
5262
7022
  async getAll() {
5263
7023
  return this.readRows();
5264
7024
  }
@@ -5271,17 +7031,17 @@ var JsonPricingStore = class {
5271
7031
  */
5272
7032
  async upsert(input, asUserEdit) {
5273
7033
  const rows = this.readRows();
5274
- const entry = this.applyUpsert(rows, input, asUserEdit);
7034
+ const entry = this.applyUpsert(rows, input, asUserEdit, "litellm");
5275
7035
  this.writeRows(rows);
5276
7036
  return entry;
5277
7037
  }
5278
7038
  /**
5279
7039
  * Apply a batch fetched from a pricing source. Rows whose local copy is
5280
7040
  * user-edited are NOT applied — they come back as `{ current, incoming }`
5281
- * conflicts; everything else is upserted (source 'litellm'). ONE file write
5282
- * for the whole batch.
7041
+ * conflicts; everything else is upserted with the supplied automatic source.
7042
+ * ONE file write for the whole batch.
5283
7043
  */
5284
- async bulkApplyFromSource(entries) {
7044
+ async bulkApplyFromSource(entries, source = "litellm") {
5285
7045
  const rows = this.readRows();
5286
7046
  const applied = [];
5287
7047
  const conflicts = [];
@@ -5297,7 +7057,8 @@ var JsonPricingStore = class {
5297
7057
  rows,
5298
7058
  incoming,
5299
7059
  /* asUserEdit */
5300
- false
7060
+ false,
7061
+ source
5301
7062
  ));
5302
7063
  }
5303
7064
  if (applied.length > 0) this.writeRows(rows);
@@ -5320,7 +7081,8 @@ var JsonPricingStore = class {
5320
7081
  rows,
5321
7082
  r.incoming,
5322
7083
  /* asUserEdit */
5323
- false
7084
+ false,
7085
+ "litellm"
5324
7086
  );
5325
7087
  overwrittenCount += 1;
5326
7088
  }
@@ -5341,7 +7103,7 @@ var JsonPricingStore = class {
5341
7103
  return true;
5342
7104
  }
5343
7105
  /** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
5344
- applyUpsert(rows, input, asUserEdit) {
7106
+ applyUpsert(rows, input, asUserEdit, automaticSource) {
5345
7107
  const now = Date.now();
5346
7108
  const entry = {
5347
7109
  providerId: input.providerId,
@@ -5350,7 +7112,7 @@ var JsonPricingStore = class {
5350
7112
  outputPricePer1m: input.outputPricePer1m,
5351
7113
  cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
5352
7114
  cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
5353
- source: asUserEdit ? "user" : "litellm",
7115
+ source: asUserEdit ? "user" : automaticSource,
5354
7116
  userEdited: asUserEdit,
5355
7117
  editedAt: asUserEdit ? now : null,
5356
7118
  updatedAt: now
@@ -5364,21 +7126,142 @@ var JsonPricingStore = class {
5364
7126
  }
5365
7127
  /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
5366
7128
  readRows() {
5367
- if (!existsSync6(this.pricingPath)) return [];
7129
+ if (!existsSync9(this.pricingPath)) return [];
5368
7130
  try {
5369
- const parsed = JSON.parse(readFileSync7(this.pricingPath, "utf8"));
7131
+ const parsed = JSON.parse(readFileSync10(this.pricingPath, "utf8"));
5370
7132
  return Array.isArray(parsed) ? parsed : [];
5371
7133
  } catch {
5372
7134
  return [];
5373
7135
  }
5374
7136
  }
5375
7137
  writeRows(rows) {
5376
- 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);
5377
7257
  }
5378
7258
  };
7259
+ function finiteOrNull(value) {
7260
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
7261
+ }
5379
7262
 
5380
7263
  // src/ports/JsonVoucherDb.ts
5381
- 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";
5382
7265
  var JsonVoucherDb = class {
5383
7266
  constructor(vouchersPath) {
5384
7267
  this.vouchersPath = vouchersPath;
@@ -5456,25 +7339,26 @@ var JsonVoucherDb = class {
5456
7339
  }
5457
7340
  /** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
5458
7341
  readRows() {
5459
- if (!existsSync7(this.vouchersPath)) return [];
7342
+ if (!existsSync11(this.vouchersPath)) return [];
5460
7343
  try {
5461
- const parsed = JSON.parse(readFileSync8(this.vouchersPath, "utf8"));
7344
+ const parsed = JSON.parse(readFileSync12(this.vouchersPath, "utf8"));
5462
7345
  return Array.isArray(parsed) ? parsed : [];
5463
7346
  } catch {
5464
7347
  return [];
5465
7348
  }
5466
7349
  }
5467
7350
  writeRows(rows) {
5468
- writeFileSync6(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
7351
+ writeFileSync9(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
5469
7352
  }
5470
7353
  };
5471
7354
 
5472
7355
  // src/ports/JsonSubscriptionCredentialStore.ts
5473
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
5474
- import { dirname as dirname4 } from "path";
5475
- import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
5476
- import { fetchUpstream as fetchUpstream2 } from "@omnicross/core/pipeline/upstreamFetch";
5477
- 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";
5478
7362
  import {
5479
7363
  claudeOAuth as claudeOAuth2,
5480
7364
  codexOAuth as codexOAuth2,
@@ -5482,33 +7366,9 @@ import {
5482
7366
  } from "@omnicross/subscriptions";
5483
7367
 
5484
7368
  // src/ports/account-sync.ts
5485
- var IMPORT_EXPIRY_MARGIN_MS = 6e4;
5486
7369
  function viewOf(tokens) {
5487
7370
  return tokens;
5488
7371
  }
5489
- function decideExternalImport(captured, external, now = Date.now()) {
5490
- if (!external?.accessToken) return "no-credential";
5491
- const capturedRt = viewOf(captured).refreshToken;
5492
- const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
5493
- const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
5494
- return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
5495
- }
5496
- function buildImportedTokens(captured, external) {
5497
- const imported = {
5498
- ...captured,
5499
- accessToken: external.accessToken,
5500
- status: "authorized",
5501
- errorMessage: void 0,
5502
- syncWarning: void 0,
5503
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
5504
- };
5505
- if (external.refreshToken) imported.refreshToken = external.refreshToken;
5506
- if (external.expiresAt) imported.expiresAt = external.expiresAt;
5507
- else delete imported.expiresAt;
5508
- if (external.idToken) imported.idToken = external.idToken;
5509
- if (external.scopes) imported.scopes = external.scopes;
5510
- return imported;
5511
- }
5512
7372
  function buildTokensFromExternal(provider, external) {
5513
7373
  const base = {
5514
7374
  authMethod: "oauth",
@@ -5529,14 +7389,6 @@ function buildTokensFromExternal(provider, external) {
5529
7389
  if (external.idToken) tokens.idToken = external.idToken;
5530
7390
  return tokens;
5531
7391
  }
5532
- function isExternalDivergent(stored, external) {
5533
- if (!external?.accessToken || !external.refreshToken) return false;
5534
- const view = viewOf(stored);
5535
- if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
5536
- const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
5537
- const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
5538
- return !Number.isFinite(storedExp) || externalExp > storedExp;
5539
- }
5540
7392
  function findDuplicateCredentialIds(accounts) {
5541
7393
  const byCredential = /* @__PURE__ */ new Map();
5542
7394
  for (const account of accounts) {
@@ -5555,11 +7407,11 @@ function findDuplicateCredentialIds(accounts) {
5555
7407
  }
5556
7408
 
5557
7409
  // src/ports/external-cli-credentials.ts
5558
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
5559
- import { homedir as homedir2 } from "os";
5560
- import { join as join5 } from "path";
5561
- function externalStorePath(provider, home = homedir2()) {
5562
- return provider === "claude" ? join5(home, ".claude", ".credentials.json") : join5(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");
5563
7415
  }
5564
7416
  function decodeJwtExpiryMs(token) {
5565
7417
  try {
@@ -5606,12 +7458,12 @@ function parseCodexTokensEnvelope(raw) {
5606
7458
  }
5607
7459
  return parsed;
5608
7460
  }
5609
- function readExternalCliCredentials(provider, home = homedir2()) {
7461
+ function readExternalCliCredentials(provider, home = homedir3()) {
5610
7462
  const path2 = externalStorePath(provider, home);
5611
- if (!existsSync8(path2)) return null;
7463
+ if (!existsSync12(path2)) return null;
5612
7464
  let raw;
5613
7465
  try {
5614
- const parsed = JSON.parse(readFileSync9(path2, "utf8"));
7466
+ const parsed = JSON.parse(readFileSync13(path2, "utf8"));
5615
7467
  raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
5616
7468
  } catch {
5617
7469
  return null;
@@ -5619,84 +7471,6 @@ function readExternalCliCredentials(provider, home = homedir2()) {
5619
7471
  return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
5620
7472
  }
5621
7473
 
5622
- // src/ports/external-cli-store.ts
5623
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync10, renameSync, writeFileSync as writeFileSync7 } from "fs";
5624
- import { homedir as homedir3 } from "os";
5625
- import { dirname as dirname3 } from "path";
5626
- function markerPath(provider, home) {
5627
- return `${externalStorePath(provider, home)}.omnicross-managed`;
5628
- }
5629
- function backupPath(provider, home) {
5630
- return `${externalStorePath(provider, home)}.omnicross-backup`;
5631
- }
5632
- function buildClaudeOAuthEnvelope(tokens) {
5633
- if (!tokens.accessToken) return null;
5634
- const envelope = { accessToken: tokens.accessToken };
5635
- if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
5636
- if (tokens.expiresAt) {
5637
- const ms = Date.parse(tokens.expiresAt);
5638
- if (Number.isFinite(ms)) envelope.expiresAt = ms;
5639
- }
5640
- if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
5641
- return envelope;
5642
- }
5643
- function buildCodexTokensEnvelope(tokens) {
5644
- if (!tokens.accessToken && !tokens.idToken) return null;
5645
- const envelope = { access_token: tokens.accessToken ?? "" };
5646
- if (tokens.idToken) envelope.id_token = tokens.idToken;
5647
- if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
5648
- return envelope;
5649
- }
5650
- function readExistingObject(path2) {
5651
- if (!existsSync9(path2)) return {};
5652
- try {
5653
- const parsed = JSON.parse(readFileSync10(path2, "utf8"));
5654
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5655
- } catch {
5656
- return {};
5657
- }
5658
- }
5659
- function writeAtomic(path2, content) {
5660
- mkdirSync2(dirname3(path2), { recursive: true });
5661
- const temp = `${path2}.omnicross-tmp`;
5662
- writeFileSync7(temp, content, "utf8");
5663
- renameSync(temp, path2);
5664
- }
5665
- function createExternalCliStore(home = homedir3()) {
5666
- return {
5667
- readMarkerAccountId(provider) {
5668
- const path2 = markerPath(provider, home);
5669
- if (!existsSync9(path2)) return void 0;
5670
- try {
5671
- const parsed = JSON.parse(readFileSync10(path2, "utf8"));
5672
- return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
5673
- } catch {
5674
- return void 0;
5675
- }
5676
- },
5677
- writeMarker(provider, accountId) {
5678
- writeAtomic(
5679
- markerPath(provider, home),
5680
- JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
5681
- );
5682
- },
5683
- writeBack(provider, accountId, tokens) {
5684
- const owner = this.readMarkerAccountId(provider);
5685
- if (owner !== accountId) return false;
5686
- const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
5687
- if (!envelope) return false;
5688
- const storePath = externalStorePath(provider, home);
5689
- if (existsSync9(storePath) && !existsSync9(backupPath(provider, home))) {
5690
- copyFileSync(storePath, backupPath(provider, home));
5691
- }
5692
- const existing = readExistingObject(storePath);
5693
- const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
5694
- writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
5695
- return true;
5696
- }
5697
- };
5698
- }
5699
-
5700
7474
  // src/ports/JsonSubscriptionCredentialStore.ts
5701
7475
  var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
5702
7476
  var JsonSubscriptionCredentialStore = class {
@@ -5709,32 +7483,30 @@ var JsonSubscriptionCredentialStore = class {
5709
7483
  * proxy-aware {@link fetchUpstream} that threads the
5710
7484
  * `{ providerId, accountId }` ctx (upstream-proxy M1) so a
5711
7485
  * per-account/per-provider proxy is honored on refresh exactly
5712
- * 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
5713
7487
  * account's traffic. NOT used by any read/write path.
5714
7488
  */
5715
- constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
7489
+ constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials) {
5716
7490
  this.tokensPath = tokensPath;
5717
7491
  this.box = box;
5718
7492
  this.fetchImpl = fetchImpl;
5719
7493
  this.externalCliReader = externalCliReader;
5720
- this.externalCliStore = externalCliStore;
5721
7494
  }
5722
7495
  tokensPath;
5723
7496
  box;
5724
7497
  fetchImpl;
5725
7498
  externalCliReader;
5726
- externalCliStore;
5727
7499
  /**
5728
7500
  * The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
5729
7501
  * TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
5730
7502
  * through {@link fetchUpstream} with the account's `{ providerId, accountId }`
5731
- * 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.
5732
7504
  */
5733
7505
  buildRefreshFetch(providerId, accountId) {
5734
- return this.fetchImpl ?? ((url, init) => fetchUpstream2(url, init, { providerId, accountId }));
7506
+ return this.fetchImpl ?? ((url, init) => fetchUpstream3(url, init, { providerId, accountId }));
5735
7507
  }
5736
7508
  /**
5737
- * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
7509
+ * In-flight refresh coalescing. OAuth refresh tokens are
5738
7510
  * SINGLE-USE: two concurrent refreshes of one account each spend the same
5739
7511
  * token and the loser bricks a healthy account. Every refresh entry point
5740
7512
  * (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
@@ -5749,13 +7521,13 @@ var JsonSubscriptionCredentialStore = class {
5749
7521
  return run;
5750
7522
  }
5751
7523
  /** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
5752
- * 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
5753
7525
  * strategies pull `accessToken` / `expiresAt` / `status` from it. */
5754
7526
  async getFullConfig() {
5755
7527
  return this.readConfig();
5756
7528
  }
5757
7529
  /** Current Claude OAuth access token, or `null` when none is stored. No inline
5758
- * 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
5759
7531
  * subscription auth strategy, which calls `refreshClaudeToken` (now real). */
5760
7532
  async getValidClaudeAccessToken() {
5761
7533
  return this.readConfig().claude?.accessToken ?? null;
@@ -5780,13 +7552,14 @@ var JsonSubscriptionCredentialStore = class {
5780
7552
  /**
5781
7553
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
5782
7554
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
5783
- * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
7555
+ * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
5784
7556
  * Used by the admin accounts GET (secret-IN-never-OUT).
5785
7557
  */
5786
7558
  async listSanitizedAccounts() {
5787
7559
  const config = this.readConfig();
5788
- const health2 = getSharedAccountHealth();
5789
- const identityStore = getSharedIdentityStore();
7560
+ const health2 = getSharedAccountHealth2();
7561
+ const allowanceScheduling = getSharedAccountAllowanceScheduling3();
7562
+ const identityStore = getSharedIdentityStore2();
5790
7563
  const fingerprintOn = identityStore.isEnabled();
5791
7564
  const now = Date.now();
5792
7565
  const out = {};
@@ -5795,7 +7568,13 @@ var JsonSubscriptionCredentialStore = class {
5795
7568
  if (sanitized.length === 0) continue;
5796
7569
  for (const account of sanitized) {
5797
7570
  const status = health2.getStatus(provider, account.id, now);
7571
+ const allowance = allowanceScheduling.preview(provider, account.id, account.priority ?? 50, now);
5798
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;
5799
7578
  account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
5800
7579
  if (fingerprintOn && provider === "claude") {
5801
7580
  account.identityCaptured = identityStore.hasIdentity(provider, account.id);
@@ -5803,31 +7582,25 @@ var JsonSubscriptionCredentialStore = class {
5803
7582
  account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
5804
7583
  }
5805
7584
  }
5806
- out[provider] = this.attachSyncWarnings(config, provider, sanitized);
7585
+ out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
5807
7586
  }
5808
7587
  return out;
5809
7588
  }
5810
7589
  /**
5811
- * List-time credential-conflict warnings (external-cli-sync). Computed, not
5812
- * persisted: (a) `duplicate-token` when two accounts of one provider share a
5813
- * credential, (b) `external-divergent` when the external CLI native store has
5814
- * rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
5815
- * a failed refresh (`external-not-rotated`) takes precedence — it is the most
5816
- * 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.
5817
7593
  */
5818
- attachSyncWarnings(config, provider, sanitized) {
7594
+ attachDuplicateWarnings(config, provider, sanitized) {
5819
7595
  const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
5820
- let divergentId;
5821
- if (provider === "claude" || provider === "codex") {
5822
- const active = getActiveAccount(config, provider);
5823
- if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
5824
- divergentId = active.id;
5825
- }
5826
- }
5827
- if (duplicates.size === 0 && !divergentId) return sanitized;
5828
7596
  return sanitized.map((account) => {
5829
- const computed = account.id === divergentId ? "external-divergent" : duplicates.has(account.id) ? "duplicate-token" : void 0;
5830
- 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 };
5831
7604
  });
5832
7605
  }
5833
7606
  /** Read the external CLI store, never letting an fs/parse error escape. */
@@ -5840,11 +7613,11 @@ var JsonSubscriptionCredentialStore = class {
5840
7613
  }
5841
7614
  /**
5842
7615
  * Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
5843
- * 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
5844
7617
  * block is untouched. Otherwise mint via the shared claude refresh flow and
5845
7618
  * write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
5846
- * On failure status:expired +
5847
- * errorMessage `false`.
7619
+ * On failure status:expired +
7620
+ * errorMessage `false`.
5848
7621
  */
5849
7622
  async refreshClaudeToken() {
5850
7623
  return this.coalesce("claude:active", async () => {
@@ -5869,19 +7642,8 @@ var JsonSubscriptionCredentialStore = class {
5869
7642
  syncWarning: void 0
5870
7643
  };
5871
7644
  this.writeBackById("claude", capturedId, next);
5872
- this.resyncExternal("claude", capturedId, next);
5873
7645
  return true;
5874
7646
  } catch (error) {
5875
- if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
5876
- const r = await claudeOAuth2.refreshAccessToken(rt, refreshFetch);
5877
- return {
5878
- accessToken: r.accessToken,
5879
- refreshToken: r.refreshToken,
5880
- expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
5881
- };
5882
- })) {
5883
- return true;
5884
- }
5885
7647
  this.markExpiredById("claude", capturedId, claude, error);
5886
7648
  return false;
5887
7649
  }
@@ -5916,20 +7678,8 @@ var JsonSubscriptionCredentialStore = class {
5916
7678
  syncWarning: void 0
5917
7679
  };
5918
7680
  this.writeBackById("codex", capturedId, next);
5919
- this.resyncExternal("codex", capturedId, next);
5920
7681
  return true;
5921
7682
  } catch (error) {
5922
- if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
5923
- const r = await codexOAuth2.refreshAccessToken(rt, refreshFetch);
5924
- return {
5925
- accessToken: r.accessToken,
5926
- refreshToken: r.refreshToken,
5927
- idToken: r.idToken,
5928
- expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
5929
- };
5930
- })) {
5931
- return true;
5932
- }
5933
7683
  this.markExpiredById("codex", capturedId, codex, error);
5934
7684
  return false;
5935
7685
  }
@@ -5972,11 +7722,10 @@ var JsonSubscriptionCredentialStore = class {
5972
7722
  });
5973
7723
  }
5974
7724
  /**
5975
- * Refresh a SPECIFIC account by id (background scheduler sweep,
5976
- * external-cli-sync). Unlike the active-account refreshers it does NOT
5977
- * attempt the external-import fallback the external CLI file's lineage can
5978
- * only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
5979
- * 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`.
5980
7729
  */
5981
7730
  async refreshAccountById(provider, id) {
5982
7731
  return this.coalesce(`${provider}:${id}`, async () => {
@@ -5990,7 +7739,7 @@ var JsonSubscriptionCredentialStore = class {
5990
7739
  const next = {
5991
7740
  ...captured,
5992
7741
  accessToken: refreshed.accessToken,
5993
- // 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.
5994
7743
  refreshToken: refreshed.refreshToken ?? captured.refreshToken,
5995
7744
  expiresAt: refreshed.expiresAt,
5996
7745
  status: "authorized",
@@ -6000,7 +7749,6 @@ var JsonSubscriptionCredentialStore = class {
6000
7749
  };
6001
7750
  if (refreshed.idToken) next.idToken = refreshed.idToken;
6002
7751
  this.writeBackById(provider, id, next);
6003
- if (provider !== "gemini") this.resyncExternal(provider, id, next);
6004
7752
  return true;
6005
7753
  } catch (error) {
6006
7754
  this.markExpiredById(provider, id, captured, error);
@@ -6008,7 +7756,7 @@ var JsonSubscriptionCredentialStore = class {
6008
7756
  }
6009
7757
  });
6010
7758
  }
6011
- // ── By-id account-pool surface (subscription-account-scheduling, design D6) ──
7759
+ // By-id account-pool surface (subscription-account-scheduling, design D6)
6012
7760
  /**
6013
7761
  * Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
6014
7762
  * provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
@@ -6041,7 +7789,7 @@ var JsonSubscriptionCredentialStore = class {
6041
7789
  /**
6042
7790
  * Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
6043
7791
  * `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
6044
- * `false` (no refresh affordance).
7792
+ * `false` (no refresh affordance).
6045
7793
  */
6046
7794
  async refreshAccountToken(providerId, accountId) {
6047
7795
  if (providerId === "opencodego") return false;
@@ -6063,7 +7811,7 @@ var JsonSubscriptionCredentialStore = class {
6063
7811
  * (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
6064
7812
  * whitelisted fingerprint headers; the token mirror is untouched); a no-op for
6065
7813
  * an unknown id. Called by the identity store's persistence port on a first-seen
6066
- * 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
6067
7815
  * store's port wrapper swallows a rejection so the relay hot path is unaffected.
6068
7816
  */
6069
7817
  async setAccountIdentity(providerId, accountId, identity) {
@@ -6088,7 +7836,7 @@ var JsonSubscriptionCredentialStore = class {
6088
7836
  * DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
6089
7837
  * the port). Passing `undefined` clears the override. Write-only password: when
6090
7838
  * the incoming structured proxy omits the password but the account already had
6091
- * one, the current (decrypted) password is preserved editing host/port never
7839
+ * one, the current (decrypted) password is preserved editing host/port never
6092
7840
  * wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
6093
7841
  */
6094
7842
  async setAccountProxy(providerId, accountId, proxy) {
@@ -6123,75 +7871,25 @@ var JsonSubscriptionCredentialStore = class {
6123
7871
  expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
6124
7872
  };
6125
7873
  }
6126
- /**
6127
- * External-import fallback for a FAILED active-account refresh
6128
- * (external-cli-sync). Reads the CLI native store; imports when the external
6129
- * lineage ROTATED (different refresh token) or its access token is still
6130
- * valid. When the imported access token is already expired it refreshes once
6131
- * with the rotated refresh token. A `not-rotated` outcome persists the
6132
- * `external-not-rotated` warning on the (about-to-be-expired) account so the
6133
- * UI can tell "genuine revocation" apart from a plain refresh failure.
6134
- */
6135
- async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
6136
- const markerOwner = this.safeReadMarker(provider);
6137
- if (markerOwner && markerOwner !== capturedId) return false;
6138
- const external = this.safeReadExternal(provider);
6139
- const decision = decideExternalImport(captured, external);
6140
- if (decision === "not-rotated") {
6141
- captured.syncWarning = "external-not-rotated";
6142
- return false;
6143
- }
6144
- if (decision !== "import" || !external) return false;
6145
- let imported = buildImportedTokens(
6146
- captured,
6147
- external
6148
- );
6149
- const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
6150
- if (!accessStillValid) {
6151
- try {
6152
- const refreshed = await refreshWithToken(external.refreshToken);
6153
- imported = {
6154
- ...imported,
6155
- accessToken: refreshed.accessToken,
6156
- refreshToken: refreshed.refreshToken ?? imported.refreshToken,
6157
- expiresAt: refreshed.expiresAt,
6158
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
6159
- };
6160
- if (refreshed.idToken) imported.idToken = refreshed.idToken;
6161
- } catch {
6162
- return false;
6163
- }
6164
- }
6165
- this.writeBackById(provider, capturedId, imported);
6166
- this.resyncExternal(provider, capturedId, imported);
6167
- return true;
6168
- }
6169
- /**
6170
- * Marker-gated external write-back (external-cli-sync). After a successful
6171
- * refresh of the account that OWNS the provider's native CLI store (imported
6172
- * via `importExternalCliAccount`), push the rotated credential back into the
6173
- * file — otherwise the daemon's refresh invalidates the single-use refresh
6174
- * token and silently logs the bare CLI out. NON-FATAL: the internal store is
6175
- * already persisted; a failed external write only leaves the file stale,
6176
- * which the `external-divergent` warning surfaces.
6177
- */
6178
- resyncExternal(provider, accountId, tokens) {
6179
- try {
6180
- this.externalCliStore.writeBack(provider, accountId, tokens);
6181
- } catch {
6182
- }
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;
6183
7881
  }
6184
- /** Read the marker's owning account id, never letting an fs error escape. */
6185
- safeReadMarker(provider) {
6186
- try {
6187
- return this.externalCliStore.readMarkerAccountId(provider);
6188
- } catch {
6189
- return void 0;
6190
- }
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;
6191
7889
  }
6192
7890
  /**
6193
7891
  * DAEMON-ONLY (admin import button): which providers have a usable external
6194
- * CLI credential on THIS machine. Pure detection reads the native files,
7892
+ * CLI credential on THIS machine. Pure detection reads the native files,
6195
7893
  * never mutates anything, never returns a token.
6196
7894
  */
6197
7895
  async listExternalCliAvailability() {
@@ -6202,21 +7900,22 @@ var JsonSubscriptionCredentialStore = class {
6202
7900
  }
6203
7901
  /**
6204
7902
  * DAEMON-ONLY (admin import button): import the external CLI's current login
6205
- * as a NEW account (+ activate), and take MANAGED ownership of the native
6206
- * store (marker) so subsequent refreshes write back keeping the bare CLI
6207
- * and the daemon on the same live credential instead of silently killing one
6208
- * 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.
6209
7907
  */
6210
7908
  async importExternalCliAccount(provider, label) {
6211
7909
  const external = this.safeReadExternal(provider);
6212
7910
  if (!external?.accessToken) return { ok: false, reason: "no-credential" };
6213
7911
  const tokens = buildTokensFromExternal(provider, external);
6214
7912
  const result = await this.appendProviderAccount(provider, tokens, label);
6215
- try {
6216
- this.externalCliStore.writeMarker(provider, result.id);
6217
- } catch {
6218
- }
6219
- return { ok: true, id: result.id };
7913
+ return {
7914
+ ok: true,
7915
+ id: result.id,
7916
+ nativeCredentialMode: "read-only",
7917
+ refreshWritesNativeCredentials: false
7918
+ };
6220
7919
  }
6221
7920
  /**
6222
7921
  * Materialize a lazily-synthesized account id to disk (design D3). On a legacy
@@ -6249,7 +7948,8 @@ var JsonSubscriptionCredentialStore = class {
6249
7948
  this.writeBackById(providerId, capturedId, {
6250
7949
  ...block,
6251
7950
  status: "expired",
6252
- errorMessage
7951
+ errorMessage,
7952
+ syncWarning: "syncWarning" in block && block.syncWarning === "duplicate-token" ? "duplicate-token" : void 0
6253
7953
  });
6254
7954
  }
6255
7955
  /**
@@ -6258,7 +7958,7 @@ var JsonSubscriptionCredentialStore = class {
6258
7958
  * `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
6259
7959
  * OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
6260
7960
  * tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
6261
- * 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
6262
7962
  * sees this write.
6263
7963
  */
6264
7964
  async writeProviderTokens(providerId, config) {
@@ -6268,7 +7968,7 @@ var JsonSubscriptionCredentialStore = class {
6268
7968
  }
6269
7969
  /**
6270
7970
  * DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
6271
- * (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
6272
7972
  * `omnicross login <provider> --label` to add an account instead of overwriting.
6273
7973
  */
6274
7974
  async appendProviderAccount(providerId, config, label) {
@@ -6302,7 +8002,7 @@ var JsonSubscriptionCredentialStore = class {
6302
8002
  }
6303
8003
  /**
6304
8004
  * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
6305
- * 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
6306
8006
  * (the secret-free invariant holds).
6307
8007
  */
6308
8008
  async renameAccount(providerId, id, label) {
@@ -6325,12 +8025,12 @@ var JsonSubscriptionCredentialStore = class {
6325
8025
  }
6326
8026
  /** Write the merged config to disk as pretty JSON (mkdir parent if needed).
6327
8027
  * Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
6328
- * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
6329
- * 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. */
6330
8030
  persist(config) {
6331
- mkdirSync3(dirname4(this.tokensPath), { recursive: true });
8031
+ mkdirSync4(dirname6(this.tokensPath), { recursive: true });
6332
8032
  const encrypted = encryptTokens(config, this.box);
6333
- writeFileSync8(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
8033
+ writeFileSync10(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
6334
8034
  }
6335
8035
  /**
6336
8036
  * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
@@ -6338,18 +8038,18 @@ var JsonSubscriptionCredentialStore = class {
6338
8038
  * subscription bearer path is byte-identical).
6339
8039
  *
6340
8040
  * The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
6341
- * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
8041
+ * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
6342
8042
  * wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
6343
- * box's clear, secret-free error (secrets spec "错误密钥 / 篡改的解密失败 UX":
6344
- * SHALL fail-fast, SHALL NOT 静默降级 — a swallowed decrypt would report "no
6345
- * 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
6346
8046
  * `config.ts loadConfig`, which decrypts outside its parse try.
6347
8047
  */
6348
8048
  readConfig() {
6349
- if (!existsSync10(this.tokensPath)) return { updatedAt: "" };
8049
+ if (!existsSync13(this.tokensPath)) return { updatedAt: "" };
6350
8050
  let parsed;
6351
8051
  try {
6352
- const raw = JSON.parse(readFileSync11(this.tokensPath, "utf8"));
8052
+ const raw = JSON.parse(readFileSync14(this.tokensPath, "utf8"));
6353
8053
  parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
6354
8054
  } catch {
6355
8055
  parsed = null;
@@ -6361,7 +8061,7 @@ var JsonSubscriptionCredentialStore = class {
6361
8061
  };
6362
8062
 
6363
8063
  // src/AccountHealthProbeScheduler.ts
6364
- import { fetchUpstream as fetchUpstream3 } from "@omnicross/core/pipeline/upstreamFetch";
8064
+ import { fetchUpstream as fetchUpstream4 } from "@omnicross/core/pipeline/upstreamFetch";
6365
8065
 
6366
8066
  // src/probe/ProbeStrategy.ts
6367
8067
  var PROVIDER_PROBE_PLANS = {
@@ -6404,7 +8104,7 @@ var AccountHealthProbeScheduler = class {
6404
8104
  this.logger = logger;
6405
8105
  this.config = config;
6406
8106
  this.now = opts.now ?? Date.now;
6407
- this.fetchImpl = opts.fetchImpl ?? fetchUpstream3;
8107
+ this.fetchImpl = opts.fetchImpl ?? fetchUpstream4;
6408
8108
  this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6409
8109
  this.planFor = opts.planFor ?? probePlanFor;
6410
8110
  }
@@ -6677,8 +8377,8 @@ var AccountHealthSweeper = class {
6677
8377
  };
6678
8378
 
6679
8379
  // src/audit/AuditPruneSweeper.ts
6680
- import { existsSync as existsSync11, readdirSync, unlinkSync } from "fs";
6681
- import { join as join6 } from "path";
8380
+ import { existsSync as existsSync14, readdirSync, unlinkSync as unlinkSync3 } from "fs";
8381
+ import { join as join7 } from "path";
6682
8382
 
6683
8383
  // src/audit/auditFiles.ts
6684
8384
  var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
@@ -6752,7 +8452,7 @@ var AuditPruneSweeper = class {
6752
8452
  if (!this.config.enabled || this.sweeping) return 0;
6753
8453
  this.sweeping = true;
6754
8454
  try {
6755
- if (!existsSync11(this.auditDir)) return 0;
8455
+ if (!existsSync14(this.auditDir)) return 0;
6756
8456
  const today = new Date(this.now());
6757
8457
  const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
6758
8458
  const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
@@ -6761,7 +8461,7 @@ var AuditPruneSweeper = class {
6761
8461
  const dateMs = auditFileDateMs(file);
6762
8462
  if (dateMs === null || dateMs >= cutoff) continue;
6763
8463
  try {
6764
- unlinkSync(join6(this.auditDir, file));
8464
+ unlinkSync3(join7(this.auditDir, file));
6765
8465
  removed += 1;
6766
8466
  } catch (error) {
6767
8467
  this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
@@ -6784,26 +8484,26 @@ var AuditPruneSweeper = class {
6784
8484
  };
6785
8485
 
6786
8486
  // src/audit/auditReader.ts
6787
- import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync12 } from "fs";
6788
- import { join as join7 } from "path";
8487
+ import { existsSync as existsSync15, readdirSync as readdirSync2, readFileSync as readFileSync15 } from "fs";
8488
+ import { join as join8 } from "path";
6789
8489
  var DEFAULT_LIMIT = 200;
6790
8490
  var MAX_LIMIT = 2e3;
6791
- function readAuditRecords(auditDir2, query = {}) {
6792
- if (!existsSync12(auditDir2)) return [];
8491
+ function readAuditRecords(auditDir2, query2 = {}) {
8492
+ if (!existsSync15(auditDir2)) return [];
6793
8493
  let files;
6794
8494
  try {
6795
8495
  files = readdirSync2(auditDir2).filter((f) => AUDIT_FILE_RE.test(f));
6796
8496
  } catch {
6797
8497
  return [];
6798
8498
  }
6799
- const from = typeof query.from === "number" ? query.from : -Infinity;
6800
- const to = typeof query.to === "number" ? query.to : Infinity;
6801
- 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)));
6802
8502
  const matched = [];
6803
8503
  for (const file of files.sort().reverse()) {
6804
8504
  let raw;
6805
8505
  try {
6806
- raw = readFileSync12(join7(auditDir2, file), "utf8");
8506
+ raw = readFileSync15(join8(auditDir2, file), "utf8");
6807
8507
  } catch {
6808
8508
  continue;
6809
8509
  }
@@ -6817,7 +8517,7 @@ function readAuditRecords(auditDir2, query = {}) {
6817
8517
  continue;
6818
8518
  }
6819
8519
  if (!isAuditRecord(rec)) continue;
6820
- if (query.keyId !== void 0 && rec.keyId !== query.keyId) continue;
8520
+ if (query2.keyId !== void 0 && rec.keyId !== query2.keyId) continue;
6821
8521
  if (rec.ts < from || rec.ts > to) continue;
6822
8522
  matched.push(rec);
6823
8523
  }
@@ -6832,8 +8532,8 @@ function isAuditRecord(value) {
6832
8532
  }
6833
8533
 
6834
8534
  // src/audit/AuditWriter.ts
6835
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "fs";
6836
- import { join as join8 } from "path";
8535
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
8536
+ import { join as join9 } from "path";
6837
8537
  var AuditWriter = class {
6838
8538
  constructor(auditDir2, logger, defer = (fn) => setTimeout(fn, 0)) {
6839
8539
  this.auditDir = auditDir2;
@@ -6866,19 +8566,19 @@ var AuditWriter = class {
6866
8566
  */
6867
8567
  appendNow(record) {
6868
8568
  if (!this.dirEnsured) {
6869
- mkdirSync4(this.auditDir, { recursive: true });
8569
+ mkdirSync5(this.auditDir, { recursive: true });
6870
8570
  this.dirEnsured = true;
6871
8571
  }
6872
- const file = join8(this.auditDir, auditFileName(record.ts));
8572
+ const file = join9(this.auditDir, auditFileName(record.ts));
6873
8573
  appendFileSync2(file, JSON.stringify(record) + "\n", "utf8");
6874
8574
  }
6875
8575
  };
6876
8576
 
6877
8577
  // src/billing/BillingPublisher.ts
6878
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync5 } from "fs";
8578
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync6 } from "fs";
6879
8579
  import { createHmac } from "crypto";
6880
- import { join as join9 } from "path";
6881
- 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";
6882
8582
 
6883
8583
  // src/billing/billingFiles.ts
6884
8584
  var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
@@ -6901,7 +8601,7 @@ var BillingPublisher = class {
6901
8601
  constructor(billingDir, logger, opts = {}) {
6902
8602
  this.billingDir = billingDir;
6903
8603
  this.logger = logger;
6904
- this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream4(url, init));
8604
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
6905
8605
  this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
6906
8606
  this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
6907
8607
  this.now = opts.now ?? Date.now;
@@ -6948,7 +8648,7 @@ var BillingPublisher = class {
6948
8648
  */
6949
8649
  appendNow(event) {
6950
8650
  this.ensureDir();
6951
- const file = join9(this.billingDir, billingFileName(event.ts));
8651
+ const file = join10(this.billingDir, billingFileName(event.ts));
6952
8652
  appendFileSync3(file, JSON.stringify(event) + "\n", "utf8");
6953
8653
  }
6954
8654
  /**
@@ -6998,7 +8698,7 @@ var BillingPublisher = class {
6998
8698
  markDelivered(event) {
6999
8699
  try {
7000
8700
  this.ensureDir();
7001
- const file = join9(this.billingDir, deliveredFileName(event.ts));
8701
+ const file = join10(this.billingDir, deliveredFileName(event.ts));
7002
8702
  appendFileSync3(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
7003
8703
  } catch (error) {
7004
8704
  this.logger.warn("[BillingPublisher] failed to append delivery marker", {
@@ -7008,17 +8708,17 @@ var BillingPublisher = class {
7008
8708
  }
7009
8709
  ensureDir() {
7010
8710
  if (this.dirEnsured) return;
7011
- mkdirSync5(this.billingDir, { recursive: true });
8711
+ mkdirSync6(this.billingDir, { recursive: true });
7012
8712
  this.dirEnsured = true;
7013
8713
  }
7014
8714
  };
7015
8715
 
7016
8716
  // src/billing/billingReader.ts
7017
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync13 } from "fs";
7018
- import { join as join10 } from "path";
8717
+ import { existsSync as existsSync16, readdirSync as readdirSync3, readFileSync as readFileSync16 } from "fs";
8718
+ import { join as join11 } from "path";
7019
8719
  function readBillingLedger(billingDir) {
7020
8720
  const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
7021
- if (!existsSync13(billingDir)) return view;
8721
+ if (!existsSync16(billingDir)) return view;
7022
8722
  let files;
7023
8723
  try {
7024
8724
  files = readdirSync3(billingDir);
@@ -7052,7 +8752,7 @@ function readBillingStatus(billingDir) {
7052
8752
  function parseLines(dir, file) {
7053
8753
  let raw;
7054
8754
  try {
7055
- raw = readFileSync13(join10(dir, file), "utf8");
8755
+ raw = readFileSync16(join11(dir, file), "utf8");
7056
8756
  } catch {
7057
8757
  return [];
7058
8758
  }
@@ -7207,8 +8907,9 @@ var TokenRefreshScheduler = class {
7207
8907
  const expiresAt = Date.parse(t.expiresAt);
7208
8908
  return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
7209
8909
  }
7210
- /** Refresh one account; failures are logged, never thrown (the store has
7211
- * 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
+ */
7212
8913
  async refreshOne(provider, id, isActive) {
7213
8914
  try {
7214
8915
  const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
@@ -7239,7 +8940,7 @@ var TokenRefreshScheduler = class {
7239
8940
 
7240
8941
  // src/webhook/WebhookDispatcher.ts
7241
8942
  import { createHmac as createHmac2 } from "crypto";
7242
- import { fetchUpstream as fetchUpstream5 } from "@omnicross/core/pipeline/upstreamFetch";
8943
+ import { fetchUpstream as fetchUpstream6 } from "@omnicross/core/pipeline/upstreamFetch";
7243
8944
  var WEBHOOK_MAX_ATTEMPTS = 3;
7244
8945
  var WEBHOOK_QUEUE_MAX = 1e3;
7245
8946
  var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
@@ -7259,7 +8960,7 @@ var WebhookDispatcher = class {
7259
8960
  sleep;
7260
8961
  now;
7261
8962
  constructor(opts = {}) {
7262
- this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
8963
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream6(url, init));
7263
8964
  this.logger = opts.logger;
7264
8965
  this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
7265
8966
  this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
@@ -7413,11 +9114,32 @@ function buildDaemon(config, paths) {
7413
9114
  setSecretBox(secretBox3);
7414
9115
  setSecretBox2(secretBox3);
7415
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
+ );
7416
9126
  const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
7417
9127
  const keyDb = new JsonOutboundKeyDb(paths.keysPath);
7418
9128
  const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
7419
9129
  const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
9130
+ const integrationStateStore = new IntegrationStateStore(
9131
+ defaultIntegrationsPath(paths.configPath),
9132
+ secretBox3
9133
+ );
7420
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
+ );
7421
9143
  const subscriptionAccounts = new SubscriptionAccountService(credentialStore);
7422
9144
  setSubscriptionAccountService(subscriptionAccounts);
7423
9145
  const subscriptionRegistry = new SubscriptionProviderRegistry(
@@ -7446,7 +9168,17 @@ function buildDaemon(config, paths) {
7446
9168
  }
7447
9169
  );
7448
9170
  const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
7449
- 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
+ );
7450
9182
  const usageEventStore = new JsonlUsageEventStore(
7451
9183
  defaultUsageEventsPath(paths.configPath),
7452
9184
  async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
@@ -7459,7 +9191,7 @@ function buildDaemon(config, paths) {
7459
9191
  llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
7460
9192
  const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
7461
9193
  credentialStore,
7462
- getSharedAccountHealth2(),
9194
+ getSharedAccountHealth3(),
7463
9195
  logger,
7464
9196
  DEFAULT_ACCOUNT_PROBE
7465
9197
  );
@@ -7507,6 +9239,9 @@ function buildDaemon(config, paths) {
7507
9239
  settingsStore,
7508
9240
  outboundApiServer,
7509
9241
  subscriptionAccounts,
9242
+ accountAllowanceService,
9243
+ allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
9244
+ accountProbeService: accountHealthProbeScheduler,
7510
9245
  // Least-authority token WRITER (design D4) — the concrete credential store
7511
9246
  // exposes `writeProviderTokens` / `clearProvider` as daemon-only methods (NOT
7512
9247
  // on the `SubscriptionCredentialStore` port). The admin API sees ONLY these two
@@ -7527,7 +9262,7 @@ function buildDaemon(config, paths) {
7527
9262
  // inject a mock so no real token endpoint is hit.
7528
9263
  // upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
7529
9264
  // helper so interactive login honors a configured proxy (global/env layers).
7530
- oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream6(url, init)),
9265
+ oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream7(url, init)),
7531
9266
  subscriptionAccountAppender: credentialStore,
7532
9267
  // Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
7533
9268
  // + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
@@ -7545,6 +9280,16 @@ function buildDaemon(config, paths) {
7545
9280
  cliTerminalOpener: paths.cliTerminalOpener,
7546
9281
  cliPathProbe: paths.cliPathProbe,
7547
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
+ },
7548
9293
  // Usage/pricing admin surface (usage-pricing child): stats queries go
7549
9294
  // through the recorder facade, pricing mutations through the engine, and
7550
9295
  // the row DELETE through the concrete store (delete is store-local — the
@@ -7569,16 +9314,16 @@ function buildDaemon(config, paths) {
7569
9314
  // date-rotated audit store. Bound to the store dir here so the AdminServer
7570
9315
  // carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
7571
9316
  // NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
7572
- auditReader: (query) => readAuditRecords(auditDir2, query),
9317
+ auditReader: (query2) => readAuditRecords(auditDir2, query2),
7573
9318
  // billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
7574
9319
  // secret-free total/delivered/pending counts of the durable ledger.
7575
9320
  billingStatusReader: () => readBillingStatus(billingDir)
7576
9321
  });
7577
9322
  const webhookDispatcher = new WebhookDispatcher({
7578
9323
  logger,
7579
- fetchImpl: (url, init) => fetchUpstream6(url, init)
9324
+ fetchImpl: (url, init) => fetchUpstream7(url, init)
7580
9325
  });
7581
- setWebhookRuntime(webhookDispatcher, getSharedAccountHealth2());
9326
+ setWebhookRuntime(webhookDispatcher, getSharedAccountHealth3());
7582
9327
  const auditWriter = new AuditWriter(auditDir2, logger);
7583
9328
  const auditPruneSweeper = new AuditPruneSweeper(auditDir2, logger, DEFAULT_AUDIT_CONFIG);
7584
9329
  setAuditRuntime(auditWriter, auditPruneSweeper, auditDir2);
@@ -7593,7 +9338,7 @@ function buildDaemon(config, paths) {
7593
9338
  const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
7594
9339
  const accountHealthSweeper = new AccountHealthSweeper(
7595
9340
  credentialStore,
7596
- getSharedAccountHealth2(),
9341
+ getSharedAccountHealth3(),
7597
9342
  logger
7598
9343
  );
7599
9344
  return {
@@ -7608,8 +9353,11 @@ function buildDaemon(config, paths) {
7608
9353
  credentialStore,
7609
9354
  subscriptionRegistry,
7610
9355
  subscriptionAccounts,
9356
+ accountAllowanceService,
9357
+ claudeAllowanceRefreshScheduler,
7611
9358
  pricingStore,
7612
9359
  pricingEngine,
9360
+ pricingRefreshScheduler,
7613
9361
  usageRecorder,
7614
9362
  adminServer,
7615
9363
  tokenRefreshScheduler,
@@ -7624,7 +9372,7 @@ function buildDaemon(config, paths) {
7624
9372
  }
7625
9373
  function isTokensStoreReadable(tokensPath) {
7626
9374
  try {
7627
- if (!existsSync14(tokensPath)) return true;
9375
+ if (!existsSync17(tokensPath)) return true;
7628
9376
  accessSync(tokensPath, fsConstants.R_OK);
7629
9377
  return true;
7630
9378
  } catch {
@@ -7672,8 +9420,8 @@ function buildCliSpawnPlan(opts) {
7672
9420
  function resolveInPathDefault(candidate) {
7673
9421
  const segments = (process.env["PATH"] ?? "").split(delimiter2).filter(Boolean);
7674
9422
  for (const seg of segments) {
7675
- const full = join11(seg, candidate);
7676
- if (existsSync15(full)) return full;
9423
+ const full = join12(seg, candidate);
9424
+ if (existsSync18(full)) return full;
7677
9425
  }
7678
9426
  return null;
7679
9427
  }
@@ -7681,7 +9429,7 @@ async function runLaunch(argv, deps) {
7681
9429
  const sep = argv.indexOf("--");
7682
9430
  const own = sep === -1 ? argv : argv.slice(0, sep);
7683
9431
  const passthrough = sep === -1 ? [] : argv.slice(sep + 1);
7684
- const { values, positionals } = parseArgs3({
9432
+ const { values, positionals } = parseArgs4({
7685
9433
  args: own,
7686
9434
  options: {
7687
9435
  provider: { type: "string", short: "p" },
@@ -7716,10 +9464,12 @@ async function runLaunch(argv, deps) {
7716
9464
  } catch (err5) {
7717
9465
  daemon.apiKeyPool.dispose();
7718
9466
  daemon.tokenRefreshScheduler.dispose();
9467
+ daemon.claudeAllowanceRefreshScheduler.dispose();
7719
9468
  daemon.accountHealthSweeper.dispose();
7720
9469
  daemon.accountHealthProbeScheduler.dispose();
7721
9470
  daemon.auditPruneSweeper.dispose();
7722
9471
  daemon.billingRetrySweeper.dispose();
9472
+ daemon.pricingRefreshScheduler.dispose();
7723
9473
  throw err5;
7724
9474
  }
7725
9475
  let launch;
@@ -7732,10 +9482,12 @@ async function runLaunch(argv, deps) {
7732
9482
  await daemon.providerProxy.stop();
7733
9483
  daemon.apiKeyPool.dispose();
7734
9484
  daemon.tokenRefreshScheduler.dispose();
9485
+ daemon.claudeAllowanceRefreshScheduler.dispose();
7735
9486
  daemon.accountHealthSweeper.dispose();
7736
9487
  daemon.accountHealthProbeScheduler.dispose();
7737
9488
  daemon.auditPruneSweeper.dispose();
7738
9489
  daemon.billingRetrySweeper.dispose();
9490
+ daemon.pricingRefreshScheduler.dispose();
7739
9491
  throw err5;
7740
9492
  }
7741
9493
  try {
@@ -7758,10 +9510,12 @@ async function runLaunch(argv, deps) {
7758
9510
  await daemon.providerProxy.stop();
7759
9511
  daemon.apiKeyPool.dispose();
7760
9512
  daemon.tokenRefreshScheduler.dispose();
9513
+ daemon.claudeAllowanceRefreshScheduler.dispose();
7761
9514
  daemon.accountHealthSweeper.dispose();
7762
9515
  daemon.accountHealthProbeScheduler.dispose();
7763
9516
  daemon.auditPruneSweeper.dispose();
7764
9517
  daemon.billingRetrySweeper.dispose();
9518
+ daemon.pricingRefreshScheduler.dispose();
7765
9519
  }
7766
9520
  }
7767
9521
  async function buildLaunchConfig(cli, llmConfig, opts) {
@@ -7791,7 +9545,7 @@ async function buildLaunchConfig(cli, llmConfig, opts) {
7791
9545
  }
7792
9546
  }
7793
9547
  function spawnCliInherit(plan) {
7794
- return new Promise((resolve, reject) => {
9548
+ return new Promise((resolve3, reject) => {
7795
9549
  const child = spawn2(plan.command, plan.args, {
7796
9550
  stdio: "inherit",
7797
9551
  env: plan.env,
@@ -7825,7 +9579,7 @@ function spawnCliInherit(plan) {
7825
9579
  });
7826
9580
  child.on("exit", (code, signal) => {
7827
9581
  detach();
7828
- resolve(code ?? (signal ? 1 : 0));
9582
+ resolve3(code ?? (signal ? 1 : 0));
7829
9583
  });
7830
9584
  });
7831
9585
  }
@@ -7833,12 +9587,12 @@ function spawnCliInherit(plan) {
7833
9587
  // src/commands/login.ts
7834
9588
  import { spawn as spawn3 } from "child_process";
7835
9589
  import { createInterface } from "readline";
7836
- import { parseArgs as parseArgs4 } from "util";
7837
- 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";
7838
9592
  import { claudeOAuth as claudeOAuth3, codexOAuth as codexOAuth3, geminiOAuth as geminiOAuth3 } from "@omnicross/subscriptions";
7839
9593
  var PROVIDERS = ["claude", "codex", "gemini"];
7840
9594
  async function runLogin(argv, deps) {
7841
- const { values, positionals } = parseArgs4({
9595
+ const { values, positionals } = parseArgs5({
7842
9596
  args: argv,
7843
9597
  options: {
7844
9598
  config: { type: "string", short: "c" },
@@ -7869,7 +9623,7 @@ async function runLogin(argv, deps) {
7869
9623
  setUpstreamProxyResolver2(createUpstreamProxyResolver());
7870
9624
  try {
7871
9625
  const tokensPath = defaultTokensPath(values.config);
7872
- const exchangeFetch = resolved.tokensFetch ?? ((url, init) => fetchUpstream7(url, init, { providerId: provider }));
9626
+ const exchangeFetch = resolved.tokensFetch ?? ((url, init) => fetchUpstream8(url, init, { providerId: provider }));
7873
9627
  const store = new JsonSubscriptionCredentialStore(tokensPath, box, exchangeFetch);
7874
9628
  const expiresAt = await runProviderLogin(
7875
9629
  provider,
@@ -7982,33 +9736,33 @@ function buildOpenBrowserCommand(platform, url) {
7982
9736
  return { command: "xdg-open", args: [url] };
7983
9737
  }
7984
9738
  function openBrowser(url) {
7985
- return new Promise((resolve) => {
9739
+ return new Promise((resolve3) => {
7986
9740
  try {
7987
9741
  const { command, args } = buildOpenBrowserCommand(process.platform, url);
7988
9742
  const child = spawn3(command, args, { stdio: "ignore", detached: true });
7989
- child.on("error", () => resolve(false));
9743
+ child.on("error", () => resolve3(false));
7990
9744
  child.unref();
7991
- resolve(true);
9745
+ resolve3(true);
7992
9746
  } catch {
7993
- resolve(false);
9747
+ resolve3(false);
7994
9748
  }
7995
9749
  });
7996
9750
  }
7997
9751
  function promptPaste(prompt) {
7998
9752
  const rl = createInterface({ input: process.stdin, output: process.stdout });
7999
- return new Promise((resolve) => {
9753
+ return new Promise((resolve3) => {
8000
9754
  rl.question(prompt, (answer) => {
8001
9755
  rl.close();
8002
- resolve(answer);
9756
+ resolve3(answer);
8003
9757
  });
8004
9758
  });
8005
9759
  }
8006
9760
 
8007
9761
  // src/commands/providers.ts
8008
- import { randomUUID as randomUUID4 } from "crypto";
8009
- import { parseArgs as parseArgs5 } from "util";
9762
+ import { randomUUID as randomUUID6 } from "crypto";
9763
+ import { parseArgs as parseArgs6 } from "util";
8010
9764
  async function runProviders(argv) {
8011
- const { values, positionals } = parseArgs5({
9765
+ const { values, positionals } = parseArgs6({
8012
9766
  args: argv,
8013
9767
  options: {
8014
9768
  config: { type: "string", short: "c" },
@@ -8127,7 +9881,7 @@ function providersAddKey(configPath, providerId, opts) {
8127
9881
  const cfg = loadConfig(configPath);
8128
9882
  const row = cfg.providers.find((p) => p.id === providerId);
8129
9883
  if (!row) throw new Error(`providers add-key: unknown provider '${providerId}'`);
8130
- const entry = { id: randomUUID4(), apiKey: opts.key };
9884
+ const entry = { id: randomUUID6(), apiKey: opts.key };
8131
9885
  if (opts.label) entry.label = opts.label;
8132
9886
  if (opts.weight !== void 0) {
8133
9887
  const w = Number(opts.weight);
@@ -8155,10 +9909,10 @@ function providersRmKey(configPath, providerId, keyId) {
8155
9909
  }
8156
9910
 
8157
9911
  // src/commands/secrets.ts
8158
- import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
8159
- 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";
8160
9914
  async function runSecrets(argv) {
8161
- const { values, positionals } = parseArgs6({
9915
+ const { values, positionals } = parseArgs7({
8162
9916
  args: argv,
8163
9917
  options: {
8164
9918
  config: { type: "string", short: "c" },
@@ -8184,7 +9938,7 @@ async function runSecrets(argv) {
8184
9938
  case "status":
8185
9939
  return secretsStatus(args);
8186
9940
  case "rotate":
8187
- return secretsRotate(args);
9941
+ return await secretsRotate(args);
8188
9942
  case "decrypt":
8189
9943
  return secretsDecrypt(args);
8190
9944
  default:
@@ -8200,6 +9954,7 @@ function secretsEncrypt(args) {
8200
9954
  const cfg = loadConfig(args.config);
8201
9955
  saveConfig(args.config, cfg);
8202
9956
  encryptTokensFileInPlace(args.config, box);
9957
+ rewriteIntegrationState(args.config, box, box);
8203
9958
  } finally {
8204
9959
  setSecretBox(null);
8205
9960
  }
@@ -8227,10 +9982,27 @@ function secretsStatus(args) {
8227
9982
  reportField("admin.token", cfg.admin.token);
8228
9983
  }
8229
9984
  const tokensPath = defaultTokensPath(args.config);
8230
- if (existsSync16(tokensPath)) {
9985
+ if (existsSync19(tokensPath)) {
8231
9986
  console.info(`Secret status for ${tokensPath}:`);
8232
9987
  reportTokenFields(tokensPath);
8233
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
+ }
8234
10006
  }
8235
10007
  function reportField(name, raw) {
8236
10008
  const cls = classify(raw);
@@ -8255,7 +10027,7 @@ function reportTokenFields(tokensPath) {
8255
10027
  }
8256
10028
  }
8257
10029
  }
8258
- function secretsRotate(args) {
10030
+ async function secretsRotate(args) {
8259
10031
  if (!args.newMasterKeyFile) {
8260
10032
  throw new Error("secrets rotate: --new-master-key-file <path> is required");
8261
10033
  }
@@ -8264,10 +10036,15 @@ function secretsRotate(args) {
8264
10036
  setSecretBox(oldBox);
8265
10037
  let cfg;
8266
10038
  let tokensPlain = null;
10039
+ let integrationsPlain = null;
8267
10040
  const tokensPath = defaultTokensPath(args.config);
10041
+ const integrationsPath = defaultIntegrationsPath(args.config);
8268
10042
  try {
8269
10043
  cfg = loadConfig(args.config);
8270
- 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
+ }
8271
10048
  } finally {
8272
10049
  setSecretBox(null);
8273
10050
  }
@@ -8275,6 +10052,10 @@ function secretsRotate(args) {
8275
10052
  try {
8276
10053
  saveConfig(args.config, cfg);
8277
10054
  if (tokensPlain) writeTokensEncrypted(tokensPath, tokensPlain, newBox);
10055
+ if (integrationsPlain) {
10056
+ const newStore = new IntegrationStateStore(integrationsPath, newBox);
10057
+ newStore.save(integrationsPlain);
10058
+ }
8278
10059
  } finally {
8279
10060
  setSecretBox(null);
8280
10061
  }
@@ -8296,20 +10077,20 @@ function secretsDecrypt(args) {
8296
10077
  let tokensPlain = null;
8297
10078
  try {
8298
10079
  cfg = loadConfig(args.config);
8299
- if (existsSync16(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
10080
+ if (existsSync19(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
8300
10081
  } finally {
8301
10082
  setSecretBox(null);
8302
10083
  }
8303
10084
  saveConfig(args.config, cfg);
8304
10085
  if (tokensPlain) {
8305
- writeFileSync9(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
10086
+ writeFileSync11(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
8306
10087
  }
8307
10088
  console.info(`Decrypted secrets to plaintext in ${args.config}` + tokensSuffix(args.config));
8308
10089
  }
8309
10090
  function readRawConfig(path2) {
8310
10091
  let parsed;
8311
10092
  try {
8312
- parsed = JSON.parse(readFileSync14(path2, "utf8"));
10093
+ parsed = JSON.parse(readFileSync17(path2, "utf8"));
8313
10094
  } catch {
8314
10095
  throw new Error(`secrets: cannot read or parse '${path2}'`);
8315
10096
  }
@@ -8317,7 +10098,7 @@ function readRawConfig(path2) {
8317
10098
  }
8318
10099
  function readRawJson(path2) {
8319
10100
  try {
8320
- const parsed = JSON.parse(readFileSync14(path2, "utf8"));
10101
+ const parsed = JSON.parse(readFileSync17(path2, "utf8"));
8321
10102
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
8322
10103
  return parsed;
8323
10104
  }
@@ -8327,10 +10108,16 @@ function readRawJson(path2) {
8327
10108
  }
8328
10109
  function encryptTokensFileInPlace(configPath, box) {
8329
10110
  const tokensPath = defaultTokensPath(configPath);
8330
- if (!existsSync16(tokensPath)) return;
10111
+ if (!existsSync19(tokensPath)) return;
8331
10112
  const plain = decryptTokensFile(tokensPath, box);
8332
10113
  writeTokensEncrypted(tokensPath, plain, box);
8333
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
+ }
8334
10121
  function decryptTokensFile(tokensPath, box) {
8335
10122
  const raw = readRawJson(tokensPath);
8336
10123
  return walkTokens(raw, (v) => box.decryptMaybe(v));
@@ -8340,7 +10127,7 @@ function writeTokensEncrypted(tokensPath, plain, box) {
8340
10127
  { updatedAt: "", ...plain },
8341
10128
  box
8342
10129
  );
8343
- writeFileSync9(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
10130
+ writeFileSync11(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
8344
10131
  }
8345
10132
  var TOKEN_FIELDS2 = {
8346
10133
  claude: ["accessToken", "refreshToken"],
@@ -8363,18 +10150,19 @@ function walkTokens(raw, fn) {
8363
10150
  return next;
8364
10151
  }
8365
10152
  function tokensSuffix(configPath) {
8366
- return existsSync16(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
10153
+ return existsSync19(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
8367
10154
  }
8368
10155
 
8369
10156
  // src/commands/start.ts
8370
- import { parseArgs as parseArgs7 } from "util";
8371
- import { loadServerConfig as loadServerConfig3, OutboundApiConfigError } from "@omnicross/core/outbound-api";
8372
- 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";
8373
10161
 
8374
10162
  // src/identity/identityRuntime.ts
8375
- import { getSharedIdentityStore as getSharedIdentityStore2 } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
10163
+ import { getSharedIdentityStore as getSharedIdentityStore3 } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
8376
10164
  async function applyFingerprintConfig(config, credentialStore) {
8377
- const store = getSharedIdentityStore2();
10165
+ const store = getSharedIdentityStore3();
8378
10166
  const enabled = config?.enabled === true;
8379
10167
  store.configure({ enabled, ua: config?.ua ?? null });
8380
10168
  if (!enabled) {
@@ -8405,7 +10193,7 @@ async function seedIdentities(store, credentialStore) {
8405
10193
 
8406
10194
  // src/commands/start.ts
8407
10195
  async function runStart(argv) {
8408
- const { values } = parseArgs7({
10196
+ const { values } = parseArgs8({
8409
10197
  args: argv,
8410
10198
  options: {
8411
10199
  config: { type: "string", short: "c" },
@@ -8429,35 +10217,32 @@ async function runStart(argv) {
8429
10217
  await daemon.llmConfig.ready();
8430
10218
  await daemon.providerProxy.start();
8431
10219
  const serverConfig = await loadServerConfig3(daemon.settingsStore);
8432
- getSharedAccountHealth3().configure({
10220
+ getSharedAccountHealth4().configure({
8433
10221
  overloadEnabled: serverConfig.accountHealth?.overloadCooldownEnabled,
8434
10222
  overloadTtlMs: serverConfig.accountHealth?.overloadCooldownMs
8435
10223
  });
8436
- try {
8437
- await daemon.outboundApiServer.applyConfig({
8438
- enabled: true,
8439
- networkBinding: serverConfig.networkBinding,
8440
- endpoints: serverConfig.endpoints,
8441
- port: serverConfig.port,
8442
- userMessageQueue: serverConfig.userMessageQueue,
8443
- concurrencyQueue: serverConfig.concurrencyQueue,
8444
- // voucher-redemption #9: carry the persisted flag so `POST /redeem` works on
8445
- // boot when the operator has enabled the product.
8446
- voucher: serverConfig.voucher
8447
- });
8448
- } catch (err5) {
8449
- if (err5 instanceof OutboundApiConfigError) {
8450
- console.warn(`[outbound] not started \u2014 incomplete model configuration: ${err5.message}`);
8451
- } else {
8452
- throw err5;
8453
- }
8454
- }
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
+ });
8455
10238
  let dashboardUrl = null;
8456
10239
  if (!values["no-dashboard"]) {
8457
10240
  await daemon.adminServer.start();
8458
10241
  dashboardUrl = daemon.adminServer.getStatus().url;
8459
10242
  }
8460
10243
  daemon.tokenRefreshScheduler.start();
10244
+ daemon.claudeAllowanceRefreshScheduler.start();
10245
+ daemon.pricingRefreshScheduler.start();
8461
10246
  daemon.accountHealthSweeper.start();
8462
10247
  if (serverConfig.accountProbe) {
8463
10248
  daemon.accountHealthProbeScheduler.configure(serverConfig.accountProbe);
@@ -8534,6 +10319,16 @@ Usage:
8534
10319
  omnicross launch <cli> --provider <id> --model <m> --config <p> [--cwd <dir>] [-- <cli-args\u2026>]
8535
10320
  Launch a Code CLI (claude|codex|gemini|qwen|copilot|opencode)
8536
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.
8537
10332
  omnicross import-ccr <ccr.json> [--out <p>] Translate a CCR config.
8538
10333
  omnicross secrets encrypt --config <p> Encrypt all at-rest secrets in place.
8539
10334
  omnicross secrets status --config <p> Report each secret field (no values shown).
@@ -8560,6 +10355,9 @@ async function main() {
8560
10355
  case "launch":
8561
10356
  process.exitCode = await runLaunch(rest);
8562
10357
  return;
10358
+ case "integrations":
10359
+ await runIntegrations(rest);
10360
+ return;
8563
10361
  case "import-ccr":
8564
10362
  await runImportCcr(rest);
8565
10363
  return;