@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.cjs +2425 -660
- package/dist/cli.js +2416 -618
- package/dist/index.cjs +2286 -622
- package/dist/index.d.cts +452 -189
- package/dist/index.d.ts +452 -189
- package/dist/index.js +2273 -576
- package/package.json +1 -1
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 = [
|
|
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
|
-
|
|
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/
|
|
789
|
+
// src/commands/integrations.ts
|
|
790
|
+
import { resolve as resolve2 } from "path";
|
|
733
791
|
import { parseArgs as parseArgs2 } from "util";
|
|
734
|
-
|
|
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
|
|
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 (!
|
|
1600
|
+
if (!existsSync4(this.keysPath)) return [];
|
|
841
1601
|
try {
|
|
842
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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 } =
|
|
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
|
|
914
|
-
import { delimiter as delimiter2, join as
|
|
915
|
-
import { parseArgs as
|
|
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
|
|
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
|
-
|
|
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
|
|
936
|
-
import {
|
|
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
|
|
2359
|
+
const query2 = {};
|
|
1080
2360
|
const keyId = url.searchParams.get("keyId");
|
|
1081
|
-
if (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)
|
|
2363
|
+
if (from !== void 0) query2.from = from;
|
|
1084
2364
|
const to = intParam(url.searchParams.get("to"));
|
|
1085
|
-
if (to !== void 0)
|
|
2365
|
+
if (to !== void 0) query2.to = to;
|
|
1086
2366
|
const limit = intParam(url.searchParams.get("limit"));
|
|
1087
|
-
if (limit !== void 0)
|
|
1088
|
-
const records = reader ? reader(
|
|
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((
|
|
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
|
-
|
|
2440
|
+
resolve3(parsed && typeof parsed === "object" ? parsed : {});
|
|
1161
2441
|
} catch {
|
|
1162
|
-
|
|
2442
|
+
resolve3({});
|
|
1163
2443
|
}
|
|
1164
2444
|
});
|
|
1165
|
-
req.on("error", () =>
|
|
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 {
|
|
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":
|
|
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
|
|
1670
|
-
import { delimiter, join as
|
|
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 =
|
|
1701
|
-
if (
|
|
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((
|
|
3130
|
+
var defaultCommandRunner = (command) => new Promise((resolve3) => {
|
|
1789
3131
|
exec(command, { timeout: 18e4 }, (err5, _stdout, stderr) => {
|
|
1790
|
-
if (err5)
|
|
1791
|
-
else
|
|
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 =
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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((
|
|
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
|
|
3488
|
+
if (!raw.trim()) return resolve3({});
|
|
2057
3489
|
try {
|
|
2058
3490
|
const parsed = JSON.parse(raw);
|
|
2059
|
-
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 ?
|
|
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
|
|
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 =
|
|
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(
|
|
2805
|
-
const startTs = parseFiniteInt(
|
|
2806
|
-
const endTs = parseFiniteInt(
|
|
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,
|
|
2820
|
-
const range = parseRange(
|
|
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 =
|
|
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(
|
|
2915
|
-
const providerId =
|
|
2916
|
-
const modelId =
|
|
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((
|
|
4538
|
+
return new Promise((resolve3, reject) => {
|
|
2984
4539
|
const chunks = [];
|
|
2985
4540
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
2986
|
-
req.on("end", () =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4773
|
+
return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
|
|
3214
4774
|
}
|
|
3215
4775
|
if (method === "GET") {
|
|
3216
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4869
|
+
return writeJson3(res, 200, { models });
|
|
3310
4870
|
} catch (err5) {
|
|
3311
4871
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
3312
|
-
return
|
|
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
|
|
4883
|
+
return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
|
|
3324
4884
|
}
|
|
3325
4885
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
3326
4886
|
if (!resolvedKey) {
|
|
3327
|
-
return
|
|
4887
|
+
return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
|
|
3328
4888
|
}
|
|
3329
|
-
|
|
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
|
|
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
|
|
4926
|
+
return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
|
|
3363
4927
|
}
|
|
3364
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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")
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
}
|
|
3833
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5641
|
+
return writeJson3(res, 200, { ok: true });
|
|
3975
5642
|
}
|
|
3976
|
-
if (method === "DELETE" && rest.length
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4040
|
-
|
|
4041
|
-
|
|
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 (
|
|
4044
|
-
return {
|
|
5779
|
+
if (endpoint === "chat") {
|
|
5780
|
+
return {
|
|
5781
|
+
endpoint,
|
|
5782
|
+
models: [...new Set(routes.flatMap((route) => route.models ?? []))],
|
|
5783
|
+
useSubscription
|
|
5784
|
+
};
|
|
4045
5785
|
}
|
|
4046
|
-
return {
|
|
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
|
|
5794
|
+
return writeJson3(res, 200, { ...status, endpoints, queueStatus });
|
|
4051
5795
|
}
|
|
4052
|
-
return
|
|
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,
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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.
|
|
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((
|
|
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(
|
|
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
|
-
|
|
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((
|
|
4358
|
-
server.close(() =>
|
|
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((
|
|
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, () =>
|
|
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
|
|
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(
|
|
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 =
|
|
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((
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
4966
|
-
import { appendFileSync, existsSync as
|
|
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:
|
|
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(
|
|
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:
|
|
5081
|
-
if (row.apiKeyId !==
|
|
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 >=
|
|
5084
|
-
if (row.ts >=
|
|
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 (!
|
|
6912
|
+
if (!existsSync8(this.eventsPath)) return [];
|
|
5170
6913
|
let raw;
|
|
5171
6914
|
try {
|
|
5172
|
-
raw =
|
|
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
|
|
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
|
|
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" :
|
|
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 (!
|
|
7129
|
+
if (!existsSync9(this.pricingPath)) return [];
|
|
5368
7130
|
try {
|
|
5369
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
7342
|
+
if (!existsSync11(this.vouchersPath)) return [];
|
|
5460
7343
|
try {
|
|
5461
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
|
5474
|
-
import { dirname as
|
|
5475
|
-
import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
|
|
5476
|
-
import {
|
|
5477
|
-
import {
|
|
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
|
|
5559
|
-
import { homedir as
|
|
5560
|
-
import { join as
|
|
5561
|
-
function externalStorePath(provider, home =
|
|
5562
|
-
return provider === "claude" ?
|
|
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 =
|
|
7461
|
+
function readExternalCliCredentials(provider, home = homedir3()) {
|
|
5610
7462
|
const path2 = externalStorePath(provider, home);
|
|
5611
|
-
if (!
|
|
7463
|
+
if (!existsSync12(path2)) return null;
|
|
5612
7464
|
let raw;
|
|
5613
7465
|
try {
|
|
5614
|
-
const parsed = JSON.parse(
|
|
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
|
|
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
|
|
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`
|
|
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) =>
|
|
7506
|
+
return this.fetchImpl ?? ((url, init) => fetchUpstream3(url, init, { providerId, accountId }));
|
|
5735
7507
|
}
|
|
5736
7508
|
/**
|
|
5737
|
-
* In-flight refresh coalescing
|
|
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
|
|
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
|
|
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)
|
|
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 =
|
|
5789
|
-
const
|
|
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.
|
|
7585
|
+
out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
|
|
5807
7586
|
}
|
|
5808
7587
|
return out;
|
|
5809
7588
|
}
|
|
5810
7589
|
/**
|
|
5811
|
-
* List-time credential
|
|
5812
|
-
*
|
|
5813
|
-
* credential
|
|
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
|
-
|
|
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 =
|
|
5830
|
-
|
|
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)
|
|
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
|
|
5847
|
-
* errorMessage
|
|
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
|
-
*
|
|
5977
|
-
*
|
|
5978
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
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
|
-
/**
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
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
|
|
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)
|
|
6206
|
-
*
|
|
6207
|
-
*
|
|
6208
|
-
*
|
|
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
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
6329
|
-
* write
|
|
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
|
-
|
|
8031
|
+
mkdirSync4(dirname6(this.tokensPath), { recursive: true });
|
|
6332
8032
|
const encrypted = encryptTokens(config, this.box);
|
|
6333
|
-
|
|
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
|
|
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 "
|
|
6344
|
-
* SHALL fail-fast, SHALL NOT
|
|
6345
|
-
* tokens" and silently send the WRONG bearer upstream
|
|
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 (!
|
|
8049
|
+
if (!existsSync13(this.tokensPath)) return { updatedAt: "" };
|
|
6350
8050
|
let parsed;
|
|
6351
8051
|
try {
|
|
6352
|
-
const raw = JSON.parse(
|
|
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
|
|
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 ??
|
|
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
|
|
6681
|
-
import { join as
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
6788
|
-
import { join as
|
|
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,
|
|
6792
|
-
if (!
|
|
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
|
|
6800
|
-
const to = typeof
|
|
6801
|
-
const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(
|
|
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 =
|
|
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 (
|
|
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
|
|
6836
|
-
import { join as
|
|
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
|
-
|
|
8569
|
+
mkdirSync5(this.auditDir, { recursive: true });
|
|
6870
8570
|
this.dirEnsured = true;
|
|
6871
8571
|
}
|
|
6872
|
-
const file =
|
|
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
|
|
8578
|
+
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync6 } from "fs";
|
|
6879
8579
|
import { createHmac } from "crypto";
|
|
6880
|
-
import { join as
|
|
6881
|
-
import { fetchUpstream as
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
7018
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
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
|
|
7211
|
-
*
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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) =>
|
|
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: (
|
|
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) =>
|
|
9324
|
+
fetchImpl: (url, init) => fetchUpstream7(url, init)
|
|
7580
9325
|
});
|
|
7581
|
-
setWebhookRuntime(webhookDispatcher,
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
7676
|
-
if (
|
|
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 } =
|
|
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((
|
|
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
|
-
|
|
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
|
|
7837
|
-
import { fetchUpstream as
|
|
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 } =
|
|
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) =>
|
|
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((
|
|
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", () =>
|
|
9743
|
+
child.on("error", () => resolve3(false));
|
|
7990
9744
|
child.unref();
|
|
7991
|
-
|
|
9745
|
+
resolve3(true);
|
|
7992
9746
|
} catch {
|
|
7993
|
-
|
|
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((
|
|
9753
|
+
return new Promise((resolve3) => {
|
|
8000
9754
|
rl.question(prompt, (answer) => {
|
|
8001
9755
|
rl.close();
|
|
8002
|
-
|
|
9756
|
+
resolve3(answer);
|
|
8003
9757
|
});
|
|
8004
9758
|
});
|
|
8005
9759
|
}
|
|
8006
9760
|
|
|
8007
9761
|
// src/commands/providers.ts
|
|
8008
|
-
import { randomUUID as
|
|
8009
|
-
import { parseArgs as
|
|
9762
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
9763
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
8010
9764
|
async function runProviders(argv) {
|
|
8011
|
-
const { values, positionals } =
|
|
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:
|
|
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
|
|
8159
|
-
import { parseArgs as
|
|
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 } =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
10153
|
+
return existsSync19(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
|
|
8367
10154
|
}
|
|
8368
10155
|
|
|
8369
10156
|
// src/commands/start.ts
|
|
8370
|
-
import { parseArgs as
|
|
8371
|
-
import { loadServerConfig as loadServerConfig3
|
|
8372
|
-
import { getSharedAccountHealth as
|
|
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
|
|
10163
|
+
import { getSharedIdentityStore as getSharedIdentityStore3 } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
|
|
8376
10164
|
async function applyFingerprintConfig(config, credentialStore) {
|
|
8377
|
-
const store =
|
|
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 } =
|
|
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
|
-
|
|
10220
|
+
getSharedAccountHealth4().configure({
|
|
8433
10221
|
overloadEnabled: serverConfig.accountHealth?.overloadCooldownEnabled,
|
|
8434
10222
|
overloadTtlMs: serverConfig.accountHealth?.overloadCooldownMs
|
|
8435
10223
|
});
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
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;
|