@omnicross/daemon 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2438 -667
- package/dist/cli.js +2430 -626
- package/dist/index.cjs +2302 -630
- package/dist/index.d.cts +452 -189
- package/dist/index.d.ts +452 -189
- package/dist/index.js +2289 -584
- 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,12 +3666,16 @@ function preserveWebhookSecrets(incoming, current) {
|
|
|
2234
3666
|
}
|
|
2235
3667
|
|
|
2236
3668
|
// src/audit/auditRuntime.ts
|
|
3669
|
+
import { join as join5 } from "path";
|
|
2237
3670
|
import { setAuditCaptureConfig, setAuditSink } from "@omnicross/core/pipeline/auditSink";
|
|
3671
|
+
import { setUpstreamTracePath } from "@omnicross/core/pipeline/upstreamTrace";
|
|
2238
3672
|
var writer = null;
|
|
2239
3673
|
var sweeper = null;
|
|
2240
|
-
|
|
3674
|
+
var auditDir = "";
|
|
3675
|
+
function setAuditRuntime(w, s, dir) {
|
|
2241
3676
|
writer = w;
|
|
2242
3677
|
sweeper = s;
|
|
3678
|
+
auditDir = dir;
|
|
2243
3679
|
}
|
|
2244
3680
|
function applyAuditConfig(config) {
|
|
2245
3681
|
const enabled = config?.enabled === true && writer !== null;
|
|
@@ -2251,9 +3687,11 @@ function applyAuditConfig(config) {
|
|
|
2251
3687
|
sweeper.configure(config);
|
|
2252
3688
|
sweeper.start();
|
|
2253
3689
|
}
|
|
3690
|
+
setUpstreamTracePath(config.captureBodies ? join5(auditDir, "upstream-trace.jsonl") : null);
|
|
2254
3691
|
} else {
|
|
2255
3692
|
setAuditCaptureConfig(null);
|
|
2256
3693
|
setAuditSink(null);
|
|
3694
|
+
setUpstreamTracePath(null);
|
|
2257
3695
|
if (sweeper) {
|
|
2258
3696
|
if (config) sweeper.configure(config);
|
|
2259
3697
|
sweeper.dispose();
|
|
@@ -2291,7 +3729,7 @@ function applyBillingConfig(config) {
|
|
|
2291
3729
|
}
|
|
2292
3730
|
|
|
2293
3731
|
// src/ports/account-multi.ts
|
|
2294
|
-
import { randomUUID as
|
|
3732
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2295
3733
|
var PROVIDER_KEYS = {
|
|
2296
3734
|
claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
|
|
2297
3735
|
codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
|
|
@@ -2357,7 +3795,7 @@ function migrateLazily(config) {
|
|
|
2357
3795
|
}
|
|
2358
3796
|
function addAccount(config, p, tokens, label) {
|
|
2359
3797
|
const accounts = [...getAccounts(config, p)];
|
|
2360
|
-
const id =
|
|
3798
|
+
const id = randomUUID3();
|
|
2361
3799
|
accounts.push({
|
|
2362
3800
|
id,
|
|
2363
3801
|
label: label ?? `Account ${accounts.length + 1}`,
|
|
@@ -2433,9 +3871,13 @@ function sanitizeAccounts(config, p) {
|
|
|
2433
3871
|
const activeId = getActiveId(config, p);
|
|
2434
3872
|
return accounts.map((a) => {
|
|
2435
3873
|
const t = a.tokens;
|
|
3874
|
+
const enabled = a.enabled !== false;
|
|
2436
3875
|
return {
|
|
2437
3876
|
id: a.id,
|
|
2438
3877
|
label: a.label,
|
|
3878
|
+
enabled,
|
|
3879
|
+
group: a.group?.trim() || p,
|
|
3880
|
+
tags: a.tags ?? [],
|
|
2439
3881
|
status: t.status ?? "unconfigured",
|
|
2440
3882
|
authMethod: t.authMethod,
|
|
2441
3883
|
subscriptionLevel: t.subscriptionLevel,
|
|
@@ -2444,6 +3886,8 @@ function sanitizeAccounts(config, p) {
|
|
|
2444
3886
|
isSetupToken: t.isSetupToken,
|
|
2445
3887
|
hasAccessToken: !!(t.accessToken || t.apiKey),
|
|
2446
3888
|
isActive: a.id === activeId,
|
|
3889
|
+
schedulable: enabled,
|
|
3890
|
+
errorMessage: sanitizeDiagnosticMessage(t.errorMessage),
|
|
2447
3891
|
// Scheduling metadata (subscription-account-scheduling): editable priority
|
|
2448
3892
|
// (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
|
|
2449
3893
|
priority: a.priority,
|
|
@@ -2458,6 +3902,54 @@ function sanitizeAccounts(config, p) {
|
|
|
2458
3902
|
};
|
|
2459
3903
|
});
|
|
2460
3904
|
}
|
|
3905
|
+
function sanitizeDiagnosticMessage(value) {
|
|
3906
|
+
if (!value) return void 0;
|
|
3907
|
+
const lower = value.toLowerCase();
|
|
3908
|
+
if (lower.includes("timeout") || lower.includes("timed out")) return "Credential operation timed out.";
|
|
3909
|
+
if (lower.includes("network") || lower.includes("fetch")) return "Credential network request failed.";
|
|
3910
|
+
if (lower.includes("401") || lower.includes("unauthorized") || lower.includes("revoked")) {
|
|
3911
|
+
return "Credential authorization was rejected.";
|
|
3912
|
+
}
|
|
3913
|
+
return "Credential operation failed.";
|
|
3914
|
+
}
|
|
3915
|
+
function patchAccountMetadata(config, p, id, patch) {
|
|
3916
|
+
const accounts = getAccounts(config, p);
|
|
3917
|
+
if (!accounts.some((account) => account.id === id)) return { ok: false };
|
|
3918
|
+
setAccounts(config, p, accounts.map((account) => {
|
|
3919
|
+
if (account.id !== id) return account;
|
|
3920
|
+
const next = { ...account };
|
|
3921
|
+
if (patch.label !== void 0) next.label = patch.label;
|
|
3922
|
+
if (patch.enabled !== void 0) next.enabled = patch.enabled;
|
|
3923
|
+
if (patch.priority !== void 0) next.priority = patch.priority;
|
|
3924
|
+
if (patch.group !== void 0) {
|
|
3925
|
+
if (patch.group === null || patch.group === "") delete next.group;
|
|
3926
|
+
else next.group = patch.group;
|
|
3927
|
+
}
|
|
3928
|
+
if (patch.tags !== void 0) next.tags = patch.tags;
|
|
3929
|
+
return next;
|
|
3930
|
+
}));
|
|
3931
|
+
return { ok: true };
|
|
3932
|
+
}
|
|
3933
|
+
function batchManageAccounts(config, refs, mutation) {
|
|
3934
|
+
for (const ref of refs) {
|
|
3935
|
+
if (!getAccounts(config, ref.providerId).some((account) => account.id === ref.accountId)) {
|
|
3936
|
+
return { ok: false, missing: ref };
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
for (const ref of refs) {
|
|
3940
|
+
if (mutation.action === "delete") {
|
|
3941
|
+
removeAccount(config, ref.providerId, ref.accountId);
|
|
3942
|
+
} else {
|
|
3943
|
+
patchAccountMetadata(
|
|
3944
|
+
config,
|
|
3945
|
+
ref.providerId,
|
|
3946
|
+
ref.accountId,
|
|
3947
|
+
mutation.action === "set-group" ? { group: mutation.group } : { enabled: mutation.action === "enable" }
|
|
3948
|
+
);
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
return { ok: true, affected: refs.length };
|
|
3952
|
+
}
|
|
2461
3953
|
function renameAccount(config, p, id, label) {
|
|
2462
3954
|
const accounts = getAccounts(config, p);
|
|
2463
3955
|
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
@@ -2795,9 +4287,9 @@ function parseFiniteInt(raw) {
|
|
|
2795
4287
|
const n = Number(raw);
|
|
2796
4288
|
return Number.isFinite(n) && Number.isInteger(n) ? n : null;
|
|
2797
4289
|
}
|
|
2798
|
-
function parseRange(
|
|
2799
|
-
const startTs = parseFiniteInt(
|
|
2800
|
-
const endTs = parseFiniteInt(
|
|
4290
|
+
function parseRange(query2) {
|
|
4291
|
+
const startTs = parseFiniteInt(query2.get("startTs"));
|
|
4292
|
+
const endTs = parseFiniteInt(query2.get("endTs"));
|
|
2801
4293
|
if (startTs === null || endTs === null) {
|
|
2802
4294
|
return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
|
|
2803
4295
|
}
|
|
@@ -2810,8 +4302,8 @@ var BUCKET_SPAN_MS = {
|
|
|
2810
4302
|
month: 28 * 864e5
|
|
2811
4303
|
};
|
|
2812
4304
|
var MAX_TIMESERIES_BUCKETS = 2e3;
|
|
2813
|
-
async function handleUsageGet(view,
|
|
2814
|
-
const range = parseRange(
|
|
4305
|
+
async function handleUsageGet(view, query2, deps) {
|
|
4306
|
+
const range = parseRange(query2);
|
|
2815
4307
|
if (!isRange(range)) return range;
|
|
2816
4308
|
switch (view) {
|
|
2817
4309
|
case "totals":
|
|
@@ -2819,7 +4311,7 @@ async function handleUsageGet(view, query, deps) {
|
|
|
2819
4311
|
case "by-model":
|
|
2820
4312
|
return { status: 200, body: await deps.usageRecorder.getByModel(range) };
|
|
2821
4313
|
case "timeseries": {
|
|
2822
|
-
const bucket =
|
|
4314
|
+
const bucket = query2.get("bucket");
|
|
2823
4315
|
if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
|
|
2824
4316
|
return err4(400, "bucket must be one of 'hour', 'day', 'month'");
|
|
2825
4317
|
}
|
|
@@ -2905,9 +4397,9 @@ async function handlePricingUpsert(body, deps) {
|
|
|
2905
4397
|
const entry = await deps.pricingEngine.upsertManual(input);
|
|
2906
4398
|
return { status: 200, body: { entry } };
|
|
2907
4399
|
}
|
|
2908
|
-
async function handlePricingDelete(
|
|
2909
|
-
const providerId =
|
|
2910
|
-
const modelId =
|
|
4400
|
+
async function handlePricingDelete(query2, deps) {
|
|
4401
|
+
const providerId = query2.get("providerId")?.trim() ?? "";
|
|
4402
|
+
const modelId = query2.get("modelId")?.trim() ?? "";
|
|
2911
4403
|
if (!providerId || !modelId) {
|
|
2912
4404
|
return err4(400, "delete requires providerId and modelId query params");
|
|
2913
4405
|
}
|
|
@@ -2924,7 +4416,8 @@ async function handlePricingFetchLatest(deps) {
|
|
|
2924
4416
|
appliedCount: result.applied.length,
|
|
2925
4417
|
conflicts: result.conflicts,
|
|
2926
4418
|
fetchedAt: result.fetchedAt,
|
|
2927
|
-
sourceUrl: result.sourceUrl
|
|
4419
|
+
sourceUrl: result.sourceUrl,
|
|
4420
|
+
sources: result.sources
|
|
2928
4421
|
}
|
|
2929
4422
|
};
|
|
2930
4423
|
} catch (e) {
|
|
@@ -2972,12 +4465,80 @@ async function handlePricingResolveConflicts(body, deps) {
|
|
|
2972
4465
|
return { status: 200, body: { ...resolution, staleCount } };
|
|
2973
4466
|
}
|
|
2974
4467
|
|
|
4468
|
+
// src/admin/accountAllowanceApi.ts
|
|
4469
|
+
function writeJson2(res, status, body) {
|
|
4470
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
4471
|
+
res.end(JSON.stringify(body));
|
|
4472
|
+
}
|
|
4473
|
+
function writeError(res, status, message) {
|
|
4474
|
+
writeJson2(res, status, { error: { type: "account_allowance_error", message } });
|
|
4475
|
+
}
|
|
4476
|
+
function readJson(req) {
|
|
4477
|
+
return new Promise((resolve3, reject) => {
|
|
4478
|
+
const chunks = [];
|
|
4479
|
+
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
4480
|
+
req.on("end", () => {
|
|
4481
|
+
try {
|
|
4482
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
4483
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
4484
|
+
resolve3(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
|
|
4485
|
+
} catch (error) {
|
|
4486
|
+
reject(error);
|
|
4487
|
+
}
|
|
4488
|
+
});
|
|
4489
|
+
req.on("error", reject);
|
|
4490
|
+
});
|
|
4491
|
+
}
|
|
4492
|
+
function query(req) {
|
|
4493
|
+
const raw = req.url ?? "";
|
|
4494
|
+
const index = raw.indexOf("?");
|
|
4495
|
+
return new URLSearchParams(index >= 0 ? raw.slice(index + 1) : "");
|
|
4496
|
+
}
|
|
4497
|
+
function allowanceProvider(value) {
|
|
4498
|
+
if (!value) return void 0;
|
|
4499
|
+
return value === "claude" || value === "codex" ? value : null;
|
|
4500
|
+
}
|
|
4501
|
+
async function handleAccountAllowanceApi(req, res, method, rest, service) {
|
|
4502
|
+
if (!service) return writeError(res, 501, "account allowance service is not available");
|
|
4503
|
+
if (method === "GET" && rest.length === 1 && rest[0] === "scheduling") {
|
|
4504
|
+
if (!service.getSchedulingStatus) {
|
|
4505
|
+
return writeError(res, 501, "allowance scheduling diagnostics are not available");
|
|
4506
|
+
}
|
|
4507
|
+
return writeJson2(res, 200, { scheduling: service.getSchedulingStatus() });
|
|
4508
|
+
}
|
|
4509
|
+
if (method === "GET") {
|
|
4510
|
+
const params = query(req);
|
|
4511
|
+
const pathProvider = rest.length >= 2 ? rest[0] : null;
|
|
4512
|
+
const providerId = allowanceProvider(pathProvider ?? params.get("providerId") ?? params.get("provider"));
|
|
4513
|
+
if (providerId === null) return writeError(res, 400, "providerId must be claude or codex");
|
|
4514
|
+
const accountId = rest.length >= 2 ? rest[1] : params.get("accountId") ?? void 0;
|
|
4515
|
+
const allowances = await service.list({ providerId, accountId });
|
|
4516
|
+
return writeJson2(res, 200, { allowances });
|
|
4517
|
+
}
|
|
4518
|
+
if (method === "POST" && rest[0] === "refresh") {
|
|
4519
|
+
const body = await readJson(req);
|
|
4520
|
+
const requestedProvider = allowanceProvider(
|
|
4521
|
+
typeof body["providerId"] === "string" ? body["providerId"] : "claude"
|
|
4522
|
+
);
|
|
4523
|
+
if (requestedProvider !== "claude") {
|
|
4524
|
+
return writeError(res, 400, "only Claude allowances support explicit refresh");
|
|
4525
|
+
}
|
|
4526
|
+
const accountId = typeof body["accountId"] === "string" && body["accountId"].trim() ? body["accountId"].trim() : void 0;
|
|
4527
|
+
const allowances = await service.refreshClaude(accountId);
|
|
4528
|
+
if (accountId && allowances.length === 0) {
|
|
4529
|
+
return writeError(res, 404, `Claude account '${accountId}' not found`);
|
|
4530
|
+
}
|
|
4531
|
+
return writeJson2(res, 200, { allowances });
|
|
4532
|
+
}
|
|
4533
|
+
return writeError(res, 405, `method ${method} not allowed on account allowances`);
|
|
4534
|
+
}
|
|
4535
|
+
|
|
2975
4536
|
// src/admin/adminApi.ts
|
|
2976
4537
|
function readBody(req) {
|
|
2977
|
-
return new Promise((
|
|
4538
|
+
return new Promise((resolve3, reject) => {
|
|
2978
4539
|
const chunks = [];
|
|
2979
4540
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
2980
|
-
req.on("end", () =>
|
|
4541
|
+
req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
|
|
2981
4542
|
req.on("error", reject);
|
|
2982
4543
|
});
|
|
2983
4544
|
}
|
|
@@ -2991,12 +4552,12 @@ async function readJsonBody3(req) {
|
|
|
2991
4552
|
return {};
|
|
2992
4553
|
}
|
|
2993
4554
|
}
|
|
2994
|
-
function
|
|
4555
|
+
function writeJson3(res, status, body) {
|
|
2995
4556
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
2996
4557
|
res.end(JSON.stringify(body));
|
|
2997
4558
|
}
|
|
2998
4559
|
function writeJsonError(res, status, message) {
|
|
2999
|
-
|
|
4560
|
+
writeJson3(res, status, { error: { type: "admin_api_error", message } });
|
|
3000
4561
|
}
|
|
3001
4562
|
function maskProviderApiKey(apiKey) {
|
|
3002
4563
|
if (!apiKey) return "";
|
|
@@ -3013,6 +4574,9 @@ function toKeyInfo(row) {
|
|
|
3013
4574
|
createdAt: row.createdAt,
|
|
3014
4575
|
lastUsedAt: row.lastUsedAt,
|
|
3015
4576
|
revoked: row.revokedAt !== null,
|
|
4577
|
+
kind: row.kind,
|
|
4578
|
+
allowedEndpoints: row.allowedEndpoints,
|
|
4579
|
+
loopbackOnly: row.loopbackOnly,
|
|
3016
4580
|
maxConcurrency: row.maxConcurrency,
|
|
3017
4581
|
// Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
|
|
3018
4582
|
// the UI reads them to render + pre-fill the policy editor.
|
|
@@ -3098,6 +4662,8 @@ async function handleAdminApi(req, res, path2, deps) {
|
|
|
3098
4662
|
return await handleAccounts(req, res, method, rest, deps);
|
|
3099
4663
|
case "cli":
|
|
3100
4664
|
return await handleCli(req, res, method, rest, deps);
|
|
4665
|
+
case "integrations":
|
|
4666
|
+
return await handleIntegrations(req, res, method, rest, deps);
|
|
3101
4667
|
case "status":
|
|
3102
4668
|
return await handleStatus(res, method, deps);
|
|
3103
4669
|
case "playground":
|
|
@@ -3125,7 +4691,7 @@ function requestQuery(req) {
|
|
|
3125
4691
|
return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
|
|
3126
4692
|
}
|
|
3127
4693
|
function writeResult(res, result) {
|
|
3128
|
-
|
|
4694
|
+
writeJson3(res, result.status, result.body);
|
|
3129
4695
|
}
|
|
3130
4696
|
async function handleUsage(req, res, method, rest, deps) {
|
|
3131
4697
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
|
|
@@ -3134,7 +4700,7 @@ async function handleUsage(req, res, method, rest, deps) {
|
|
|
3134
4700
|
async function handleDashboardRoute(res, method, deps) {
|
|
3135
4701
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
|
|
3136
4702
|
const result = await handleDashboard(deps);
|
|
3137
|
-
return
|
|
4703
|
+
return writeJson3(res, result.status, result.body);
|
|
3138
4704
|
}
|
|
3139
4705
|
async function handlePricing(req, res, method, rest, deps) {
|
|
3140
4706
|
if (rest.length === 0) {
|
|
@@ -3167,13 +4733,13 @@ async function handleMigrationExport(req, res, method, deps) {
|
|
|
3167
4733
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
|
|
3168
4734
|
const body = await readJsonBody3(req);
|
|
3169
4735
|
const result = await handleExport(body, migrationDeps(deps));
|
|
3170
|
-
return
|
|
4736
|
+
return writeJson3(res, result.status, result.body);
|
|
3171
4737
|
}
|
|
3172
4738
|
async function handleMigrationImport(req, res, method, deps) {
|
|
3173
4739
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
|
|
3174
4740
|
const body = await readJsonBody3(req);
|
|
3175
4741
|
const result = await handleImport(body, migrationDeps(deps));
|
|
3176
|
-
return
|
|
4742
|
+
return writeJson3(res, result.status, result.body);
|
|
3177
4743
|
}
|
|
3178
4744
|
async function handleProviders(req, res, method, rest, deps) {
|
|
3179
4745
|
const cfg = loadConfig(deps.configPath);
|
|
@@ -3204,10 +4770,10 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
3204
4770
|
if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
|
|
3205
4771
|
const row = cfg.providers.find((p) => p.id === rest[0]);
|
|
3206
4772
|
if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
|
|
3207
|
-
return
|
|
4773
|
+
return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
|
|
3208
4774
|
}
|
|
3209
4775
|
if (method === "GET") {
|
|
3210
|
-
return
|
|
4776
|
+
return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
|
|
3211
4777
|
}
|
|
3212
4778
|
if (method === "POST") {
|
|
3213
4779
|
const body = await readJsonBody3(req);
|
|
@@ -3218,7 +4784,7 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
3218
4784
|
}
|
|
3219
4785
|
cfg.providers.push(provider);
|
|
3220
4786
|
persistProviders(cfg, deps);
|
|
3221
|
-
return
|
|
4787
|
+
return writeJson3(res, 201, { provider: toProviderView(provider) });
|
|
3222
4788
|
}
|
|
3223
4789
|
const id = rest[0];
|
|
3224
4790
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3231,12 +4797,12 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
3231
4797
|
if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
|
|
3232
4798
|
cfg.providers[idx] = updated;
|
|
3233
4799
|
persistProviders(cfg, deps);
|
|
3234
|
-
return
|
|
4800
|
+
return writeJson3(res, 200, { provider: toProviderView(updated) });
|
|
3235
4801
|
}
|
|
3236
4802
|
if (method === "DELETE") {
|
|
3237
4803
|
cfg.providers.splice(idx, 1);
|
|
3238
4804
|
persistProviders(cfg, deps);
|
|
3239
|
-
return
|
|
4805
|
+
return writeJson3(res, 200, { ok: true });
|
|
3240
4806
|
}
|
|
3241
4807
|
return writeJsonError(res, 405, `method ${method} not allowed on providers`);
|
|
3242
4808
|
}
|
|
@@ -3269,14 +4835,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
|
|
|
3269
4835
|
}
|
|
3270
4836
|
cfg.providers = reordered;
|
|
3271
4837
|
persistProviders(cfg, deps);
|
|
3272
|
-
return
|
|
4838
|
+
return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
|
|
3273
4839
|
}
|
|
3274
4840
|
async function handleDiscoverModels(res, id, cfg) {
|
|
3275
4841
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
3276
4842
|
const row = cfg.providers.find((p) => p.id === id);
|
|
3277
4843
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
3278
|
-
if (row.apiFormat !== "openai") {
|
|
3279
|
-
return
|
|
4844
|
+
if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
|
|
4845
|
+
return writeJson3(res, 200, { models: [], unsupportedFormat: true });
|
|
3280
4846
|
}
|
|
3281
4847
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
3282
4848
|
const base = row.baseUrl.replace(/\/+$/, "");
|
|
@@ -3284,7 +4850,7 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
3284
4850
|
try {
|
|
3285
4851
|
const headers = { Accept: "application/json" };
|
|
3286
4852
|
if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
3287
|
-
const response = await
|
|
4853
|
+
const response = await fetchUpstream2(url, { method: "GET", headers }, { providerId: "byo" });
|
|
3288
4854
|
if (!response.ok) {
|
|
3289
4855
|
const text = await response.text().catch(() => "");
|
|
3290
4856
|
let message = text.slice(0, 300);
|
|
@@ -3293,17 +4859,17 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
3293
4859
|
message = parsed?.error?.message || parsed?.message || message;
|
|
3294
4860
|
} catch {
|
|
3295
4861
|
}
|
|
3296
|
-
return
|
|
4862
|
+
return writeJson3(res, 200, {
|
|
3297
4863
|
models: [],
|
|
3298
4864
|
error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
|
|
3299
4865
|
});
|
|
3300
4866
|
}
|
|
3301
4867
|
const data = await response.json();
|
|
3302
4868
|
const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
|
|
3303
|
-
return
|
|
4869
|
+
return writeJson3(res, 200, { models });
|
|
3304
4870
|
} catch (err5) {
|
|
3305
4871
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
3306
|
-
return
|
|
4872
|
+
return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
|
|
3307
4873
|
}
|
|
3308
4874
|
}
|
|
3309
4875
|
async function handleTestModel(req, res, id, cfg) {
|
|
@@ -3314,13 +4880,13 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3314
4880
|
const model = typeof body["model"] === "string" ? body["model"].trim() : "";
|
|
3315
4881
|
if (!model) return writeJsonError(res, 400, "test requires a { model } string");
|
|
3316
4882
|
if (row.apiFormat === "gemini") {
|
|
3317
|
-
return
|
|
4883
|
+
return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
|
|
3318
4884
|
}
|
|
3319
4885
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
3320
4886
|
if (!resolvedKey) {
|
|
3321
|
-
return
|
|
4887
|
+
return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
|
|
3322
4888
|
}
|
|
3323
|
-
|
|
4889
|
+
let url = row.baseUrl.replace(/\/+$/, "");
|
|
3324
4890
|
const prompt = "Reply with the single word: OK.";
|
|
3325
4891
|
const headers = { "Content-Type": "application/json" };
|
|
3326
4892
|
let payload;
|
|
@@ -3328,6 +4894,10 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3328
4894
|
headers["x-api-key"] = resolvedKey;
|
|
3329
4895
|
headers["anthropic-version"] = "2023-06-01";
|
|
3330
4896
|
payload = { model, max_tokens: 16, messages: [{ role: "user", content: prompt }] };
|
|
4897
|
+
} else if (row.apiFormat === "openai-response") {
|
|
4898
|
+
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
4899
|
+
if (!/\/responses$/.test(url)) url = `${url}/v1/responses`;
|
|
4900
|
+
payload = { model, max_output_tokens: 16, stream: false, input: prompt };
|
|
3331
4901
|
} else {
|
|
3332
4902
|
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
3333
4903
|
payload = {
|
|
@@ -3339,7 +4909,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3339
4909
|
}
|
|
3340
4910
|
const startedAt = Date.now();
|
|
3341
4911
|
try {
|
|
3342
|
-
const response = await
|
|
4912
|
+
const response = await fetchUpstream2(
|
|
3343
4913
|
url,
|
|
3344
4914
|
{ method: "POST", headers, body: JSON.stringify(payload) },
|
|
3345
4915
|
{ providerId: "byo" }
|
|
@@ -3353,9 +4923,9 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3353
4923
|
message = parsed?.error?.message || parsed?.message || message;
|
|
3354
4924
|
} catch {
|
|
3355
4925
|
}
|
|
3356
|
-
return
|
|
4926
|
+
return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
|
|
3357
4927
|
}
|
|
3358
|
-
return
|
|
4928
|
+
return writeJson3(res, 200, {
|
|
3359
4929
|
ok: true,
|
|
3360
4930
|
status: response.status,
|
|
3361
4931
|
latencyMs,
|
|
@@ -3363,7 +4933,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3363
4933
|
});
|
|
3364
4934
|
} catch (err5) {
|
|
3365
4935
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
3366
|
-
return
|
|
4936
|
+
return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
|
|
3367
4937
|
}
|
|
3368
4938
|
}
|
|
3369
4939
|
function extractSampleText(text, apiFormat) {
|
|
@@ -3404,7 +4974,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
|
|
|
3404
4974
|
const row = cfg.providers.find((p) => p.id === id);
|
|
3405
4975
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
3406
4976
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3407
|
-
return
|
|
4977
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3408
4978
|
}
|
|
3409
4979
|
function parsePoolKeyInput(body, existing) {
|
|
3410
4980
|
const out = {};
|
|
@@ -3435,7 +5005,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
|
|
|
3435
5005
|
row.apiKeys = [...row.apiKeys ?? [], entry];
|
|
3436
5006
|
persistProviders(cfg, deps);
|
|
3437
5007
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3438
|
-
return
|
|
5008
|
+
return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3439
5009
|
}
|
|
3440
5010
|
async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
3441
5011
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3455,7 +5025,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
3455
5025
|
row.apiKeys[keyIdx] = entry;
|
|
3456
5026
|
persistProviders(cfg, deps);
|
|
3457
5027
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3458
|
-
return
|
|
5028
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3459
5029
|
}
|
|
3460
5030
|
async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
3461
5031
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3469,7 +5039,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
|
3469
5039
|
if (row.apiKeys.length === 0) row.apiKeys = void 0;
|
|
3470
5040
|
persistProviders(cfg, deps);
|
|
3471
5041
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3472
|
-
return
|
|
5042
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3473
5043
|
}
|
|
3474
5044
|
async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
3475
5045
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3483,7 +5053,7 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
3483
5053
|
row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
|
|
3484
5054
|
persistProviders(cfg, deps);
|
|
3485
5055
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3486
|
-
return
|
|
5056
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3487
5057
|
}
|
|
3488
5058
|
function parseApiKeysInput(raw, existing) {
|
|
3489
5059
|
if (!Array.isArray(raw)) return existing;
|
|
@@ -3601,7 +5171,9 @@ function parseProviderInput(body, existing) {
|
|
|
3601
5171
|
const baseUrl = body["baseUrl"];
|
|
3602
5172
|
if (!id) return null;
|
|
3603
5173
|
const name = typeof body["name"] === "string" && body["name"].length > 0 ? body["name"] : body["name"] === null ? void 0 : existing?.name;
|
|
3604
|
-
if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini")
|
|
5174
|
+
if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini" && apiFormat !== "openai-response") {
|
|
5175
|
+
return null;
|
|
5176
|
+
}
|
|
3605
5177
|
if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
|
|
3606
5178
|
const rawKey = body["apiKey"];
|
|
3607
5179
|
let apiKey = typeof rawKey === "string" && rawKey.length > 0 ? rawKey : existing?.apiKey ?? "";
|
|
@@ -3623,10 +5195,11 @@ function parseProviderInput(body, existing) {
|
|
|
3623
5195
|
apiKey = mode.apiKey;
|
|
3624
5196
|
}
|
|
3625
5197
|
}
|
|
5198
|
+
const migrated = migrateFormatAxis(apiFormat, transformer);
|
|
3626
5199
|
return {
|
|
3627
5200
|
id,
|
|
3628
5201
|
name,
|
|
3629
|
-
apiFormat,
|
|
5202
|
+
apiFormat: migrated.apiFormat,
|
|
3630
5203
|
baseUrl: baseUrl.trim(),
|
|
3631
5204
|
apiKey,
|
|
3632
5205
|
models,
|
|
@@ -3637,7 +5210,7 @@ function parseProviderInput(body, existing) {
|
|
|
3637
5210
|
apiVersion,
|
|
3638
5211
|
maxConcurrency,
|
|
3639
5212
|
modelsEndpoint,
|
|
3640
|
-
transformer,
|
|
5213
|
+
transformer: migrated.transformer,
|
|
3641
5214
|
codingPlan,
|
|
3642
5215
|
apiModes,
|
|
3643
5216
|
selectedApiModeId
|
|
@@ -3654,13 +5227,13 @@ function handlePresets(res, method) {
|
|
|
3654
5227
|
baseUrl: p.baseUrl,
|
|
3655
5228
|
models: p.models
|
|
3656
5229
|
}));
|
|
3657
|
-
return
|
|
5230
|
+
return writeJson3(res, 200, { presets, excluded });
|
|
3658
5231
|
}
|
|
3659
5232
|
async function handleKeys(req, res, method, rest, deps) {
|
|
3660
5233
|
if (method === "GET" && rest.length === 0) {
|
|
3661
5234
|
const rows = await deps.keyDb.outboundApiKeysList();
|
|
3662
5235
|
const reader = deps.keySpendReader;
|
|
3663
|
-
if (!reader) return
|
|
5236
|
+
if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
|
|
3664
5237
|
const now = Date.now();
|
|
3665
5238
|
const keys = await Promise.all(
|
|
3666
5239
|
rows.map(async (row) => {
|
|
@@ -3672,13 +5245,13 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3672
5245
|
return info;
|
|
3673
5246
|
})
|
|
3674
5247
|
);
|
|
3675
|
-
return
|
|
5248
|
+
return writeJson3(res, 200, { keys });
|
|
3676
5249
|
}
|
|
3677
5250
|
if (method === "POST" && rest.length === 0) {
|
|
3678
5251
|
const body = await readJsonBody3(req);
|
|
3679
5252
|
const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
|
|
3680
5253
|
const created = await createNamedKey2(deps.keyDb, name);
|
|
3681
|
-
return
|
|
5254
|
+
return writeJson3(res, 201, {
|
|
3682
5255
|
id: created.id,
|
|
3683
5256
|
name: created.name,
|
|
3684
5257
|
keyPrefix: created.keyPrefix,
|
|
@@ -3690,13 +5263,13 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3690
5263
|
const action = rest[1];
|
|
3691
5264
|
if (method === "POST" && id && action === "revoke") {
|
|
3692
5265
|
const ok = await deps.keyDb.outboundApiKeysRevoke(id);
|
|
3693
|
-
return
|
|
5266
|
+
return writeJson3(res, ok ? 200 : 404, { ok });
|
|
3694
5267
|
}
|
|
3695
5268
|
if (method === "POST" && id && action === "enabled") {
|
|
3696
5269
|
const body = await readJsonBody3(req);
|
|
3697
5270
|
const enabled = body["enabled"] === true;
|
|
3698
5271
|
const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
|
|
3699
|
-
return
|
|
5272
|
+
return writeJson3(res, ok ? 200 : 404, { ok, enabled });
|
|
3700
5273
|
}
|
|
3701
5274
|
if (method === "POST" && id && action === "max-concurrency") {
|
|
3702
5275
|
const body = await readJsonBody3(req);
|
|
@@ -3714,14 +5287,14 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3714
5287
|
);
|
|
3715
5288
|
}
|
|
3716
5289
|
const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
|
|
3717
|
-
return
|
|
5290
|
+
return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
|
|
3718
5291
|
}
|
|
3719
5292
|
if (method === "POST" && id && action === "policy") {
|
|
3720
5293
|
const body = await readJsonBody3(req);
|
|
3721
5294
|
const parsed = parseKeyPolicyBody(body);
|
|
3722
5295
|
if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
|
|
3723
5296
|
const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
|
|
3724
|
-
return
|
|
5297
|
+
return writeJson3(res, ok ? 200 : 404, { ok });
|
|
3725
5298
|
}
|
|
3726
5299
|
return writeJsonError(res, 405, `method ${method} not allowed on keys`);
|
|
3727
5300
|
}
|
|
@@ -3732,10 +5305,10 @@ function validateQueueSegments(patch) {
|
|
|
3732
5305
|
errors.push(`${label} must be a number ${min}..${max}`);
|
|
3733
5306
|
}
|
|
3734
5307
|
};
|
|
3735
|
-
const
|
|
5308
|
+
const isPlainObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3736
5309
|
const umq = patch.userMessageQueue;
|
|
3737
5310
|
if (umq !== void 0) {
|
|
3738
|
-
if (!
|
|
5311
|
+
if (!isPlainObject5(umq)) {
|
|
3739
5312
|
errors.push("userMessageQueue must be an object");
|
|
3740
5313
|
} else {
|
|
3741
5314
|
if (typeof umq.enabled !== "boolean") {
|
|
@@ -3747,7 +5320,7 @@ function validateQueueSegments(patch) {
|
|
|
3747
5320
|
}
|
|
3748
5321
|
const cq = patch.concurrencyQueue;
|
|
3749
5322
|
if (cq !== void 0) {
|
|
3750
|
-
if (!
|
|
5323
|
+
if (!isPlainObject5(cq)) {
|
|
3751
5324
|
errors.push("concurrencyQueue must be an object");
|
|
3752
5325
|
} else {
|
|
3753
5326
|
checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
|
|
@@ -3757,7 +5330,7 @@ function validateQueueSegments(patch) {
|
|
|
3757
5330
|
}
|
|
3758
5331
|
const ah = patch.accountHealth;
|
|
3759
5332
|
if (ah !== void 0) {
|
|
3760
|
-
if (!
|
|
5333
|
+
if (!isPlainObject5(ah)) {
|
|
3761
5334
|
errors.push("accountHealth must be an object");
|
|
3762
5335
|
} else {
|
|
3763
5336
|
if (typeof ah.overloadCooldownEnabled !== "boolean") {
|
|
@@ -3768,6 +5341,31 @@ function validateQueueSegments(patch) {
|
|
|
3768
5341
|
}
|
|
3769
5342
|
return errors;
|
|
3770
5343
|
}
|
|
5344
|
+
function validateAllowanceSchedulingSegment(patch) {
|
|
5345
|
+
const value = patch.allowanceScheduling;
|
|
5346
|
+
if (value === void 0) return [];
|
|
5347
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5348
|
+
return ["allowanceScheduling must be an object"];
|
|
5349
|
+
}
|
|
5350
|
+
const allowance = value;
|
|
5351
|
+
const errors = [];
|
|
5352
|
+
const checkNumber = (field, min, max) => {
|
|
5353
|
+
const candidate = allowance[field];
|
|
5354
|
+
if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate < min || candidate > max) {
|
|
5355
|
+
errors.push(`allowanceScheduling.${field} must be a number ${min}..${max}`);
|
|
5356
|
+
}
|
|
5357
|
+
};
|
|
5358
|
+
if (typeof allowance.enabled !== "boolean") {
|
|
5359
|
+
errors.push("allowanceScheduling.enabled must be a boolean");
|
|
5360
|
+
}
|
|
5361
|
+
checkNumber("demoteAtPercent", 0, 100);
|
|
5362
|
+
checkNumber("pauseAtPercent", 0, 100);
|
|
5363
|
+
checkNumber("priorityPenalty", 1, 1e3);
|
|
5364
|
+
if (typeof allowance.demoteAtPercent === "number" && typeof allowance.pauseAtPercent === "number" && allowance.pauseAtPercent < allowance.demoteAtPercent) {
|
|
5365
|
+
errors.push("allowanceScheduling.pauseAtPercent must be >= demoteAtPercent");
|
|
5366
|
+
}
|
|
5367
|
+
return errors;
|
|
5368
|
+
}
|
|
3771
5369
|
async function handleServer(req, res, method, deps) {
|
|
3772
5370
|
if (method === "GET") {
|
|
3773
5371
|
const config = await loadServerConfig2(deps.settingsStore);
|
|
@@ -3775,7 +5373,7 @@ async function handleServer(req, res, method, deps) {
|
|
|
3775
5373
|
if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
|
|
3776
5374
|
if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
|
|
3777
5375
|
if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
|
|
3778
|
-
return
|
|
5376
|
+
return writeJson3(res, 200, { server });
|
|
3779
5377
|
}
|
|
3780
5378
|
if (method === "PUT") {
|
|
3781
5379
|
const patch = await readJsonBody3(req);
|
|
@@ -3783,6 +5381,18 @@ async function handleServer(req, res, method, deps) {
|
|
|
3783
5381
|
if (queueErrors.length > 0) {
|
|
3784
5382
|
return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
|
|
3785
5383
|
}
|
|
5384
|
+
const allowanceErrors = validateAllowanceSchedulingSegment(patch);
|
|
5385
|
+
if (allowanceErrors.length > 0) {
|
|
5386
|
+
return writeJsonError(
|
|
5387
|
+
res,
|
|
5388
|
+
400,
|
|
5389
|
+
`invalid allowance scheduling config: ${allowanceErrors.join("; ")}`
|
|
5390
|
+
);
|
|
5391
|
+
}
|
|
5392
|
+
const bindingErrors = validateGatewayBindingsSegment(patch);
|
|
5393
|
+
if (bindingErrors.length > 0) {
|
|
5394
|
+
return writeJsonError(res, 400, `invalid gateway bindings: ${bindingErrors.join("; ")}`);
|
|
5395
|
+
}
|
|
3786
5396
|
const webhookErrors = validateWebhookSegment(patch);
|
|
3787
5397
|
if (webhookErrors.length > 0) {
|
|
3788
5398
|
return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
|
|
@@ -3809,66 +5419,113 @@ async function handleServer(req, res, method, deps) {
|
|
|
3809
5419
|
const merged = mergeServerConfig(current, effectivePatch);
|
|
3810
5420
|
await saveServerConfig(deps.settingsStore, merged);
|
|
3811
5421
|
setServerProxyConfig(merged.proxy);
|
|
5422
|
+
getSharedAccountAllowanceScheduling2().configure(merged.allowanceScheduling);
|
|
5423
|
+
deps.allowanceRefreshScheduler?.configure(merged.allowanceScheduling);
|
|
3812
5424
|
applyWebhookConfig(merged.webhook);
|
|
3813
5425
|
applyAuditConfig(merged.audit);
|
|
3814
5426
|
applyBillingConfig(merged.billing);
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
}
|
|
3827
|
-
|
|
3828
|
-
await deps.outboundApiServer.applyConfig({
|
|
3829
|
-
enabled: merged.enabled,
|
|
3830
|
-
networkBinding: merged.networkBinding,
|
|
3831
|
-
endpoints: merged.endpoints,
|
|
3832
|
-
port: merged.port,
|
|
3833
|
-
userMessageQueue: merged.userMessageQueue,
|
|
3834
|
-
concurrencyQueue: merged.concurrencyQueue,
|
|
3835
|
-
// voucher-redemption #9: hot-apply the voucher flag so enabling the product
|
|
3836
|
-
// takes effect without a restart.
|
|
3837
|
-
voucher: merged.voucher
|
|
3838
|
-
});
|
|
3839
|
-
} catch (err5) {
|
|
3840
|
-
const missing = incompleteConfigMissing(err5);
|
|
3841
|
-
if (missing) {
|
|
3842
|
-
return writeJson2(res, 200, {
|
|
3843
|
-
server: merged,
|
|
3844
|
-
error: { code: "incomplete-model-config", missing }
|
|
3845
|
-
});
|
|
3846
|
-
}
|
|
3847
|
-
throw err5;
|
|
3848
|
-
}
|
|
3849
|
-
return writeJson2(res, 200, { server: merged });
|
|
5427
|
+
await deps.outboundApiServer.applyConfig({
|
|
5428
|
+
enabled: merged.enabled,
|
|
5429
|
+
networkBinding: merged.networkBinding,
|
|
5430
|
+
endpoints: merged.endpoints,
|
|
5431
|
+
bindings: merged.bindings,
|
|
5432
|
+
port: merged.port,
|
|
5433
|
+
userMessageQueue: merged.userMessageQueue,
|
|
5434
|
+
concurrencyQueue: merged.concurrencyQueue,
|
|
5435
|
+
// voucher-redemption #9: hot-apply the voucher flag so enabling the product
|
|
5436
|
+
// takes effect without a restart.
|
|
5437
|
+
voucher: merged.voucher
|
|
5438
|
+
});
|
|
5439
|
+
return writeJson3(res, 200, { server: merged });
|
|
3850
5440
|
}
|
|
3851
5441
|
return writeJsonError(res, 405, `method ${method} not allowed on server`);
|
|
3852
5442
|
}
|
|
3853
|
-
function incompleteConfigMissing(err5) {
|
|
3854
|
-
if (typeof err5 !== "object" || err5 === null) return null;
|
|
3855
|
-
const missing = err5.missing;
|
|
3856
|
-
return Array.isArray(missing) ? missing : null;
|
|
3857
|
-
}
|
|
3858
5443
|
async function handleAccounts(req, res, method, rest, deps) {
|
|
5444
|
+
if (rest[0] === "allowances") {
|
|
5445
|
+
return handleAccountAllowanceApi(
|
|
5446
|
+
req,
|
|
5447
|
+
res,
|
|
5448
|
+
method,
|
|
5449
|
+
rest.slice(1),
|
|
5450
|
+
deps.accountAllowanceService
|
|
5451
|
+
);
|
|
5452
|
+
}
|
|
3859
5453
|
if (method === "GET" && rest.length === 0) {
|
|
3860
5454
|
const accounts = await deps.subscriptionAccounts.listAll();
|
|
3861
5455
|
const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
3862
5456
|
const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
|
|
3863
|
-
return
|
|
5457
|
+
return writeJson3(res, 200, { accounts, providerAccounts, externalCli });
|
|
5458
|
+
}
|
|
5459
|
+
if (method === "POST" && rest[0] === "batch" && rest.length === 1) {
|
|
5460
|
+
const body = await readJsonBody3(req);
|
|
5461
|
+
const parsed = validateAccountBatchBody(body);
|
|
5462
|
+
if (!parsed) return writeJsonError(res, 400, "invalid account batch request");
|
|
5463
|
+
const result = await deps.subscriptionTokenWriter.batchManageAccounts(parsed.refs, parsed.mutation);
|
|
5464
|
+
if (!result.ok) {
|
|
5465
|
+
return writeJsonError(
|
|
5466
|
+
res,
|
|
5467
|
+
404,
|
|
5468
|
+
`account '${result.missing.accountId}' not found for provider '${result.missing.providerId}'`
|
|
5469
|
+
);
|
|
5470
|
+
}
|
|
5471
|
+
if (parsed.mutation.action === "delete") {
|
|
5472
|
+
for (const ref of parsed.refs) {
|
|
5473
|
+
deps.accountAllowanceService?.removeAccountSnapshot?.(ref.providerId, ref.accountId);
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
return writeJson3(res, 200, { ok: true, affected: result.affected });
|
|
3864
5477
|
}
|
|
3865
5478
|
if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
|
|
3866
5479
|
const result = handleCodexOAuthStatus(rest[2], deps);
|
|
3867
|
-
return
|
|
5480
|
+
return writeJson3(res, result.status, result.body);
|
|
3868
5481
|
}
|
|
3869
5482
|
if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
|
|
3870
5483
|
const result = handleCodexOAuthCancel(rest[2], deps);
|
|
3871
|
-
return
|
|
5484
|
+
return writeJson3(res, result.status, result.body);
|
|
5485
|
+
}
|
|
5486
|
+
if (method === "GET" && rest.length === 3 && rest[2] === "diagnostics") {
|
|
5487
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5488
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5489
|
+
const accountId = rest[1];
|
|
5490
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5491
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5492
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5493
|
+
}
|
|
5494
|
+
const health2 = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
|
|
5495
|
+
const allowance = deps.accountAllowanceService?.getSchedulingStatus?.()?.history.filter((entry) => entry.providerId === providerId && entry.accountId === accountId).map((entry) => ({
|
|
5496
|
+
kind: "allowance-policy",
|
|
5497
|
+
at: Date.parse(entry.decidedAt),
|
|
5498
|
+
providerId: entry.providerId,
|
|
5499
|
+
accountId: entry.accountId,
|
|
5500
|
+
action: entry.action,
|
|
5501
|
+
reason: entry.reason,
|
|
5502
|
+
usedPercent: entry.usedPercent,
|
|
5503
|
+
resumeAt: entry.resumeAt
|
|
5504
|
+
})) ?? [];
|
|
5505
|
+
const diagnostics = [...health2, ...allowance].sort((left, right) => right.at - left.at).slice(0, 200);
|
|
5506
|
+
return writeJson3(res, 200, { diagnostics });
|
|
5507
|
+
}
|
|
5508
|
+
if (method === "GET" && rest.length === 3 && rest[2] === "events") {
|
|
5509
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5510
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5511
|
+
const accountId = rest[1];
|
|
5512
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5513
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5514
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5515
|
+
}
|
|
5516
|
+
const snapshot = deps.accountProbeService?.getAllHistory().find((entry) => entry.providerId === providerId && entry.accountId === accountId);
|
|
5517
|
+
const diagnostics = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
|
|
5518
|
+
return writeJson3(res, 200, { events: snapshot?.records ?? [], diagnostics });
|
|
5519
|
+
}
|
|
5520
|
+
if (method === "PATCH" && rest.length === 2) {
|
|
5521
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5522
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5523
|
+
const body = await readJsonBody3(req);
|
|
5524
|
+
const patch = validateAccountMetadataPatch(body);
|
|
5525
|
+
if (!patch) return writeJsonError(res, 400, "invalid account metadata patch");
|
|
5526
|
+
const result = await deps.subscriptionTokenWriter.patchAccountMetadata(providerId, rest[1], patch);
|
|
5527
|
+
if (!result.ok) return writeJsonError(res, 404, `account '${rest[1]}' not found`);
|
|
5528
|
+
return writeJson3(res, 200, { ok: true });
|
|
3872
5529
|
}
|
|
3873
5530
|
if (method === "PUT" || method === "POST" || method === "DELETE") {
|
|
3874
5531
|
const providerId = asSubscriptionProviderId(rest[0]);
|
|
@@ -3877,12 +5534,12 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3877
5534
|
}
|
|
3878
5535
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
|
|
3879
5536
|
const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
|
|
3880
|
-
return
|
|
5537
|
+
return writeJson3(res, result.status, result.body);
|
|
3881
5538
|
}
|
|
3882
5539
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
|
|
3883
5540
|
const body2 = await readJsonBody3(req);
|
|
3884
5541
|
const result = await handleOAuthComplete(providerId, body2, deps);
|
|
3885
|
-
return
|
|
5542
|
+
return writeJson3(res, result.status, result.body);
|
|
3886
5543
|
}
|
|
3887
5544
|
if (method === "POST" && rest[1] === "accounts") {
|
|
3888
5545
|
const body2 = await readJsonBody3(req);
|
|
@@ -3893,7 +5550,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3893
5550
|
const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
|
|
3894
5551
|
await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
|
|
3895
5552
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3896
|
-
return
|
|
5553
|
+
return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
|
|
3897
5554
|
}
|
|
3898
5555
|
if (method === "POST" && rest[1] === "import-external") {
|
|
3899
5556
|
if (providerId !== "claude" && providerId !== "codex") {
|
|
@@ -3906,7 +5563,13 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3906
5563
|
return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
|
|
3907
5564
|
}
|
|
3908
5565
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3909
|
-
return
|
|
5566
|
+
return writeJson3(res, 200, {
|
|
5567
|
+
ok: true,
|
|
5568
|
+
account: status2 ?? void 0,
|
|
5569
|
+
nativeCredentialMode: result.nativeCredentialMode,
|
|
5570
|
+
refreshWritesNativeCredentials: result.refreshWritesNativeCredentials,
|
|
5571
|
+
message: "Imported a read-only copy. Omnicross does not manage the native CLI credential file and future refreshes do not write it."
|
|
5572
|
+
});
|
|
3910
5573
|
}
|
|
3911
5574
|
if (method === "POST" && rest[1] === "refresh") {
|
|
3912
5575
|
if (providerId === "opencodego") {
|
|
@@ -3915,7 +5578,17 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3915
5578
|
const writer2 = deps.subscriptionTokenWriter;
|
|
3916
5579
|
const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
|
|
3917
5580
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3918
|
-
return
|
|
5581
|
+
return writeJson3(res, 200, { ok, account: status2 ?? void 0 });
|
|
5582
|
+
}
|
|
5583
|
+
if (method === "POST" && rest.length === 3 && rest[2] === "test") {
|
|
5584
|
+
const accountId = rest[1];
|
|
5585
|
+
if (!deps.accountProbeService) return writeJsonError(res, 501, "account probe service unavailable");
|
|
5586
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5587
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5588
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5589
|
+
}
|
|
5590
|
+
const result = await deps.accountProbeService.probeAccount(providerId, accountId);
|
|
5591
|
+
return writeJson3(res, 200, { ok: result.ok, marked: result.marked });
|
|
3919
5592
|
}
|
|
3920
5593
|
if (method === "POST" && rest[2] === "label") {
|
|
3921
5594
|
const accountId = rest[1];
|
|
@@ -3923,7 +5596,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3923
5596
|
const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
|
|
3924
5597
|
const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
|
|
3925
5598
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3926
|
-
return
|
|
5599
|
+
return writeJson3(res, 200, { ok: true });
|
|
3927
5600
|
}
|
|
3928
5601
|
if (method === "POST" && rest[2] === "priority") {
|
|
3929
5602
|
const accountId = rest[1];
|
|
@@ -3935,7 +5608,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3935
5608
|
}
|
|
3936
5609
|
const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
|
|
3937
5610
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3938
|
-
return
|
|
5611
|
+
return writeJson3(res, 200, { ok: true });
|
|
3939
5612
|
}
|
|
3940
5613
|
if (method === "POST" && rest[2] === "proxy") {
|
|
3941
5614
|
const accountId = rest[1];
|
|
@@ -3948,7 +5621,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3948
5621
|
}
|
|
3949
5622
|
const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
|
|
3950
5623
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3951
|
-
return
|
|
5624
|
+
return writeJson3(res, 200, { ok: true });
|
|
3952
5625
|
}
|
|
3953
5626
|
if (method === "POST" && rest[2] === "supported-models") {
|
|
3954
5627
|
const accountId = rest[1];
|
|
@@ -3957,7 +5630,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3957
5630
|
if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
|
|
3958
5631
|
const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
|
|
3959
5632
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3960
|
-
return
|
|
5633
|
+
return writeJson3(res, 200, { ok: true });
|
|
3961
5634
|
}
|
|
3962
5635
|
if (method === "PUT" && rest[1] === "active") {
|
|
3963
5636
|
const body2 = await readJsonBody3(req);
|
|
@@ -3965,17 +5638,22 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3965
5638
|
if (!id) return writeJsonError(res, 400, "active switch requires { id }");
|
|
3966
5639
|
const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
|
|
3967
5640
|
if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
|
|
3968
|
-
return
|
|
5641
|
+
return writeJson3(res, 200, { ok: true });
|
|
3969
5642
|
}
|
|
3970
|
-
if (method === "DELETE" && rest.length
|
|
5643
|
+
if (method === "DELETE" && rest.length === 2) {
|
|
3971
5644
|
const accountId = rest[1];
|
|
3972
5645
|
const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
|
|
3973
5646
|
if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3974
|
-
|
|
5647
|
+
deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
|
|
5648
|
+
return writeJson3(res, 200, { ok: true });
|
|
3975
5649
|
}
|
|
3976
|
-
if (method === "DELETE") {
|
|
5650
|
+
if (method === "DELETE" && rest.length === 1) {
|
|
3977
5651
|
await deps.subscriptionTokenWriter.clearProvider(providerId);
|
|
3978
|
-
|
|
5652
|
+
deps.accountAllowanceService?.removeProviderSnapshots?.(providerId);
|
|
5653
|
+
return writeJson3(res, 200, { ok: true });
|
|
5654
|
+
}
|
|
5655
|
+
if (method === "DELETE") {
|
|
5656
|
+
return writeJsonError(res, 405, "method DELETE not allowed on this accounts path");
|
|
3979
5657
|
}
|
|
3980
5658
|
const body = await readJsonBody3(req);
|
|
3981
5659
|
const config = validateTokenBody(providerId, body);
|
|
@@ -3984,22 +5662,22 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3984
5662
|
}
|
|
3985
5663
|
await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
|
|
3986
5664
|
const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3987
|
-
return
|
|
5665
|
+
return writeJson3(res, 200, status ? { account: status } : { ok: true });
|
|
3988
5666
|
}
|
|
3989
5667
|
return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
|
|
3990
5668
|
}
|
|
3991
5669
|
async function handleCli(req, res, method, rest, deps) {
|
|
3992
5670
|
if (method === "GET" && rest.length === 0) {
|
|
3993
5671
|
const result = handleCliList(process.platform, deps.cliPathProbe);
|
|
3994
|
-
return
|
|
5672
|
+
return writeJson3(res, result.status, result.body);
|
|
3995
5673
|
}
|
|
3996
5674
|
if (method === "GET" && rest[0] === "sessions") {
|
|
3997
5675
|
const result = handleCliSessions();
|
|
3998
|
-
return
|
|
5676
|
+
return writeJson3(res, result.status, result.body);
|
|
3999
5677
|
}
|
|
4000
5678
|
if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
|
|
4001
5679
|
const result = handleCliStop(rest[1]);
|
|
4002
|
-
return
|
|
5680
|
+
return writeJson3(res, result.status, result.body);
|
|
4003
5681
|
}
|
|
4004
5682
|
if (method === "POST" && rest[1] === "install") {
|
|
4005
5683
|
const cli = rest[0];
|
|
@@ -4007,7 +5685,7 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
4007
5685
|
return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
|
|
4008
5686
|
}
|
|
4009
5687
|
const result = await handleCliInstall(cli, deps.cliCommandRunner);
|
|
4010
|
-
return
|
|
5688
|
+
return writeJson3(res, result.status, result.body);
|
|
4011
5689
|
}
|
|
4012
5690
|
if (method === "POST" && rest[1] === "launch") {
|
|
4013
5691
|
const cli = rest[0];
|
|
@@ -4022,28 +5700,100 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
4022
5700
|
opener: deps.cliTerminalOpener,
|
|
4023
5701
|
probe: deps.cliPathProbe
|
|
4024
5702
|
});
|
|
4025
|
-
return
|
|
5703
|
+
return writeJson3(res, result.status, result.body);
|
|
4026
5704
|
}
|
|
4027
5705
|
return writeJsonError(res, 405, `method ${method} not allowed on cli`);
|
|
4028
5706
|
}
|
|
5707
|
+
async function handleIntegrations(req, res, method, rest, deps) {
|
|
5708
|
+
const factory = deps.integrationManagerFactory;
|
|
5709
|
+
if (!factory) return writeJsonError(res, 501, "native CLI integration is not available");
|
|
5710
|
+
const manager = factory();
|
|
5711
|
+
try {
|
|
5712
|
+
if (method === "GET" && rest.length === 0) {
|
|
5713
|
+
return writeJson3(res, 200, {
|
|
5714
|
+
integrations: await manager.listStatus(),
|
|
5715
|
+
gateway: deps.outboundApiServer.getStatus()
|
|
5716
|
+
});
|
|
5717
|
+
}
|
|
5718
|
+
if (method === "POST" && rest.length === 1 && rest[0] === "rotate") {
|
|
5719
|
+
await manager.rotateGatewayKey();
|
|
5720
|
+
return writeJson3(res, 200, { ok: true, integrations: await manager.listStatus() });
|
|
5721
|
+
}
|
|
5722
|
+
const client = rest[0];
|
|
5723
|
+
if (!isIntegrationClient(client)) {
|
|
5724
|
+
return writeJsonError(res, 400, `unknown integration client '${client ?? ""}'`);
|
|
5725
|
+
}
|
|
5726
|
+
if (method === "POST" && rest[1] === "plan") {
|
|
5727
|
+
const body = await readJsonBody3(req);
|
|
5728
|
+
const configPath = body.configPath;
|
|
5729
|
+
if (configPath !== void 0 && typeof configPath !== "string") {
|
|
5730
|
+
return writeJsonError(res, 400, "configPath must be a string");
|
|
5731
|
+
}
|
|
5732
|
+
const plan = await manager.plan(client, configPath);
|
|
5733
|
+
return writeJson3(res, 200, { plan });
|
|
5734
|
+
}
|
|
5735
|
+
if (method === "POST" && (rest[1] === "install" || rest[1] === "apply")) {
|
|
5736
|
+
const body = await readJsonBody3(req);
|
|
5737
|
+
const configPath = body.configPath;
|
|
5738
|
+
if (configPath !== void 0 && typeof configPath !== "string") {
|
|
5739
|
+
return writeJsonError(res, 400, "configPath must be a string");
|
|
5740
|
+
}
|
|
5741
|
+
const status = await manager.install(client, configPath);
|
|
5742
|
+
return writeJson3(res, 200, { integration: status });
|
|
5743
|
+
}
|
|
5744
|
+
if (method === "POST" && rest[1] === "repair") {
|
|
5745
|
+
const status = await manager.repair(client);
|
|
5746
|
+
return writeJson3(res, 200, { integration: status });
|
|
5747
|
+
}
|
|
5748
|
+
if (method === "DELETE" && rest.length === 1 || method === "POST" && rest[1] === "remove") {
|
|
5749
|
+
const status = await manager.remove(client);
|
|
5750
|
+
return writeJson3(res, 200, { integration: status });
|
|
5751
|
+
}
|
|
5752
|
+
return writeJsonError(res, 405, `method ${method} not allowed on integrations`);
|
|
5753
|
+
} catch (error) {
|
|
5754
|
+
if (error instanceof IntegrationConflictError) {
|
|
5755
|
+
return writeJsonError(res, 409, error.message);
|
|
5756
|
+
}
|
|
5757
|
+
throw error;
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
function isIntegrationClient(value) {
|
|
5761
|
+
return value === "codex" || value === "claude";
|
|
5762
|
+
}
|
|
4029
5763
|
async function handleStatus(res, method, deps) {
|
|
4030
5764
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
|
|
4031
5765
|
const status = deps.outboundApiServer.getStatus();
|
|
4032
5766
|
const serverConfig = await loadServerConfig2(deps.settingsStore);
|
|
4033
|
-
const endpoints =
|
|
4034
|
-
|
|
4035
|
-
|
|
5767
|
+
const endpoints = ["chat", "responses", "messages", "gemini"].map((endpoint) => {
|
|
5768
|
+
const routes = (serverConfig.bindings ?? []).filter((binding) => binding.enabled && binding.endpoint === endpoint).map((binding) => gatewayBindingToEndpointConfig(binding));
|
|
5769
|
+
const useSubscription = routes.some((route) => route.useSubscription);
|
|
5770
|
+
if (isKindMappedEndpoint(endpoint)) {
|
|
5771
|
+
const kinds = {};
|
|
5772
|
+
for (const route of routes) {
|
|
5773
|
+
for (const [kind, ref] of Object.entries(route.modelMap ?? {})) {
|
|
5774
|
+
if (ref?.trim() && !kinds[kind]) kinds[kind] = ref;
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
return { endpoint, kinds, useSubscription };
|
|
4036
5778
|
}
|
|
4037
|
-
if (
|
|
4038
|
-
return {
|
|
5779
|
+
if (endpoint === "chat") {
|
|
5780
|
+
return {
|
|
5781
|
+
endpoint,
|
|
5782
|
+
models: [...new Set(routes.flatMap((route) => route.models ?? []))],
|
|
5783
|
+
useSubscription
|
|
5784
|
+
};
|
|
4039
5785
|
}
|
|
4040
|
-
return {
|
|
5786
|
+
return {
|
|
5787
|
+
endpoint,
|
|
5788
|
+
model: routes.find((route) => route.defaultModel?.trim())?.defaultModel ?? "",
|
|
5789
|
+
useSubscription
|
|
5790
|
+
};
|
|
4041
5791
|
});
|
|
4042
5792
|
if (status.running) {
|
|
4043
5793
|
const queueStatus = deps.outboundApiServer.getQueueStatus();
|
|
4044
|
-
return
|
|
5794
|
+
return writeJson3(res, 200, { ...status, endpoints, queueStatus });
|
|
4045
5795
|
}
|
|
4046
|
-
return
|
|
5796
|
+
return writeJson3(res, 200, { ...status, endpoints });
|
|
4047
5797
|
}
|
|
4048
5798
|
function resolvePlaygroundPath(endpoint, body) {
|
|
4049
5799
|
switch (endpoint) {
|
|
@@ -4069,16 +5819,16 @@ async function handlePlayground(req, res, method, deps) {
|
|
|
4069
5819
|
const payload = body["body"];
|
|
4070
5820
|
const status = deps.outboundApiServer.getStatus();
|
|
4071
5821
|
if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
|
|
4072
|
-
const path2 = resolvePlaygroundPath(endpoint,
|
|
5822
|
+
const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
|
|
4073
5823
|
if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
|
|
4074
5824
|
const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
4075
5825
|
await proxyToOutbound(res, status.port, path2, key, upstreamBody);
|
|
4076
5826
|
}
|
|
4077
|
-
function
|
|
5827
|
+
function isRecord2(v) {
|
|
4078
5828
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4079
5829
|
}
|
|
4080
5830
|
function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
4081
|
-
return new Promise((
|
|
5831
|
+
return new Promise((resolve3) => {
|
|
4082
5832
|
const upstream = http.request(
|
|
4083
5833
|
{
|
|
4084
5834
|
host: "127.0.0.1",
|
|
@@ -4099,14 +5849,14 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
|
4099
5849
|
proxRes.on("data", (chunk) => res.write(chunk));
|
|
4100
5850
|
proxRes.on("end", () => {
|
|
4101
5851
|
res.end();
|
|
4102
|
-
|
|
5852
|
+
resolve3();
|
|
4103
5853
|
});
|
|
4104
5854
|
}
|
|
4105
5855
|
);
|
|
4106
5856
|
upstream.on("error", (err5) => {
|
|
4107
5857
|
if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
|
|
4108
5858
|
else res.end();
|
|
4109
|
-
|
|
5859
|
+
resolve3();
|
|
4110
5860
|
});
|
|
4111
5861
|
upstream.write(body);
|
|
4112
5862
|
upstream.end();
|
|
@@ -4114,7 +5864,7 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
|
4114
5864
|
}
|
|
4115
5865
|
|
|
4116
5866
|
// src/admin/uiStatic.ts
|
|
4117
|
-
import { existsSync as
|
|
5867
|
+
import { existsSync as existsSync7, statSync as statSync2 } from "fs";
|
|
4118
5868
|
import { readFile } from "fs/promises";
|
|
4119
5869
|
import { createRequire } from "module";
|
|
4120
5870
|
import path from "path";
|
|
@@ -4137,13 +5887,13 @@ var CONTENT_TYPES = {
|
|
|
4137
5887
|
function resolveUiDist() {
|
|
4138
5888
|
const fromEnv = process.env["OMNICROSS_UI_DIST"];
|
|
4139
5889
|
if (fromEnv) {
|
|
4140
|
-
return
|
|
5890
|
+
return existsSync7(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
|
|
4141
5891
|
}
|
|
4142
5892
|
try {
|
|
4143
5893
|
const req = createRequire(typeof __filename !== "undefined" ? __filename : import.meta.url);
|
|
4144
5894
|
const pkgJson = req.resolve("@omnicross/ui/package.json");
|
|
4145
5895
|
const dist = path.join(path.dirname(pkgJson), "dist");
|
|
4146
|
-
return
|
|
5896
|
+
return existsSync7(path.join(dist, "index.html")) ? dist : null;
|
|
4147
5897
|
} catch {
|
|
4148
5898
|
return null;
|
|
4149
5899
|
}
|
|
@@ -4192,7 +5942,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
4192
5942
|
return true;
|
|
4193
5943
|
}
|
|
4194
5944
|
let target = filePath;
|
|
4195
|
-
if (!
|
|
5945
|
+
if (!existsSync7(target) || statSync2(target).isDirectory()) {
|
|
4196
5946
|
if (path.extname(rel) === "") {
|
|
4197
5947
|
target = path.join(uiDist, "index.html");
|
|
4198
5948
|
} else {
|
|
@@ -4209,7 +5959,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
4209
5959
|
}
|
|
4210
5960
|
|
|
4211
5961
|
// src/admin/version.ts
|
|
4212
|
-
var DAEMON_VERSION = true ? "0.1.
|
|
5962
|
+
var DAEMON_VERSION = true ? "0.1.6" : "0.0.0-dev";
|
|
4213
5963
|
|
|
4214
5964
|
// src/admin/AdminServer.ts
|
|
4215
5965
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
@@ -4248,14 +5998,14 @@ var AdminServer = class {
|
|
|
4248
5998
|
}
|
|
4249
5999
|
/** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
|
|
4250
6000
|
listen(bindAddr, port) {
|
|
4251
|
-
return new Promise((
|
|
6001
|
+
return new Promise((resolve3, reject) => {
|
|
4252
6002
|
const server = http2.createServer((req, res) => {
|
|
4253
6003
|
this.onRequest(req, res);
|
|
4254
6004
|
});
|
|
4255
6005
|
const onError = (err5) => {
|
|
4256
6006
|
if (err5.code === "EADDRINUSE" && port !== 0) {
|
|
4257
6007
|
server.removeListener("error", onError);
|
|
4258
|
-
this.listen(bindAddr, 0).then(
|
|
6008
|
+
this.listen(bindAddr, 0).then(resolve3, reject);
|
|
4259
6009
|
return;
|
|
4260
6010
|
}
|
|
4261
6011
|
reject(err5);
|
|
@@ -4267,7 +6017,7 @@ var AdminServer = class {
|
|
|
4267
6017
|
server.removeListener("error", onError);
|
|
4268
6018
|
server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
|
|
4269
6019
|
this.server = server;
|
|
4270
|
-
|
|
6020
|
+
resolve3(addr.port);
|
|
4271
6021
|
} else {
|
|
4272
6022
|
reject(new Error("Failed to get admin server address"));
|
|
4273
6023
|
}
|
|
@@ -4348,8 +6098,8 @@ var AdminServer = class {
|
|
|
4348
6098
|
if (!server) return;
|
|
4349
6099
|
this.server = null;
|
|
4350
6100
|
this.boundPort = 0;
|
|
4351
|
-
return new Promise((
|
|
4352
|
-
server.close(() =>
|
|
6101
|
+
return new Promise((resolve3) => {
|
|
6102
|
+
server.close(() => resolve3());
|
|
4353
6103
|
});
|
|
4354
6104
|
}
|
|
4355
6105
|
/** A live status snapshot. */
|
|
@@ -4465,7 +6215,7 @@ function pageHtml(message) {
|
|
|
4465
6215
|
return `<!doctype html><meta charset="utf-8"><title>omnicross login</title><body style="font-family:sans-serif;padding:2rem"><h2>${message}</h2><p>You can close this window and return to the terminal.</p></body>`;
|
|
4466
6216
|
}
|
|
4467
6217
|
function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
|
|
4468
|
-
return new Promise((
|
|
6218
|
+
return new Promise((resolve3, reject) => {
|
|
4469
6219
|
let settled = false;
|
|
4470
6220
|
const finish = (server2, fn) => {
|
|
4471
6221
|
if (settled) return;
|
|
@@ -4496,7 +6246,7 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal
|
|
|
4496
6246
|
}
|
|
4497
6247
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4498
6248
|
res.end(pageHtml("Login complete."));
|
|
4499
|
-
finish(server, () =>
|
|
6249
|
+
finish(server, () => resolve3(code));
|
|
4500
6250
|
});
|
|
4501
6251
|
const abort = () => finish(server, () => reject(new Error("login: cancelled")));
|
|
4502
6252
|
if (signal?.aborted) {
|
|
@@ -4600,8 +6350,10 @@ var EMPTY_CHAIN = {
|
|
|
4600
6350
|
modelTransformers: []
|
|
4601
6351
|
};
|
|
4602
6352
|
var FORMAT_TRANSFORMER = {
|
|
6353
|
+
openai: "openai",
|
|
4603
6354
|
anthropic: "anthropic",
|
|
4604
|
-
gemini: "gemini"
|
|
6355
|
+
gemini: "gemini",
|
|
6356
|
+
"openai-response": "openai-response"
|
|
4605
6357
|
};
|
|
4606
6358
|
var ConfigFileProviderConfigSource = class {
|
|
4607
6359
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -4670,7 +6422,7 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4670
6422
|
}
|
|
4671
6423
|
async getMainTransformer(providerId) {
|
|
4672
6424
|
const row = this.providers.get(providerId);
|
|
4673
|
-
if (!row
|
|
6425
|
+
if (!row) return null;
|
|
4674
6426
|
const name = FORMAT_TRANSFORMER[row.apiFormat];
|
|
4675
6427
|
const instances = this.transformerService.resolveTransformerReferences([name]);
|
|
4676
6428
|
return instances[0] ?? null;
|
|
@@ -4680,11 +6432,8 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4680
6432
|
if (!row) return EMPTY_CHAIN;
|
|
4681
6433
|
const customRefs = row.transformer?.use ?? [];
|
|
4682
6434
|
if (customRefs.length === 0) return EMPTY_CHAIN;
|
|
4683
|
-
const formatName = row.apiFormat === "openai" ? void 0 : FORMAT_TRANSFORMER[row.apiFormat];
|
|
4684
|
-
const effectiveRefs = formatName ? customRefs.filter((ref) => (typeof ref === "string" ? ref : ref[0]) !== formatName) : customRefs;
|
|
4685
|
-
if (effectiveRefs.length === 0) return EMPTY_CHAIN;
|
|
4686
6435
|
return {
|
|
4687
|
-
providerTransformers: this.transformerService.resolveTransformerReferences(
|
|
6436
|
+
providerTransformers: this.transformerService.resolveTransformerReferences(customRefs),
|
|
4688
6437
|
modelTransformers: []
|
|
4689
6438
|
};
|
|
4690
6439
|
}
|
|
@@ -4715,7 +6464,7 @@ function resolvePreferredApiKey(row) {
|
|
|
4715
6464
|
}
|
|
4716
6465
|
function toLLMProvider(row) {
|
|
4717
6466
|
const apiFormat = row.apiFormat === "gemini" ? "google" : row.apiFormat;
|
|
4718
|
-
const transformer =
|
|
6467
|
+
const transformer = { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
|
|
4719
6468
|
const allModels = row.models ?? [];
|
|
4720
6469
|
const models = row.modelConfigs ? allModels.filter((id) => row.modelConfigs.find((c) => c.id === id)?.enabled !== false) : allModels;
|
|
4721
6470
|
return {
|
|
@@ -4785,7 +6534,7 @@ var ConfigurableLogger = class {
|
|
|
4785
6534
|
const stream = this.fileStream;
|
|
4786
6535
|
this.fileStream = null;
|
|
4787
6536
|
if (!stream) return Promise.resolve();
|
|
4788
|
-
return new Promise((
|
|
6537
|
+
return new Promise((resolve3) => stream.end(() => resolve3()));
|
|
4789
6538
|
}
|
|
4790
6539
|
emit(level, message, error, meta) {
|
|
4791
6540
|
if (LEVEL_ORDER[level] > this.threshold) return;
|
|
@@ -4896,7 +6645,7 @@ function safeStringify(value) {
|
|
|
4896
6645
|
}
|
|
4897
6646
|
|
|
4898
6647
|
// src/ports/JsonApiServerSettingsStore.ts
|
|
4899
|
-
import { readFileSync as
|
|
6648
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
4900
6649
|
import { OUTBOUND_API_SERVER_CONFIG_KEY } from "@omnicross/core/outbound-api";
|
|
4901
6650
|
var JsonApiServerSettingsStore = class {
|
|
4902
6651
|
/**
|
|
@@ -4923,7 +6672,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
4923
6672
|
if (key !== OUTBOUND_API_SERVER_CONFIG_KEY) return;
|
|
4924
6673
|
const file = this.readFile();
|
|
4925
6674
|
file.server = this.encryptSecrets(value);
|
|
4926
|
-
|
|
6675
|
+
writeFileSync6(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
4927
6676
|
}
|
|
4928
6677
|
/** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
|
|
4929
6678
|
encryptSecrets(config) {
|
|
@@ -4946,7 +6695,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
4946
6695
|
/** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
|
|
4947
6696
|
readFile() {
|
|
4948
6697
|
try {
|
|
4949
|
-
const raw =
|
|
6698
|
+
const raw = readFileSync8(this.configPath, "utf8");
|
|
4950
6699
|
const parsed = JSON.parse(raw);
|
|
4951
6700
|
if (parsed && typeof parsed === "object") return parsed;
|
|
4952
6701
|
} catch {
|
|
@@ -4956,8 +6705,8 @@ var JsonApiServerSettingsStore = class {
|
|
|
4956
6705
|
};
|
|
4957
6706
|
|
|
4958
6707
|
// src/ports/JsonlUsageEventStore.ts
|
|
4959
|
-
import { randomUUID as
|
|
4960
|
-
import { appendFileSync, existsSync as
|
|
6708
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
6709
|
+
import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
4961
6710
|
var JsonlUsageEventStore = class {
|
|
4962
6711
|
constructor(eventsPath, isPriced) {
|
|
4963
6712
|
this.eventsPath = eventsPath;
|
|
@@ -4969,7 +6718,7 @@ var JsonlUsageEventStore = class {
|
|
|
4969
6718
|
async insert(input) {
|
|
4970
6719
|
const row = {
|
|
4971
6720
|
...input,
|
|
4972
|
-
id:
|
|
6721
|
+
id: randomUUID4(),
|
|
4973
6722
|
ts: input.ts ?? Date.now()
|
|
4974
6723
|
};
|
|
4975
6724
|
appendFileSync(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
|
|
@@ -5067,15 +6816,15 @@ var JsonlUsageEventStore = class {
|
|
|
5067
6816
|
* Used to lazily seed the outbound key-policy spend tracker (once per key). A
|
|
5068
6817
|
* key with no attributed events yields all zeros.
|
|
5069
6818
|
*/
|
|
5070
|
-
async getSpendByKey(
|
|
6819
|
+
async getSpendByKey(query2) {
|
|
5071
6820
|
let totalUsd = 0;
|
|
5072
6821
|
let dailyUsd = 0;
|
|
5073
6822
|
let weeklyUsd = 0;
|
|
5074
|
-
for (const row of this.readRows({ startTs: 0, endTs:
|
|
5075
|
-
if (row.apiKeyId !==
|
|
6823
|
+
for (const row of this.readRows({ startTs: 0, endTs: query2.endTs })) {
|
|
6824
|
+
if (row.apiKeyId !== query2.apiKeyId) continue;
|
|
5076
6825
|
totalUsd += row.costUsd;
|
|
5077
|
-
if (row.ts >=
|
|
5078
|
-
if (row.ts >=
|
|
6826
|
+
if (row.ts >= query2.dayStartTs) dailyUsd += row.costUsd;
|
|
6827
|
+
if (row.ts >= query2.weekStartTs) weeklyUsd += row.costUsd;
|
|
5079
6828
|
}
|
|
5080
6829
|
return { totalUsd, dailyUsd, weeklyUsd };
|
|
5081
6830
|
}
|
|
@@ -5160,10 +6909,10 @@ var JsonlUsageEventStore = class {
|
|
|
5160
6909
|
}
|
|
5161
6910
|
/** Parse every line, skipping malformed/torn lines defensively. */
|
|
5162
6911
|
readAllRows() {
|
|
5163
|
-
if (!
|
|
6912
|
+
if (!existsSync8(this.eventsPath)) return [];
|
|
5164
6913
|
let raw;
|
|
5165
6914
|
try {
|
|
5166
|
-
raw =
|
|
6915
|
+
raw = readFileSync9(this.eventsPath, "utf8");
|
|
5167
6916
|
} catch {
|
|
5168
6917
|
return [];
|
|
5169
6918
|
}
|
|
@@ -5247,12 +6996,29 @@ function isUsageEventRecord(parsed) {
|
|
|
5247
6996
|
}
|
|
5248
6997
|
|
|
5249
6998
|
// src/ports/JsonPricingStore.ts
|
|
5250
|
-
import { existsSync as
|
|
6999
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
7000
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
5251
7001
|
var JsonPricingStore = class {
|
|
5252
7002
|
constructor(pricingPath) {
|
|
5253
7003
|
this.pricingPath = pricingPath;
|
|
5254
7004
|
}
|
|
5255
7005
|
pricingPath;
|
|
7006
|
+
/**
|
|
7007
|
+
* Return whether the durable snapshot can actually serve at least one price.
|
|
7008
|
+
*
|
|
7009
|
+
* This intentionally checks the file itself instead of relying on refresh
|
|
7010
|
+
* metadata: a recent `lastSuccessAt` must not hide a deleted, truncated, or
|
|
7011
|
+
* otherwise unusable pricing table after a crash or manual file edit.
|
|
7012
|
+
*/
|
|
7013
|
+
hasUsableSnapshot() {
|
|
7014
|
+
if (!existsSync9(this.pricingPath)) return false;
|
|
7015
|
+
try {
|
|
7016
|
+
const parsed = JSON.parse(readFileSync10(this.pricingPath, "utf8"));
|
|
7017
|
+
return Array.isArray(parsed) && parsed.some(isUsablePricingRow);
|
|
7018
|
+
} catch {
|
|
7019
|
+
return false;
|
|
7020
|
+
}
|
|
7021
|
+
}
|
|
5256
7022
|
async getAll() {
|
|
5257
7023
|
return this.readRows();
|
|
5258
7024
|
}
|
|
@@ -5265,17 +7031,17 @@ var JsonPricingStore = class {
|
|
|
5265
7031
|
*/
|
|
5266
7032
|
async upsert(input, asUserEdit) {
|
|
5267
7033
|
const rows = this.readRows();
|
|
5268
|
-
const entry = this.applyUpsert(rows, input, asUserEdit);
|
|
7034
|
+
const entry = this.applyUpsert(rows, input, asUserEdit, "litellm");
|
|
5269
7035
|
this.writeRows(rows);
|
|
5270
7036
|
return entry;
|
|
5271
7037
|
}
|
|
5272
7038
|
/**
|
|
5273
7039
|
* Apply a batch fetched from a pricing source. Rows whose local copy is
|
|
5274
7040
|
* user-edited are NOT applied — they come back as `{ current, incoming }`
|
|
5275
|
-
* conflicts; everything else is upserted
|
|
5276
|
-
* for the whole batch.
|
|
7041
|
+
* conflicts; everything else is upserted with the supplied automatic source.
|
|
7042
|
+
* ONE file write for the whole batch.
|
|
5277
7043
|
*/
|
|
5278
|
-
async bulkApplyFromSource(entries) {
|
|
7044
|
+
async bulkApplyFromSource(entries, source = "litellm") {
|
|
5279
7045
|
const rows = this.readRows();
|
|
5280
7046
|
const applied = [];
|
|
5281
7047
|
const conflicts = [];
|
|
@@ -5291,7 +7057,8 @@ var JsonPricingStore = class {
|
|
|
5291
7057
|
rows,
|
|
5292
7058
|
incoming,
|
|
5293
7059
|
/* asUserEdit */
|
|
5294
|
-
false
|
|
7060
|
+
false,
|
|
7061
|
+
source
|
|
5295
7062
|
));
|
|
5296
7063
|
}
|
|
5297
7064
|
if (applied.length > 0) this.writeRows(rows);
|
|
@@ -5314,7 +7081,8 @@ var JsonPricingStore = class {
|
|
|
5314
7081
|
rows,
|
|
5315
7082
|
r.incoming,
|
|
5316
7083
|
/* asUserEdit */
|
|
5317
|
-
false
|
|
7084
|
+
false,
|
|
7085
|
+
"litellm"
|
|
5318
7086
|
);
|
|
5319
7087
|
overwrittenCount += 1;
|
|
5320
7088
|
}
|
|
@@ -5335,7 +7103,7 @@ var JsonPricingStore = class {
|
|
|
5335
7103
|
return true;
|
|
5336
7104
|
}
|
|
5337
7105
|
/** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
|
|
5338
|
-
applyUpsert(rows, input, asUserEdit) {
|
|
7106
|
+
applyUpsert(rows, input, asUserEdit, automaticSource) {
|
|
5339
7107
|
const now = Date.now();
|
|
5340
7108
|
const entry = {
|
|
5341
7109
|
providerId: input.providerId,
|
|
@@ -5344,7 +7112,7 @@ var JsonPricingStore = class {
|
|
|
5344
7112
|
outputPricePer1m: input.outputPricePer1m,
|
|
5345
7113
|
cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
|
|
5346
7114
|
cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
|
|
5347
|
-
source: asUserEdit ? "user" :
|
|
7115
|
+
source: asUserEdit ? "user" : automaticSource,
|
|
5348
7116
|
userEdited: asUserEdit,
|
|
5349
7117
|
editedAt: asUserEdit ? now : null,
|
|
5350
7118
|
updatedAt: now
|
|
@@ -5358,21 +7126,142 @@ var JsonPricingStore = class {
|
|
|
5358
7126
|
}
|
|
5359
7127
|
/** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5360
7128
|
readRows() {
|
|
5361
|
-
if (!
|
|
7129
|
+
if (!existsSync9(this.pricingPath)) return [];
|
|
5362
7130
|
try {
|
|
5363
|
-
const parsed = JSON.parse(
|
|
7131
|
+
const parsed = JSON.parse(readFileSync10(this.pricingPath, "utf8"));
|
|
5364
7132
|
return Array.isArray(parsed) ? parsed : [];
|
|
5365
7133
|
} catch {
|
|
5366
7134
|
return [];
|
|
5367
7135
|
}
|
|
5368
7136
|
}
|
|
5369
7137
|
writeRows(rows) {
|
|
5370
|
-
|
|
7138
|
+
const temporaryPath = `${this.pricingPath}.${process.pid}.${randomUUID5()}.tmp`;
|
|
7139
|
+
try {
|
|
7140
|
+
writeFileSync7(temporaryPath, JSON.stringify(rows, null, 2) + "\n", {
|
|
7141
|
+
encoding: "utf8",
|
|
7142
|
+
flag: "wx"
|
|
7143
|
+
});
|
|
7144
|
+
this.replaceFile(temporaryPath);
|
|
7145
|
+
} finally {
|
|
7146
|
+
rmSync2(temporaryPath, { force: true });
|
|
7147
|
+
}
|
|
7148
|
+
}
|
|
7149
|
+
/** Isolated for deterministic failure testing; never removes the target. */
|
|
7150
|
+
replaceFile(temporaryPath) {
|
|
7151
|
+
renameSync3(temporaryPath, this.pricingPath);
|
|
7152
|
+
}
|
|
7153
|
+
};
|
|
7154
|
+
function isUsablePricingRow(value) {
|
|
7155
|
+
if (!value || typeof value !== "object") return false;
|
|
7156
|
+
const row = value;
|
|
7157
|
+
return typeof row.providerId === "string" && row.providerId.length > 0 && typeof row.modelId === "string" && row.modelId.length > 0 && typeof row.inputPricePer1m === "number" && Number.isFinite(row.inputPricePer1m) && typeof row.outputPricePer1m === "number" && Number.isFinite(row.outputPricePer1m);
|
|
7158
|
+
}
|
|
7159
|
+
|
|
7160
|
+
// src/pricing/PricingRefreshScheduler.ts
|
|
7161
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
7162
|
+
var EMPTY_STATE2 = {
|
|
7163
|
+
lastAttemptAt: null,
|
|
7164
|
+
lastSuccessAt: null,
|
|
7165
|
+
lastError: null,
|
|
7166
|
+
sources: []
|
|
7167
|
+
};
|
|
7168
|
+
var PricingRefreshScheduler = class {
|
|
7169
|
+
constructor(engine, catalog2, statePath, logger, options = {}) {
|
|
7170
|
+
this.engine = engine;
|
|
7171
|
+
this.catalog = catalog2;
|
|
7172
|
+
this.statePath = statePath;
|
|
7173
|
+
this.logger = logger;
|
|
7174
|
+
this.staleAfterMs = options.staleAfterMs ?? 24 * 60 * 60 * 1e3;
|
|
7175
|
+
this.intervalMs = options.intervalMs ?? 60 * 60 * 1e3;
|
|
7176
|
+
this.now = options.now ?? Date.now;
|
|
7177
|
+
}
|
|
7178
|
+
engine;
|
|
7179
|
+
catalog;
|
|
7180
|
+
statePath;
|
|
7181
|
+
logger;
|
|
7182
|
+
staleAfterMs;
|
|
7183
|
+
intervalMs;
|
|
7184
|
+
now;
|
|
7185
|
+
timer = null;
|
|
7186
|
+
inFlight = null;
|
|
7187
|
+
/** Fire one stale check immediately and arm an unref'ed periodic check. */
|
|
7188
|
+
start() {
|
|
7189
|
+
if (this.timer) return;
|
|
7190
|
+
void this.refreshIfStale();
|
|
7191
|
+
this.timer = setInterval(() => void this.refreshIfStale(), this.intervalMs);
|
|
7192
|
+
this.timer.unref?.();
|
|
7193
|
+
}
|
|
7194
|
+
dispose() {
|
|
7195
|
+
if (this.timer) clearInterval(this.timer);
|
|
7196
|
+
this.timer = null;
|
|
7197
|
+
}
|
|
7198
|
+
getState() {
|
|
7199
|
+
if (!existsSync10(this.statePath)) return { ...EMPTY_STATE2, sources: [] };
|
|
7200
|
+
try {
|
|
7201
|
+
const value = JSON.parse(readFileSync11(this.statePath, "utf8"));
|
|
7202
|
+
return {
|
|
7203
|
+
lastAttemptAt: finiteOrNull(value.lastAttemptAt),
|
|
7204
|
+
lastSuccessAt: finiteOrNull(value.lastSuccessAt),
|
|
7205
|
+
lastError: typeof value.lastError === "string" ? value.lastError : null,
|
|
7206
|
+
sources: Array.isArray(value.sources) ? value.sources : []
|
|
7207
|
+
};
|
|
7208
|
+
} catch {
|
|
7209
|
+
return { ...EMPTY_STATE2, sources: [] };
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
/** Public for admin/manual tests; concurrent checks share one promise. */
|
|
7213
|
+
refreshIfStale(force = false) {
|
|
7214
|
+
if (this.inFlight) return this.inFlight;
|
|
7215
|
+
const state = this.getState();
|
|
7216
|
+
if (!force && this.catalog.hasUsableSnapshot() && state.lastSuccessAt !== null && this.now() - state.lastSuccessAt < this.staleAfterMs) {
|
|
7217
|
+
return Promise.resolve();
|
|
7218
|
+
}
|
|
7219
|
+
const task = this.runRefresh(state);
|
|
7220
|
+
this.inFlight = task;
|
|
7221
|
+
return task.finally(() => {
|
|
7222
|
+
if (this.inFlight === task) this.inFlight = null;
|
|
7223
|
+
});
|
|
7224
|
+
}
|
|
7225
|
+
async runRefresh(previous) {
|
|
7226
|
+
const lastAttemptAt = this.now();
|
|
7227
|
+
try {
|
|
7228
|
+
const result = await this.engine.fetchLatestFromSource();
|
|
7229
|
+
const failed = result.sources.filter((source) => source.status === "failed");
|
|
7230
|
+
const complete = failed.length === 0;
|
|
7231
|
+
this.writeState({
|
|
7232
|
+
lastAttemptAt,
|
|
7233
|
+
// A partial refresh keeps useful rows, but remains stale so the failed
|
|
7234
|
+
// source is retried on the next hourly check instead of 24 hours later.
|
|
7235
|
+
lastSuccessAt: complete ? this.now() : previous.lastSuccessAt,
|
|
7236
|
+
lastError: complete ? null : failed.map((source) => `${source.source}: ${source.error ?? "failed"}`).join("; "),
|
|
7237
|
+
sources: result.sources
|
|
7238
|
+
});
|
|
7239
|
+
} catch (error) {
|
|
7240
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7241
|
+
this.writeState({
|
|
7242
|
+
lastAttemptAt,
|
|
7243
|
+
lastSuccessAt: previous.lastSuccessAt,
|
|
7244
|
+
lastError: message,
|
|
7245
|
+
sources: previous.sources
|
|
7246
|
+
});
|
|
7247
|
+
this.logger.warn("[PricingRefreshScheduler] background refresh failed; cached prices retained", {
|
|
7248
|
+
error: message
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
7252
|
+
writeState(state) {
|
|
7253
|
+
const temporaryPath = `${this.statePath}.tmp`;
|
|
7254
|
+
writeFileSync8(temporaryPath, `${JSON.stringify(state, null, 2)}
|
|
7255
|
+
`, "utf8");
|
|
7256
|
+
renameSync4(temporaryPath, this.statePath);
|
|
5371
7257
|
}
|
|
5372
7258
|
};
|
|
7259
|
+
function finiteOrNull(value) {
|
|
7260
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
7261
|
+
}
|
|
5373
7262
|
|
|
5374
7263
|
// src/ports/JsonVoucherDb.ts
|
|
5375
|
-
import { existsSync as
|
|
7264
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
5376
7265
|
var JsonVoucherDb = class {
|
|
5377
7266
|
constructor(vouchersPath) {
|
|
5378
7267
|
this.vouchersPath = vouchersPath;
|
|
@@ -5450,25 +7339,26 @@ var JsonVoucherDb = class {
|
|
|
5450
7339
|
}
|
|
5451
7340
|
/** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5452
7341
|
readRows() {
|
|
5453
|
-
if (!
|
|
7342
|
+
if (!existsSync11(this.vouchersPath)) return [];
|
|
5454
7343
|
try {
|
|
5455
|
-
const parsed = JSON.parse(
|
|
7344
|
+
const parsed = JSON.parse(readFileSync12(this.vouchersPath, "utf8"));
|
|
5456
7345
|
return Array.isArray(parsed) ? parsed : [];
|
|
5457
7346
|
} catch {
|
|
5458
7347
|
return [];
|
|
5459
7348
|
}
|
|
5460
7349
|
}
|
|
5461
7350
|
writeRows(rows) {
|
|
5462
|
-
|
|
7351
|
+
writeFileSync9(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
5463
7352
|
}
|
|
5464
7353
|
};
|
|
5465
7354
|
|
|
5466
7355
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5467
|
-
import { existsSync as
|
|
5468
|
-
import { dirname as
|
|
5469
|
-
import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
|
|
5470
|
-
import {
|
|
5471
|
-
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";
|
|
5472
7362
|
import {
|
|
5473
7363
|
claudeOAuth as claudeOAuth2,
|
|
5474
7364
|
codexOAuth as codexOAuth2,
|
|
@@ -5476,33 +7366,9 @@ import {
|
|
|
5476
7366
|
} from "@omnicross/subscriptions";
|
|
5477
7367
|
|
|
5478
7368
|
// src/ports/account-sync.ts
|
|
5479
|
-
var IMPORT_EXPIRY_MARGIN_MS = 6e4;
|
|
5480
7369
|
function viewOf(tokens) {
|
|
5481
7370
|
return tokens;
|
|
5482
7371
|
}
|
|
5483
|
-
function decideExternalImport(captured, external, now = Date.now()) {
|
|
5484
|
-
if (!external?.accessToken) return "no-credential";
|
|
5485
|
-
const capturedRt = viewOf(captured).refreshToken;
|
|
5486
|
-
const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
|
|
5487
|
-
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
|
|
5488
|
-
return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
|
|
5489
|
-
}
|
|
5490
|
-
function buildImportedTokens(captured, external) {
|
|
5491
|
-
const imported = {
|
|
5492
|
-
...captured,
|
|
5493
|
-
accessToken: external.accessToken,
|
|
5494
|
-
status: "authorized",
|
|
5495
|
-
errorMessage: void 0,
|
|
5496
|
-
syncWarning: void 0,
|
|
5497
|
-
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5498
|
-
};
|
|
5499
|
-
if (external.refreshToken) imported.refreshToken = external.refreshToken;
|
|
5500
|
-
if (external.expiresAt) imported.expiresAt = external.expiresAt;
|
|
5501
|
-
else delete imported.expiresAt;
|
|
5502
|
-
if (external.idToken) imported.idToken = external.idToken;
|
|
5503
|
-
if (external.scopes) imported.scopes = external.scopes;
|
|
5504
|
-
return imported;
|
|
5505
|
-
}
|
|
5506
7372
|
function buildTokensFromExternal(provider, external) {
|
|
5507
7373
|
const base = {
|
|
5508
7374
|
authMethod: "oauth",
|
|
@@ -5523,14 +7389,6 @@ function buildTokensFromExternal(provider, external) {
|
|
|
5523
7389
|
if (external.idToken) tokens.idToken = external.idToken;
|
|
5524
7390
|
return tokens;
|
|
5525
7391
|
}
|
|
5526
|
-
function isExternalDivergent(stored, external) {
|
|
5527
|
-
if (!external?.accessToken || !external.refreshToken) return false;
|
|
5528
|
-
const view = viewOf(stored);
|
|
5529
|
-
if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
|
|
5530
|
-
const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
|
|
5531
|
-
const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
|
|
5532
|
-
return !Number.isFinite(storedExp) || externalExp > storedExp;
|
|
5533
|
-
}
|
|
5534
7392
|
function findDuplicateCredentialIds(accounts) {
|
|
5535
7393
|
const byCredential = /* @__PURE__ */ new Map();
|
|
5536
7394
|
for (const account of accounts) {
|
|
@@ -5549,11 +7407,11 @@ function findDuplicateCredentialIds(accounts) {
|
|
|
5549
7407
|
}
|
|
5550
7408
|
|
|
5551
7409
|
// src/ports/external-cli-credentials.ts
|
|
5552
|
-
import { existsSync as
|
|
5553
|
-
import { homedir as
|
|
5554
|
-
import { join as
|
|
5555
|
-
function externalStorePath(provider, home =
|
|
5556
|
-
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");
|
|
5557
7415
|
}
|
|
5558
7416
|
function decodeJwtExpiryMs(token) {
|
|
5559
7417
|
try {
|
|
@@ -5600,12 +7458,12 @@ function parseCodexTokensEnvelope(raw) {
|
|
|
5600
7458
|
}
|
|
5601
7459
|
return parsed;
|
|
5602
7460
|
}
|
|
5603
|
-
function readExternalCliCredentials(provider, home =
|
|
7461
|
+
function readExternalCliCredentials(provider, home = homedir3()) {
|
|
5604
7462
|
const path2 = externalStorePath(provider, home);
|
|
5605
|
-
if (!
|
|
7463
|
+
if (!existsSync12(path2)) return null;
|
|
5606
7464
|
let raw;
|
|
5607
7465
|
try {
|
|
5608
|
-
const parsed = JSON.parse(
|
|
7466
|
+
const parsed = JSON.parse(readFileSync13(path2, "utf8"));
|
|
5609
7467
|
raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
5610
7468
|
} catch {
|
|
5611
7469
|
return null;
|
|
@@ -5613,84 +7471,6 @@ function readExternalCliCredentials(provider, home = homedir2()) {
|
|
|
5613
7471
|
return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
|
|
5614
7472
|
}
|
|
5615
7473
|
|
|
5616
|
-
// src/ports/external-cli-store.ts
|
|
5617
|
-
import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync10, renameSync, writeFileSync as writeFileSync7 } from "fs";
|
|
5618
|
-
import { homedir as homedir3 } from "os";
|
|
5619
|
-
import { dirname as dirname3 } from "path";
|
|
5620
|
-
function markerPath(provider, home) {
|
|
5621
|
-
return `${externalStorePath(provider, home)}.omnicross-managed`;
|
|
5622
|
-
}
|
|
5623
|
-
function backupPath(provider, home) {
|
|
5624
|
-
return `${externalStorePath(provider, home)}.omnicross-backup`;
|
|
5625
|
-
}
|
|
5626
|
-
function buildClaudeOAuthEnvelope(tokens) {
|
|
5627
|
-
if (!tokens.accessToken) return null;
|
|
5628
|
-
const envelope = { accessToken: tokens.accessToken };
|
|
5629
|
-
if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
|
|
5630
|
-
if (tokens.expiresAt) {
|
|
5631
|
-
const ms = Date.parse(tokens.expiresAt);
|
|
5632
|
-
if (Number.isFinite(ms)) envelope.expiresAt = ms;
|
|
5633
|
-
}
|
|
5634
|
-
if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
|
|
5635
|
-
return envelope;
|
|
5636
|
-
}
|
|
5637
|
-
function buildCodexTokensEnvelope(tokens) {
|
|
5638
|
-
if (!tokens.accessToken && !tokens.idToken) return null;
|
|
5639
|
-
const envelope = { access_token: tokens.accessToken ?? "" };
|
|
5640
|
-
if (tokens.idToken) envelope.id_token = tokens.idToken;
|
|
5641
|
-
if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
|
|
5642
|
-
return envelope;
|
|
5643
|
-
}
|
|
5644
|
-
function readExistingObject(path2) {
|
|
5645
|
-
if (!existsSync9(path2)) return {};
|
|
5646
|
-
try {
|
|
5647
|
-
const parsed = JSON.parse(readFileSync10(path2, "utf8"));
|
|
5648
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5649
|
-
} catch {
|
|
5650
|
-
return {};
|
|
5651
|
-
}
|
|
5652
|
-
}
|
|
5653
|
-
function writeAtomic(path2, content) {
|
|
5654
|
-
mkdirSync2(dirname3(path2), { recursive: true });
|
|
5655
|
-
const temp = `${path2}.omnicross-tmp`;
|
|
5656
|
-
writeFileSync7(temp, content, "utf8");
|
|
5657
|
-
renameSync(temp, path2);
|
|
5658
|
-
}
|
|
5659
|
-
function createExternalCliStore(home = homedir3()) {
|
|
5660
|
-
return {
|
|
5661
|
-
readMarkerAccountId(provider) {
|
|
5662
|
-
const path2 = markerPath(provider, home);
|
|
5663
|
-
if (!existsSync9(path2)) return void 0;
|
|
5664
|
-
try {
|
|
5665
|
-
const parsed = JSON.parse(readFileSync10(path2, "utf8"));
|
|
5666
|
-
return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
|
|
5667
|
-
} catch {
|
|
5668
|
-
return void 0;
|
|
5669
|
-
}
|
|
5670
|
-
},
|
|
5671
|
-
writeMarker(provider, accountId) {
|
|
5672
|
-
writeAtomic(
|
|
5673
|
-
markerPath(provider, home),
|
|
5674
|
-
JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
5675
|
-
);
|
|
5676
|
-
},
|
|
5677
|
-
writeBack(provider, accountId, tokens) {
|
|
5678
|
-
const owner = this.readMarkerAccountId(provider);
|
|
5679
|
-
if (owner !== accountId) return false;
|
|
5680
|
-
const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
|
|
5681
|
-
if (!envelope) return false;
|
|
5682
|
-
const storePath = externalStorePath(provider, home);
|
|
5683
|
-
if (existsSync9(storePath) && !existsSync9(backupPath(provider, home))) {
|
|
5684
|
-
copyFileSync(storePath, backupPath(provider, home));
|
|
5685
|
-
}
|
|
5686
|
-
const existing = readExistingObject(storePath);
|
|
5687
|
-
const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
|
|
5688
|
-
writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
|
|
5689
|
-
return true;
|
|
5690
|
-
}
|
|
5691
|
-
};
|
|
5692
|
-
}
|
|
5693
|
-
|
|
5694
7474
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5695
7475
|
var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
|
|
5696
7476
|
var JsonSubscriptionCredentialStore = class {
|
|
@@ -5703,32 +7483,30 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5703
7483
|
* proxy-aware {@link fetchUpstream} that threads the
|
|
5704
7484
|
* `{ providerId, accountId }` ctx (upstream-proxy M1) so a
|
|
5705
7485
|
* per-account/per-provider proxy is honored on refresh exactly
|
|
5706
|
-
* as on relay
|
|
7486
|
+
* as on relay refresh egresses from the SAME proxy IP as the
|
|
5707
7487
|
* account's traffic. NOT used by any read/write path.
|
|
5708
7488
|
*/
|
|
5709
|
-
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials
|
|
7489
|
+
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials) {
|
|
5710
7490
|
this.tokensPath = tokensPath;
|
|
5711
7491
|
this.box = box;
|
|
5712
7492
|
this.fetchImpl = fetchImpl;
|
|
5713
7493
|
this.externalCliReader = externalCliReader;
|
|
5714
|
-
this.externalCliStore = externalCliStore;
|
|
5715
7494
|
}
|
|
5716
7495
|
tokensPath;
|
|
5717
7496
|
box;
|
|
5718
7497
|
fetchImpl;
|
|
5719
7498
|
externalCliReader;
|
|
5720
|
-
externalCliStore;
|
|
5721
7499
|
/**
|
|
5722
7500
|
* The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
|
|
5723
7501
|
* TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
|
|
5724
7502
|
* through {@link fetchUpstream} with the account's `{ providerId, accountId }`
|
|
5725
|
-
* ctx so the per-account/provider proxy applies. `@internal`
|
|
7503
|
+
* ctx so the per-account/provider proxy applies. `@internal` also a test seam.
|
|
5726
7504
|
*/
|
|
5727
7505
|
buildRefreshFetch(providerId, accountId) {
|
|
5728
|
-
return this.fetchImpl ?? ((url, init) =>
|
|
7506
|
+
return this.fetchImpl ?? ((url, init) => fetchUpstream3(url, init, { providerId, accountId }));
|
|
5729
7507
|
}
|
|
5730
7508
|
/**
|
|
5731
|
-
* In-flight refresh coalescing
|
|
7509
|
+
* In-flight refresh coalescing. OAuth refresh tokens are
|
|
5732
7510
|
* SINGLE-USE: two concurrent refreshes of one account each spend the same
|
|
5733
7511
|
* token and the loser bricks a healthy account. Every refresh entry point
|
|
5734
7512
|
* (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
|
|
@@ -5743,13 +7521,13 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5743
7521
|
return run;
|
|
5744
7522
|
}
|
|
5745
7523
|
/** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
|
|
5746
|
-
* file is absent/corrupt). This is the hot read
|
|
7524
|
+
* file is absent/corrupt). This is the hot read the codex / gemini auth
|
|
5747
7525
|
* strategies pull `accessToken` / `expiresAt` / `status` from it. */
|
|
5748
7526
|
async getFullConfig() {
|
|
5749
7527
|
return this.readConfig();
|
|
5750
7528
|
}
|
|
5751
7529
|
/** Current Claude OAuth access token, or `null` when none is stored. No inline
|
|
5752
|
-
* refresh here
|
|
7530
|
+
* refresh here the lead-window / 401-retry refresh is driven by the
|
|
5753
7531
|
* subscription auth strategy, which calls `refreshClaudeToken` (now real). */
|
|
5754
7532
|
async getValidClaudeAccessToken() {
|
|
5755
7533
|
return this.readConfig().claude?.accessToken ?? null;
|
|
@@ -5774,13 +7552,14 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5774
7552
|
/**
|
|
5775
7553
|
* DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
|
|
5776
7554
|
* each provider's accounts to the secret-free `SubscriptionAccountSanitized`
|
|
5777
|
-
* shape (id/label/status/expiresAt/hasAccessToken/isActive)
|
|
7555
|
+
* shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
|
|
5778
7556
|
* Used by the admin accounts GET (secret-IN-never-OUT).
|
|
5779
7557
|
*/
|
|
5780
7558
|
async listSanitizedAccounts() {
|
|
5781
7559
|
const config = this.readConfig();
|
|
5782
|
-
const health2 =
|
|
5783
|
-
const
|
|
7560
|
+
const health2 = getSharedAccountHealth2();
|
|
7561
|
+
const allowanceScheduling = getSharedAccountAllowanceScheduling3();
|
|
7562
|
+
const identityStore = getSharedIdentityStore2();
|
|
5784
7563
|
const fingerprintOn = identityStore.isEnabled();
|
|
5785
7564
|
const now = Date.now();
|
|
5786
7565
|
const out = {};
|
|
@@ -5789,7 +7568,13 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5789
7568
|
if (sanitized.length === 0) continue;
|
|
5790
7569
|
for (const account of sanitized) {
|
|
5791
7570
|
const status = health2.getStatus(provider, account.id, now);
|
|
7571
|
+
const allowance = allowanceScheduling.preview(provider, account.id, account.priority ?? 50, now);
|
|
5792
7572
|
account.health = status.state;
|
|
7573
|
+
account.schedulable = account.enabled && status.state === "healthy" && allowance.schedulable;
|
|
7574
|
+
account.allowanceAction = allowance.action;
|
|
7575
|
+
account.allowanceEffectivePriority = allowance.effectivePriority;
|
|
7576
|
+
account.allowanceUsedPercent = allowance.usedPercent;
|
|
7577
|
+
account.allowanceResumeAt = allowance.resumeAt;
|
|
5793
7578
|
account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
|
|
5794
7579
|
if (fingerprintOn && provider === "claude") {
|
|
5795
7580
|
account.identityCaptured = identityStore.hasIdentity(provider, account.id);
|
|
@@ -5797,31 +7582,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5797
7582
|
account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
|
|
5798
7583
|
}
|
|
5799
7584
|
}
|
|
5800
|
-
out[provider] = this.
|
|
7585
|
+
out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
|
|
5801
7586
|
}
|
|
5802
7587
|
return out;
|
|
5803
7588
|
}
|
|
5804
7589
|
/**
|
|
5805
|
-
* List-time credential
|
|
5806
|
-
*
|
|
5807
|
-
* credential
|
|
5808
|
-
* rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
|
|
5809
|
-
* a failed refresh (`external-not-rotated`) takes precedence — it is the most
|
|
5810
|
-
* actionable state.
|
|
7590
|
+
* List-time managed-credential conflict warnings. Computed, not persisted:
|
|
7591
|
+
* `duplicate-token` is projected when two accounts of one provider share a
|
|
7592
|
+
* credential. This deliberately does not inspect either native CLI file.
|
|
5811
7593
|
*/
|
|
5812
|
-
|
|
7594
|
+
attachDuplicateWarnings(config, provider, sanitized) {
|
|
5813
7595
|
const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
|
|
5814
|
-
let divergentId;
|
|
5815
|
-
if (provider === "claude" || provider === "codex") {
|
|
5816
|
-
const active = getActiveAccount(config, provider);
|
|
5817
|
-
if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
|
|
5818
|
-
divergentId = active.id;
|
|
5819
|
-
}
|
|
5820
|
-
}
|
|
5821
|
-
if (duplicates.size === 0 && !divergentId) return sanitized;
|
|
5822
7596
|
return sanitized.map((account) => {
|
|
5823
|
-
const computed =
|
|
5824
|
-
|
|
7597
|
+
const computed = duplicates.has(account.id) ? "duplicate-token" : void 0;
|
|
7598
|
+
const persisted = account.syncWarning === "duplicate-token" ? account.syncWarning : void 0;
|
|
7599
|
+
if (!persisted && !computed) {
|
|
7600
|
+
const { syncWarning: _obsoleteWarning, ...withoutWarning } = account;
|
|
7601
|
+
return withoutWarning;
|
|
7602
|
+
}
|
|
7603
|
+
return { ...account, syncWarning: persisted ?? computed };
|
|
5825
7604
|
});
|
|
5826
7605
|
}
|
|
5827
7606
|
/** Read the external CLI store, never letting an fs/parse error escape. */
|
|
@@ -5834,11 +7613,11 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5834
7613
|
}
|
|
5835
7614
|
/**
|
|
5836
7615
|
* Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
|
|
5837
|
-
* the block has no refresh_token (setup-token / manual)
|
|
7616
|
+
* the block has no refresh_token (setup-token / manual) no upstream call, the
|
|
5838
7617
|
* block is untouched. Otherwise mint via the shared claude refresh flow and
|
|
5839
7618
|
* write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
|
|
5840
|
-
* On failure
|
|
5841
|
-
* errorMessage
|
|
7619
|
+
* On failure status:expired +
|
|
7620
|
+
* errorMessage `false`.
|
|
5842
7621
|
*/
|
|
5843
7622
|
async refreshClaudeToken() {
|
|
5844
7623
|
return this.coalesce("claude:active", async () => {
|
|
@@ -5863,19 +7642,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5863
7642
|
syncWarning: void 0
|
|
5864
7643
|
};
|
|
5865
7644
|
this.writeBackById("claude", capturedId, next);
|
|
5866
|
-
this.resyncExternal("claude", capturedId, next);
|
|
5867
7645
|
return true;
|
|
5868
7646
|
} catch (error) {
|
|
5869
|
-
if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
|
|
5870
|
-
const r = await claudeOAuth2.refreshAccessToken(rt, refreshFetch);
|
|
5871
|
-
return {
|
|
5872
|
-
accessToken: r.accessToken,
|
|
5873
|
-
refreshToken: r.refreshToken,
|
|
5874
|
-
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5875
|
-
};
|
|
5876
|
-
})) {
|
|
5877
|
-
return true;
|
|
5878
|
-
}
|
|
5879
7647
|
this.markExpiredById("claude", capturedId, claude, error);
|
|
5880
7648
|
return false;
|
|
5881
7649
|
}
|
|
@@ -5910,20 +7678,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5910
7678
|
syncWarning: void 0
|
|
5911
7679
|
};
|
|
5912
7680
|
this.writeBackById("codex", capturedId, next);
|
|
5913
|
-
this.resyncExternal("codex", capturedId, next);
|
|
5914
7681
|
return true;
|
|
5915
7682
|
} catch (error) {
|
|
5916
|
-
if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
|
|
5917
|
-
const r = await codexOAuth2.refreshAccessToken(rt, refreshFetch);
|
|
5918
|
-
return {
|
|
5919
|
-
accessToken: r.accessToken,
|
|
5920
|
-
refreshToken: r.refreshToken,
|
|
5921
|
-
idToken: r.idToken,
|
|
5922
|
-
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5923
|
-
};
|
|
5924
|
-
})) {
|
|
5925
|
-
return true;
|
|
5926
|
-
}
|
|
5927
7683
|
this.markExpiredById("codex", capturedId, codex, error);
|
|
5928
7684
|
return false;
|
|
5929
7685
|
}
|
|
@@ -5966,11 +7722,10 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5966
7722
|
});
|
|
5967
7723
|
}
|
|
5968
7724
|
/**
|
|
5969
|
-
* Refresh a SPECIFIC account by id (background scheduler sweep
|
|
5970
|
-
*
|
|
5971
|
-
*
|
|
5972
|
-
*
|
|
5973
|
-
* failure flags ONLY that account `expired`.
|
|
7725
|
+
* Refresh a SPECIFIC managed account by id (background scheduler sweep and
|
|
7726
|
+
* account-pool resolution). It uses only that account's stored refresh
|
|
7727
|
+
* token. Coalesced per `provider:id`; on failure flags ONLY that account
|
|
7728
|
+
* `expired`.
|
|
5974
7729
|
*/
|
|
5975
7730
|
async refreshAccountById(provider, id) {
|
|
5976
7731
|
return this.coalesce(`${provider}:${id}`, async () => {
|
|
@@ -5984,7 +7739,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5984
7739
|
const next = {
|
|
5985
7740
|
...captured,
|
|
5986
7741
|
accessToken: refreshed.accessToken,
|
|
5987
|
-
// Gemini's refresh response omits a new refresh token
|
|
7742
|
+
// Gemini's refresh response omits a new refresh token keep the captured.
|
|
5988
7743
|
refreshToken: refreshed.refreshToken ?? captured.refreshToken,
|
|
5989
7744
|
expiresAt: refreshed.expiresAt,
|
|
5990
7745
|
status: "authorized",
|
|
@@ -5994,7 +7749,6 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5994
7749
|
};
|
|
5995
7750
|
if (refreshed.idToken) next.idToken = refreshed.idToken;
|
|
5996
7751
|
this.writeBackById(provider, id, next);
|
|
5997
|
-
if (provider !== "gemini") this.resyncExternal(provider, id, next);
|
|
5998
7752
|
return true;
|
|
5999
7753
|
} catch (error) {
|
|
6000
7754
|
this.markExpiredById(provider, id, captured, error);
|
|
@@ -6002,7 +7756,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6002
7756
|
}
|
|
6003
7757
|
});
|
|
6004
7758
|
}
|
|
6005
|
-
//
|
|
7759
|
+
// By-id account-pool surface (subscription-account-scheduling, design D6)
|
|
6006
7760
|
/**
|
|
6007
7761
|
* Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
|
|
6008
7762
|
* provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
|
|
@@ -6035,7 +7789,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6035
7789
|
/**
|
|
6036
7790
|
* Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
|
|
6037
7791
|
* `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
|
|
6038
|
-
*
|
|
7792
|
+
* `false` (no refresh affordance).
|
|
6039
7793
|
*/
|
|
6040
7794
|
async refreshAccountToken(providerId, accountId) {
|
|
6041
7795
|
if (providerId === "opencodego") return false;
|
|
@@ -6057,7 +7811,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6057
7811
|
* (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
|
|
6058
7812
|
* whitelisted fingerprint headers; the token mirror is untouched); a no-op for
|
|
6059
7813
|
* an unknown id. Called by the identity store's persistence port on a first-seen
|
|
6060
|
-
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller
|
|
7814
|
+
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
|
|
6061
7815
|
* store's port wrapper swallows a rejection so the relay hot path is unaffected.
|
|
6062
7816
|
*/
|
|
6063
7817
|
async setAccountIdentity(providerId, accountId, identity) {
|
|
@@ -6082,7 +7836,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6082
7836
|
* DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
|
|
6083
7837
|
* the port). Passing `undefined` clears the override. Write-only password: when
|
|
6084
7838
|
* the incoming structured proxy omits the password but the account already had
|
|
6085
|
-
* one, the current (decrypted) password is preserved
|
|
7839
|
+
* one, the current (decrypted) password is preserved editing host/port never
|
|
6086
7840
|
* wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
|
|
6087
7841
|
*/
|
|
6088
7842
|
async setAccountProxy(providerId, accountId, proxy) {
|
|
@@ -6117,75 +7871,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6117
7871
|
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
6118
7872
|
};
|
|
6119
7873
|
}
|
|
6120
|
-
/**
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
* UI can tell "genuine revocation" apart from a plain refresh failure.
|
|
6128
|
-
*/
|
|
6129
|
-
async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
|
|
6130
|
-
const markerOwner = this.safeReadMarker(provider);
|
|
6131
|
-
if (markerOwner && markerOwner !== capturedId) return false;
|
|
6132
|
-
const external = this.safeReadExternal(provider);
|
|
6133
|
-
const decision = decideExternalImport(captured, external);
|
|
6134
|
-
if (decision === "not-rotated") {
|
|
6135
|
-
captured.syncWarning = "external-not-rotated";
|
|
6136
|
-
return false;
|
|
6137
|
-
}
|
|
6138
|
-
if (decision !== "import" || !external) return false;
|
|
6139
|
-
let imported = buildImportedTokens(
|
|
6140
|
-
captured,
|
|
6141
|
-
external
|
|
6142
|
-
);
|
|
6143
|
-
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
|
|
6144
|
-
if (!accessStillValid) {
|
|
6145
|
-
try {
|
|
6146
|
-
const refreshed = await refreshWithToken(external.refreshToken);
|
|
6147
|
-
imported = {
|
|
6148
|
-
...imported,
|
|
6149
|
-
accessToken: refreshed.accessToken,
|
|
6150
|
-
refreshToken: refreshed.refreshToken ?? imported.refreshToken,
|
|
6151
|
-
expiresAt: refreshed.expiresAt,
|
|
6152
|
-
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6153
|
-
};
|
|
6154
|
-
if (refreshed.idToken) imported.idToken = refreshed.idToken;
|
|
6155
|
-
} catch {
|
|
6156
|
-
return false;
|
|
6157
|
-
}
|
|
6158
|
-
}
|
|
6159
|
-
this.writeBackById(provider, capturedId, imported);
|
|
6160
|
-
this.resyncExternal(provider, capturedId, imported);
|
|
6161
|
-
return true;
|
|
6162
|
-
}
|
|
6163
|
-
/**
|
|
6164
|
-
* Marker-gated external write-back (external-cli-sync). After a successful
|
|
6165
|
-
* refresh of the account that OWNS the provider's native CLI store (imported
|
|
6166
|
-
* via `importExternalCliAccount`), push the rotated credential back into the
|
|
6167
|
-
* file — otherwise the daemon's refresh invalidates the single-use refresh
|
|
6168
|
-
* token and silently logs the bare CLI out. NON-FATAL: the internal store is
|
|
6169
|
-
* already persisted; a failed external write only leaves the file stale,
|
|
6170
|
-
* which the `external-divergent` warning surfaces.
|
|
6171
|
-
*/
|
|
6172
|
-
resyncExternal(provider, accountId, tokens) {
|
|
6173
|
-
try {
|
|
6174
|
-
this.externalCliStore.writeBack(provider, accountId, tokens);
|
|
6175
|
-
} catch {
|
|
6176
|
-
}
|
|
7874
|
+
/** Atomically patch one account's non-secret management metadata. */
|
|
7875
|
+
async patchAccountMetadata(providerId, accountId, patch) {
|
|
7876
|
+
const config = this.readConfig();
|
|
7877
|
+
const result = patchAccountMetadata(config, providerId, accountId, patch);
|
|
7878
|
+
if (!result.ok) return result;
|
|
7879
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7880
|
+
return result;
|
|
6177
7881
|
}
|
|
6178
|
-
/**
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
7882
|
+
/** Validate every target, then persist one all-or-nothing batch mutation. */
|
|
7883
|
+
async batchManageAccounts(refs, mutation) {
|
|
7884
|
+
const config = this.readConfig();
|
|
7885
|
+
const result = batchManageAccounts(config, refs, mutation);
|
|
7886
|
+
if (!result.ok) return result;
|
|
7887
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7888
|
+
return result;
|
|
6185
7889
|
}
|
|
6186
7890
|
/**
|
|
6187
7891
|
* DAEMON-ONLY (admin import button): which providers have a usable external
|
|
6188
|
-
* CLI credential on THIS machine. Pure detection
|
|
7892
|
+
* CLI credential on THIS machine. Pure detection reads the native files,
|
|
6189
7893
|
* never mutates anything, never returns a token.
|
|
6190
7894
|
*/
|
|
6191
7895
|
async listExternalCliAvailability() {
|
|
@@ -6196,21 +7900,22 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6196
7900
|
}
|
|
6197
7901
|
/**
|
|
6198
7902
|
* DAEMON-ONLY (admin import button): import the external CLI's current login
|
|
6199
|
-
* as a NEW account (+ activate)
|
|
6200
|
-
*
|
|
6201
|
-
*
|
|
6202
|
-
*
|
|
7903
|
+
* as a NEW account (+ activate). This is a COPY-ONLY import: Omnicross never
|
|
7904
|
+
* claims, writes, moves, restores, or deletes the native CLI credential file
|
|
7905
|
+
* or any legacy `.omnicross-managed` marker/backup beside it. Subsequent
|
|
7906
|
+
* refreshes persist only Omnicross's encrypted token store.
|
|
6203
7907
|
*/
|
|
6204
7908
|
async importExternalCliAccount(provider, label) {
|
|
6205
7909
|
const external = this.safeReadExternal(provider);
|
|
6206
7910
|
if (!external?.accessToken) return { ok: false, reason: "no-credential" };
|
|
6207
7911
|
const tokens = buildTokensFromExternal(provider, external);
|
|
6208
7912
|
const result = await this.appendProviderAccount(provider, tokens, label);
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
7913
|
+
return {
|
|
7914
|
+
ok: true,
|
|
7915
|
+
id: result.id,
|
|
7916
|
+
nativeCredentialMode: "read-only",
|
|
7917
|
+
refreshWritesNativeCredentials: false
|
|
7918
|
+
};
|
|
6214
7919
|
}
|
|
6215
7920
|
/**
|
|
6216
7921
|
* Materialize a lazily-synthesized account id to disk (design D3). On a legacy
|
|
@@ -6243,7 +7948,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6243
7948
|
this.writeBackById(providerId, capturedId, {
|
|
6244
7949
|
...block,
|
|
6245
7950
|
status: "expired",
|
|
6246
|
-
errorMessage
|
|
7951
|
+
errorMessage,
|
|
7952
|
+
syncWarning: "syncWarning" in block && block.syncWarning === "duplicate-token" ? "duplicate-token" : void 0
|
|
6247
7953
|
});
|
|
6248
7954
|
}
|
|
6249
7955
|
/**
|
|
@@ -6252,7 +7958,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6252
7958
|
* `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
|
|
6253
7959
|
* OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
|
|
6254
7960
|
* tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
|
|
6255
|
-
* so a first-ever write still produces a valid config. No cache
|
|
7961
|
+
* so a first-ever write still produces a valid config. No cache the next read
|
|
6256
7962
|
* sees this write.
|
|
6257
7963
|
*/
|
|
6258
7964
|
async writeProviderTokens(providerId, config) {
|
|
@@ -6262,7 +7968,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6262
7968
|
}
|
|
6263
7969
|
/**
|
|
6264
7970
|
* DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
|
|
6265
|
-
* (optional label) and set it active, then re-derive the mirror
|
|
7971
|
+
* (optional label) and set it active, then re-derive the mirror used by
|
|
6266
7972
|
* `omnicross login <provider> --label` to add an account instead of overwriting.
|
|
6267
7973
|
*/
|
|
6268
7974
|
async appendProviderAccount(providerId, config, label) {
|
|
@@ -6296,7 +8002,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6296
8002
|
}
|
|
6297
8003
|
/**
|
|
6298
8004
|
* DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
|
|
6299
|
-
* rejects an unknown id. Label-only
|
|
8005
|
+
* rejects an unknown id. Label-only no token material is read or written
|
|
6300
8006
|
* (the secret-free invariant holds).
|
|
6301
8007
|
*/
|
|
6302
8008
|
async renameAccount(providerId, id, label) {
|
|
@@ -6319,12 +8025,12 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6319
8025
|
}
|
|
6320
8026
|
/** Write the merged config to disk as pretty JSON (mkdir parent if needed).
|
|
6321
8027
|
* Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
|
|
6322
|
-
*
|
|
6323
|
-
* write
|
|
8028
|
+
* `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
8029
|
+
* write incl. child 4's future refresh writes lands encrypted. */
|
|
6324
8030
|
persist(config) {
|
|
6325
|
-
|
|
8031
|
+
mkdirSync4(dirname6(this.tokensPath), { recursive: true });
|
|
6326
8032
|
const encrypted = encryptTokens(config, this.box);
|
|
6327
|
-
|
|
8033
|
+
writeFileSync10(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
|
|
6328
8034
|
}
|
|
6329
8035
|
/**
|
|
6330
8036
|
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
@@ -6332,18 +8038,18 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6332
8038
|
* subscription bearer path is byte-identical).
|
|
6333
8039
|
*
|
|
6334
8040
|
* The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
|
|
6335
|
-
* file
|
|
8041
|
+
* file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
|
|
6336
8042
|
* wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
|
|
6337
|
-
* box's clear, secret-free error (secrets spec "
|
|
6338
|
-
* SHALL fail-fast, SHALL NOT
|
|
6339
|
-
* 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
|
|
6340
8046
|
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
6341
8047
|
*/
|
|
6342
8048
|
readConfig() {
|
|
6343
|
-
if (!
|
|
8049
|
+
if (!existsSync13(this.tokensPath)) return { updatedAt: "" };
|
|
6344
8050
|
let parsed;
|
|
6345
8051
|
try {
|
|
6346
|
-
const raw = JSON.parse(
|
|
8052
|
+
const raw = JSON.parse(readFileSync14(this.tokensPath, "utf8"));
|
|
6347
8053
|
parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
6348
8054
|
} catch {
|
|
6349
8055
|
parsed = null;
|
|
@@ -6355,7 +8061,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6355
8061
|
};
|
|
6356
8062
|
|
|
6357
8063
|
// src/AccountHealthProbeScheduler.ts
|
|
6358
|
-
import { fetchUpstream as
|
|
8064
|
+
import { fetchUpstream as fetchUpstream4 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
6359
8065
|
|
|
6360
8066
|
// src/probe/ProbeStrategy.ts
|
|
6361
8067
|
var PROVIDER_PROBE_PLANS = {
|
|
@@ -6398,7 +8104,7 @@ var AccountHealthProbeScheduler = class {
|
|
|
6398
8104
|
this.logger = logger;
|
|
6399
8105
|
this.config = config;
|
|
6400
8106
|
this.now = opts.now ?? Date.now;
|
|
6401
|
-
this.fetchImpl = opts.fetchImpl ??
|
|
8107
|
+
this.fetchImpl = opts.fetchImpl ?? fetchUpstream4;
|
|
6402
8108
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6403
8109
|
this.planFor = opts.planFor ?? probePlanFor;
|
|
6404
8110
|
}
|
|
@@ -6671,8 +8377,8 @@ var AccountHealthSweeper = class {
|
|
|
6671
8377
|
};
|
|
6672
8378
|
|
|
6673
8379
|
// src/audit/AuditPruneSweeper.ts
|
|
6674
|
-
import { existsSync as
|
|
6675
|
-
import { join as
|
|
8380
|
+
import { existsSync as existsSync14, readdirSync, unlinkSync as unlinkSync3 } from "fs";
|
|
8381
|
+
import { join as join7 } from "path";
|
|
6676
8382
|
|
|
6677
8383
|
// src/audit/auditFiles.ts
|
|
6678
8384
|
var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
@@ -6698,8 +8404,8 @@ function auditFileDateMs(fileName) {
|
|
|
6698
8404
|
var DAY_MS = 24 * 60 * 6e4;
|
|
6699
8405
|
var SWEEP_INTERVAL_MS2 = 60 * 6e4;
|
|
6700
8406
|
var AuditPruneSweeper = class {
|
|
6701
|
-
constructor(
|
|
6702
|
-
this.auditDir =
|
|
8407
|
+
constructor(auditDir2, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
|
|
8408
|
+
this.auditDir = auditDir2;
|
|
6703
8409
|
this.logger = logger;
|
|
6704
8410
|
this.config = config;
|
|
6705
8411
|
this.intervalMs = intervalMs;
|
|
@@ -6746,7 +8452,7 @@ var AuditPruneSweeper = class {
|
|
|
6746
8452
|
if (!this.config.enabled || this.sweeping) return 0;
|
|
6747
8453
|
this.sweeping = true;
|
|
6748
8454
|
try {
|
|
6749
|
-
if (!
|
|
8455
|
+
if (!existsSync14(this.auditDir)) return 0;
|
|
6750
8456
|
const today = new Date(this.now());
|
|
6751
8457
|
const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
|
|
6752
8458
|
const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
|
|
@@ -6755,7 +8461,7 @@ var AuditPruneSweeper = class {
|
|
|
6755
8461
|
const dateMs = auditFileDateMs(file);
|
|
6756
8462
|
if (dateMs === null || dateMs >= cutoff) continue;
|
|
6757
8463
|
try {
|
|
6758
|
-
|
|
8464
|
+
unlinkSync3(join7(this.auditDir, file));
|
|
6759
8465
|
removed += 1;
|
|
6760
8466
|
} catch (error) {
|
|
6761
8467
|
this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
|
|
@@ -6778,26 +8484,26 @@ var AuditPruneSweeper = class {
|
|
|
6778
8484
|
};
|
|
6779
8485
|
|
|
6780
8486
|
// src/audit/auditReader.ts
|
|
6781
|
-
import { existsSync as
|
|
6782
|
-
import { join as
|
|
8487
|
+
import { existsSync as existsSync15, readdirSync as readdirSync2, readFileSync as readFileSync15 } from "fs";
|
|
8488
|
+
import { join as join8 } from "path";
|
|
6783
8489
|
var DEFAULT_LIMIT = 200;
|
|
6784
8490
|
var MAX_LIMIT = 2e3;
|
|
6785
|
-
function readAuditRecords(
|
|
6786
|
-
if (!
|
|
8491
|
+
function readAuditRecords(auditDir2, query2 = {}) {
|
|
8492
|
+
if (!existsSync15(auditDir2)) return [];
|
|
6787
8493
|
let files;
|
|
6788
8494
|
try {
|
|
6789
|
-
files = readdirSync2(
|
|
8495
|
+
files = readdirSync2(auditDir2).filter((f) => AUDIT_FILE_RE.test(f));
|
|
6790
8496
|
} catch {
|
|
6791
8497
|
return [];
|
|
6792
8498
|
}
|
|
6793
|
-
const from = typeof
|
|
6794
|
-
const to = typeof
|
|
6795
|
-
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)));
|
|
6796
8502
|
const matched = [];
|
|
6797
8503
|
for (const file of files.sort().reverse()) {
|
|
6798
8504
|
let raw;
|
|
6799
8505
|
try {
|
|
6800
|
-
raw =
|
|
8506
|
+
raw = readFileSync15(join8(auditDir2, file), "utf8");
|
|
6801
8507
|
} catch {
|
|
6802
8508
|
continue;
|
|
6803
8509
|
}
|
|
@@ -6811,7 +8517,7 @@ function readAuditRecords(auditDir, query = {}) {
|
|
|
6811
8517
|
continue;
|
|
6812
8518
|
}
|
|
6813
8519
|
if (!isAuditRecord(rec)) continue;
|
|
6814
|
-
if (
|
|
8520
|
+
if (query2.keyId !== void 0 && rec.keyId !== query2.keyId) continue;
|
|
6815
8521
|
if (rec.ts < from || rec.ts > to) continue;
|
|
6816
8522
|
matched.push(rec);
|
|
6817
8523
|
}
|
|
@@ -6826,11 +8532,11 @@ function isAuditRecord(value) {
|
|
|
6826
8532
|
}
|
|
6827
8533
|
|
|
6828
8534
|
// src/audit/AuditWriter.ts
|
|
6829
|
-
import { appendFileSync as appendFileSync2, mkdirSync as
|
|
6830
|
-
import { join as
|
|
8535
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
8536
|
+
import { join as join9 } from "path";
|
|
6831
8537
|
var AuditWriter = class {
|
|
6832
|
-
constructor(
|
|
6833
|
-
this.auditDir =
|
|
8538
|
+
constructor(auditDir2, logger, defer = (fn) => setTimeout(fn, 0)) {
|
|
8539
|
+
this.auditDir = auditDir2;
|
|
6834
8540
|
this.logger = logger;
|
|
6835
8541
|
this.defer = defer;
|
|
6836
8542
|
}
|
|
@@ -6860,19 +8566,19 @@ var AuditWriter = class {
|
|
|
6860
8566
|
*/
|
|
6861
8567
|
appendNow(record) {
|
|
6862
8568
|
if (!this.dirEnsured) {
|
|
6863
|
-
|
|
8569
|
+
mkdirSync5(this.auditDir, { recursive: true });
|
|
6864
8570
|
this.dirEnsured = true;
|
|
6865
8571
|
}
|
|
6866
|
-
const file =
|
|
8572
|
+
const file = join9(this.auditDir, auditFileName(record.ts));
|
|
6867
8573
|
appendFileSync2(file, JSON.stringify(record) + "\n", "utf8");
|
|
6868
8574
|
}
|
|
6869
8575
|
};
|
|
6870
8576
|
|
|
6871
8577
|
// src/billing/BillingPublisher.ts
|
|
6872
|
-
import { appendFileSync as appendFileSync3, mkdirSync as
|
|
8578
|
+
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync6 } from "fs";
|
|
6873
8579
|
import { createHmac } from "crypto";
|
|
6874
|
-
import { join as
|
|
6875
|
-
import { fetchUpstream as
|
|
8580
|
+
import { join as join10 } from "path";
|
|
8581
|
+
import { fetchUpstream as fetchUpstream5 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
6876
8582
|
|
|
6877
8583
|
// src/billing/billingFiles.ts
|
|
6878
8584
|
var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
@@ -6895,7 +8601,7 @@ var BillingPublisher = class {
|
|
|
6895
8601
|
constructor(billingDir, logger, opts = {}) {
|
|
6896
8602
|
this.billingDir = billingDir;
|
|
6897
8603
|
this.logger = logger;
|
|
6898
|
-
this.fetchImpl = opts.fetchImpl ?? ((url, init) =>
|
|
8604
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
|
|
6899
8605
|
this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
|
|
6900
8606
|
this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
|
|
6901
8607
|
this.now = opts.now ?? Date.now;
|
|
@@ -6942,7 +8648,7 @@ var BillingPublisher = class {
|
|
|
6942
8648
|
*/
|
|
6943
8649
|
appendNow(event) {
|
|
6944
8650
|
this.ensureDir();
|
|
6945
|
-
const file =
|
|
8651
|
+
const file = join10(this.billingDir, billingFileName(event.ts));
|
|
6946
8652
|
appendFileSync3(file, JSON.stringify(event) + "\n", "utf8");
|
|
6947
8653
|
}
|
|
6948
8654
|
/**
|
|
@@ -6992,7 +8698,7 @@ var BillingPublisher = class {
|
|
|
6992
8698
|
markDelivered(event) {
|
|
6993
8699
|
try {
|
|
6994
8700
|
this.ensureDir();
|
|
6995
|
-
const file =
|
|
8701
|
+
const file = join10(this.billingDir, deliveredFileName(event.ts));
|
|
6996
8702
|
appendFileSync3(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
|
|
6997
8703
|
} catch (error) {
|
|
6998
8704
|
this.logger.warn("[BillingPublisher] failed to append delivery marker", {
|
|
@@ -7002,17 +8708,17 @@ var BillingPublisher = class {
|
|
|
7002
8708
|
}
|
|
7003
8709
|
ensureDir() {
|
|
7004
8710
|
if (this.dirEnsured) return;
|
|
7005
|
-
|
|
8711
|
+
mkdirSync6(this.billingDir, { recursive: true });
|
|
7006
8712
|
this.dirEnsured = true;
|
|
7007
8713
|
}
|
|
7008
8714
|
};
|
|
7009
8715
|
|
|
7010
8716
|
// src/billing/billingReader.ts
|
|
7011
|
-
import { existsSync as
|
|
7012
|
-
import { join as
|
|
8717
|
+
import { existsSync as existsSync16, readdirSync as readdirSync3, readFileSync as readFileSync16 } from "fs";
|
|
8718
|
+
import { join as join11 } from "path";
|
|
7013
8719
|
function readBillingLedger(billingDir) {
|
|
7014
8720
|
const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
|
|
7015
|
-
if (!
|
|
8721
|
+
if (!existsSync16(billingDir)) return view;
|
|
7016
8722
|
let files;
|
|
7017
8723
|
try {
|
|
7018
8724
|
files = readdirSync3(billingDir);
|
|
@@ -7046,7 +8752,7 @@ function readBillingStatus(billingDir) {
|
|
|
7046
8752
|
function parseLines(dir, file) {
|
|
7047
8753
|
let raw;
|
|
7048
8754
|
try {
|
|
7049
|
-
raw =
|
|
8755
|
+
raw = readFileSync16(join11(dir, file), "utf8");
|
|
7050
8756
|
} catch {
|
|
7051
8757
|
return [];
|
|
7052
8758
|
}
|
|
@@ -7201,8 +8907,9 @@ var TokenRefreshScheduler = class {
|
|
|
7201
8907
|
const expiresAt = Date.parse(t.expiresAt);
|
|
7202
8908
|
return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
|
|
7203
8909
|
}
|
|
7204
|
-
/** Refresh one account; failures are logged, never thrown
|
|
7205
|
-
*
|
|
8910
|
+
/** Refresh one managed account; failures are logged, never thrown. The
|
|
8911
|
+
* store marks only the targeted account `expired` on a failed refresh.
|
|
8912
|
+
*/
|
|
7206
8913
|
async refreshOne(provider, id, isActive) {
|
|
7207
8914
|
try {
|
|
7208
8915
|
const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
|
|
@@ -7233,7 +8940,7 @@ var TokenRefreshScheduler = class {
|
|
|
7233
8940
|
|
|
7234
8941
|
// src/webhook/WebhookDispatcher.ts
|
|
7235
8942
|
import { createHmac as createHmac2 } from "crypto";
|
|
7236
|
-
import { fetchUpstream as
|
|
8943
|
+
import { fetchUpstream as fetchUpstream6 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
7237
8944
|
var WEBHOOK_MAX_ATTEMPTS = 3;
|
|
7238
8945
|
var WEBHOOK_QUEUE_MAX = 1e3;
|
|
7239
8946
|
var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
|
|
@@ -7253,7 +8960,7 @@ var WebhookDispatcher = class {
|
|
|
7253
8960
|
sleep;
|
|
7254
8961
|
now;
|
|
7255
8962
|
constructor(opts = {}) {
|
|
7256
|
-
this.fetchImpl = opts.fetchImpl ?? ((url, init) =>
|
|
8963
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream6(url, init));
|
|
7257
8964
|
this.logger = opts.logger;
|
|
7258
8965
|
this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
|
|
7259
8966
|
this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
|
|
@@ -7407,11 +9114,32 @@ function buildDaemon(config, paths) {
|
|
|
7407
9114
|
setSecretBox(secretBox3);
|
|
7408
9115
|
setSecretBox2(secretBox3);
|
|
7409
9116
|
const decryptedConfig = decryptConfigSecrets(config, secretBox3);
|
|
9117
|
+
const accountAllowanceStore = new AccountAllowanceStore3(
|
|
9118
|
+
Date.now,
|
|
9119
|
+
void 0,
|
|
9120
|
+
new JsonAccountAllowancePersistence(defaultAccountAllowancePath(paths.configPath))
|
|
9121
|
+
);
|
|
9122
|
+
setSharedAccountAllowanceStore(accountAllowanceStore);
|
|
9123
|
+
getSharedAccountAllowanceScheduling4().configure(
|
|
9124
|
+
normalizeServerConfig(decryptedConfig.server).allowanceScheduling
|
|
9125
|
+
);
|
|
7410
9126
|
const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
|
|
7411
9127
|
const keyDb = new JsonOutboundKeyDb(paths.keysPath);
|
|
7412
9128
|
const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
|
|
7413
9129
|
const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
|
|
9130
|
+
const integrationStateStore = new IntegrationStateStore(
|
|
9131
|
+
defaultIntegrationsPath(paths.configPath),
|
|
9132
|
+
secretBox3
|
|
9133
|
+
);
|
|
7414
9134
|
const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
|
|
9135
|
+
const accountAllowanceService = new AccountAllowanceService(credentialStore, accountAllowanceStore);
|
|
9136
|
+
const claudeAllowanceRefreshScheduler = new ClaudeAllowanceRefreshScheduler(
|
|
9137
|
+
accountAllowanceService,
|
|
9138
|
+
logger
|
|
9139
|
+
);
|
|
9140
|
+
claudeAllowanceRefreshScheduler.configure(
|
|
9141
|
+
normalizeServerConfig(decryptedConfig.server).allowanceScheduling
|
|
9142
|
+
);
|
|
7415
9143
|
const subscriptionAccounts = new SubscriptionAccountService(credentialStore);
|
|
7416
9144
|
setSubscriptionAccountService(subscriptionAccounts);
|
|
7417
9145
|
const subscriptionRegistry = new SubscriptionProviderRegistry(
|
|
@@ -7440,7 +9168,17 @@ function buildDaemon(config, paths) {
|
|
|
7440
9168
|
}
|
|
7441
9169
|
);
|
|
7442
9170
|
const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
|
|
7443
|
-
const pricingEngine = new PricingEngine(pricingStore, logger
|
|
9171
|
+
const pricingEngine = new PricingEngine(pricingStore, logger, {
|
|
9172
|
+
// Catalog egress follows the same global/env proxy policy as every other
|
|
9173
|
+
// daemon upstream call; no provider/account override applies here.
|
|
9174
|
+
fetchImpl: ((input, init) => fetchUpstream7(String(input), init ?? {}))
|
|
9175
|
+
});
|
|
9176
|
+
const pricingRefreshScheduler = new PricingRefreshScheduler(
|
|
9177
|
+
pricingEngine,
|
|
9178
|
+
pricingStore,
|
|
9179
|
+
defaultPricingRefreshStatePath(paths.configPath),
|
|
9180
|
+
logger
|
|
9181
|
+
);
|
|
7444
9182
|
const usageEventStore = new JsonlUsageEventStore(
|
|
7445
9183
|
defaultUsageEventsPath(paths.configPath),
|
|
7446
9184
|
async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
|
|
@@ -7453,7 +9191,7 @@ function buildDaemon(config, paths) {
|
|
|
7453
9191
|
llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
|
|
7454
9192
|
const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
|
|
7455
9193
|
credentialStore,
|
|
7456
|
-
|
|
9194
|
+
getSharedAccountHealth3(),
|
|
7457
9195
|
logger,
|
|
7458
9196
|
DEFAULT_ACCOUNT_PROBE
|
|
7459
9197
|
);
|
|
@@ -7487,7 +9225,7 @@ function buildDaemon(config, paths) {
|
|
|
7487
9225
|
// lines through the injected logger (honors level/format/file sink).
|
|
7488
9226
|
logger
|
|
7489
9227
|
});
|
|
7490
|
-
const
|
|
9228
|
+
const auditDir2 = defaultAuditDir(paths.configPath);
|
|
7491
9229
|
const billingDir = defaultBillingDir(paths.configPath);
|
|
7492
9230
|
const adminServer = new AdminServer({
|
|
7493
9231
|
configPath: paths.configPath,
|
|
@@ -7501,6 +9239,9 @@ function buildDaemon(config, paths) {
|
|
|
7501
9239
|
settingsStore,
|
|
7502
9240
|
outboundApiServer,
|
|
7503
9241
|
subscriptionAccounts,
|
|
9242
|
+
accountAllowanceService,
|
|
9243
|
+
allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
|
|
9244
|
+
accountProbeService: accountHealthProbeScheduler,
|
|
7504
9245
|
// Least-authority token WRITER (design D4) — the concrete credential store
|
|
7505
9246
|
// exposes `writeProviderTokens` / `clearProvider` as daemon-only methods (NOT
|
|
7506
9247
|
// on the `SubscriptionCredentialStore` port). The admin API sees ONLY these two
|
|
@@ -7521,7 +9262,7 @@ function buildDaemon(config, paths) {
|
|
|
7521
9262
|
// inject a mock so no real token endpoint is hit.
|
|
7522
9263
|
// upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
|
|
7523
9264
|
// helper so interactive login honors a configured proxy (global/env layers).
|
|
7524
|
-
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) =>
|
|
9265
|
+
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream7(url, init)),
|
|
7525
9266
|
subscriptionAccountAppender: credentialStore,
|
|
7526
9267
|
// Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
|
|
7527
9268
|
// + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
|
|
@@ -7539,6 +9280,16 @@ function buildDaemon(config, paths) {
|
|
|
7539
9280
|
cliTerminalOpener: paths.cliTerminalOpener,
|
|
7540
9281
|
cliPathProbe: paths.cliPathProbe,
|
|
7541
9282
|
cliCommandRunner: paths.cliCommandRunner,
|
|
9283
|
+
integrationManagerFactory: () => {
|
|
9284
|
+
const live = outboundApiServer.getStatus();
|
|
9285
|
+
const port = live.port || decryptedConfig.server?.port || DEFAULT_OUTBOUND_PORT;
|
|
9286
|
+
return new IntegrationManager({
|
|
9287
|
+
configPath: paths.configPath,
|
|
9288
|
+
gatewayBaseUrl: live.loopbackUrl ?? `http://127.0.0.1:${port}`,
|
|
9289
|
+
keyDb,
|
|
9290
|
+
stateStore: integrationStateStore
|
|
9291
|
+
});
|
|
9292
|
+
},
|
|
7542
9293
|
// Usage/pricing admin surface (usage-pricing child): stats queries go
|
|
7543
9294
|
// through the recorder facade, pricing mutations through the engine, and
|
|
7544
9295
|
// the row DELETE through the concrete store (delete is store-local — the
|
|
@@ -7563,19 +9314,19 @@ function buildDaemon(config, paths) {
|
|
|
7563
9314
|
// date-rotated audit store. Bound to the store dir here so the AdminServer
|
|
7564
9315
|
// carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
|
|
7565
9316
|
// NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
|
|
7566
|
-
auditReader: (
|
|
9317
|
+
auditReader: (query2) => readAuditRecords(auditDir2, query2),
|
|
7567
9318
|
// billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
|
|
7568
9319
|
// secret-free total/delivered/pending counts of the durable ledger.
|
|
7569
9320
|
billingStatusReader: () => readBillingStatus(billingDir)
|
|
7570
9321
|
});
|
|
7571
9322
|
const webhookDispatcher = new WebhookDispatcher({
|
|
7572
9323
|
logger,
|
|
7573
|
-
fetchImpl: (url, init) =>
|
|
9324
|
+
fetchImpl: (url, init) => fetchUpstream7(url, init)
|
|
7574
9325
|
});
|
|
7575
|
-
setWebhookRuntime(webhookDispatcher,
|
|
7576
|
-
const auditWriter = new AuditWriter(
|
|
7577
|
-
const auditPruneSweeper = new AuditPruneSweeper(
|
|
7578
|
-
setAuditRuntime(auditWriter, auditPruneSweeper);
|
|
9326
|
+
setWebhookRuntime(webhookDispatcher, getSharedAccountHealth3());
|
|
9327
|
+
const auditWriter = new AuditWriter(auditDir2, logger);
|
|
9328
|
+
const auditPruneSweeper = new AuditPruneSweeper(auditDir2, logger, DEFAULT_AUDIT_CONFIG);
|
|
9329
|
+
setAuditRuntime(auditWriter, auditPruneSweeper, auditDir2);
|
|
7579
9330
|
const billingPublisher = new BillingPublisher(billingDir, logger);
|
|
7580
9331
|
const billingRetrySweeper = new BillingRetrySweeper(
|
|
7581
9332
|
billingDir,
|
|
@@ -7587,7 +9338,7 @@ function buildDaemon(config, paths) {
|
|
|
7587
9338
|
const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
|
|
7588
9339
|
const accountHealthSweeper = new AccountHealthSweeper(
|
|
7589
9340
|
credentialStore,
|
|
7590
|
-
|
|
9341
|
+
getSharedAccountHealth3(),
|
|
7591
9342
|
logger
|
|
7592
9343
|
);
|
|
7593
9344
|
return {
|
|
@@ -7602,8 +9353,11 @@ function buildDaemon(config, paths) {
|
|
|
7602
9353
|
credentialStore,
|
|
7603
9354
|
subscriptionRegistry,
|
|
7604
9355
|
subscriptionAccounts,
|
|
9356
|
+
accountAllowanceService,
|
|
9357
|
+
claudeAllowanceRefreshScheduler,
|
|
7605
9358
|
pricingStore,
|
|
7606
9359
|
pricingEngine,
|
|
9360
|
+
pricingRefreshScheduler,
|
|
7607
9361
|
usageRecorder,
|
|
7608
9362
|
adminServer,
|
|
7609
9363
|
tokenRefreshScheduler,
|
|
@@ -7618,7 +9372,7 @@ function buildDaemon(config, paths) {
|
|
|
7618
9372
|
}
|
|
7619
9373
|
function isTokensStoreReadable(tokensPath) {
|
|
7620
9374
|
try {
|
|
7621
|
-
if (!
|
|
9375
|
+
if (!existsSync17(tokensPath)) return true;
|
|
7622
9376
|
accessSync(tokensPath, fsConstants.R_OK);
|
|
7623
9377
|
return true;
|
|
7624
9378
|
} catch {
|
|
@@ -7666,8 +9420,8 @@ function buildCliSpawnPlan(opts) {
|
|
|
7666
9420
|
function resolveInPathDefault(candidate) {
|
|
7667
9421
|
const segments = (process.env["PATH"] ?? "").split(delimiter2).filter(Boolean);
|
|
7668
9422
|
for (const seg of segments) {
|
|
7669
|
-
const full =
|
|
7670
|
-
if (
|
|
9423
|
+
const full = join12(seg, candidate);
|
|
9424
|
+
if (existsSync18(full)) return full;
|
|
7671
9425
|
}
|
|
7672
9426
|
return null;
|
|
7673
9427
|
}
|
|
@@ -7675,7 +9429,7 @@ async function runLaunch(argv, deps) {
|
|
|
7675
9429
|
const sep = argv.indexOf("--");
|
|
7676
9430
|
const own = sep === -1 ? argv : argv.slice(0, sep);
|
|
7677
9431
|
const passthrough = sep === -1 ? [] : argv.slice(sep + 1);
|
|
7678
|
-
const { values, positionals } =
|
|
9432
|
+
const { values, positionals } = parseArgs4({
|
|
7679
9433
|
args: own,
|
|
7680
9434
|
options: {
|
|
7681
9435
|
provider: { type: "string", short: "p" },
|
|
@@ -7710,10 +9464,12 @@ async function runLaunch(argv, deps) {
|
|
|
7710
9464
|
} catch (err5) {
|
|
7711
9465
|
daemon.apiKeyPool.dispose();
|
|
7712
9466
|
daemon.tokenRefreshScheduler.dispose();
|
|
9467
|
+
daemon.claudeAllowanceRefreshScheduler.dispose();
|
|
7713
9468
|
daemon.accountHealthSweeper.dispose();
|
|
7714
9469
|
daemon.accountHealthProbeScheduler.dispose();
|
|
7715
9470
|
daemon.auditPruneSweeper.dispose();
|
|
7716
9471
|
daemon.billingRetrySweeper.dispose();
|
|
9472
|
+
daemon.pricingRefreshScheduler.dispose();
|
|
7717
9473
|
throw err5;
|
|
7718
9474
|
}
|
|
7719
9475
|
let launch;
|
|
@@ -7726,10 +9482,12 @@ async function runLaunch(argv, deps) {
|
|
|
7726
9482
|
await daemon.providerProxy.stop();
|
|
7727
9483
|
daemon.apiKeyPool.dispose();
|
|
7728
9484
|
daemon.tokenRefreshScheduler.dispose();
|
|
9485
|
+
daemon.claudeAllowanceRefreshScheduler.dispose();
|
|
7729
9486
|
daemon.accountHealthSweeper.dispose();
|
|
7730
9487
|
daemon.accountHealthProbeScheduler.dispose();
|
|
7731
9488
|
daemon.auditPruneSweeper.dispose();
|
|
7732
9489
|
daemon.billingRetrySweeper.dispose();
|
|
9490
|
+
daemon.pricingRefreshScheduler.dispose();
|
|
7733
9491
|
throw err5;
|
|
7734
9492
|
}
|
|
7735
9493
|
try {
|
|
@@ -7752,10 +9510,12 @@ async function runLaunch(argv, deps) {
|
|
|
7752
9510
|
await daemon.providerProxy.stop();
|
|
7753
9511
|
daemon.apiKeyPool.dispose();
|
|
7754
9512
|
daemon.tokenRefreshScheduler.dispose();
|
|
9513
|
+
daemon.claudeAllowanceRefreshScheduler.dispose();
|
|
7755
9514
|
daemon.accountHealthSweeper.dispose();
|
|
7756
9515
|
daemon.accountHealthProbeScheduler.dispose();
|
|
7757
9516
|
daemon.auditPruneSweeper.dispose();
|
|
7758
9517
|
daemon.billingRetrySweeper.dispose();
|
|
9518
|
+
daemon.pricingRefreshScheduler.dispose();
|
|
7759
9519
|
}
|
|
7760
9520
|
}
|
|
7761
9521
|
async function buildLaunchConfig(cli, llmConfig, opts) {
|
|
@@ -7785,7 +9545,7 @@ async function buildLaunchConfig(cli, llmConfig, opts) {
|
|
|
7785
9545
|
}
|
|
7786
9546
|
}
|
|
7787
9547
|
function spawnCliInherit(plan) {
|
|
7788
|
-
return new Promise((
|
|
9548
|
+
return new Promise((resolve3, reject) => {
|
|
7789
9549
|
const child = spawn2(plan.command, plan.args, {
|
|
7790
9550
|
stdio: "inherit",
|
|
7791
9551
|
env: plan.env,
|
|
@@ -7819,7 +9579,7 @@ function spawnCliInherit(plan) {
|
|
|
7819
9579
|
});
|
|
7820
9580
|
child.on("exit", (code, signal) => {
|
|
7821
9581
|
detach();
|
|
7822
|
-
|
|
9582
|
+
resolve3(code ?? (signal ? 1 : 0));
|
|
7823
9583
|
});
|
|
7824
9584
|
});
|
|
7825
9585
|
}
|
|
@@ -7827,12 +9587,12 @@ function spawnCliInherit(plan) {
|
|
|
7827
9587
|
// src/commands/login.ts
|
|
7828
9588
|
import { spawn as spawn3 } from "child_process";
|
|
7829
9589
|
import { createInterface } from "readline";
|
|
7830
|
-
import { parseArgs as
|
|
7831
|
-
import { fetchUpstream as
|
|
9590
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
9591
|
+
import { fetchUpstream as fetchUpstream8, setUpstreamProxyResolver as setUpstreamProxyResolver2 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
7832
9592
|
import { claudeOAuth as claudeOAuth3, codexOAuth as codexOAuth3, geminiOAuth as geminiOAuth3 } from "@omnicross/subscriptions";
|
|
7833
9593
|
var PROVIDERS = ["claude", "codex", "gemini"];
|
|
7834
9594
|
async function runLogin(argv, deps) {
|
|
7835
|
-
const { values, positionals } =
|
|
9595
|
+
const { values, positionals } = parseArgs5({
|
|
7836
9596
|
args: argv,
|
|
7837
9597
|
options: {
|
|
7838
9598
|
config: { type: "string", short: "c" },
|
|
@@ -7863,7 +9623,7 @@ async function runLogin(argv, deps) {
|
|
|
7863
9623
|
setUpstreamProxyResolver2(createUpstreamProxyResolver());
|
|
7864
9624
|
try {
|
|
7865
9625
|
const tokensPath = defaultTokensPath(values.config);
|
|
7866
|
-
const exchangeFetch = resolved.tokensFetch ?? ((url, init) =>
|
|
9626
|
+
const exchangeFetch = resolved.tokensFetch ?? ((url, init) => fetchUpstream8(url, init, { providerId: provider }));
|
|
7867
9627
|
const store = new JsonSubscriptionCredentialStore(tokensPath, box, exchangeFetch);
|
|
7868
9628
|
const expiresAt = await runProviderLogin(
|
|
7869
9629
|
provider,
|
|
@@ -7976,33 +9736,33 @@ function buildOpenBrowserCommand(platform, url) {
|
|
|
7976
9736
|
return { command: "xdg-open", args: [url] };
|
|
7977
9737
|
}
|
|
7978
9738
|
function openBrowser(url) {
|
|
7979
|
-
return new Promise((
|
|
9739
|
+
return new Promise((resolve3) => {
|
|
7980
9740
|
try {
|
|
7981
9741
|
const { command, args } = buildOpenBrowserCommand(process.platform, url);
|
|
7982
9742
|
const child = spawn3(command, args, { stdio: "ignore", detached: true });
|
|
7983
|
-
child.on("error", () =>
|
|
9743
|
+
child.on("error", () => resolve3(false));
|
|
7984
9744
|
child.unref();
|
|
7985
|
-
|
|
9745
|
+
resolve3(true);
|
|
7986
9746
|
} catch {
|
|
7987
|
-
|
|
9747
|
+
resolve3(false);
|
|
7988
9748
|
}
|
|
7989
9749
|
});
|
|
7990
9750
|
}
|
|
7991
9751
|
function promptPaste(prompt) {
|
|
7992
9752
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7993
|
-
return new Promise((
|
|
9753
|
+
return new Promise((resolve3) => {
|
|
7994
9754
|
rl.question(prompt, (answer) => {
|
|
7995
9755
|
rl.close();
|
|
7996
|
-
|
|
9756
|
+
resolve3(answer);
|
|
7997
9757
|
});
|
|
7998
9758
|
});
|
|
7999
9759
|
}
|
|
8000
9760
|
|
|
8001
9761
|
// src/commands/providers.ts
|
|
8002
|
-
import { randomUUID as
|
|
8003
|
-
import { parseArgs as
|
|
9762
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
9763
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
8004
9764
|
async function runProviders(argv) {
|
|
8005
|
-
const { values, positionals } =
|
|
9765
|
+
const { values, positionals } = parseArgs6({
|
|
8006
9766
|
args: argv,
|
|
8007
9767
|
options: {
|
|
8008
9768
|
config: { type: "string", short: "c" },
|
|
@@ -8121,7 +9881,7 @@ function providersAddKey(configPath, providerId, opts) {
|
|
|
8121
9881
|
const cfg = loadConfig(configPath);
|
|
8122
9882
|
const row = cfg.providers.find((p) => p.id === providerId);
|
|
8123
9883
|
if (!row) throw new Error(`providers add-key: unknown provider '${providerId}'`);
|
|
8124
|
-
const entry = { id:
|
|
9884
|
+
const entry = { id: randomUUID6(), apiKey: opts.key };
|
|
8125
9885
|
if (opts.label) entry.label = opts.label;
|
|
8126
9886
|
if (opts.weight !== void 0) {
|
|
8127
9887
|
const w = Number(opts.weight);
|
|
@@ -8149,10 +9909,10 @@ function providersRmKey(configPath, providerId, keyId) {
|
|
|
8149
9909
|
}
|
|
8150
9910
|
|
|
8151
9911
|
// src/commands/secrets.ts
|
|
8152
|
-
import { existsSync as
|
|
8153
|
-
import { parseArgs as
|
|
9912
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
|
|
9913
|
+
import { parseArgs as parseArgs7 } from "util";
|
|
8154
9914
|
async function runSecrets(argv) {
|
|
8155
|
-
const { values, positionals } =
|
|
9915
|
+
const { values, positionals } = parseArgs7({
|
|
8156
9916
|
args: argv,
|
|
8157
9917
|
options: {
|
|
8158
9918
|
config: { type: "string", short: "c" },
|
|
@@ -8178,7 +9938,7 @@ async function runSecrets(argv) {
|
|
|
8178
9938
|
case "status":
|
|
8179
9939
|
return secretsStatus(args);
|
|
8180
9940
|
case "rotate":
|
|
8181
|
-
return secretsRotate(args);
|
|
9941
|
+
return await secretsRotate(args);
|
|
8182
9942
|
case "decrypt":
|
|
8183
9943
|
return secretsDecrypt(args);
|
|
8184
9944
|
default:
|
|
@@ -8194,6 +9954,7 @@ function secretsEncrypt(args) {
|
|
|
8194
9954
|
const cfg = loadConfig(args.config);
|
|
8195
9955
|
saveConfig(args.config, cfg);
|
|
8196
9956
|
encryptTokensFileInPlace(args.config, box);
|
|
9957
|
+
rewriteIntegrationState(args.config, box, box);
|
|
8197
9958
|
} finally {
|
|
8198
9959
|
setSecretBox(null);
|
|
8199
9960
|
}
|
|
@@ -8221,10 +9982,27 @@ function secretsStatus(args) {
|
|
|
8221
9982
|
reportField("admin.token", cfg.admin.token);
|
|
8222
9983
|
}
|
|
8223
9984
|
const tokensPath = defaultTokensPath(args.config);
|
|
8224
|
-
if (
|
|
9985
|
+
if (existsSync19(tokensPath)) {
|
|
8225
9986
|
console.info(`Secret status for ${tokensPath}:`);
|
|
8226
9987
|
reportTokenFields(tokensPath);
|
|
8227
9988
|
}
|
|
9989
|
+
const integrationsPath = defaultIntegrationsPath(args.config);
|
|
9990
|
+
if (existsSync19(integrationsPath)) {
|
|
9991
|
+
const state = readRawJson(integrationsPath);
|
|
9992
|
+
const key = state.gatewayKey;
|
|
9993
|
+
if (key && typeof key === "object" && !Array.isArray(key)) {
|
|
9994
|
+
const secret = key.secret;
|
|
9995
|
+
if (typeof secret === "string") reportField("integrations.gatewayKey", secret);
|
|
9996
|
+
}
|
|
9997
|
+
const clients = state.clients;
|
|
9998
|
+
if (clients && typeof clients === "object" && !Array.isArray(clients)) {
|
|
9999
|
+
for (const client of ["codex", "claude"]) {
|
|
10000
|
+
const record = clients[client];
|
|
10001
|
+
const snapshot = record && typeof record === "object" && !Array.isArray(record) ? record.originalContent : void 0;
|
|
10002
|
+
if (typeof snapshot === "string") reportField(`integrations.${client}.snapshot`, snapshot);
|
|
10003
|
+
}
|
|
10004
|
+
}
|
|
10005
|
+
}
|
|
8228
10006
|
}
|
|
8229
10007
|
function reportField(name, raw) {
|
|
8230
10008
|
const cls = classify(raw);
|
|
@@ -8249,7 +10027,7 @@ function reportTokenFields(tokensPath) {
|
|
|
8249
10027
|
}
|
|
8250
10028
|
}
|
|
8251
10029
|
}
|
|
8252
|
-
function secretsRotate(args) {
|
|
10030
|
+
async function secretsRotate(args) {
|
|
8253
10031
|
if (!args.newMasterKeyFile) {
|
|
8254
10032
|
throw new Error("secrets rotate: --new-master-key-file <path> is required");
|
|
8255
10033
|
}
|
|
@@ -8258,10 +10036,15 @@ function secretsRotate(args) {
|
|
|
8258
10036
|
setSecretBox(oldBox);
|
|
8259
10037
|
let cfg;
|
|
8260
10038
|
let tokensPlain = null;
|
|
10039
|
+
let integrationsPlain = null;
|
|
8261
10040
|
const tokensPath = defaultTokensPath(args.config);
|
|
10041
|
+
const integrationsPath = defaultIntegrationsPath(args.config);
|
|
8262
10042
|
try {
|
|
8263
10043
|
cfg = loadConfig(args.config);
|
|
8264
|
-
if (
|
|
10044
|
+
if (existsSync19(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, oldBox);
|
|
10045
|
+
if (existsSync19(integrationsPath)) {
|
|
10046
|
+
integrationsPlain = new IntegrationStateStore(integrationsPath, oldBox).load();
|
|
10047
|
+
}
|
|
8265
10048
|
} finally {
|
|
8266
10049
|
setSecretBox(null);
|
|
8267
10050
|
}
|
|
@@ -8269,6 +10052,10 @@ function secretsRotate(args) {
|
|
|
8269
10052
|
try {
|
|
8270
10053
|
saveConfig(args.config, cfg);
|
|
8271
10054
|
if (tokensPlain) writeTokensEncrypted(tokensPath, tokensPlain, newBox);
|
|
10055
|
+
if (integrationsPlain) {
|
|
10056
|
+
const newStore = new IntegrationStateStore(integrationsPath, newBox);
|
|
10057
|
+
newStore.save(integrationsPlain);
|
|
10058
|
+
}
|
|
8272
10059
|
} finally {
|
|
8273
10060
|
setSecretBox(null);
|
|
8274
10061
|
}
|
|
@@ -8290,20 +10077,20 @@ function secretsDecrypt(args) {
|
|
|
8290
10077
|
let tokensPlain = null;
|
|
8291
10078
|
try {
|
|
8292
10079
|
cfg = loadConfig(args.config);
|
|
8293
|
-
if (
|
|
10080
|
+
if (existsSync19(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
|
|
8294
10081
|
} finally {
|
|
8295
10082
|
setSecretBox(null);
|
|
8296
10083
|
}
|
|
8297
10084
|
saveConfig(args.config, cfg);
|
|
8298
10085
|
if (tokensPlain) {
|
|
8299
|
-
|
|
10086
|
+
writeFileSync11(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
|
|
8300
10087
|
}
|
|
8301
10088
|
console.info(`Decrypted secrets to plaintext in ${args.config}` + tokensSuffix(args.config));
|
|
8302
10089
|
}
|
|
8303
10090
|
function readRawConfig(path2) {
|
|
8304
10091
|
let parsed;
|
|
8305
10092
|
try {
|
|
8306
|
-
parsed = JSON.parse(
|
|
10093
|
+
parsed = JSON.parse(readFileSync17(path2, "utf8"));
|
|
8307
10094
|
} catch {
|
|
8308
10095
|
throw new Error(`secrets: cannot read or parse '${path2}'`);
|
|
8309
10096
|
}
|
|
@@ -8311,7 +10098,7 @@ function readRawConfig(path2) {
|
|
|
8311
10098
|
}
|
|
8312
10099
|
function readRawJson(path2) {
|
|
8313
10100
|
try {
|
|
8314
|
-
const parsed = JSON.parse(
|
|
10101
|
+
const parsed = JSON.parse(readFileSync17(path2, "utf8"));
|
|
8315
10102
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
8316
10103
|
return parsed;
|
|
8317
10104
|
}
|
|
@@ -8321,10 +10108,16 @@ function readRawJson(path2) {
|
|
|
8321
10108
|
}
|
|
8322
10109
|
function encryptTokensFileInPlace(configPath, box) {
|
|
8323
10110
|
const tokensPath = defaultTokensPath(configPath);
|
|
8324
|
-
if (!
|
|
10111
|
+
if (!existsSync19(tokensPath)) return;
|
|
8325
10112
|
const plain = decryptTokensFile(tokensPath, box);
|
|
8326
10113
|
writeTokensEncrypted(tokensPath, plain, box);
|
|
8327
10114
|
}
|
|
10115
|
+
function rewriteIntegrationState(configPath, readBox, writeBox) {
|
|
10116
|
+
const path2 = defaultIntegrationsPath(configPath);
|
|
10117
|
+
if (!existsSync19(path2)) return;
|
|
10118
|
+
const state = new IntegrationStateStore(path2, readBox).load();
|
|
10119
|
+
new IntegrationStateStore(path2, writeBox).save(state);
|
|
10120
|
+
}
|
|
8328
10121
|
function decryptTokensFile(tokensPath, box) {
|
|
8329
10122
|
const raw = readRawJson(tokensPath);
|
|
8330
10123
|
return walkTokens(raw, (v) => box.decryptMaybe(v));
|
|
@@ -8334,7 +10127,7 @@ function writeTokensEncrypted(tokensPath, plain, box) {
|
|
|
8334
10127
|
{ updatedAt: "", ...plain },
|
|
8335
10128
|
box
|
|
8336
10129
|
);
|
|
8337
|
-
|
|
10130
|
+
writeFileSync11(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
|
|
8338
10131
|
}
|
|
8339
10132
|
var TOKEN_FIELDS2 = {
|
|
8340
10133
|
claude: ["accessToken", "refreshToken"],
|
|
@@ -8357,18 +10150,19 @@ function walkTokens(raw, fn) {
|
|
|
8357
10150
|
return next;
|
|
8358
10151
|
}
|
|
8359
10152
|
function tokensSuffix(configPath) {
|
|
8360
|
-
return
|
|
10153
|
+
return existsSync19(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
|
|
8361
10154
|
}
|
|
8362
10155
|
|
|
8363
10156
|
// src/commands/start.ts
|
|
8364
|
-
import { parseArgs as
|
|
8365
|
-
import { loadServerConfig as loadServerConfig3
|
|
8366
|
-
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";
|
|
8367
10161
|
|
|
8368
10162
|
// src/identity/identityRuntime.ts
|
|
8369
|
-
import { getSharedIdentityStore as
|
|
10163
|
+
import { getSharedIdentityStore as getSharedIdentityStore3 } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
|
|
8370
10164
|
async function applyFingerprintConfig(config, credentialStore) {
|
|
8371
|
-
const store =
|
|
10165
|
+
const store = getSharedIdentityStore3();
|
|
8372
10166
|
const enabled = config?.enabled === true;
|
|
8373
10167
|
store.configure({ enabled, ua: config?.ua ?? null });
|
|
8374
10168
|
if (!enabled) {
|
|
@@ -8399,7 +10193,7 @@ async function seedIdentities(store, credentialStore) {
|
|
|
8399
10193
|
|
|
8400
10194
|
// src/commands/start.ts
|
|
8401
10195
|
async function runStart(argv) {
|
|
8402
|
-
const { values } =
|
|
10196
|
+
const { values } = parseArgs8({
|
|
8403
10197
|
args: argv,
|
|
8404
10198
|
options: {
|
|
8405
10199
|
config: { type: "string", short: "c" },
|
|
@@ -8423,35 +10217,32 @@ async function runStart(argv) {
|
|
|
8423
10217
|
await daemon.llmConfig.ready();
|
|
8424
10218
|
await daemon.providerProxy.start();
|
|
8425
10219
|
const serverConfig = await loadServerConfig3(daemon.settingsStore);
|
|
8426
|
-
|
|
10220
|
+
getSharedAccountHealth4().configure({
|
|
8427
10221
|
overloadEnabled: serverConfig.accountHealth?.overloadCooldownEnabled,
|
|
8428
10222
|
overloadTtlMs: serverConfig.accountHealth?.overloadCooldownMs
|
|
8429
10223
|
});
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
console.warn(`[outbound] not started \u2014 incomplete model configuration: ${err5.message}`);
|
|
8445
|
-
} else {
|
|
8446
|
-
throw err5;
|
|
8447
|
-
}
|
|
8448
|
-
}
|
|
10224
|
+
getSharedAccountAllowanceScheduling5().configure(serverConfig.allowanceScheduling);
|
|
10225
|
+
daemon.claudeAllowanceRefreshScheduler.configure(serverConfig.allowanceScheduling);
|
|
10226
|
+
await daemon.outboundApiServer.applyConfig({
|
|
10227
|
+
enabled: true,
|
|
10228
|
+
networkBinding: serverConfig.networkBinding,
|
|
10229
|
+
endpoints: serverConfig.endpoints,
|
|
10230
|
+
bindings: serverConfig.bindings,
|
|
10231
|
+
port: serverConfig.port,
|
|
10232
|
+
userMessageQueue: serverConfig.userMessageQueue,
|
|
10233
|
+
concurrencyQueue: serverConfig.concurrencyQueue,
|
|
10234
|
+
// voucher-redemption #9: carry the persisted flag so `POST /redeem` works on
|
|
10235
|
+
// boot when the operator has enabled the product.
|
|
10236
|
+
voucher: serverConfig.voucher
|
|
10237
|
+
});
|
|
8449
10238
|
let dashboardUrl = null;
|
|
8450
10239
|
if (!values["no-dashboard"]) {
|
|
8451
10240
|
await daemon.adminServer.start();
|
|
8452
10241
|
dashboardUrl = daemon.adminServer.getStatus().url;
|
|
8453
10242
|
}
|
|
8454
10243
|
daemon.tokenRefreshScheduler.start();
|
|
10244
|
+
daemon.claudeAllowanceRefreshScheduler.start();
|
|
10245
|
+
daemon.pricingRefreshScheduler.start();
|
|
8455
10246
|
daemon.accountHealthSweeper.start();
|
|
8456
10247
|
if (serverConfig.accountProbe) {
|
|
8457
10248
|
daemon.accountHealthProbeScheduler.configure(serverConfig.accountProbe);
|
|
@@ -8528,6 +10319,16 @@ Usage:
|
|
|
8528
10319
|
omnicross launch <cli> --provider <id> --model <m> --config <p> [--cwd <dir>] [-- <cli-args\u2026>]
|
|
8529
10320
|
Launch a Code CLI (claude|codex|gemini|qwen|copilot|opencode)
|
|
8530
10321
|
against an in-process proxy (route-token auth; BYO).
|
|
10322
|
+
omnicross integrations status --config <p> Inspect native CLI gateway integration.
|
|
10323
|
+
omnicross integrations plan <codex|claude> --config <p> [--target <path>]
|
|
10324
|
+
Preview redacted configuration changes.
|
|
10325
|
+
omnicross integrations install <codex|claude> --config <p> --gateway-base-url <url>
|
|
10326
|
+
Reversibly configure a native CLI for Omnicross.
|
|
10327
|
+
omnicross integrations repair <codex|claude> --config <p>
|
|
10328
|
+
Repair managed fields while preserving unrelated edits.
|
|
10329
|
+
omnicross integrations remove <codex|claude> --config <p>
|
|
10330
|
+
Restore the exact pre-install configuration.
|
|
10331
|
+
omnicross integrations rotate --config <p> Rotate the shared local integration key.
|
|
8531
10332
|
omnicross import-ccr <ccr.json> [--out <p>] Translate a CCR config.
|
|
8532
10333
|
omnicross secrets encrypt --config <p> Encrypt all at-rest secrets in place.
|
|
8533
10334
|
omnicross secrets status --config <p> Report each secret field (no values shown).
|
|
@@ -8554,6 +10355,9 @@ async function main() {
|
|
|
8554
10355
|
case "launch":
|
|
8555
10356
|
process.exitCode = await runLaunch(rest);
|
|
8556
10357
|
return;
|
|
10358
|
+
case "integrations":
|
|
10359
|
+
await runIntegrations(rest);
|
|
10360
|
+
return;
|
|
8557
10361
|
case "import-ccr":
|
|
8558
10362
|
await runImportCcr(rest);
|
|
8559
10363
|
return;
|