@omnicross/daemon 0.1.0 → 0.1.2
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 +1713 -625
- package/dist/cli.js +1707 -615
- package/dist/index.cjs +1647 -593
- package/dist/index.d.cts +415 -26
- package/dist/index.d.ts +415 -26
- package/dist/index.js +1637 -578
- package/package.json +3 -2
package/dist/cli.cjs
CHANGED
|
@@ -180,23 +180,23 @@ function decodeEnvKey(raw) {
|
|
|
180
180
|
}
|
|
181
181
|
return buf;
|
|
182
182
|
}
|
|
183
|
-
function readKeyFile(
|
|
184
|
-
const raw = (0, import_node_fs.readFileSync)(
|
|
183
|
+
function readKeyFile(path2) {
|
|
184
|
+
const raw = (0, import_node_fs.readFileSync)(path2);
|
|
185
185
|
if (raw.length === KEY_BYTES2) return raw;
|
|
186
186
|
const text = raw.toString("utf8").trim();
|
|
187
187
|
if (/^[0-9a-fA-F]{64}$/.test(text)) return Buffer.from(text, "hex");
|
|
188
188
|
const b64 = Buffer.from(text, "base64");
|
|
189
189
|
if (b64.length === KEY_BYTES2) return b64;
|
|
190
190
|
throw new Error(
|
|
191
|
-
`master key file '${
|
|
191
|
+
`master key file '${path2}' is invalid: expected 32 raw bytes, 64 hex chars, or 32-byte base64`
|
|
192
192
|
);
|
|
193
193
|
}
|
|
194
|
-
function generateKeyFile(
|
|
194
|
+
function generateKeyFile(path2) {
|
|
195
195
|
const key = (0, import_node_crypto2.randomBytes)(KEY_BYTES2);
|
|
196
|
-
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(
|
|
197
|
-
(0, import_node_fs.writeFileSync)(
|
|
196
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path2), { recursive: true });
|
|
197
|
+
(0, import_node_fs.writeFileSync)(path2, key, { mode: 384 });
|
|
198
198
|
try {
|
|
199
|
-
(0, import_node_fs.chmodSync)(
|
|
199
|
+
(0, import_node_fs.chmodSync)(path2, 384);
|
|
200
200
|
} catch {
|
|
201
201
|
}
|
|
202
202
|
return key;
|
|
@@ -575,25 +575,25 @@ var secretBox = null;
|
|
|
575
575
|
function setSecretBox(box) {
|
|
576
576
|
secretBox = box;
|
|
577
577
|
}
|
|
578
|
-
function loadConfig(
|
|
578
|
+
function loadConfig(path2) {
|
|
579
579
|
let raw;
|
|
580
580
|
try {
|
|
581
|
-
raw = (0, import_node_fs2.readFileSync)(
|
|
581
|
+
raw = (0, import_node_fs2.readFileSync)(path2, "utf8");
|
|
582
582
|
} catch {
|
|
583
|
-
throw new Error(`config: cannot read file at '${
|
|
583
|
+
throw new Error(`config: cannot read file at '${path2}'`);
|
|
584
584
|
}
|
|
585
585
|
let parsed;
|
|
586
586
|
try {
|
|
587
587
|
parsed = JSON.parse(raw);
|
|
588
588
|
} catch {
|
|
589
|
-
throw new Error(`config: '${
|
|
589
|
+
throw new Error(`config: '${path2}' is not valid JSON`);
|
|
590
590
|
}
|
|
591
591
|
const validated = validateConfig(parsed);
|
|
592
592
|
return secretBox ? decryptConfigSecrets(validated, secretBox) : validated;
|
|
593
593
|
}
|
|
594
|
-
function saveConfig(
|
|
594
|
+
function saveConfig(path2, cfg) {
|
|
595
595
|
const toWrite = secretBox ? encryptConfigSecrets(cfg, secretBox) : cfg;
|
|
596
|
-
(0, import_node_fs2.writeFileSync)(
|
|
596
|
+
(0, import_node_fs2.writeFileSync)(path2, JSON.stringify(toWrite, null, 2) + "\n", "utf8");
|
|
597
597
|
}
|
|
598
598
|
|
|
599
599
|
// src/commands/paths.ts
|
|
@@ -604,6 +604,12 @@ function defaultKeysPath(configPath) {
|
|
|
604
604
|
function defaultTokensPath(configPath) {
|
|
605
605
|
return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "tokens.json");
|
|
606
606
|
}
|
|
607
|
+
function defaultPricingPath(configPath) {
|
|
608
|
+
return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "pricing.json");
|
|
609
|
+
}
|
|
610
|
+
function defaultUsageEventsPath(configPath) {
|
|
611
|
+
return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "usage-events.jsonl");
|
|
612
|
+
}
|
|
607
613
|
function resolveSecretBox(masterKeyFilePath) {
|
|
608
614
|
return new SecretBox(() => resolveMasterKey({ keyFilePath: masterKeyFilePath }));
|
|
609
615
|
}
|
|
@@ -781,11 +787,11 @@ async function keysRevoke(db, id) {
|
|
|
781
787
|
}
|
|
782
788
|
|
|
783
789
|
// src/commands/launch.ts
|
|
784
|
-
var
|
|
785
|
-
var
|
|
786
|
-
var
|
|
790
|
+
var import_node_child_process2 = require("child_process");
|
|
791
|
+
var import_node_fs13 = require("fs");
|
|
792
|
+
var import_node_path8 = require("path");
|
|
787
793
|
var import_node_util3 = require("util");
|
|
788
|
-
var
|
|
794
|
+
var import_cli_launcher2 = require("@omnicross/cli-launcher");
|
|
789
795
|
|
|
790
796
|
// src/bootstrap.ts
|
|
791
797
|
var import_GeminiCodeAssistProjectResolver = require("@omnicross/core/auth/GeminiCodeAssistProjectResolver");
|
|
@@ -794,6 +800,7 @@ var import_outbound_api4 = require("@omnicross/core/outbound-api");
|
|
|
794
800
|
var import_subscriptionRegistryPort = require("@omnicross/core/outbound-api/subscriptionRegistryPort");
|
|
795
801
|
var import_gemini_code_assist_resolver = require("@omnicross/core/ports/gemini-code-assist-resolver");
|
|
796
802
|
var import_provider_proxy = require("@omnicross/core/provider-proxy");
|
|
803
|
+
var import_usage = require("@omnicross/core/usage");
|
|
797
804
|
var import_subscriptions4 = require("@omnicross/subscriptions");
|
|
798
805
|
|
|
799
806
|
// src/admin/accountsCodexOAuth.ts
|
|
@@ -888,7 +895,7 @@ function handleCodexOAuthStatus(sessionId, deps) {
|
|
|
888
895
|
}
|
|
889
896
|
|
|
890
897
|
// src/admin/AdminServer.ts
|
|
891
|
-
var
|
|
898
|
+
var import_node_crypto7 = require("crypto");
|
|
892
899
|
var import_node_http2 = __toESM(require("http"), 1);
|
|
893
900
|
|
|
894
901
|
// src/admin/adminApi.ts
|
|
@@ -1158,7 +1165,8 @@ async function handleOAuthComplete(providerId, body, deps) {
|
|
|
1158
1165
|
const reason = exchangeError instanceof Error ? exchangeError.message : "token exchange failed";
|
|
1159
1166
|
return err2(502, `oauth token exchange failed for '${providerId}': ${reason}`);
|
|
1160
1167
|
}
|
|
1161
|
-
|
|
1168
|
+
const label = typeof body["label"] === "string" && body["label"].trim() ? body["label"].trim() : void 0;
|
|
1169
|
+
await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
|
|
1162
1170
|
const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
1163
1171
|
return { status: 200, body: status ? { account: status } : { ok: true } };
|
|
1164
1172
|
}
|
|
@@ -1191,8 +1199,202 @@ async function exchangeGemini(code, codeVerifier, exchangeFetch) {
|
|
|
1191
1199
|
};
|
|
1192
1200
|
}
|
|
1193
1201
|
|
|
1194
|
-
// src/
|
|
1202
|
+
// src/admin/cliLaunch.ts
|
|
1203
|
+
var import_node_child_process = require("child_process");
|
|
1195
1204
|
var import_node_crypto4 = require("crypto");
|
|
1205
|
+
var import_node_fs5 = require("fs");
|
|
1206
|
+
var import_node_path3 = require("path");
|
|
1207
|
+
var import_cli_launcher = require("@omnicross/cli-launcher");
|
|
1208
|
+
var LAUNCHABLE_CLIS = [
|
|
1209
|
+
{ id: "claude", displayName: "Claude Code", command: "claude" },
|
|
1210
|
+
{ id: "codex", displayName: "Codex CLI", command: "codex" },
|
|
1211
|
+
{ id: "gemini", displayName: "Gemini CLI", command: "gemini" },
|
|
1212
|
+
{ id: "qwen", displayName: "Qwen Code", command: "qwen" },
|
|
1213
|
+
{ id: "copilot", displayName: "GitHub Copilot CLI", command: "copilot" },
|
|
1214
|
+
{ id: "opencode", displayName: "OpenCode", command: "opencode" }
|
|
1215
|
+
];
|
|
1216
|
+
var INSTALL_COMMANDS = {
|
|
1217
|
+
claude: "npm install -g @anthropic-ai/claude-code",
|
|
1218
|
+
codex: "npm install -g @openai/codex",
|
|
1219
|
+
gemini: "npm install -g @google/gemini-cli",
|
|
1220
|
+
qwen: "npm install -g @qwen-code/qwen-code",
|
|
1221
|
+
copilot: "npm install -g @github/copilot",
|
|
1222
|
+
opencode: "npm install -g opencode-ai"
|
|
1223
|
+
};
|
|
1224
|
+
var LAUNCHABLE_IDS = new Set(LAUNCHABLE_CLIS.map((c) => c.id));
|
|
1225
|
+
function isLaunchCliId(id) {
|
|
1226
|
+
return id !== void 0 && LAUNCHABLE_IDS.has(id);
|
|
1227
|
+
}
|
|
1228
|
+
function probeDefault(candidate) {
|
|
1229
|
+
const segments = (process.env["PATH"] ?? "").split(import_node_path3.delimiter).filter(Boolean);
|
|
1230
|
+
for (const seg of segments) {
|
|
1231
|
+
const full = (0, import_node_path3.join)(seg, candidate);
|
|
1232
|
+
if ((0, import_node_fs5.existsSync)(full)) return full;
|
|
1233
|
+
}
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
function isCliInstalled(command, platform = process.platform, probe = probeDefault) {
|
|
1237
|
+
if (platform === "win32") {
|
|
1238
|
+
return Boolean(probe(`${command}.exe`) || probe(`${command}.cmd`) || probe(`${command}.bat`));
|
|
1239
|
+
}
|
|
1240
|
+
return Boolean(probe(command));
|
|
1241
|
+
}
|
|
1242
|
+
function detectClis(platform = process.platform, probe = probeDefault) {
|
|
1243
|
+
return LAUNCHABLE_CLIS.map((c) => ({
|
|
1244
|
+
id: c.id,
|
|
1245
|
+
displayName: c.displayName,
|
|
1246
|
+
command: c.command,
|
|
1247
|
+
installed: isCliInstalled(c.command, platform, probe),
|
|
1248
|
+
installable: Boolean(INSTALL_COMMANDS[c.id])
|
|
1249
|
+
}));
|
|
1250
|
+
}
|
|
1251
|
+
function resolveLaunchTarget(providers, requested) {
|
|
1252
|
+
const pick = (requested?.providerId ? providers.find((p) => p.id === requested.providerId) : void 0) ?? providers.find((p) => p.enabled !== false && firstModel(p)) ?? providers.find((p) => firstModel(p));
|
|
1253
|
+
if (!pick) {
|
|
1254
|
+
throw new Error("no provider with a model is configured \u2014 add one on the Providers page first");
|
|
1255
|
+
}
|
|
1256
|
+
const model = requested?.model || firstModel(pick);
|
|
1257
|
+
if (!model) {
|
|
1258
|
+
throw new Error(`provider "${pick.id}" has no models \u2014 add a model on the Providers page first`);
|
|
1259
|
+
}
|
|
1260
|
+
return { providerId: pick.id, model };
|
|
1261
|
+
}
|
|
1262
|
+
function firstModel(p) {
|
|
1263
|
+
return p.models?.[0] ?? p.modelConfigs?.[0]?.id;
|
|
1264
|
+
}
|
|
1265
|
+
async function buildLaunchEnv(cli, llmConfig, target) {
|
|
1266
|
+
const common = {
|
|
1267
|
+
llmConfig,
|
|
1268
|
+
providerId: target.providerId,
|
|
1269
|
+
model: target.model,
|
|
1270
|
+
sessionId: `dashboard:${cli}`
|
|
1271
|
+
};
|
|
1272
|
+
switch (cli) {
|
|
1273
|
+
case "claude":
|
|
1274
|
+
return (0, import_cli_launcher.buildClaudeCliLaunchConfig)(common);
|
|
1275
|
+
case "codex":
|
|
1276
|
+
return (0, import_cli_launcher.buildCodexLaunchConfig)(common);
|
|
1277
|
+
case "gemini":
|
|
1278
|
+
return (0, import_cli_launcher.buildGeminiCliLaunchConfig)(common);
|
|
1279
|
+
case "qwen":
|
|
1280
|
+
case "copilot":
|
|
1281
|
+
case "opencode":
|
|
1282
|
+
return (0, import_cli_launcher.buildChatCliLaunchConfig)({ backendId: cli, ...common });
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function shq(s) {
|
|
1286
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1287
|
+
}
|
|
1288
|
+
var defaultTerminalOpener = ({ cli, command, extraArgs, env, cwd, platform }) => {
|
|
1289
|
+
const childEnv = { ...process.env, ...env };
|
|
1290
|
+
if (platform === "win32") {
|
|
1291
|
+
const args = ["/c", "start", `"omnicross ${cli}"`];
|
|
1292
|
+
if (cwd) args.push("/D", `"${cwd}"`);
|
|
1293
|
+
args.push("cmd", "/k", command, ...extraArgs);
|
|
1294
|
+
(0, import_node_child_process.spawn)(process.env["ComSpec"] || "cmd.exe", args, {
|
|
1295
|
+
env: childEnv,
|
|
1296
|
+
windowsVerbatimArguments: true,
|
|
1297
|
+
detached: true,
|
|
1298
|
+
stdio: "ignore"
|
|
1299
|
+
}).unref();
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const exportLine = Object.entries(env).map(([k, v]) => `export ${k}=${shq(v)}`).join("; ");
|
|
1303
|
+
const runLine = [command, ...extraArgs].map(shq).join(" ");
|
|
1304
|
+
const script = `${exportLine}; ${cwd ? `cd ${shq(cwd)}; ` : ""}${runLine}`;
|
|
1305
|
+
if (platform === "darwin") {
|
|
1306
|
+
const osa = `tell application "Terminal" to do script ${JSON.stringify(script)}`;
|
|
1307
|
+
(0, import_node_child_process.spawn)("osascript", ["-e", osa], { detached: true, stdio: "ignore" }).unref();
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
(0, import_node_child_process.spawn)("x-terminal-emulator", ["-e", "bash", "-lc", `${script}; exec bash`], {
|
|
1311
|
+
detached: true,
|
|
1312
|
+
stdio: "ignore"
|
|
1313
|
+
}).unref();
|
|
1314
|
+
};
|
|
1315
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
1316
|
+
function errBody(message) {
|
|
1317
|
+
return { error: { type: "admin_api_error", message } };
|
|
1318
|
+
}
|
|
1319
|
+
var defaultCommandRunner = (command) => new Promise((resolve) => {
|
|
1320
|
+
(0, import_node_child_process.exec)(command, { timeout: 18e4 }, (err5, _stdout, stderr) => {
|
|
1321
|
+
if (err5) resolve({ ok: false, error: stderr.trim() || err5.message });
|
|
1322
|
+
else resolve({ ok: true });
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
async function handleCliInstall(cli, runner = defaultCommandRunner) {
|
|
1326
|
+
const cmd = INSTALL_COMMANDS[cli];
|
|
1327
|
+
if (!cmd) {
|
|
1328
|
+
return { status: 400, body: errBody(`no install command for cli '${cli}' (manual install only)`) };
|
|
1329
|
+
}
|
|
1330
|
+
const result = await runner(cmd);
|
|
1331
|
+
if (!result.ok) {
|
|
1332
|
+
return { status: 500, body: errBody(result.error || "install failed") };
|
|
1333
|
+
}
|
|
1334
|
+
return { status: 200, body: { ok: true } };
|
|
1335
|
+
}
|
|
1336
|
+
function handleCliList(platform = process.platform, probe = probeDefault) {
|
|
1337
|
+
return { status: 200, body: { clis: detectClis(platform, probe) } };
|
|
1338
|
+
}
|
|
1339
|
+
function handleCliSessions() {
|
|
1340
|
+
const list = [...sessions.values()].map(({ onSessionEnd: _drop, ...rest }) => rest);
|
|
1341
|
+
return { status: 200, body: { sessions: list } };
|
|
1342
|
+
}
|
|
1343
|
+
function handleCliStop(id) {
|
|
1344
|
+
const s = sessions.get(id);
|
|
1345
|
+
if (!s) return { status: 404, body: errBody(`session '${id}' not found`) };
|
|
1346
|
+
try {
|
|
1347
|
+
s.onSessionEnd();
|
|
1348
|
+
} catch {
|
|
1349
|
+
}
|
|
1350
|
+
sessions.delete(id);
|
|
1351
|
+
return { status: 200, body: { ok: true } };
|
|
1352
|
+
}
|
|
1353
|
+
async function handleCliLaunch(cli, body, ctx) {
|
|
1354
|
+
const platform = ctx.platform ?? process.platform;
|
|
1355
|
+
const probe = ctx.probe ?? probeDefault;
|
|
1356
|
+
const meta = LAUNCHABLE_CLIS.find((c) => c.id === cli);
|
|
1357
|
+
if (!meta) return { status: 404, body: errBody(`unknown cli '${cli}'`) };
|
|
1358
|
+
if (!isCliInstalled(meta.command, platform, probe)) {
|
|
1359
|
+
return { status: 400, body: errBody(`"${meta.command}" is not installed (not found on PATH)`) };
|
|
1360
|
+
}
|
|
1361
|
+
let target;
|
|
1362
|
+
try {
|
|
1363
|
+
target = resolveLaunchTarget(ctx.providers, {
|
|
1364
|
+
providerId: typeof body["providerId"] === "string" ? body["providerId"] : void 0,
|
|
1365
|
+
model: typeof body["model"] === "string" ? body["model"] : void 0
|
|
1366
|
+
});
|
|
1367
|
+
} catch (err5) {
|
|
1368
|
+
return { status: 400, body: errBody(err5 instanceof Error ? err5.message : "no launch target") };
|
|
1369
|
+
}
|
|
1370
|
+
let launch;
|
|
1371
|
+
try {
|
|
1372
|
+
launch = await buildLaunchEnv(cli, ctx.llmConfig, target);
|
|
1373
|
+
} catch (err5) {
|
|
1374
|
+
return { status: 400, body: errBody(err5 instanceof Error ? err5.message : "failed to build launch env") };
|
|
1375
|
+
}
|
|
1376
|
+
const cwd = typeof body["cwd"] === "string" && body["cwd"].trim() ? body["cwd"].trim() : void 0;
|
|
1377
|
+
const opener = ctx.opener ?? defaultTerminalOpener;
|
|
1378
|
+
try {
|
|
1379
|
+
opener({ cli, command: meta.command, extraArgs: launch.extraArgs ?? [], env: launch.env, cwd, platform });
|
|
1380
|
+
} catch (err5) {
|
|
1381
|
+
launch.onSessionEnd();
|
|
1382
|
+
return { status: 500, body: errBody(err5 instanceof Error ? err5.message : "failed to open terminal") };
|
|
1383
|
+
}
|
|
1384
|
+
const id = (0, import_node_crypto4.randomUUID)();
|
|
1385
|
+
sessions.set(id, {
|
|
1386
|
+
id,
|
|
1387
|
+
cli,
|
|
1388
|
+
providerId: target.providerId,
|
|
1389
|
+
model: target.model,
|
|
1390
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1391
|
+
onSessionEnd: launch.onSessionEnd
|
|
1392
|
+
});
|
|
1393
|
+
return { status: 200, body: { sessionId: id, providerId: target.providerId, model: target.model } };
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/ports/account-multi.ts
|
|
1397
|
+
var import_node_crypto5 = require("crypto");
|
|
1196
1398
|
var PROVIDER_KEYS = {
|
|
1197
1399
|
claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
|
|
1198
1400
|
codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
|
|
@@ -1258,7 +1460,7 @@ function migrateLazily(config) {
|
|
|
1258
1460
|
}
|
|
1259
1461
|
function addAccount(config, p, tokens, label) {
|
|
1260
1462
|
const accounts = [...getAccounts(config, p)];
|
|
1261
|
-
const id = (0,
|
|
1463
|
+
const id = (0, import_node_crypto5.randomUUID)();
|
|
1262
1464
|
accounts.push({
|
|
1263
1465
|
id,
|
|
1264
1466
|
label: label ?? `Account ${accounts.length + 1}`,
|
|
@@ -1295,6 +1497,13 @@ function setActiveAccount(config, p, id) {
|
|
|
1295
1497
|
deriveMirror(config, p);
|
|
1296
1498
|
return { ok: true };
|
|
1297
1499
|
}
|
|
1500
|
+
function listAccounts(config, p) {
|
|
1501
|
+
return getAccounts(config, p);
|
|
1502
|
+
}
|
|
1503
|
+
function getAccountById(config, p, id) {
|
|
1504
|
+
const account = getAccounts(config, p).find((a) => a.id === id);
|
|
1505
|
+
return account ? { id: account.id, tokens: account.tokens } : void 0;
|
|
1506
|
+
}
|
|
1298
1507
|
function getActiveAccount(config, p) {
|
|
1299
1508
|
const active = getAccounts(config, p).find((a) => a.id === getActiveId(config, p));
|
|
1300
1509
|
return active ? { id: active.id, tokens: active.tokens } : void 0;
|
|
@@ -1328,12 +1537,27 @@ function sanitizeAccounts(config, p) {
|
|
|
1328
1537
|
id: a.id,
|
|
1329
1538
|
label: a.label,
|
|
1330
1539
|
status: t.status ?? "unconfigured",
|
|
1540
|
+
authMethod: t.authMethod,
|
|
1541
|
+
subscriptionLevel: t.subscriptionLevel,
|
|
1331
1542
|
expiresAt: t.expiresAt,
|
|
1543
|
+
lastRefreshedAt: t.lastRefreshedAt,
|
|
1544
|
+
isSetupToken: t.isSetupToken,
|
|
1332
1545
|
hasAccessToken: !!(t.accessToken || t.apiKey),
|
|
1333
|
-
isActive: a.id === activeId
|
|
1546
|
+
isActive: a.id === activeId,
|
|
1547
|
+
syncWarning: t.syncWarning
|
|
1334
1548
|
};
|
|
1335
1549
|
});
|
|
1336
1550
|
}
|
|
1551
|
+
function renameAccount(config, p, id, label) {
|
|
1552
|
+
const accounts = getAccounts(config, p);
|
|
1553
|
+
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
1554
|
+
setAccounts(
|
|
1555
|
+
config,
|
|
1556
|
+
p,
|
|
1557
|
+
accounts.map((a) => a.id === id ? { ...a, label } : a)
|
|
1558
|
+
);
|
|
1559
|
+
return { ok: true };
|
|
1560
|
+
}
|
|
1337
1561
|
function clearProvider(config, p) {
|
|
1338
1562
|
setBlock(config, p, void 0);
|
|
1339
1563
|
setAccounts(config, p, void 0);
|
|
@@ -1342,7 +1566,7 @@ function clearProvider(config, p) {
|
|
|
1342
1566
|
var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
|
|
1343
1567
|
|
|
1344
1568
|
// src/migration/packCodec.ts
|
|
1345
|
-
var
|
|
1569
|
+
var import_node_crypto6 = require("crypto");
|
|
1346
1570
|
var PACK_MAGIC = "OMCXPACK";
|
|
1347
1571
|
var PACK_VERSION = 1;
|
|
1348
1572
|
var KDF_ALGORITHM = "scrypt";
|
|
@@ -1380,17 +1604,17 @@ function fromB64Url(s) {
|
|
|
1380
1604
|
return Buffer.from(s, "base64url").toString("utf8");
|
|
1381
1605
|
}
|
|
1382
1606
|
function deriveKey(passphrase, salt, N, r, p) {
|
|
1383
|
-
return (0,
|
|
1607
|
+
return (0, import_node_crypto6.scryptSync)(passphrase, salt, KEY_BYTES3, { N, r, p, maxmem: SCRYPT_MAXMEM });
|
|
1384
1608
|
}
|
|
1385
1609
|
function aadFor(magic, version, kdf) {
|
|
1386
1610
|
return Buffer.from(`${magic}|${version}|${kdf}`, "utf8");
|
|
1387
1611
|
}
|
|
1388
1612
|
function sealPack(bundleJson, passphrase) {
|
|
1389
1613
|
assertPassphraseStrength(passphrase);
|
|
1390
|
-
const salt = (0,
|
|
1391
|
-
const iv = (0,
|
|
1614
|
+
const salt = (0, import_node_crypto6.randomBytes)(SCRYPT_SALT_BYTES);
|
|
1615
|
+
const iv = (0, import_node_crypto6.randomBytes)(IV_BYTES2);
|
|
1392
1616
|
const key = deriveKey(passphrase, salt, SCRYPT_N, SCRYPT_R, SCRYPT_P);
|
|
1393
|
-
const cipher = (0,
|
|
1617
|
+
const cipher = (0, import_node_crypto6.createCipheriv)("aes-256-gcm", key, iv);
|
|
1394
1618
|
cipher.setAAD(aadFor(PACK_MAGIC, PACK_VERSION, KDF_ALGORITHM));
|
|
1395
1619
|
const ciphertext = Buffer.concat([cipher.update(bundleJson, "utf8"), cipher.final()]);
|
|
1396
1620
|
const tag = cipher.getAuthTag();
|
|
@@ -1438,7 +1662,7 @@ function openPack(packString, passphrase) {
|
|
|
1438
1662
|
throw new PackAuthError("migration pack is malformed (invalid iv/tag length)");
|
|
1439
1663
|
}
|
|
1440
1664
|
const key = deriveKey(passphrase, salt, header.N, header.r, header.p);
|
|
1441
|
-
const decipher = (0,
|
|
1665
|
+
const decipher = (0, import_node_crypto6.createDecipheriv)("aes-256-gcm", key, iv);
|
|
1442
1666
|
decipher.setAAD(aadFor(PACK_MAGIC, PACK_VERSION, KDF_ALGORITHM));
|
|
1443
1667
|
decipher.setAuthTag(tag);
|
|
1444
1668
|
try {
|
|
@@ -1580,6 +1804,169 @@ async function handleImport(body, deps) {
|
|
|
1580
1804
|
}
|
|
1581
1805
|
}
|
|
1582
1806
|
|
|
1807
|
+
// src/admin/usagePricing.ts
|
|
1808
|
+
var err4 = (status, message) => ({
|
|
1809
|
+
status,
|
|
1810
|
+
body: { error: { type: "admin_api_error", message } }
|
|
1811
|
+
});
|
|
1812
|
+
function parseFiniteInt(raw) {
|
|
1813
|
+
if (raw === null || raw.trim() === "") return null;
|
|
1814
|
+
const n = Number(raw);
|
|
1815
|
+
return Number.isFinite(n) && Number.isInteger(n) ? n : null;
|
|
1816
|
+
}
|
|
1817
|
+
function parseRange(query) {
|
|
1818
|
+
const startTs = parseFiniteInt(query.get("startTs"));
|
|
1819
|
+
const endTs = parseFiniteInt(query.get("endTs"));
|
|
1820
|
+
if (startTs === null || endTs === null) {
|
|
1821
|
+
return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
|
|
1822
|
+
}
|
|
1823
|
+
return { startTs, endTs };
|
|
1824
|
+
}
|
|
1825
|
+
var isRange = (v) => v.startTs !== void 0 && !("status" in v);
|
|
1826
|
+
async function handleUsageGet(view, query, deps) {
|
|
1827
|
+
const range = parseRange(query);
|
|
1828
|
+
if (!isRange(range)) return range;
|
|
1829
|
+
switch (view) {
|
|
1830
|
+
case "totals":
|
|
1831
|
+
return { status: 200, body: await deps.usageRecorder.getTotals(range) };
|
|
1832
|
+
case "by-model":
|
|
1833
|
+
return { status: 200, body: await deps.usageRecorder.getByModel(range) };
|
|
1834
|
+
case "by-api-key": {
|
|
1835
|
+
const rows = await deps.usageRecorder.getByApiKey(range);
|
|
1836
|
+
const labels = poolKeyLabels(loadConfig(deps.configPath));
|
|
1837
|
+
return {
|
|
1838
|
+
status: 200,
|
|
1839
|
+
body: rows.map((r) => {
|
|
1840
|
+
if (r.apiKeyId === null) {
|
|
1841
|
+
return { ...r, label: "unattributed", providerId: null };
|
|
1842
|
+
}
|
|
1843
|
+
const known = labels.get(r.apiKeyId);
|
|
1844
|
+
return known ? { ...r, label: known.label, providerId: known.providerId } : { ...r, label: r.apiKeyId };
|
|
1845
|
+
})
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
default:
|
|
1849
|
+
return err4(404, `unknown usage view '${view ?? ""}'`);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
function poolKeyLabels(cfg) {
|
|
1853
|
+
const out = /* @__PURE__ */ new Map();
|
|
1854
|
+
for (const provider of cfg.providers) {
|
|
1855
|
+
for (const key of provider.apiKeys ?? []) {
|
|
1856
|
+
out.set(key.id, {
|
|
1857
|
+
label: key.label && key.label.length > 0 ? key.label : key.id,
|
|
1858
|
+
providerId: provider.id
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return out;
|
|
1863
|
+
}
|
|
1864
|
+
var INVALID_PRICE = /* @__PURE__ */ Symbol("invalid-price");
|
|
1865
|
+
function parseOptionalPrice(b, key) {
|
|
1866
|
+
if (!(key in b) || b[key] === null || b[key] === void 0) return null;
|
|
1867
|
+
const v = b[key];
|
|
1868
|
+
return typeof v === "number" && Number.isFinite(v) ? v : INVALID_PRICE;
|
|
1869
|
+
}
|
|
1870
|
+
function parsePricingEntryInput(raw) {
|
|
1871
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1872
|
+
const b = raw;
|
|
1873
|
+
const providerId = typeof b["providerId"] === "string" && b["providerId"].trim() ? b["providerId"].trim() : "";
|
|
1874
|
+
const modelId = typeof b["modelId"] === "string" && b["modelId"].trim() ? b["modelId"].trim() : "";
|
|
1875
|
+
const inputPrice = b["inputPricePer1m"];
|
|
1876
|
+
const outputPrice = b["outputPricePer1m"];
|
|
1877
|
+
if (!providerId || !modelId) return null;
|
|
1878
|
+
if (typeof inputPrice !== "number" || !Number.isFinite(inputPrice)) return null;
|
|
1879
|
+
if (typeof outputPrice !== "number" || !Number.isFinite(outputPrice)) return null;
|
|
1880
|
+
const cacheRead = parseOptionalPrice(b, "cacheReadPricePer1m");
|
|
1881
|
+
const cacheWrite = parseOptionalPrice(b, "cacheWritePricePer1m");
|
|
1882
|
+
if (cacheRead === INVALID_PRICE || cacheWrite === INVALID_PRICE) return null;
|
|
1883
|
+
return {
|
|
1884
|
+
providerId,
|
|
1885
|
+
modelId,
|
|
1886
|
+
inputPricePer1m: inputPrice,
|
|
1887
|
+
outputPricePer1m: outputPrice,
|
|
1888
|
+
cacheReadPricePer1m: cacheRead,
|
|
1889
|
+
cacheWritePricePer1m: cacheWrite
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
async function handlePricingList(deps) {
|
|
1893
|
+
return { status: 200, body: { entries: await deps.pricingEngine.getAll() } };
|
|
1894
|
+
}
|
|
1895
|
+
async function handlePricingUpsert(body, deps) {
|
|
1896
|
+
const input = parsePricingEntryInput(body);
|
|
1897
|
+
if (!input) {
|
|
1898
|
+
return err4(400, "invalid pricing entry (providerId, modelId, finite numeric inputPricePer1m/outputPricePer1m required)");
|
|
1899
|
+
}
|
|
1900
|
+
const entry = await deps.pricingEngine.upsertManual(input);
|
|
1901
|
+
return { status: 200, body: { entry } };
|
|
1902
|
+
}
|
|
1903
|
+
async function handlePricingDelete(query, deps) {
|
|
1904
|
+
const providerId = query.get("providerId")?.trim() ?? "";
|
|
1905
|
+
const modelId = query.get("modelId")?.trim() ?? "";
|
|
1906
|
+
if (!providerId || !modelId) {
|
|
1907
|
+
return err4(400, "delete requires providerId and modelId query params");
|
|
1908
|
+
}
|
|
1909
|
+
const deleted = await deps.pricingStore.delete(providerId, modelId);
|
|
1910
|
+
if (deleted) await deps.pricingEngine.invalidateCache();
|
|
1911
|
+
return { status: 200, body: { deleted } };
|
|
1912
|
+
}
|
|
1913
|
+
async function handlePricingFetchLatest(deps) {
|
|
1914
|
+
try {
|
|
1915
|
+
const result = await deps.pricingEngine.fetchLatestFromSource();
|
|
1916
|
+
return {
|
|
1917
|
+
status: 200,
|
|
1918
|
+
body: {
|
|
1919
|
+
appliedCount: result.applied.length,
|
|
1920
|
+
conflicts: result.conflicts,
|
|
1921
|
+
fetchedAt: result.fetchedAt,
|
|
1922
|
+
sourceUrl: result.sourceUrl
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
} catch (e) {
|
|
1926
|
+
return err4(502, `pricing-source fetch failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
async function handlePricingResolveConflicts(body, deps) {
|
|
1930
|
+
const raw = body["resolutions"];
|
|
1931
|
+
if (!Array.isArray(raw)) {
|
|
1932
|
+
return err4(400, "resolve-conflicts requires { resolutions: [...] }");
|
|
1933
|
+
}
|
|
1934
|
+
const currentRows = await deps.pricingStore.getAll();
|
|
1935
|
+
const userEditedKeys = new Set(
|
|
1936
|
+
currentRows.filter((r) => r.userEdited).map((r) => `${r.providerId}::${r.modelId}`)
|
|
1937
|
+
);
|
|
1938
|
+
const decisions = [];
|
|
1939
|
+
const pendingIncoming = /* @__PURE__ */ new Map();
|
|
1940
|
+
let staleCount = 0;
|
|
1941
|
+
for (const item of raw) {
|
|
1942
|
+
if (!item || typeof item !== "object") return err4(400, "invalid resolution entry");
|
|
1943
|
+
const r = item;
|
|
1944
|
+
const action = r["action"];
|
|
1945
|
+
if (action !== "overwrite" && action !== "skip") {
|
|
1946
|
+
return err4(400, "resolution action must be 'overwrite' or 'skip'");
|
|
1947
|
+
}
|
|
1948
|
+
const providerId = typeof r["providerId"] === "string" && r["providerId"].trim() ? r["providerId"].trim() : "";
|
|
1949
|
+
const modelId = typeof r["modelId"] === "string" && r["modelId"].trim() ? r["modelId"].trim() : "";
|
|
1950
|
+
if (!providerId || !modelId) {
|
|
1951
|
+
return err4(400, "each resolution requires top-level providerId and modelId");
|
|
1952
|
+
}
|
|
1953
|
+
const incoming = parsePricingEntryInput(r["incoming"]);
|
|
1954
|
+
if (!incoming) return err4(400, "each resolution must echo a valid incoming pricing entry");
|
|
1955
|
+
if (incoming.providerId !== providerId || incoming.modelId !== modelId) {
|
|
1956
|
+
return err4(400, "resolution providerId/modelId must match the echoed incoming entry");
|
|
1957
|
+
}
|
|
1958
|
+
const key = `${providerId}::${modelId}`;
|
|
1959
|
+
if (action === "overwrite" && !userEditedKeys.has(key)) {
|
|
1960
|
+
staleCount += 1;
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
decisions.push({ providerId, modelId, action });
|
|
1964
|
+
pendingIncoming.set(key, incoming);
|
|
1965
|
+
}
|
|
1966
|
+
const resolution = await deps.pricingEngine.resolveConflicts(decisions, pendingIncoming);
|
|
1967
|
+
return { status: 200, body: { ...resolution, staleCount } };
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1583
1970
|
// src/admin/adminApi.ts
|
|
1584
1971
|
function readBody(req) {
|
|
1585
1972
|
return new Promise((resolve, reject) => {
|
|
@@ -1670,9 +2057,9 @@ function toProviderView(row) {
|
|
|
1670
2057
|
selectedApiModeId: row.selectedApiModeId
|
|
1671
2058
|
};
|
|
1672
2059
|
}
|
|
1673
|
-
async function handleAdminApi(req, res,
|
|
2060
|
+
async function handleAdminApi(req, res, path2, deps) {
|
|
1674
2061
|
const method = (req.method ?? "GET").toUpperCase();
|
|
1675
|
-
const sub =
|
|
2062
|
+
const sub = path2.slice("/admin/api/".length);
|
|
1676
2063
|
const [resource, ...rest] = sub.split("/").filter((s) => s.length > 0);
|
|
1677
2064
|
try {
|
|
1678
2065
|
switch (resource) {
|
|
@@ -1686,6 +2073,8 @@ async function handleAdminApi(req, res, path, deps) {
|
|
|
1686
2073
|
return await handleServer(req, res, method, deps);
|
|
1687
2074
|
case "accounts":
|
|
1688
2075
|
return await handleAccounts(req, res, method, rest, deps);
|
|
2076
|
+
case "cli":
|
|
2077
|
+
return await handleCli(req, res, method, rest, deps);
|
|
1689
2078
|
case "status":
|
|
1690
2079
|
return await handleStatus(res, method, deps);
|
|
1691
2080
|
case "playground":
|
|
@@ -1694,12 +2083,47 @@ async function handleAdminApi(req, res, path, deps) {
|
|
|
1694
2083
|
return await handleMigrationExport(req, res, method, deps);
|
|
1695
2084
|
case "import":
|
|
1696
2085
|
return await handleMigrationImport(req, res, method, deps);
|
|
2086
|
+
case "usage":
|
|
2087
|
+
return await handleUsage(req, res, method, rest, deps);
|
|
2088
|
+
case "pricing":
|
|
2089
|
+
return await handlePricing(req, res, method, rest, deps);
|
|
1697
2090
|
default:
|
|
1698
2091
|
return writeJsonError(res, 404, `unknown admin resource '${resource}'`);
|
|
1699
2092
|
}
|
|
1700
|
-
} catch (
|
|
1701
|
-
writeJsonError(res, 500,
|
|
2093
|
+
} catch (err5) {
|
|
2094
|
+
writeJsonError(res, 500, err5 instanceof Error ? err5.message : String(err5));
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
function requestQuery(req) {
|
|
2098
|
+
const raw = req.url ?? "";
|
|
2099
|
+
const qIdx = raw.indexOf("?");
|
|
2100
|
+
return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
|
|
2101
|
+
}
|
|
2102
|
+
function writeResult(res, result) {
|
|
2103
|
+
writeJson(res, result.status, result.body);
|
|
2104
|
+
}
|
|
2105
|
+
async function handleUsage(req, res, method, rest, deps) {
|
|
2106
|
+
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
|
|
2107
|
+
return writeResult(res, await handleUsageGet(rest[0], requestQuery(req), deps));
|
|
2108
|
+
}
|
|
2109
|
+
async function handlePricing(req, res, method, rest, deps) {
|
|
2110
|
+
if (rest.length === 0) {
|
|
2111
|
+
if (method === "GET") return writeResult(res, await handlePricingList(deps));
|
|
2112
|
+
if (method === "PUT") {
|
|
2113
|
+
return writeResult(res, await handlePricingUpsert(await readJsonBody(req), deps));
|
|
2114
|
+
}
|
|
2115
|
+
if (method === "DELETE") {
|
|
2116
|
+
return writeResult(res, await handlePricingDelete(requestQuery(req), deps));
|
|
2117
|
+
}
|
|
2118
|
+
return writeJsonError(res, 405, `method ${method} not allowed on pricing`);
|
|
2119
|
+
}
|
|
2120
|
+
if (method === "POST" && rest.length === 1 && rest[0] === "fetch-latest") {
|
|
2121
|
+
return writeResult(res, await handlePricingFetchLatest(deps));
|
|
1702
2122
|
}
|
|
2123
|
+
if (method === "POST" && rest.length === 1 && rest[0] === "resolve-conflicts") {
|
|
2124
|
+
return writeResult(res, await handlePricingResolveConflicts(await readJsonBody(req), deps));
|
|
2125
|
+
}
|
|
2126
|
+
return writeJsonError(res, 404, `unknown pricing route '${rest.join("/")}'`);
|
|
1703
2127
|
}
|
|
1704
2128
|
function migrationDeps(deps) {
|
|
1705
2129
|
return {
|
|
@@ -1747,6 +2171,11 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
1747
2171
|
if (method === "POST" && rest.length === 2 && rest[1] === "test") {
|
|
1748
2172
|
return await handleTestModel(req, res, rest[0], cfg);
|
|
1749
2173
|
}
|
|
2174
|
+
if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
|
|
2175
|
+
const row = cfg.providers.find((p) => p.id === rest[0]);
|
|
2176
|
+
if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
|
|
2177
|
+
return writeJson(res, 200, { apiKey: row.apiKey ?? "" });
|
|
2178
|
+
}
|
|
1750
2179
|
if (method === "GET") {
|
|
1751
2180
|
return writeJson(res, 200, { providers: cfg.providers.map(toProviderView) });
|
|
1752
2181
|
}
|
|
@@ -1842,8 +2271,8 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
1842
2271
|
const data = await response.json();
|
|
1843
2272
|
const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
|
|
1844
2273
|
return writeJson(res, 200, { models });
|
|
1845
|
-
} catch (
|
|
1846
|
-
const message =
|
|
2274
|
+
} catch (err5) {
|
|
2275
|
+
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
1847
2276
|
return writeJson(res, 200, { models: [], error: `discovery failed: ${message}` });
|
|
1848
2277
|
}
|
|
1849
2278
|
}
|
|
@@ -1902,8 +2331,8 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
1902
2331
|
latencyMs,
|
|
1903
2332
|
sample: extractSampleText(text, row.apiFormat)
|
|
1904
2333
|
});
|
|
1905
|
-
} catch (
|
|
1906
|
-
const message =
|
|
2334
|
+
} catch (err5) {
|
|
2335
|
+
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
1907
2336
|
return writeJson(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
|
|
1908
2337
|
}
|
|
1909
2338
|
}
|
|
@@ -2252,7 +2681,8 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
2252
2681
|
if (method === "GET" && rest.length === 0) {
|
|
2253
2682
|
const accounts = await deps.subscriptionAccounts.listAll();
|
|
2254
2683
|
const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
2255
|
-
|
|
2684
|
+
const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
|
|
2685
|
+
return writeJson(res, 200, { accounts, providerAccounts, externalCli });
|
|
2256
2686
|
}
|
|
2257
2687
|
if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
|
|
2258
2688
|
const result = handleCodexOAuthStatus(rest[2], deps);
|
|
@@ -2272,6 +2702,47 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
2272
2702
|
const result = await handleOAuthComplete(providerId, body2, deps);
|
|
2273
2703
|
return writeJson(res, result.status, result.body);
|
|
2274
2704
|
}
|
|
2705
|
+
if (method === "POST" && rest[1] === "accounts") {
|
|
2706
|
+
const body2 = await readJsonBody(req);
|
|
2707
|
+
const block = validateTokenBody(providerId, body2);
|
|
2708
|
+
if (!block) {
|
|
2709
|
+
return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
|
|
2710
|
+
}
|
|
2711
|
+
const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
|
|
2712
|
+
await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
|
|
2713
|
+
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
2714
|
+
return writeJson(res, 200, status2 ? { account: status2 } : { ok: true });
|
|
2715
|
+
}
|
|
2716
|
+
if (method === "POST" && rest[1] === "import-external") {
|
|
2717
|
+
if (providerId !== "claude" && providerId !== "codex") {
|
|
2718
|
+
return writeJsonError(res, 400, `provider '${providerId}' has no external CLI store`);
|
|
2719
|
+
}
|
|
2720
|
+
const body2 = await readJsonBody(req);
|
|
2721
|
+
const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
|
|
2722
|
+
const result = await deps.subscriptionTokenWriter.importExternalCliAccount(providerId, label);
|
|
2723
|
+
if (!result.ok) {
|
|
2724
|
+
return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
|
|
2725
|
+
}
|
|
2726
|
+
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
2727
|
+
return writeJson(res, 200, { ok: true, account: status2 ?? void 0 });
|
|
2728
|
+
}
|
|
2729
|
+
if (method === "POST" && rest[1] === "refresh") {
|
|
2730
|
+
if (providerId === "opencodego") {
|
|
2731
|
+
return writeJsonError(res, 400, "opencodego credentials are not refreshable");
|
|
2732
|
+
}
|
|
2733
|
+
const writer = deps.subscriptionTokenWriter;
|
|
2734
|
+
const ok = providerId === "claude" ? await writer.refreshClaudeToken() : providerId === "codex" ? await writer.refreshCodexToken() : await writer.refreshGeminiToken();
|
|
2735
|
+
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
2736
|
+
return writeJson(res, 200, { ok, account: status2 ?? void 0 });
|
|
2737
|
+
}
|
|
2738
|
+
if (method === "POST" && rest[2] === "label") {
|
|
2739
|
+
const accountId = rest[1];
|
|
2740
|
+
const body2 = await readJsonBody(req);
|
|
2741
|
+
const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
|
|
2742
|
+
const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
|
|
2743
|
+
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
2744
|
+
return writeJson(res, 200, { ok: true });
|
|
2745
|
+
}
|
|
2275
2746
|
if (method === "PUT" && rest[1] === "active") {
|
|
2276
2747
|
const body2 = await readJsonBody(req);
|
|
2277
2748
|
const id = typeof body2["id"] === "string" ? body2["id"] : "";
|
|
@@ -2301,6 +2772,44 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
2301
2772
|
}
|
|
2302
2773
|
return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
|
|
2303
2774
|
}
|
|
2775
|
+
async function handleCli(req, res, method, rest, deps) {
|
|
2776
|
+
if (method === "GET" && rest.length === 0) {
|
|
2777
|
+
const result = handleCliList(process.platform, deps.cliPathProbe);
|
|
2778
|
+
return writeJson(res, result.status, result.body);
|
|
2779
|
+
}
|
|
2780
|
+
if (method === "GET" && rest[0] === "sessions") {
|
|
2781
|
+
const result = handleCliSessions();
|
|
2782
|
+
return writeJson(res, result.status, result.body);
|
|
2783
|
+
}
|
|
2784
|
+
if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
|
|
2785
|
+
const result = handleCliStop(rest[1]);
|
|
2786
|
+
return writeJson(res, result.status, result.body);
|
|
2787
|
+
}
|
|
2788
|
+
if (method === "POST" && rest[1] === "install") {
|
|
2789
|
+
const cli = rest[0];
|
|
2790
|
+
if (!isLaunchCliId(cli)) {
|
|
2791
|
+
return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
|
|
2792
|
+
}
|
|
2793
|
+
const result = await handleCliInstall(cli, deps.cliCommandRunner);
|
|
2794
|
+
return writeJson(res, result.status, result.body);
|
|
2795
|
+
}
|
|
2796
|
+
if (method === "POST" && rest[1] === "launch") {
|
|
2797
|
+
const cli = rest[0];
|
|
2798
|
+
if (!isLaunchCliId(cli)) {
|
|
2799
|
+
return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
|
|
2800
|
+
}
|
|
2801
|
+
const body = await readJsonBody(req);
|
|
2802
|
+
const providers = loadConfig(deps.configPath).providers ?? [];
|
|
2803
|
+
const result = await handleCliLaunch(cli, body, {
|
|
2804
|
+
llmConfig: deps.llmConfig,
|
|
2805
|
+
providers,
|
|
2806
|
+
opener: deps.cliTerminalOpener,
|
|
2807
|
+
probe: deps.cliPathProbe
|
|
2808
|
+
});
|
|
2809
|
+
return writeJson(res, result.status, result.body);
|
|
2810
|
+
}
|
|
2811
|
+
return writeJsonError(res, 405, `method ${method} not allowed on cli`);
|
|
2812
|
+
}
|
|
2304
2813
|
async function handleStatus(res, method, deps) {
|
|
2305
2814
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
|
|
2306
2815
|
const status = deps.outboundApiServer.getStatus();
|
|
@@ -2336,21 +2845,21 @@ async function handlePlayground(req, res, method, deps) {
|
|
|
2336
2845
|
const payload = body["body"];
|
|
2337
2846
|
const status = deps.outboundApiServer.getStatus();
|
|
2338
2847
|
if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
|
|
2339
|
-
const
|
|
2340
|
-
if (!
|
|
2848
|
+
const path2 = resolvePlaygroundPath(endpoint, isRecord(payload) ? payload : {});
|
|
2849
|
+
if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
|
|
2341
2850
|
const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
2342
|
-
await proxyToOutbound(res, status.port,
|
|
2851
|
+
await proxyToOutbound(res, status.port, path2, key, upstreamBody);
|
|
2343
2852
|
}
|
|
2344
2853
|
function isRecord(v) {
|
|
2345
2854
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2346
2855
|
}
|
|
2347
|
-
function proxyToOutbound(res, outboundPort,
|
|
2856
|
+
function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
2348
2857
|
return new Promise((resolve) => {
|
|
2349
2858
|
const upstream = import_node_http.default.request(
|
|
2350
2859
|
{
|
|
2351
2860
|
host: "127.0.0.1",
|
|
2352
2861
|
port: outboundPort,
|
|
2353
|
-
path,
|
|
2862
|
+
path: path2,
|
|
2354
2863
|
method: "POST",
|
|
2355
2864
|
headers: {
|
|
2356
2865
|
"Content-Type": "application/json",
|
|
@@ -2370,8 +2879,8 @@ function proxyToOutbound(res, outboundPort, path, key, body) {
|
|
|
2370
2879
|
});
|
|
2371
2880
|
}
|
|
2372
2881
|
);
|
|
2373
|
-
upstream.on("error", (
|
|
2374
|
-
if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${
|
|
2882
|
+
upstream.on("error", (err5) => {
|
|
2883
|
+
if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
|
|
2375
2884
|
else res.end();
|
|
2376
2885
|
resolve();
|
|
2377
2886
|
});
|
|
@@ -2380,463 +2889,106 @@ function proxyToOutbound(res, outboundPort, path, key, body) {
|
|
|
2380
2889
|
});
|
|
2381
2890
|
}
|
|
2382
2891
|
|
|
2383
|
-
// src/admin/
|
|
2384
|
-
var
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
function clear(el) { while (el.firstChild) el.removeChild(el.firstChild); }
|
|
2410
|
-
function td(text) { var c = document.createElement('td'); c.textContent = text == null ? '' : String(text); return c; }
|
|
2411
|
-
function btn(label, cls, fn) { var b = document.createElement('button'); b.textContent = label; if (cls) b.className = cls; b.onclick = fn; return b; }
|
|
2412
|
-
|
|
2413
|
-
// ── Status ────────────────────────────────────────────────────────────────
|
|
2414
|
-
async function loadStatus() {
|
|
2415
|
-
var r = await api('GET', 'status');
|
|
2416
|
-
var s = r.json || {};
|
|
2417
|
-
$('statusBadge').textContent = s.running ? ('running :' + s.port) : 'stopped';
|
|
2418
|
-
var html = '';
|
|
2419
|
-
if (s.running) {
|
|
2420
|
-
html += 'Outbound server <span class="pill ok">running</span> on port ' + s.port + '<br/>';
|
|
2421
|
-
if (s.formats) {
|
|
2422
|
-
html += '<div class="mono muted">' +
|
|
2423
|
-
'chat: ' + s.formats.chat + '<br/>responses: ' + s.formats.responses +
|
|
2424
|
-
'<br/>messages: ' + s.formats.messages + '<br/>gemini: ' + s.formats.gemini + '</div>';
|
|
2425
|
-
}
|
|
2426
|
-
} else {
|
|
2427
|
-
html += 'Outbound server <span class="pill bad">stopped</span>';
|
|
2428
|
-
}
|
|
2429
|
-
$('statusBody').innerHTML = html;
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
// ── Providers ───────────────────────────────────────────────────────────────
|
|
2433
|
-
// Curated presets loaded from GET /admin/api/presets. Selecting one prefills
|
|
2434
|
-
// the add-form (format/base/models); the WRITE still goes through the existing
|
|
2435
|
-
// POST/PUT /admin/api/providers path (no new write endpoint).
|
|
2436
|
-
var presetsById = {};
|
|
2437
|
-
var presetModels = []; // models staged by the last preset prefill
|
|
2438
|
-
|
|
2439
|
-
async function loadPresets() {
|
|
2440
|
-
var r = await api('GET', 'presets');
|
|
2441
|
-
var sel = $('pPreset');
|
|
2442
|
-
// Keep the placeholder; drop any previously appended options.
|
|
2443
|
-
while (sel.options.length > 1) sel.remove(1);
|
|
2444
|
-
presetsById = {};
|
|
2445
|
-
(r.json && r.json.presets || []).forEach(function (p) {
|
|
2446
|
-
presetsById[p.id] = p;
|
|
2447
|
-
var opt = document.createElement('option');
|
|
2448
|
-
opt.value = p.id;
|
|
2449
|
-
opt.textContent = p.name + ' (' + p.apiFormat + ')';
|
|
2450
|
-
sel.appendChild(opt);
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
function onPresetChange() {
|
|
2455
|
-
var p = presetsById[$('pPreset').value];
|
|
2456
|
-
if (!p) { presetModels = []; return; }
|
|
2457
|
-
if (!$('pId').value.trim()) $('pId').value = p.id;
|
|
2458
|
-
$('pFormat').value = p.apiFormat;
|
|
2459
|
-
$('pBase').value = p.baseUrl;
|
|
2460
|
-
presetModels = Array.isArray(p.models) ? p.models.slice() : [];
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
async function loadProviders() {
|
|
2464
|
-
var r = await api('GET', 'providers');
|
|
2465
|
-
var body = $('providersTable').querySelector('tbody');
|
|
2466
|
-
clear(body);
|
|
2467
|
-
(r.json && r.json.providers || []).forEach(function (p) {
|
|
2468
|
-
var tr = document.createElement('tr');
|
|
2469
|
-
tr.appendChild(td(p.id));
|
|
2470
|
-
tr.appendChild(td(p.apiFormat));
|
|
2471
|
-
tr.appendChild(td(p.baseUrl));
|
|
2472
|
-
tr.appendChild(td(p.hasApiKey ? p.apiKeyMasked : '(none)'));
|
|
2473
|
-
var act = document.createElement('td');
|
|
2474
|
-
act.appendChild(btn('Edit', 'secondary', function () {
|
|
2475
|
-
$('pId').value = p.id; $('pFormat').value = p.apiFormat; $('pBase').value = p.baseUrl; $('pKey').value = '';
|
|
2476
|
-
}));
|
|
2477
|
-
act.appendChild(btn('Pool', 'secondary', function () { loadProviderKeys(p.id); }));
|
|
2478
|
-
act.appendChild(btn('Delete', 'danger', async function () {
|
|
2479
|
-
if (window.confirm('Delete provider ' + p.id + '?')) { await api('DELETE', 'providers/' + encodeURIComponent(p.id)); loadProviders(); }
|
|
2480
|
-
}));
|
|
2481
|
-
tr.appendChild(act);
|
|
2482
|
-
body.appendChild(tr);
|
|
2483
|
-
});
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
// ── Pool health (read-only; key-pool change) ──────────────────────────────
|
|
2487
|
-
// GET /admin/api/providers/:id/keys → masked pool view. Multi-key is
|
|
2488
|
-
// cold-standby + observable in v1 (no outbound failover yet — see panel note).
|
|
2489
|
-
async function loadProviderKeys(providerId) {
|
|
2490
|
-
var r = await api('GET', 'providers/' + encodeURIComponent(providerId) + '/keys');
|
|
2491
|
-
var panel = $('poolPanel');
|
|
2492
|
-
var body = $('poolTable').querySelector('tbody');
|
|
2493
|
-
clear(body);
|
|
2494
|
-
$('poolTitle').textContent = 'API key pool — ' + providerId;
|
|
2495
|
-
(r.json && r.json.keys || []).forEach(function (k) {
|
|
2496
|
-
var tr = document.createElement('tr');
|
|
2497
|
-
tr.appendChild(td(k.id));
|
|
2498
|
-
tr.appendChild(td(k.label));
|
|
2499
|
-
tr.appendChild(td(k.apiKeyMasked));
|
|
2500
|
-
tr.appendChild(td(k.enabled ? 'yes' : 'no'));
|
|
2501
|
-
tr.appendChild(td(k.weight));
|
|
2502
|
-
var h = '';
|
|
2503
|
-
if (k.health && k.health.autoDisabled) h += 'auto-disabled (' + k.health.autoDisabled.status + ') ';
|
|
2504
|
-
if (k.health && k.health.cooldown) h += 'cooldown until ' + new Date(k.health.cooldown.until).toLocaleTimeString();
|
|
2505
|
-
tr.appendChild(td(h || 'ok'));
|
|
2506
|
-
body.appendChild(tr);
|
|
2507
|
-
});
|
|
2508
|
-
panel.style.display = 'block';
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
async function saveProvider() {
|
|
2512
|
-
$('pErr').textContent = '';
|
|
2513
|
-
var id = $('pId').value.trim();
|
|
2514
|
-
if (!id) { $('pErr').textContent = 'id required'; return; }
|
|
2515
|
-
var payload = { id: id, apiFormat: $('pFormat').value, baseUrl: $('pBase').value.trim(), apiKey: $('pKey').value };
|
|
2516
|
-
// Carry the preset-prefilled models (existing parseProviderInput accepts them).
|
|
2517
|
-
if (presetModels.length) payload.models = presetModels;
|
|
2518
|
-
// Try PUT first (edit, blank key keeps existing); fall back to POST (create).
|
|
2519
|
-
var r = await api('PUT', 'providers/' + encodeURIComponent(id), payload);
|
|
2520
|
-
if (r.status === 404) r = await api('POST', 'providers', payload);
|
|
2521
|
-
if (r.status >= 400) { $('pErr').textContent = (r.json && r.json.error && r.json.error.message) || ('error ' + r.status); return; }
|
|
2522
|
-
$('pId').value = ''; $('pBase').value = ''; $('pKey').value = '';
|
|
2523
|
-
$('pPreset').value = ''; presetModels = []; // reset the picker after a write
|
|
2524
|
-
loadProviders();
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
// ── Keys ────────────────────────────────────────────────────────────────────
|
|
2528
|
-
async function loadKeys() {
|
|
2529
|
-
var r = await api('GET', 'keys');
|
|
2530
|
-
var body = $('keysTable').querySelector('tbody');
|
|
2531
|
-
clear(body);
|
|
2532
|
-
(r.json && r.json.keys || []).forEach(function (k) {
|
|
2533
|
-
var tr = document.createElement('tr');
|
|
2534
|
-
tr.appendChild(td(k.name));
|
|
2535
|
-
tr.appendChild(td(k.keyPrefix));
|
|
2536
|
-
tr.appendChild(td(k.enabled ? 'yes' : 'no'));
|
|
2537
|
-
tr.appendChild(td(k.revoked ? 'yes' : 'no'));
|
|
2538
|
-
var act = document.createElement('td');
|
|
2539
|
-
if (!k.revoked) {
|
|
2540
|
-
act.appendChild(btn(k.enabled ? 'Disable' : 'Enable', 'secondary', async function () {
|
|
2541
|
-
await api('POST', 'keys/' + encodeURIComponent(k.id) + '/enabled', { enabled: !k.enabled }); loadKeys();
|
|
2542
|
-
}));
|
|
2543
|
-
act.appendChild(btn('Revoke', 'danger', async function () {
|
|
2544
|
-
if (window.confirm('Revoke ' + k.name + '?')) { await api('POST', 'keys/' + encodeURIComponent(k.id) + '/revoke'); loadKeys(); }
|
|
2545
|
-
}));
|
|
2546
|
-
}
|
|
2547
|
-
tr.appendChild(act);
|
|
2548
|
-
body.appendChild(tr);
|
|
2549
|
-
});
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
function showKeyModal(plaintext) {
|
|
2553
|
-
$('keyPlaintext').textContent = plaintext;
|
|
2554
|
-
$('keyModalBg').classList.add('show');
|
|
2555
|
-
$('keyCopy').onclick = function () { navigator.clipboard && navigator.clipboard.writeText(plaintext); };
|
|
2556
|
-
$('keyClose').onclick = function () {
|
|
2557
|
-
$('keyModalBg').classList.remove('show');
|
|
2558
|
-
$('keyPlaintext').textContent = ''; // never persist the plaintext
|
|
2559
|
-
};
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
|
-
async function createKey() {
|
|
2563
|
-
var name = $('kName').value.trim() || 'key';
|
|
2564
|
-
var r = await api('POST', 'keys', { name: name });
|
|
2565
|
-
if (r.json && r.json.plaintextOnce) { showKeyModal(r.json.plaintextOnce); $('kName').value = ''; loadKeys(); }
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
// ── Server config ───────────────────────────────────────────────────────────
|
|
2569
|
-
async function loadServer() {
|
|
2570
|
-
var r = await api('GET', 'server');
|
|
2571
|
-
var s = (r.json && r.json.server) || {};
|
|
2572
|
-
$('sEnabled').checked = !!s.enabled;
|
|
2573
|
-
$('sLan').checked = !!s.networkBinding;
|
|
2574
|
-
$('sPort').value = s.port || '';
|
|
2575
|
-
var body = $('endpointsTable').querySelector('tbody');
|
|
2576
|
-
clear(body);
|
|
2577
|
-
(s.endpoints || []).forEach(function (e) {
|
|
2578
|
-
var tr = document.createElement('tr');
|
|
2579
|
-
tr.appendChild(td(e.endpoint));
|
|
2580
|
-
tr.appendChild(td(e.defaultModel));
|
|
2581
|
-
tr.appendChild(td(e.useSubscription ? 'yes' : 'no'));
|
|
2582
|
-
body.appendChild(tr);
|
|
2583
|
-
});
|
|
2892
|
+
// src/admin/uiStatic.ts
|
|
2893
|
+
var import_node_fs6 = require("fs");
|
|
2894
|
+
var import_promises = require("fs/promises");
|
|
2895
|
+
var import_node_module = require("module");
|
|
2896
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
2897
|
+
var import_meta = {};
|
|
2898
|
+
var CONTENT_TYPES = {
|
|
2899
|
+
".html": "text/html; charset=utf-8",
|
|
2900
|
+
".js": "text/javascript; charset=utf-8",
|
|
2901
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
2902
|
+
".css": "text/css; charset=utf-8",
|
|
2903
|
+
".json": "application/json; charset=utf-8",
|
|
2904
|
+
".svg": "image/svg+xml",
|
|
2905
|
+
".png": "image/png",
|
|
2906
|
+
".ico": "image/x-icon",
|
|
2907
|
+
".webp": "image/webp",
|
|
2908
|
+
".woff": "font/woff",
|
|
2909
|
+
".woff2": "font/woff2",
|
|
2910
|
+
".ttf": "font/ttf",
|
|
2911
|
+
".map": "application/json; charset=utf-8",
|
|
2912
|
+
".txt": "text/plain; charset=utf-8"
|
|
2913
|
+
};
|
|
2914
|
+
function resolveUiDist() {
|
|
2915
|
+
const fromEnv = process.env["OMNICROSS_UI_DIST"];
|
|
2916
|
+
if (fromEnv) {
|
|
2917
|
+
return (0, import_node_fs6.existsSync)(import_node_path4.default.join(fromEnv, "index.html")) ? import_node_path4.default.resolve(fromEnv) : null;
|
|
2584
2918
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2919
|
+
try {
|
|
2920
|
+
const req = (0, import_node_module.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta.url);
|
|
2921
|
+
const pkgJson = req.resolve("@omnicross/ui/package.json");
|
|
2922
|
+
const dist = import_node_path4.default.join(import_node_path4.default.dirname(pkgJson), "dist");
|
|
2923
|
+
return (0, import_node_fs6.existsSync)(import_node_path4.default.join(dist, "index.html")) ? dist : null;
|
|
2924
|
+
} catch {
|
|
2925
|
+
return null;
|
|
2592
2926
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
clear(body);
|
|
2601
|
-
(r.json && r.json.accounts || []).forEach(function (a) {
|
|
2602
|
-
var tr = document.createElement('tr');
|
|
2603
|
-
tr.appendChild(td(a.displayName || a.providerId));
|
|
2604
|
-
tr.appendChild(td(a.kind));
|
|
2605
|
-
var st = document.createElement('td');
|
|
2606
|
-
var ok = a.credentialStatus && a.credentialStatus.ok;
|
|
2607
|
-
var pill = document.createElement('span');
|
|
2608
|
-
pill.className = 'pill ' + (ok ? 'ok' : 'bad');
|
|
2609
|
-
pill.textContent = ok ? 'ok' : ((a.credentialStatus && a.credentialStatus.reason) || 'no credential');
|
|
2610
|
-
st.appendChild(pill);
|
|
2611
|
-
tr.appendChild(st);
|
|
2612
|
-
var act = document.createElement('td');
|
|
2613
|
-
act.appendChild(btn('Clear', 'danger', async function () {
|
|
2614
|
-
if (window.confirm('Clear ' + a.providerId + ' token?')) {
|
|
2615
|
-
await api('DELETE', 'accounts/' + encodeURIComponent(a.providerId));
|
|
2616
|
-
loadAccounts();
|
|
2617
|
-
}
|
|
2618
|
-
}));
|
|
2619
|
-
tr.appendChild(act);
|
|
2620
|
-
body.appendChild(tr);
|
|
2621
|
-
});
|
|
2622
|
-
renderProviderAccounts(r.json && r.json.providerAccounts || {});
|
|
2927
|
+
}
|
|
2928
|
+
async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
2929
|
+
if (urlPath !== "/ui" && !urlPath.startsWith("/ui/")) return false;
|
|
2930
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2931
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
2932
|
+
res.end(JSON.stringify({ error: { type: "method_not_allowed", message: "GET/HEAD only" } }));
|
|
2933
|
+
return true;
|
|
2623
2934
|
}
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
(byProvider[provider] || []).forEach(function (acc) {
|
|
2632
|
-
var tr = document.createElement('tr');
|
|
2633
|
-
tr.appendChild(td(provider));
|
|
2634
|
-
tr.appendChild(td(acc.label || acc.id));
|
|
2635
|
-
tr.appendChild(td(acc.status));
|
|
2636
|
-
tr.appendChild(td(acc.isActive ? 'yes' : ''));
|
|
2637
|
-
var act = document.createElement('td');
|
|
2638
|
-
if (!acc.isActive) {
|
|
2639
|
-
act.appendChild(btn('Set active', '', async function () {
|
|
2640
|
-
await api('PUT', 'accounts/' + encodeURIComponent(provider) + '/active', { id: acc.id });
|
|
2641
|
-
loadAccounts();
|
|
2642
|
-
}));
|
|
2935
|
+
if (!uiDist) {
|
|
2936
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2937
|
+
res.end(
|
|
2938
|
+
JSON.stringify({
|
|
2939
|
+
error: {
|
|
2940
|
+
type: "ui_not_installed",
|
|
2941
|
+
message: "Control Panel UI not installed (@omnicross/ui has no built dist). Install/build @omnicross/ui or set OMNICROSS_UI_DIST."
|
|
2643
2942
|
}
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
loadAccounts();
|
|
2648
|
-
}
|
|
2649
|
-
}));
|
|
2650
|
-
tr.appendChild(act);
|
|
2651
|
-
body.appendChild(tr);
|
|
2652
|
-
});
|
|
2653
|
-
});
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
async function saveAccount() {
|
|
2657
|
-
$('acErr').textContent = '';
|
|
2658
|
-
var provider = $('acProvider').value;
|
|
2659
|
-
var raw = $('acBody').value.trim();
|
|
2660
|
-
if (!raw) { $('acErr').textContent = 'paste a token JSON'; return; }
|
|
2661
|
-
var payload; try { payload = JSON.parse(raw); } catch (e) { $('acErr').textContent = 'invalid JSON'; return; }
|
|
2662
|
-
var r = await api('PUT', 'accounts/' + encodeURIComponent(provider), payload);
|
|
2663
|
-
if (r.status >= 400) { $('acErr').textContent = (r.json && r.json.error && r.json.error.message) || ('error ' + r.status); return; }
|
|
2664
|
-
$('acBody').value = ''; // never persist/echo the just-saved token
|
|
2665
|
-
loadAccounts();
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
// ── Playground ──────────────────────────────────────────────────────────────
|
|
2669
|
-
async function sendPlayground() {
|
|
2670
|
-
var pre = $('plResponse');
|
|
2671
|
-
pre.classList.remove('muted');
|
|
2672
|
-
pre.textContent = 'sending…';
|
|
2673
|
-
var bodyText = $('plBody').value;
|
|
2674
|
-
var parsed; try { parsed = JSON.parse(bodyText); } catch (e) { parsed = bodyText; }
|
|
2675
|
-
var r = await fetch('/admin/api/playground', {
|
|
2676
|
-
method: 'POST',
|
|
2677
|
-
headers: headers({ 'Content-Type': 'application/json' }),
|
|
2678
|
-
body: JSON.stringify({ endpoint: $('plEndpoint').value, key: $('plKey').value, body: parsed }),
|
|
2679
|
-
});
|
|
2680
|
-
var text = await r.text();
|
|
2681
|
-
pre.textContent = '[' + r.status + ']\n' + text;
|
|
2943
|
+
})
|
|
2944
|
+
);
|
|
2945
|
+
return true;
|
|
2682
2946
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
$('kCreate').onclick = createKey;
|
|
2688
|
-
$('sSave').onclick = saveServer;
|
|
2689
|
-
$('acSave').onclick = saveAccount;
|
|
2690
|
-
$('plSend').onclick = sendPlayground;
|
|
2691
|
-
refresh();
|
|
2947
|
+
if (urlPath === "/ui") {
|
|
2948
|
+
res.writeHead(302, { Location: "/ui/" });
|
|
2949
|
+
res.end();
|
|
2950
|
+
return true;
|
|
2692
2951
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2952
|
+
let rel;
|
|
2953
|
+
try {
|
|
2954
|
+
rel = decodeURIComponent(urlPath.slice("/ui/".length));
|
|
2955
|
+
} catch {
|
|
2956
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2957
|
+
res.end(JSON.stringify({ error: { type: "bad_request", message: "malformed path" } }));
|
|
2958
|
+
return true;
|
|
2959
|
+
}
|
|
2960
|
+
if (rel.includes("\\") || rel.includes("\0")) {
|
|
2961
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2962
|
+
res.end(JSON.stringify({ error: { type: "bad_request", message: "invalid path" } }));
|
|
2963
|
+
return true;
|
|
2964
|
+
}
|
|
2965
|
+
const filePath = import_node_path4.default.resolve(uiDist, rel === "" ? "index.html" : rel);
|
|
2966
|
+
if (filePath !== uiDist && !filePath.startsWith(uiDist + import_node_path4.default.sep)) {
|
|
2967
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2968
|
+
res.end(JSON.stringify({ error: { type: "forbidden", message: "path outside ui root" } }));
|
|
2969
|
+
return true;
|
|
2970
|
+
}
|
|
2971
|
+
let target = filePath;
|
|
2972
|
+
if (!(0, import_node_fs6.existsSync)(target) || (0, import_node_fs6.statSync)(target).isDirectory()) {
|
|
2973
|
+
if (import_node_path4.default.extname(rel) === "") {
|
|
2974
|
+
target = import_node_path4.default.join(uiDist, "index.html");
|
|
2975
|
+
} else {
|
|
2976
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2977
|
+
res.end(JSON.stringify({ error: { type: "not_found", message: "no such ui asset" } }));
|
|
2978
|
+
return true;
|
|
2979
|
+
}
|
|
2696
2980
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
// src/admin/html.ts
|
|
2704
|
-
var STYLE = `
|
|
2705
|
-
:root { --bg:#0f1115; --panel:#171a21; --line:#272b35; --fg:#e6e8ec; --muted:#8b91a0; --accent:#5b8cff; --danger:#ff5b6e; --ok:#3ecf8e; }
|
|
2706
|
-
* { box-sizing: border-box; }
|
|
2707
|
-
body { margin:0; font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:var(--bg); color:var(--fg); }
|
|
2708
|
-
header { padding:14px 20px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:12px; }
|
|
2709
|
-
header h1 { font-size:16px; margin:0; font-weight:600; }
|
|
2710
|
-
header .badge { font-size:12px; color:var(--muted); }
|
|
2711
|
-
main { padding:20px; display:grid; gap:20px; max-width:980px; margin:0 auto; }
|
|
2712
|
-
section { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:16px; }
|
|
2713
|
-
section h2 { font-size:14px; margin:0 0 12px; font-weight:600; }
|
|
2714
|
-
table { width:100%; border-collapse:collapse; font-size:13px; }
|
|
2715
|
-
th, td { text-align:left; padding:6px 8px; border-bottom:1px solid var(--line); }
|
|
2716
|
-
th { color:var(--muted); font-weight:500; }
|
|
2717
|
-
input, select, textarea { background:var(--bg); color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:6px 8px; font:inherit; }
|
|
2718
|
-
textarea { width:100%; min-height:90px; resize:vertical; font-family:ui-monospace,Menlo,monospace; }
|
|
2719
|
-
button { background:var(--accent); color:#fff; border:0; border-radius:6px; padding:6px 12px; cursor:pointer; font:inherit; }
|
|
2720
|
-
button.secondary { background:#2a2f3a; }
|
|
2721
|
-
button.danger { background:var(--danger); }
|
|
2722
|
-
.row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:8px; }
|
|
2723
|
-
.muted { color:var(--muted); }
|
|
2724
|
-
.mono { font-family:ui-monospace,Menlo,monospace; }
|
|
2725
|
-
.pill { padding:1px 8px; border-radius:999px; font-size:12px; }
|
|
2726
|
-
.pill.ok { background:rgba(62,207,142,.15); color:var(--ok); }
|
|
2727
|
-
.pill.bad { background:rgba(255,91,110,.15); color:var(--danger); }
|
|
2728
|
-
pre { background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:10px; overflow:auto; max-height:320px; white-space:pre-wrap; }
|
|
2729
|
-
.modal-bg { position:fixed; inset:0; background:rgba(0,0,0,.6); display:none; align-items:center; justify-content:center; }
|
|
2730
|
-
.modal-bg.show { display:flex; }
|
|
2731
|
-
.modal { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:20px; max-width:520px; width:90%; }
|
|
2732
|
-
.warn { color:var(--danger); font-size:13px; margin:8px 0; }
|
|
2733
|
-
.err { color:var(--danger); font-size:13px; }
|
|
2734
|
-
`;
|
|
2735
|
-
var BODY = `
|
|
2736
|
-
<header>
|
|
2737
|
-
<h1>omnicross daemon dashboard</h1>
|
|
2738
|
-
<span class="badge" id="statusBadge">connecting\u2026</span>
|
|
2739
|
-
</header>
|
|
2740
|
-
<main>
|
|
2741
|
-
<section id="statusSection">
|
|
2742
|
-
<h2>Runtime status</h2>
|
|
2743
|
-
<div id="statusBody" class="muted">loading\u2026</div>
|
|
2744
|
-
</section>
|
|
2745
|
-
|
|
2746
|
-
<section>
|
|
2747
|
-
<h2>Providers</h2>
|
|
2748
|
-
<table id="providersTable"><thead><tr><th>id</th><th>format</th><th>base URL</th><th>key</th><th></th></tr></thead><tbody></tbody></table>
|
|
2749
|
-
<div class="row">
|
|
2750
|
-
<select id="pPreset"><option value="">-- \u9009\u62E9\u9884\u7F6E --</option></select>
|
|
2751
|
-
<input id="pId" placeholder="id" size="10" />
|
|
2752
|
-
<select id="pFormat"><option value="openai">openai</option><option value="anthropic">anthropic</option><option value="gemini">gemini</option></select>
|
|
2753
|
-
<input id="pBase" placeholder="base URL" size="26" />
|
|
2754
|
-
<input id="pKey" placeholder="apiKey (blank = keep on edit)" size="22" />
|
|
2755
|
-
<button id="pSave">Save provider</button>
|
|
2756
|
-
</div>
|
|
2757
|
-
<div class="err" id="pErr"></div>
|
|
2758
|
-
<div id="poolPanel" style="display:none; margin-top:12px;">
|
|
2759
|
-
<h2 id="poolTitle" style="font-size:13px;">API key pool</h2>
|
|
2760
|
-
<p class="muted" style="margin:0 0 8px;">Read-only. Multi-key is <b>cold-standby + observable</b> in v1: outbound failover does not yet rotate keys (null-session boundary \u2014 pending the core seam). Keys are masked; edit the pool via the provider's <span class="mono">apiKeys</span>.</p>
|
|
2761
|
-
<table id="poolTable"><thead><tr><th>id</th><th>label</th><th>key</th><th>enabled</th><th>weight</th><th>health</th></tr></thead><tbody></tbody></table>
|
|
2762
|
-
</div>
|
|
2763
|
-
</section>
|
|
2764
|
-
|
|
2765
|
-
<section>
|
|
2766
|
-
<h2>Named keys</h2>
|
|
2767
|
-
<table id="keysTable"><thead><tr><th>name</th><th>prefix</th><th>enabled</th><th>revoked</th><th></th></tr></thead><tbody></tbody></table>
|
|
2768
|
-
<div class="row">
|
|
2769
|
-
<input id="kName" placeholder="key name" size="16" />
|
|
2770
|
-
<button id="kCreate">Create key</button>
|
|
2771
|
-
</div>
|
|
2772
|
-
</section>
|
|
2773
|
-
|
|
2774
|
-
<section>
|
|
2775
|
-
<h2>Server config</h2>
|
|
2776
|
-
<div class="row">
|
|
2777
|
-
<label><input type="checkbox" id="sEnabled" /> enabled</label>
|
|
2778
|
-
<label><input type="checkbox" id="sLan" /> networkBinding (LAN)</label>
|
|
2779
|
-
<label>port <input id="sPort" size="6" /></label>
|
|
2780
|
-
<button id="sSave">Apply server config</button>
|
|
2781
|
-
</div>
|
|
2782
|
-
<table id="endpointsTable"><thead><tr><th>endpoint</th><th>defaultModel</th><th>subscription</th></tr></thead><tbody></tbody></table>
|
|
2783
|
-
</section>
|
|
2784
|
-
|
|
2785
|
-
<section>
|
|
2786
|
-
<h2>Accounts <span class="muted">(subscription tokens)</span></h2>
|
|
2787
|
-
<table id="accountsTable"><thead><tr><th>provider</th><th>kind</th><th>status</th><th></th></tr></thead><tbody></tbody></table>
|
|
2788
|
-
<h3>Per-provider accounts <span class="muted">(multi-account \u2014 sanitized, no tokens)</span></h3>
|
|
2789
|
-
<table id="providerAccountsTable"><thead><tr><th>provider</th><th>label</th><th>status</th><th>active</th><th></th></tr></thead><tbody></tbody></table>
|
|
2790
|
-
<div class="row">
|
|
2791
|
-
<select id="acProvider"><option value="claude">claude</option><option value="codex">codex</option><option value="gemini">gemini</option><option value="opencodego">opencodego</option></select>
|
|
2792
|
-
<button id="acSave">Save token</button>
|
|
2793
|
-
</div>
|
|
2794
|
-
<textarea id="acBody" placeholder='{"authMethod":"oauth","status":"authorized","accessToken":"\u2026","refreshToken":"\u2026"}'></textarea>
|
|
2795
|
-
<p class="muted">Write-only: paste a token JSON to authorize this provider. The token is shown only on entry \u2014 it is never read back or displayed. Stored as plain JSON in <span class="mono">tokens.json</span>.</p>
|
|
2796
|
-
<div class="err" id="acErr"></div>
|
|
2797
|
-
</section>
|
|
2798
|
-
|
|
2799
|
-
<section>
|
|
2800
|
-
<h2>Playground</h2>
|
|
2801
|
-
<div class="row">
|
|
2802
|
-
<select id="plEndpoint"><option value="chat">chat</option><option value="responses">responses</option><option value="messages">messages</option><option value="gemini">gemini</option></select>
|
|
2803
|
-
<input id="plKey" placeholder="named key (sk-omnicross-\u2026)" size="30" />
|
|
2804
|
-
<button id="plSend">Send</button>
|
|
2805
|
-
</div>
|
|
2806
|
-
<textarea id="plBody">{"model":"","messages":[{"role":"user","content":"ping"}]}</textarea>
|
|
2807
|
-
<pre id="plResponse" class="muted">response will appear here</pre>
|
|
2808
|
-
</section>
|
|
2809
|
-
</main>
|
|
2810
|
-
|
|
2811
|
-
<div class="modal-bg" id="keyModalBg">
|
|
2812
|
-
<div class="modal">
|
|
2813
|
-
<h2>Key created</h2>
|
|
2814
|
-
<p class="warn">This secret is shown ONCE. Copy it now \u2014 it cannot be retrieved again.</p>
|
|
2815
|
-
<pre id="keyPlaintext" class="mono"></pre>
|
|
2816
|
-
<div class="row">
|
|
2817
|
-
<button id="keyCopy">Copy</button>
|
|
2818
|
-
<button class="secondary" id="keyClose">Close</button>
|
|
2819
|
-
</div>
|
|
2820
|
-
</div>
|
|
2821
|
-
</div>
|
|
2822
|
-
`;
|
|
2823
|
-
var DASHBOARD_HTML = `<!doctype html>
|
|
2824
|
-
<html lang="en">
|
|
2825
|
-
<head>
|
|
2826
|
-
<meta charset="utf-8" />
|
|
2827
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2828
|
-
<title>omnicross daemon dashboard</title>
|
|
2829
|
-
<style>${STYLE}</style>
|
|
2830
|
-
</head>
|
|
2831
|
-
<body>
|
|
2832
|
-
${BODY}
|
|
2833
|
-
<script>${DASHBOARD_JS}</script>
|
|
2834
|
-
</body>
|
|
2835
|
-
</html>`;
|
|
2981
|
+
const body = await (0, import_promises.readFile)(target);
|
|
2982
|
+
const type = CONTENT_TYPES[import_node_path4.default.extname(target).toLowerCase()] ?? "application/octet-stream";
|
|
2983
|
+
res.writeHead(200, { "Content-Type": type, "Content-Length": body.length });
|
|
2984
|
+
res.end(req.method === "HEAD" ? void 0 : body);
|
|
2985
|
+
return true;
|
|
2986
|
+
}
|
|
2836
2987
|
|
|
2837
2988
|
// src/admin/AdminServer.ts
|
|
2838
2989
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
2839
2990
|
var LAN_ADDR = "0.0.0.0";
|
|
2991
|
+
var DAEMON_VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
2840
2992
|
var AdminServer = class {
|
|
2841
2993
|
constructor(deps) {
|
|
2842
2994
|
this.deps = deps;
|
|
@@ -2845,6 +2997,8 @@ var AdminServer = class {
|
|
|
2845
2997
|
server = null;
|
|
2846
2998
|
boundPort = 0;
|
|
2847
2999
|
boundAddr = LOOPBACK_ADDR;
|
|
3000
|
+
/** Control Panel dist dir (resolved once at first request; null = no UI). */
|
|
3001
|
+
uiDist;
|
|
2848
3002
|
/**
|
|
2849
3003
|
* Start the admin listener honoring the resolved admin config. Returns the
|
|
2850
3004
|
* actual bound port, or `0` when it refuses/declines to bind (disabled or the
|
|
@@ -2873,13 +3027,13 @@ var AdminServer = class {
|
|
|
2873
3027
|
const server = import_node_http2.default.createServer((req, res) => {
|
|
2874
3028
|
this.onRequest(req, res);
|
|
2875
3029
|
});
|
|
2876
|
-
const onError = (
|
|
2877
|
-
if (
|
|
3030
|
+
const onError = (err5) => {
|
|
3031
|
+
if (err5.code === "EADDRINUSE" && port !== 0) {
|
|
2878
3032
|
server.removeListener("error", onError);
|
|
2879
3033
|
this.listen(bindAddr, 0).then(resolve, reject);
|
|
2880
3034
|
return;
|
|
2881
3035
|
}
|
|
2882
|
-
reject(
|
|
3036
|
+
reject(err5);
|
|
2883
3037
|
};
|
|
2884
3038
|
server.on("error", onError);
|
|
2885
3039
|
server.listen(port, bindAddr, () => {
|
|
@@ -2897,8 +3051,8 @@ var AdminServer = class {
|
|
|
2897
3051
|
}
|
|
2898
3052
|
/** Per-request handler: auth gate (when a token is set) → routing. */
|
|
2899
3053
|
onRequest(req, res) {
|
|
2900
|
-
void this.dispatch(req, res).catch((
|
|
2901
|
-
const message =
|
|
3054
|
+
void this.dispatch(req, res).catch((err5) => {
|
|
3055
|
+
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
2902
3056
|
console.error("[AdminServer] unhandled error:", message);
|
|
2903
3057
|
if (!res.headersSent) {
|
|
2904
3058
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -2908,22 +3062,26 @@ var AdminServer = class {
|
|
|
2908
3062
|
}
|
|
2909
3063
|
async dispatch(req, res) {
|
|
2910
3064
|
const cfg = this.deps.getAdminConfig();
|
|
3065
|
+
res.setHeader("x-omnicross-daemon", DAEMON_VERSION);
|
|
3066
|
+
res.setHeader("x-omnicross-pid", String(process.pid));
|
|
2911
3067
|
if (cfg.token && !this.isAuthorized(req, cfg.token)) {
|
|
2912
3068
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
2913
3069
|
res.end(JSON.stringify({ error: { type: "unauthorized", message: "admin token required" } }));
|
|
2914
3070
|
return;
|
|
2915
3071
|
}
|
|
2916
3072
|
const url = req.url ?? "/";
|
|
2917
|
-
const
|
|
2918
|
-
if ((req.method === "GET" || req.method === "HEAD") && (
|
|
2919
|
-
res.writeHead(
|
|
2920
|
-
res.end(
|
|
3073
|
+
const path2 = url.split("?")[0];
|
|
3074
|
+
if ((req.method === "GET" || req.method === "HEAD") && (path2 === "/" || path2 === "/admin")) {
|
|
3075
|
+
res.writeHead(302, { Location: "/ui/" });
|
|
3076
|
+
res.end();
|
|
2921
3077
|
return;
|
|
2922
3078
|
}
|
|
2923
|
-
if (
|
|
2924
|
-
await handleAdminApi(req, res,
|
|
3079
|
+
if (path2.startsWith("/admin/api/")) {
|
|
3080
|
+
await handleAdminApi(req, res, path2, this.deps);
|
|
2925
3081
|
return;
|
|
2926
3082
|
}
|
|
3083
|
+
if (this.uiDist === void 0) this.uiDist = resolveUiDist();
|
|
3084
|
+
if (await handleUiStatic(req, res, path2, this.uiDist)) return;
|
|
2927
3085
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2928
3086
|
res.end(JSON.stringify({ error: { type: "not_found", message: "no such admin route" } }));
|
|
2929
3087
|
}
|
|
@@ -2958,11 +3116,11 @@ function constantTimeEquals(a, b) {
|
|
|
2958
3116
|
const bufA = Buffer.from(a, "utf8");
|
|
2959
3117
|
const bufB = Buffer.from(b, "utf8");
|
|
2960
3118
|
if (bufA.length !== bufB.length) return false;
|
|
2961
|
-
return (0,
|
|
3119
|
+
return (0, import_node_crypto7.timingSafeEqual)(bufA, bufB);
|
|
2962
3120
|
}
|
|
2963
3121
|
|
|
2964
3122
|
// src/admin/oauthSessions.ts
|
|
2965
|
-
var
|
|
3123
|
+
var import_node_crypto8 = __toESM(require("crypto"), 1);
|
|
2966
3124
|
var DEFAULT_OAUTH_SESSION_TTL_MS = 10 * 60 * 1e3;
|
|
2967
3125
|
var OAuthSessionStore = class {
|
|
2968
3126
|
constructor(ttlMs = DEFAULT_OAUTH_SESSION_TTL_MS) {
|
|
@@ -2976,7 +3134,7 @@ var OAuthSessionStore = class {
|
|
|
2976
3134
|
*/
|
|
2977
3135
|
put(session) {
|
|
2978
3136
|
this.sweep();
|
|
2979
|
-
const sessionId =
|
|
3137
|
+
const sessionId = import_node_crypto8.default.randomBytes(24).toString("base64url");
|
|
2980
3138
|
this.sessions.set(sessionId, { ...session, createdAt: Date.now() });
|
|
2981
3139
|
return sessionId;
|
|
2982
3140
|
}
|
|
@@ -3046,18 +3204,18 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
3046
3204
|
res.end(pageHtml("Login complete."));
|
|
3047
3205
|
finish(server, () => resolve(code));
|
|
3048
3206
|
});
|
|
3049
|
-
server.on("error", (
|
|
3207
|
+
server.on("error", (err5) => {
|
|
3050
3208
|
if (settled) return;
|
|
3051
3209
|
settled = true;
|
|
3052
3210
|
clearTimeout(timer);
|
|
3053
|
-
if (
|
|
3211
|
+
if (err5.code === "EADDRINUSE") {
|
|
3054
3212
|
reject(
|
|
3055
3213
|
new Error(
|
|
3056
3214
|
`login: cannot bind ${LOOPBACK_HOST}:${LOOPBACK_PORT} (address in use) \u2014 another codex login or process is holding the port`
|
|
3057
3215
|
)
|
|
3058
3216
|
);
|
|
3059
3217
|
} else {
|
|
3060
|
-
reject(
|
|
3218
|
+
reject(err5);
|
|
3061
3219
|
}
|
|
3062
3220
|
});
|
|
3063
3221
|
const timer = setTimeout(() => {
|
|
@@ -3311,7 +3469,7 @@ var ConsoleLogger = class {
|
|
|
3311
3469
|
};
|
|
3312
3470
|
|
|
3313
3471
|
// src/ports/JsonApiServerSettingsStore.ts
|
|
3314
|
-
var
|
|
3472
|
+
var import_node_fs7 = require("fs");
|
|
3315
3473
|
var import_outbound_api3 = require("@omnicross/core/outbound-api");
|
|
3316
3474
|
var JsonApiServerSettingsStore = class {
|
|
3317
3475
|
constructor(configPath) {
|
|
@@ -3327,12 +3485,12 @@ var JsonApiServerSettingsStore = class {
|
|
|
3327
3485
|
if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return;
|
|
3328
3486
|
const file = this.readFile();
|
|
3329
3487
|
file.server = value;
|
|
3330
|
-
(0,
|
|
3488
|
+
(0, import_node_fs7.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
3331
3489
|
}
|
|
3332
3490
|
/** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
|
|
3333
3491
|
readFile() {
|
|
3334
3492
|
try {
|
|
3335
|
-
const raw = (0,
|
|
3493
|
+
const raw = (0, import_node_fs7.readFileSync)(this.configPath, "utf8");
|
|
3336
3494
|
const parsed = JSON.parse(raw);
|
|
3337
3495
|
if (parsed && typeof parsed === "object") return parsed;
|
|
3338
3496
|
} catch {
|
|
@@ -3341,10 +3499,556 @@ var JsonApiServerSettingsStore = class {
|
|
|
3341
3499
|
}
|
|
3342
3500
|
};
|
|
3343
3501
|
|
|
3502
|
+
// src/ports/JsonlUsageEventStore.ts
|
|
3503
|
+
var import_node_crypto9 = require("crypto");
|
|
3504
|
+
var import_node_fs8 = require("fs");
|
|
3505
|
+
var JsonlUsageEventStore = class {
|
|
3506
|
+
constructor(eventsPath, isPriced) {
|
|
3507
|
+
this.eventsPath = eventsPath;
|
|
3508
|
+
this.isPriced = isPriced;
|
|
3509
|
+
}
|
|
3510
|
+
eventsPath;
|
|
3511
|
+
isPriced;
|
|
3512
|
+
/** Persist one event: assign `id`, stamp `ts` when absent, append ONE line. */
|
|
3513
|
+
async insert(input) {
|
|
3514
|
+
const row = {
|
|
3515
|
+
...input,
|
|
3516
|
+
id: (0, import_node_crypto9.randomUUID)(),
|
|
3517
|
+
ts: input.ts ?? Date.now()
|
|
3518
|
+
};
|
|
3519
|
+
(0, import_node_fs8.appendFileSync)(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
|
|
3520
|
+
return row.id;
|
|
3521
|
+
}
|
|
3522
|
+
async getTotals(range) {
|
|
3523
|
+
const totals = {
|
|
3524
|
+
inputTokens: 0,
|
|
3525
|
+
outputTokens: 0,
|
|
3526
|
+
cacheReadTokens: 0,
|
|
3527
|
+
cacheCreationTokens: 0,
|
|
3528
|
+
reasoningTokens: 0,
|
|
3529
|
+
costUsd: 0,
|
|
3530
|
+
costSavedByCacheUsd: 0,
|
|
3531
|
+
eventCount: 0
|
|
3532
|
+
};
|
|
3533
|
+
for (const row of this.readRows(range)) {
|
|
3534
|
+
totals.inputTokens += row.inputTokens;
|
|
3535
|
+
totals.outputTokens += row.outputTokens;
|
|
3536
|
+
totals.cacheReadTokens += row.cacheReadTokens;
|
|
3537
|
+
totals.cacheCreationTokens += row.cacheCreationTokens;
|
|
3538
|
+
totals.reasoningTokens += row.reasoningTokens;
|
|
3539
|
+
totals.costUsd += row.costUsd;
|
|
3540
|
+
totals.costSavedByCacheUsd += row.costSavedByCacheUsd;
|
|
3541
|
+
totals.eventCount += 1;
|
|
3542
|
+
}
|
|
3543
|
+
return totals;
|
|
3544
|
+
}
|
|
3545
|
+
async getByModel(range) {
|
|
3546
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3547
|
+
for (const row of this.readRows(range)) {
|
|
3548
|
+
const key = `${row.providerId}::${row.model}`;
|
|
3549
|
+
let g = groups.get(key);
|
|
3550
|
+
if (!g) {
|
|
3551
|
+
g = {
|
|
3552
|
+
providerId: row.providerId,
|
|
3553
|
+
model: row.model,
|
|
3554
|
+
eventCount: 0,
|
|
3555
|
+
inputTokens: 0,
|
|
3556
|
+
outputTokens: 0,
|
|
3557
|
+
cacheReadTokens: 0,
|
|
3558
|
+
cacheCreationTokens: 0,
|
|
3559
|
+
costUsd: 0,
|
|
3560
|
+
costSavedByCacheUsd: 0,
|
|
3561
|
+
unpriced: false
|
|
3562
|
+
};
|
|
3563
|
+
groups.set(key, g);
|
|
3564
|
+
}
|
|
3565
|
+
g.eventCount += 1;
|
|
3566
|
+
g.inputTokens += row.inputTokens;
|
|
3567
|
+
g.outputTokens += row.outputTokens;
|
|
3568
|
+
g.cacheReadTokens += row.cacheReadTokens;
|
|
3569
|
+
g.cacheCreationTokens += row.cacheCreationTokens;
|
|
3570
|
+
g.costUsd += row.costUsd;
|
|
3571
|
+
g.costSavedByCacheUsd += row.costSavedByCacheUsd;
|
|
3572
|
+
}
|
|
3573
|
+
const rows = Array.from(groups.values());
|
|
3574
|
+
for (const g of rows) {
|
|
3575
|
+
g.unpriced = !await this.isPriced(g.providerId, g.model);
|
|
3576
|
+
}
|
|
3577
|
+
return rows;
|
|
3578
|
+
}
|
|
3579
|
+
/**
|
|
3580
|
+
* Group by RAW apiKeyId (null forms the unattributed sentinel group). Label
|
|
3581
|
+
* here is the raw id fallback — the admin handler resolves display labels
|
|
3582
|
+
* against the configured pool keys (the store stays config-schema-free).
|
|
3583
|
+
*/
|
|
3584
|
+
async getByApiKey(range) {
|
|
3585
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3586
|
+
for (const row of this.readRows(range)) {
|
|
3587
|
+
const key = row.apiKeyId;
|
|
3588
|
+
let g = groups.get(key);
|
|
3589
|
+
if (!g) {
|
|
3590
|
+
g = {
|
|
3591
|
+
apiKeyId: key,
|
|
3592
|
+
label: key ?? "unattributed",
|
|
3593
|
+
providerId: key === null ? null : row.providerId,
|
|
3594
|
+
eventCount: 0,
|
|
3595
|
+
inputTokens: 0,
|
|
3596
|
+
outputTokens: 0,
|
|
3597
|
+
costUsd: 0
|
|
3598
|
+
};
|
|
3599
|
+
groups.set(key, g);
|
|
3600
|
+
}
|
|
3601
|
+
g.eventCount += 1;
|
|
3602
|
+
g.inputTokens += row.inputTokens;
|
|
3603
|
+
g.outputTokens += row.outputTokens;
|
|
3604
|
+
g.costUsd += row.costUsd;
|
|
3605
|
+
}
|
|
3606
|
+
return Array.from(groups.values());
|
|
3607
|
+
}
|
|
3608
|
+
async getMessagesForSession(sessionId) {
|
|
3609
|
+
return this.readAllRows().filter((r) => r.sessionId === sessionId).sort((a, b) => a.ts - b.ts).map((r) => ({
|
|
3610
|
+
id: r.id,
|
|
3611
|
+
ts: r.ts,
|
|
3612
|
+
messageId: r.messageId,
|
|
3613
|
+
parentMessageId: r.parentMessageId,
|
|
3614
|
+
sessionId: r.sessionId,
|
|
3615
|
+
providerId: r.providerId,
|
|
3616
|
+
model: r.model,
|
|
3617
|
+
apiKeyId: r.apiKeyId,
|
|
3618
|
+
engineOrigin: r.engineOrigin,
|
|
3619
|
+
inputTokens: r.inputTokens,
|
|
3620
|
+
outputTokens: r.outputTokens,
|
|
3621
|
+
cacheReadTokens: r.cacheReadTokens,
|
|
3622
|
+
cacheCreationTokens: r.cacheCreationTokens,
|
|
3623
|
+
reasoningTokens: r.reasoningTokens,
|
|
3624
|
+
costUsd: r.costUsd,
|
|
3625
|
+
costSavedByCacheUsd: r.costSavedByCacheUsd
|
|
3626
|
+
}));
|
|
3627
|
+
}
|
|
3628
|
+
async getSessionCacheStats(sessionId) {
|
|
3629
|
+
const stats = {
|
|
3630
|
+
sessionId,
|
|
3631
|
+
inputTokens: 0,
|
|
3632
|
+
cacheReadTokens: 0,
|
|
3633
|
+
cacheCreationTokens: 0,
|
|
3634
|
+
outputTokens: 0,
|
|
3635
|
+
eventCount: 0,
|
|
3636
|
+
hitRate: 0
|
|
3637
|
+
};
|
|
3638
|
+
for (const r of this.readAllRows()) {
|
|
3639
|
+
if (r.sessionId !== sessionId) continue;
|
|
3640
|
+
stats.inputTokens += r.inputTokens;
|
|
3641
|
+
stats.cacheReadTokens += r.cacheReadTokens;
|
|
3642
|
+
stats.cacheCreationTokens += r.cacheCreationTokens;
|
|
3643
|
+
stats.outputTokens += r.outputTokens;
|
|
3644
|
+
stats.eventCount += 1;
|
|
3645
|
+
}
|
|
3646
|
+
const promptSide = stats.inputTokens + stats.cacheReadTokens + stats.cacheCreationTokens;
|
|
3647
|
+
stats.hitRate = promptSide > 0 ? stats.cacheReadTokens / promptSide : 0;
|
|
3648
|
+
return stats;
|
|
3649
|
+
}
|
|
3650
|
+
/** Rows inside `startTs <= ts < endTs` (endTs EXCLUSIVE). */
|
|
3651
|
+
readRows(range) {
|
|
3652
|
+
return this.readAllRows().filter((r) => r.ts >= range.startTs && r.ts < range.endTs);
|
|
3653
|
+
}
|
|
3654
|
+
/** Parse every line, skipping malformed/torn lines defensively. */
|
|
3655
|
+
readAllRows() {
|
|
3656
|
+
if (!(0, import_node_fs8.existsSync)(this.eventsPath)) return [];
|
|
3657
|
+
let raw;
|
|
3658
|
+
try {
|
|
3659
|
+
raw = (0, import_node_fs8.readFileSync)(this.eventsPath, "utf8");
|
|
3660
|
+
} catch {
|
|
3661
|
+
return [];
|
|
3662
|
+
}
|
|
3663
|
+
const rows = [];
|
|
3664
|
+
for (const line of raw.split("\n")) {
|
|
3665
|
+
const trimmed = line.trim();
|
|
3666
|
+
if (!trimmed) continue;
|
|
3667
|
+
try {
|
|
3668
|
+
const parsed = JSON.parse(trimmed);
|
|
3669
|
+
if (isUsageEventRecord(parsed)) rows.push(parsed);
|
|
3670
|
+
} catch {
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
return rows;
|
|
3674
|
+
}
|
|
3675
|
+
};
|
|
3676
|
+
var NUMERIC_FIELDS = [
|
|
3677
|
+
"ts",
|
|
3678
|
+
"inputTokens",
|
|
3679
|
+
"outputTokens",
|
|
3680
|
+
"cacheReadTokens",
|
|
3681
|
+
"cacheCreationTokens",
|
|
3682
|
+
"reasoningTokens",
|
|
3683
|
+
"costUsd",
|
|
3684
|
+
"costSavedByCacheUsd"
|
|
3685
|
+
];
|
|
3686
|
+
var NULLABLE_STRING_FIELDS = ["messageId", "parentMessageId", "sessionId", "apiKeyId"];
|
|
3687
|
+
var isStringOrNull = (v) => v === null || typeof v === "string";
|
|
3688
|
+
function isUsageEventRecord(parsed) {
|
|
3689
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
3690
|
+
const r = parsed;
|
|
3691
|
+
if (typeof r["id"] !== "string") return false;
|
|
3692
|
+
if (typeof r["providerId"] !== "string") return false;
|
|
3693
|
+
if (typeof r["model"] !== "string") return false;
|
|
3694
|
+
if (typeof r["engineOrigin"] !== "string") return false;
|
|
3695
|
+
for (const f of NULLABLE_STRING_FIELDS) {
|
|
3696
|
+
if (!isStringOrNull(r[f])) return false;
|
|
3697
|
+
}
|
|
3698
|
+
for (const f of NUMERIC_FIELDS) {
|
|
3699
|
+
const v = r[f];
|
|
3700
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return false;
|
|
3701
|
+
}
|
|
3702
|
+
return true;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// src/ports/JsonPricingStore.ts
|
|
3706
|
+
var import_node_fs9 = require("fs");
|
|
3707
|
+
var JsonPricingStore = class {
|
|
3708
|
+
constructor(pricingPath) {
|
|
3709
|
+
this.pricingPath = pricingPath;
|
|
3710
|
+
}
|
|
3711
|
+
pricingPath;
|
|
3712
|
+
async getAll() {
|
|
3713
|
+
return this.readRows();
|
|
3714
|
+
}
|
|
3715
|
+
/**
|
|
3716
|
+
* Insert or update one row keyed (providerId, modelId). `asUserEdit` stamps
|
|
3717
|
+
* user provenance (source 'user', userEdited, editedAt now) so the row is
|
|
3718
|
+
* protected from auto-overwrite during source refreshes; a non-user upsert
|
|
3719
|
+
* stamps source 'litellm' and clears nothing it should not (a plain source
|
|
3720
|
+
* upsert through this method overwrites the row wholesale).
|
|
3721
|
+
*/
|
|
3722
|
+
async upsert(input, asUserEdit) {
|
|
3723
|
+
const rows = this.readRows();
|
|
3724
|
+
const entry = this.applyUpsert(rows, input, asUserEdit);
|
|
3725
|
+
this.writeRows(rows);
|
|
3726
|
+
return entry;
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* Apply a batch fetched from a pricing source. Rows whose local copy is
|
|
3730
|
+
* user-edited are NOT applied — they come back as `{ current, incoming }`
|
|
3731
|
+
* conflicts; everything else is upserted (source 'litellm'). ONE file write
|
|
3732
|
+
* for the whole batch.
|
|
3733
|
+
*/
|
|
3734
|
+
async bulkApplyFromSource(entries) {
|
|
3735
|
+
const rows = this.readRows();
|
|
3736
|
+
const applied = [];
|
|
3737
|
+
const conflicts = [];
|
|
3738
|
+
for (const incoming of entries) {
|
|
3739
|
+
const current = rows.find(
|
|
3740
|
+
(r) => r.providerId === incoming.providerId && r.modelId === incoming.modelId
|
|
3741
|
+
);
|
|
3742
|
+
if (current && current.userEdited) {
|
|
3743
|
+
conflicts.push({ current, incoming });
|
|
3744
|
+
continue;
|
|
3745
|
+
}
|
|
3746
|
+
applied.push(this.applyUpsert(
|
|
3747
|
+
rows,
|
|
3748
|
+
incoming,
|
|
3749
|
+
/* asUserEdit */
|
|
3750
|
+
false
|
|
3751
|
+
));
|
|
3752
|
+
}
|
|
3753
|
+
if (applied.length > 0) this.writeRows(rows);
|
|
3754
|
+
return { applied, conflicts };
|
|
3755
|
+
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Apply per-row conflict decisions: 'overwrite' replaces the local row with
|
|
3758
|
+
* the incoming values (clearing the user-edited mark), 'skip' counts only.
|
|
3759
|
+
*/
|
|
3760
|
+
async applyResolutions(resolutions) {
|
|
3761
|
+
const rows = this.readRows();
|
|
3762
|
+
let overwrittenCount = 0;
|
|
3763
|
+
let skippedCount = 0;
|
|
3764
|
+
for (const r of resolutions) {
|
|
3765
|
+
if (r.action === "skip") {
|
|
3766
|
+
skippedCount += 1;
|
|
3767
|
+
continue;
|
|
3768
|
+
}
|
|
3769
|
+
this.applyUpsert(
|
|
3770
|
+
rows,
|
|
3771
|
+
r.incoming,
|
|
3772
|
+
/* asUserEdit */
|
|
3773
|
+
false
|
|
3774
|
+
);
|
|
3775
|
+
overwrittenCount += 1;
|
|
3776
|
+
}
|
|
3777
|
+
if (overwrittenCount > 0) this.writeRows(rows);
|
|
3778
|
+
return { overwrittenCount, skippedCount };
|
|
3779
|
+
}
|
|
3780
|
+
/**
|
|
3781
|
+
* STORE-LOCAL (not on the core port): remove one row. Returns whether a row
|
|
3782
|
+
* was actually removed. The admin DELETE handler calls this then invalidates
|
|
3783
|
+
* the engine cache.
|
|
3784
|
+
*/
|
|
3785
|
+
async delete(providerId, modelId) {
|
|
3786
|
+
const rows = this.readRows();
|
|
3787
|
+
const idx = rows.findIndex((r) => r.providerId === providerId && r.modelId === modelId);
|
|
3788
|
+
if (idx < 0) return false;
|
|
3789
|
+
rows.splice(idx, 1);
|
|
3790
|
+
this.writeRows(rows);
|
|
3791
|
+
return true;
|
|
3792
|
+
}
|
|
3793
|
+
/** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
|
|
3794
|
+
applyUpsert(rows, input, asUserEdit) {
|
|
3795
|
+
const now = Date.now();
|
|
3796
|
+
const entry = {
|
|
3797
|
+
providerId: input.providerId,
|
|
3798
|
+
modelId: input.modelId,
|
|
3799
|
+
inputPricePer1m: input.inputPricePer1m,
|
|
3800
|
+
outputPricePer1m: input.outputPricePer1m,
|
|
3801
|
+
cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
|
|
3802
|
+
cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
|
|
3803
|
+
source: asUserEdit ? "user" : "litellm",
|
|
3804
|
+
userEdited: asUserEdit,
|
|
3805
|
+
editedAt: asUserEdit ? now : null,
|
|
3806
|
+
updatedAt: now
|
|
3807
|
+
};
|
|
3808
|
+
const idx = rows.findIndex(
|
|
3809
|
+
(r) => r.providerId === input.providerId && r.modelId === input.modelId
|
|
3810
|
+
);
|
|
3811
|
+
if (idx >= 0) rows[idx] = entry;
|
|
3812
|
+
else rows.push(entry);
|
|
3813
|
+
return entry;
|
|
3814
|
+
}
|
|
3815
|
+
/** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
|
|
3816
|
+
readRows() {
|
|
3817
|
+
if (!(0, import_node_fs9.existsSync)(this.pricingPath)) return [];
|
|
3818
|
+
try {
|
|
3819
|
+
const parsed = JSON.parse((0, import_node_fs9.readFileSync)(this.pricingPath, "utf8"));
|
|
3820
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3821
|
+
} catch {
|
|
3822
|
+
return [];
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
writeRows(rows) {
|
|
3826
|
+
(0, import_node_fs9.writeFileSync)(this.pricingPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
3827
|
+
}
|
|
3828
|
+
};
|
|
3829
|
+
|
|
3344
3830
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
3345
|
-
var
|
|
3346
|
-
var
|
|
3831
|
+
var import_node_fs12 = require("fs");
|
|
3832
|
+
var import_node_path7 = require("path");
|
|
3347
3833
|
var import_subscriptions3 = require("@omnicross/subscriptions");
|
|
3834
|
+
|
|
3835
|
+
// src/ports/account-sync.ts
|
|
3836
|
+
var IMPORT_EXPIRY_MARGIN_MS = 6e4;
|
|
3837
|
+
function viewOf(tokens) {
|
|
3838
|
+
return tokens;
|
|
3839
|
+
}
|
|
3840
|
+
function decideExternalImport(captured, external, now = Date.now()) {
|
|
3841
|
+
if (!external?.accessToken) return "no-credential";
|
|
3842
|
+
const capturedRt = viewOf(captured).refreshToken;
|
|
3843
|
+
const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
|
|
3844
|
+
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
|
|
3845
|
+
return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
|
|
3846
|
+
}
|
|
3847
|
+
function buildImportedTokens(captured, external) {
|
|
3848
|
+
const imported = {
|
|
3849
|
+
...captured,
|
|
3850
|
+
accessToken: external.accessToken,
|
|
3851
|
+
status: "authorized",
|
|
3852
|
+
errorMessage: void 0,
|
|
3853
|
+
syncWarning: void 0,
|
|
3854
|
+
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3855
|
+
};
|
|
3856
|
+
if (external.refreshToken) imported.refreshToken = external.refreshToken;
|
|
3857
|
+
if (external.expiresAt) imported.expiresAt = external.expiresAt;
|
|
3858
|
+
else delete imported.expiresAt;
|
|
3859
|
+
if (external.idToken) imported.idToken = external.idToken;
|
|
3860
|
+
if (external.scopes) imported.scopes = external.scopes;
|
|
3861
|
+
return imported;
|
|
3862
|
+
}
|
|
3863
|
+
function buildTokensFromExternal(provider, external) {
|
|
3864
|
+
const base = {
|
|
3865
|
+
authMethod: "oauth",
|
|
3866
|
+
status: "authorized",
|
|
3867
|
+
accessToken: external.accessToken,
|
|
3868
|
+
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3869
|
+
};
|
|
3870
|
+
if (provider === "claude") {
|
|
3871
|
+
const tokens2 = { ...base };
|
|
3872
|
+
if (external.refreshToken) tokens2.refreshToken = external.refreshToken;
|
|
3873
|
+
if (external.expiresAt) tokens2.expiresAt = external.expiresAt;
|
|
3874
|
+
if (external.scopes) tokens2.scopes = external.scopes;
|
|
3875
|
+
return tokens2;
|
|
3876
|
+
}
|
|
3877
|
+
const tokens = { ...base };
|
|
3878
|
+
if (external.refreshToken) tokens.refreshToken = external.refreshToken;
|
|
3879
|
+
if (external.expiresAt) tokens.expiresAt = external.expiresAt;
|
|
3880
|
+
if (external.idToken) tokens.idToken = external.idToken;
|
|
3881
|
+
return tokens;
|
|
3882
|
+
}
|
|
3883
|
+
function isExternalDivergent(stored, external) {
|
|
3884
|
+
if (!external?.accessToken || !external.refreshToken) return false;
|
|
3885
|
+
const view = viewOf(stored);
|
|
3886
|
+
if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
|
|
3887
|
+
const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
|
|
3888
|
+
const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
|
|
3889
|
+
return !Number.isFinite(storedExp) || externalExp > storedExp;
|
|
3890
|
+
}
|
|
3891
|
+
function findDuplicateCredentialIds(accounts) {
|
|
3892
|
+
const byCredential = /* @__PURE__ */ new Map();
|
|
3893
|
+
for (const account of accounts) {
|
|
3894
|
+
const view = viewOf(account.tokens);
|
|
3895
|
+
const credential = view.refreshToken ?? view.apiKey ?? view.accessToken;
|
|
3896
|
+
if (!credential) continue;
|
|
3897
|
+
const ids = byCredential.get(credential) ?? [];
|
|
3898
|
+
ids.push(account.id);
|
|
3899
|
+
byCredential.set(credential, ids);
|
|
3900
|
+
}
|
|
3901
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
3902
|
+
for (const ids of byCredential.values()) {
|
|
3903
|
+
if (ids.length > 1) for (const id of ids) duplicates.add(id);
|
|
3904
|
+
}
|
|
3905
|
+
return duplicates;
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
// src/ports/external-cli-credentials.ts
|
|
3909
|
+
var import_node_fs10 = require("fs");
|
|
3910
|
+
var import_node_os2 = require("os");
|
|
3911
|
+
var import_node_path5 = require("path");
|
|
3912
|
+
function externalStorePath(provider, home = (0, import_node_os2.homedir)()) {
|
|
3913
|
+
return provider === "claude" ? (0, import_node_path5.join)(home, ".claude", ".credentials.json") : (0, import_node_path5.join)(home, ".codex", "auth.json");
|
|
3914
|
+
}
|
|
3915
|
+
function decodeJwtExpiryMs(token) {
|
|
3916
|
+
try {
|
|
3917
|
+
const payload = token.split(".")[1];
|
|
3918
|
+
if (!payload) return void 0;
|
|
3919
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
3920
|
+
if (typeof decoded.exp === "number" && Number.isFinite(decoded.exp)) {
|
|
3921
|
+
return decoded.exp * 1e3;
|
|
3922
|
+
}
|
|
3923
|
+
} catch {
|
|
3924
|
+
}
|
|
3925
|
+
return void 0;
|
|
3926
|
+
}
|
|
3927
|
+
function parseClaudeOAuthEnvelope(raw) {
|
|
3928
|
+
const oauth = raw?.claudeAiOauth;
|
|
3929
|
+
if (!oauth || typeof oauth.accessToken !== "string" || !oauth.accessToken) return null;
|
|
3930
|
+
const parsed = { accessToken: oauth.accessToken };
|
|
3931
|
+
if (typeof oauth.refreshToken === "string" && oauth.refreshToken) {
|
|
3932
|
+
parsed.refreshToken = oauth.refreshToken;
|
|
3933
|
+
}
|
|
3934
|
+
if (typeof oauth.expiresAt === "number" && Number.isFinite(oauth.expiresAt)) {
|
|
3935
|
+
parsed.expiresAt = new Date(oauth.expiresAt).toISOString();
|
|
3936
|
+
}
|
|
3937
|
+
if (Array.isArray(oauth.scopes) && oauth.scopes.every((s) => typeof s === "string")) {
|
|
3938
|
+
parsed.scopes = oauth.scopes;
|
|
3939
|
+
}
|
|
3940
|
+
return parsed;
|
|
3941
|
+
}
|
|
3942
|
+
function parseCodexTokensEnvelope(raw) {
|
|
3943
|
+
const tokens = raw?.tokens;
|
|
3944
|
+
if (!tokens) return null;
|
|
3945
|
+
const accessToken = typeof tokens.access_token === "string" && tokens.access_token ? tokens.access_token : void 0;
|
|
3946
|
+
const idToken = typeof tokens.id_token === "string" && tokens.id_token ? tokens.id_token : void 0;
|
|
3947
|
+
if (!accessToken && !idToken) return null;
|
|
3948
|
+
const parsed = {};
|
|
3949
|
+
if (accessToken) {
|
|
3950
|
+
parsed.accessToken = accessToken;
|
|
3951
|
+
const expMs = decodeJwtExpiryMs(accessToken);
|
|
3952
|
+
if (expMs !== void 0) parsed.expiresAt = new Date(expMs).toISOString();
|
|
3953
|
+
}
|
|
3954
|
+
if (idToken) parsed.idToken = idToken;
|
|
3955
|
+
if (typeof tokens.refresh_token === "string" && tokens.refresh_token) {
|
|
3956
|
+
parsed.refreshToken = tokens.refresh_token;
|
|
3957
|
+
}
|
|
3958
|
+
return parsed;
|
|
3959
|
+
}
|
|
3960
|
+
function readExternalCliCredentials(provider, home = (0, import_node_os2.homedir)()) {
|
|
3961
|
+
const path2 = externalStorePath(provider, home);
|
|
3962
|
+
if (!(0, import_node_fs10.existsSync)(path2)) return null;
|
|
3963
|
+
let raw;
|
|
3964
|
+
try {
|
|
3965
|
+
const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
|
|
3966
|
+
raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3967
|
+
} catch {
|
|
3968
|
+
return null;
|
|
3969
|
+
}
|
|
3970
|
+
return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
// src/ports/external-cli-store.ts
|
|
3974
|
+
var import_node_fs11 = require("fs");
|
|
3975
|
+
var import_node_os3 = require("os");
|
|
3976
|
+
var import_node_path6 = require("path");
|
|
3977
|
+
function markerPath(provider, home) {
|
|
3978
|
+
return `${externalStorePath(provider, home)}.omnicross-managed`;
|
|
3979
|
+
}
|
|
3980
|
+
function backupPath(provider, home) {
|
|
3981
|
+
return `${externalStorePath(provider, home)}.omnicross-backup`;
|
|
3982
|
+
}
|
|
3983
|
+
function buildClaudeOAuthEnvelope(tokens) {
|
|
3984
|
+
if (!tokens.accessToken) return null;
|
|
3985
|
+
const envelope = { accessToken: tokens.accessToken };
|
|
3986
|
+
if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
|
|
3987
|
+
if (tokens.expiresAt) {
|
|
3988
|
+
const ms = Date.parse(tokens.expiresAt);
|
|
3989
|
+
if (Number.isFinite(ms)) envelope.expiresAt = ms;
|
|
3990
|
+
}
|
|
3991
|
+
if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
|
|
3992
|
+
return envelope;
|
|
3993
|
+
}
|
|
3994
|
+
function buildCodexTokensEnvelope(tokens) {
|
|
3995
|
+
if (!tokens.accessToken && !tokens.idToken) return null;
|
|
3996
|
+
const envelope = { access_token: tokens.accessToken ?? "" };
|
|
3997
|
+
if (tokens.idToken) envelope.id_token = tokens.idToken;
|
|
3998
|
+
if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
|
|
3999
|
+
return envelope;
|
|
4000
|
+
}
|
|
4001
|
+
function readExistingObject(path2) {
|
|
4002
|
+
if (!(0, import_node_fs11.existsSync)(path2)) return {};
|
|
4003
|
+
try {
|
|
4004
|
+
const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
|
|
4005
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4006
|
+
} catch {
|
|
4007
|
+
return {};
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
function writeAtomic(path2, content) {
|
|
4011
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
|
|
4012
|
+
const temp = `${path2}.omnicross-tmp`;
|
|
4013
|
+
(0, import_node_fs11.writeFileSync)(temp, content, "utf8");
|
|
4014
|
+
(0, import_node_fs11.renameSync)(temp, path2);
|
|
4015
|
+
}
|
|
4016
|
+
function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
|
|
4017
|
+
return {
|
|
4018
|
+
readMarkerAccountId(provider) {
|
|
4019
|
+
const path2 = markerPath(provider, home);
|
|
4020
|
+
if (!(0, import_node_fs11.existsSync)(path2)) return void 0;
|
|
4021
|
+
try {
|
|
4022
|
+
const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
|
|
4023
|
+
return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
|
|
4024
|
+
} catch {
|
|
4025
|
+
return void 0;
|
|
4026
|
+
}
|
|
4027
|
+
},
|
|
4028
|
+
writeMarker(provider, accountId) {
|
|
4029
|
+
writeAtomic(
|
|
4030
|
+
markerPath(provider, home),
|
|
4031
|
+
JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
4032
|
+
);
|
|
4033
|
+
},
|
|
4034
|
+
writeBack(provider, accountId, tokens) {
|
|
4035
|
+
const owner = this.readMarkerAccountId(provider);
|
|
4036
|
+
if (owner !== accountId) return false;
|
|
4037
|
+
const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
|
|
4038
|
+
if (!envelope) return false;
|
|
4039
|
+
const storePath = externalStorePath(provider, home);
|
|
4040
|
+
if ((0, import_node_fs11.existsSync)(storePath) && !(0, import_node_fs11.existsSync)(backupPath(provider, home))) {
|
|
4041
|
+
(0, import_node_fs11.copyFileSync)(storePath, backupPath(provider, home));
|
|
4042
|
+
}
|
|
4043
|
+
const existing = readExistingObject(storePath);
|
|
4044
|
+
const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
|
|
4045
|
+
writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
|
|
4046
|
+
return true;
|
|
4047
|
+
}
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
3348
4052
|
var JsonSubscriptionCredentialStore = class {
|
|
3349
4053
|
/**
|
|
3350
4054
|
* @param tokensPath on-disk `tokens.json` location.
|
|
@@ -3354,14 +4058,33 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3354
4058
|
* is unchanged; tests inject a mock fetch. NOT used by any
|
|
3355
4059
|
* read/write path — only by `refresh*Token`.
|
|
3356
4060
|
*/
|
|
3357
|
-
constructor(tokensPath, box, fetchImpl = (url, init) => fetch(url, init)) {
|
|
4061
|
+
constructor(tokensPath, box, fetchImpl = (url, init) => fetch(url, init), externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
|
|
3358
4062
|
this.tokensPath = tokensPath;
|
|
3359
4063
|
this.box = box;
|
|
3360
4064
|
this.fetchImpl = fetchImpl;
|
|
4065
|
+
this.externalCliReader = externalCliReader;
|
|
4066
|
+
this.externalCliStore = externalCliStore;
|
|
3361
4067
|
}
|
|
3362
4068
|
tokensPath;
|
|
3363
4069
|
box;
|
|
3364
4070
|
fetchImpl;
|
|
4071
|
+
externalCliReader;
|
|
4072
|
+
externalCliStore;
|
|
4073
|
+
/**
|
|
4074
|
+
* In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
|
|
4075
|
+
* SINGLE-USE: two concurrent refreshes of one account each spend the same
|
|
4076
|
+
* token and the loser bricks a healthy account. Every refresh entry point
|
|
4077
|
+
* (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
|
|
4078
|
+
* through `coalesce`, so overlapping callers share ONE upstream round-trip.
|
|
4079
|
+
*/
|
|
4080
|
+
inFlightRefreshes = /* @__PURE__ */ new Map();
|
|
4081
|
+
coalesce(key, task) {
|
|
4082
|
+
const existing = this.inFlightRefreshes.get(key);
|
|
4083
|
+
if (existing) return existing;
|
|
4084
|
+
const run = task().finally(() => this.inFlightRefreshes.delete(key));
|
|
4085
|
+
this.inFlightRefreshes.set(key, run);
|
|
4086
|
+
return run;
|
|
4087
|
+
}
|
|
3365
4088
|
/** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
|
|
3366
4089
|
* file is absent/corrupt). This is the hot read — the codex / gemini auth
|
|
3367
4090
|
* strategies pull `accessToken` / `expiresAt` / `status` from it. */
|
|
@@ -3389,10 +4112,41 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3389
4112
|
const out = {};
|
|
3390
4113
|
for (const provider of ["claude", "codex", "gemini", "opencodego"]) {
|
|
3391
4114
|
const sanitized = sanitizeAccounts(config, provider);
|
|
3392
|
-
if (sanitized.length > 0) out[provider] = sanitized;
|
|
4115
|
+
if (sanitized.length > 0) out[provider] = this.attachSyncWarnings(config, provider, sanitized);
|
|
3393
4116
|
}
|
|
3394
4117
|
return out;
|
|
3395
4118
|
}
|
|
4119
|
+
/**
|
|
4120
|
+
* List-time credential-conflict warnings (external-cli-sync). Computed, not
|
|
4121
|
+
* persisted: (a) `duplicate-token` when two accounts of one provider share a
|
|
4122
|
+
* credential, (b) `external-divergent` when the external CLI native store has
|
|
4123
|
+
* rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
|
|
4124
|
+
* a failed refresh (`external-not-rotated`) takes precedence — it is the most
|
|
4125
|
+
* actionable state.
|
|
4126
|
+
*/
|
|
4127
|
+
attachSyncWarnings(config, provider, sanitized) {
|
|
4128
|
+
const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
|
|
4129
|
+
let divergentId;
|
|
4130
|
+
if (provider === "claude" || provider === "codex") {
|
|
4131
|
+
const active = getActiveAccount(config, provider);
|
|
4132
|
+
if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
|
|
4133
|
+
divergentId = active.id;
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
if (duplicates.size === 0 && !divergentId) return sanitized;
|
|
4137
|
+
return sanitized.map((account) => {
|
|
4138
|
+
const computed = account.id === divergentId ? "external-divergent" : duplicates.has(account.id) ? "duplicate-token" : void 0;
|
|
4139
|
+
return { ...account, syncWarning: account.syncWarning ?? computed };
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
/** Read the external CLI store, never letting an fs/parse error escape. */
|
|
4143
|
+
safeReadExternal(provider) {
|
|
4144
|
+
try {
|
|
4145
|
+
return this.externalCliReader(provider);
|
|
4146
|
+
} catch {
|
|
4147
|
+
return null;
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
3396
4150
|
/**
|
|
3397
4151
|
* Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
|
|
3398
4152
|
* the block has no refresh_token (setup-token / manual) — no upstream call, the
|
|
@@ -3402,30 +4156,44 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3402
4156
|
* errorMessage → `false`.
|
|
3403
4157
|
*/
|
|
3404
4158
|
async refreshClaudeToken() {
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
4159
|
+
return this.coalesce("claude:active", async () => {
|
|
4160
|
+
const config = this.readConfig();
|
|
4161
|
+
const active = getActiveAccount(config, "claude");
|
|
4162
|
+
const claude = active?.tokens;
|
|
4163
|
+
if (!active || !claude?.refreshToken) return false;
|
|
4164
|
+
const capturedId = active.id;
|
|
4165
|
+
this.materializeMigration(config);
|
|
4166
|
+
try {
|
|
4167
|
+
const result = await import_subscriptions3.claudeOAuth.refreshAccessToken(claude.refreshToken, this.fetchImpl);
|
|
4168
|
+
const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
|
|
4169
|
+
const next = {
|
|
4170
|
+
...claude,
|
|
4171
|
+
accessToken: result.accessToken,
|
|
4172
|
+
refreshToken: result.refreshToken,
|
|
4173
|
+
expiresAt,
|
|
4174
|
+
status: "authorized",
|
|
4175
|
+
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4176
|
+
errorMessage: void 0,
|
|
4177
|
+
syncWarning: void 0
|
|
4178
|
+
};
|
|
4179
|
+
this.writeBackById("claude", capturedId, next);
|
|
4180
|
+
this.resyncExternal("claude", capturedId, next);
|
|
4181
|
+
return true;
|
|
4182
|
+
} catch (error) {
|
|
4183
|
+
if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
|
|
4184
|
+
const r = await import_subscriptions3.claudeOAuth.refreshAccessToken(rt, this.fetchImpl);
|
|
4185
|
+
return {
|
|
4186
|
+
accessToken: r.accessToken,
|
|
4187
|
+
refreshToken: r.refreshToken,
|
|
4188
|
+
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
4189
|
+
};
|
|
4190
|
+
})) {
|
|
4191
|
+
return true;
|
|
4192
|
+
}
|
|
4193
|
+
this.markExpiredById("claude", capturedId, claude, error);
|
|
4194
|
+
return false;
|
|
4195
|
+
}
|
|
4196
|
+
});
|
|
3429
4197
|
}
|
|
3430
4198
|
/**
|
|
3431
4199
|
* Refresh the Codex (ChatGPT) OAuth access token. Same shape
|
|
@@ -3433,31 +4201,46 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3433
4201
|
* HONEST `false` when no refresh_token.
|
|
3434
4202
|
*/
|
|
3435
4203
|
async refreshCodexToken() {
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
4204
|
+
return this.coalesce("codex:active", async () => {
|
|
4205
|
+
const config = this.readConfig();
|
|
4206
|
+
const active = getActiveAccount(config, "codex");
|
|
4207
|
+
const codex = active?.tokens;
|
|
4208
|
+
if (!active || !codex?.refreshToken) return false;
|
|
4209
|
+
const capturedId = active.id;
|
|
4210
|
+
this.materializeMigration(config);
|
|
4211
|
+
try {
|
|
4212
|
+
const result = await import_subscriptions3.codexOAuth.refreshAccessToken(codex.refreshToken, this.fetchImpl);
|
|
4213
|
+
const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
|
|
4214
|
+
const next = {
|
|
4215
|
+
...codex,
|
|
4216
|
+
accessToken: result.accessToken,
|
|
4217
|
+
refreshToken: result.refreshToken,
|
|
4218
|
+
idToken: result.idToken,
|
|
4219
|
+
expiresAt,
|
|
4220
|
+
status: "authorized",
|
|
4221
|
+
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4222
|
+
errorMessage: void 0,
|
|
4223
|
+
syncWarning: void 0
|
|
4224
|
+
};
|
|
4225
|
+
this.writeBackById("codex", capturedId, next);
|
|
4226
|
+
this.resyncExternal("codex", capturedId, next);
|
|
4227
|
+
return true;
|
|
4228
|
+
} catch (error) {
|
|
4229
|
+
if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
|
|
4230
|
+
const r = await import_subscriptions3.codexOAuth.refreshAccessToken(rt, this.fetchImpl);
|
|
4231
|
+
return {
|
|
4232
|
+
accessToken: r.accessToken,
|
|
4233
|
+
refreshToken: r.refreshToken,
|
|
4234
|
+
idToken: r.idToken,
|
|
4235
|
+
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
4236
|
+
};
|
|
4237
|
+
})) {
|
|
4238
|
+
return true;
|
|
4239
|
+
}
|
|
4240
|
+
this.markExpiredById("codex", capturedId, codex, error);
|
|
4241
|
+
return false;
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
3461
4244
|
}
|
|
3462
4245
|
/**
|
|
3463
4246
|
* Refresh the Gemini (Google) OAuth access token. The Google
|
|
@@ -3467,30 +4250,175 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3467
4250
|
* destroy the ability to refresh again). HONEST `false` when no refresh_token.
|
|
3468
4251
|
*/
|
|
3469
4252
|
async refreshGeminiToken() {
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
4253
|
+
return this.coalesce("gemini:active", async () => {
|
|
4254
|
+
const config = this.readConfig();
|
|
4255
|
+
const active = getActiveAccount(config, "gemini");
|
|
4256
|
+
const gemini = active?.tokens;
|
|
4257
|
+
if (!active || !gemini?.refreshToken) return false;
|
|
4258
|
+
const capturedId = active.id;
|
|
4259
|
+
this.materializeMigration(config);
|
|
4260
|
+
try {
|
|
4261
|
+
const result = await import_subscriptions3.geminiOAuth.refreshAccessToken(gemini.refreshToken, this.fetchImpl);
|
|
4262
|
+
const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
|
|
4263
|
+
const next = {
|
|
4264
|
+
...gemini,
|
|
4265
|
+
// KEEP the existing refreshToken (response omits it).
|
|
4266
|
+
accessToken: result.accessToken,
|
|
4267
|
+
expiresAt,
|
|
4268
|
+
status: "authorized",
|
|
4269
|
+
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4270
|
+
errorMessage: void 0
|
|
4271
|
+
};
|
|
4272
|
+
this.writeBackById("gemini", capturedId, next);
|
|
4273
|
+
return true;
|
|
4274
|
+
} catch (error) {
|
|
4275
|
+
this.markExpiredById("gemini", capturedId, gemini, error);
|
|
4276
|
+
return false;
|
|
4277
|
+
}
|
|
4278
|
+
});
|
|
4279
|
+
}
|
|
4280
|
+
/**
|
|
4281
|
+
* Refresh a SPECIFIC account by id (background scheduler sweep,
|
|
4282
|
+
* external-cli-sync). Unlike the active-account refreshers it does NOT
|
|
4283
|
+
* attempt the external-import fallback — the external CLI file's lineage can
|
|
4284
|
+
* only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
|
|
4285
|
+
* failure flags ONLY that account `expired`.
|
|
4286
|
+
*/
|
|
4287
|
+
async refreshAccountById(provider, id) {
|
|
4288
|
+
return this.coalesce(`${provider}:${id}`, async () => {
|
|
4289
|
+
const config = this.readConfig();
|
|
4290
|
+
const account = getAccountById(config, provider, id);
|
|
4291
|
+
const captured = account?.tokens;
|
|
4292
|
+
if (!account || !captured?.refreshToken) return false;
|
|
4293
|
+
this.materializeMigration(config);
|
|
4294
|
+
try {
|
|
4295
|
+
const refreshed = await this.refreshUpstream(provider, captured.refreshToken);
|
|
4296
|
+
const next = {
|
|
4297
|
+
...captured,
|
|
4298
|
+
accessToken: refreshed.accessToken,
|
|
4299
|
+
// Gemini's refresh response omits a new refresh token — keep the captured.
|
|
4300
|
+
refreshToken: refreshed.refreshToken ?? captured.refreshToken,
|
|
4301
|
+
expiresAt: refreshed.expiresAt,
|
|
4302
|
+
status: "authorized",
|
|
4303
|
+
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4304
|
+
errorMessage: void 0,
|
|
4305
|
+
syncWarning: void 0
|
|
4306
|
+
};
|
|
4307
|
+
if (refreshed.idToken) next.idToken = refreshed.idToken;
|
|
4308
|
+
this.writeBackById(provider, id, next);
|
|
4309
|
+
if (provider !== "gemini") this.resyncExternal(provider, id, next);
|
|
4310
|
+
return true;
|
|
4311
|
+
} catch (error) {
|
|
4312
|
+
this.markExpiredById(provider, id, captured, error);
|
|
4313
|
+
return false;
|
|
4314
|
+
}
|
|
4315
|
+
});
|
|
4316
|
+
}
|
|
4317
|
+
/** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
|
|
4318
|
+
async refreshUpstream(provider, refreshToken) {
|
|
4319
|
+
const flow = provider === "claude" ? import_subscriptions3.claudeOAuth : provider === "codex" ? import_subscriptions3.codexOAuth : import_subscriptions3.geminiOAuth;
|
|
4320
|
+
const r = await flow.refreshAccessToken(refreshToken, this.fetchImpl);
|
|
4321
|
+
return {
|
|
4322
|
+
accessToken: r.accessToken,
|
|
4323
|
+
refreshToken: r.refreshToken,
|
|
4324
|
+
idToken: r.idToken,
|
|
4325
|
+
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
/**
|
|
4329
|
+
* External-import fallback for a FAILED active-account refresh
|
|
4330
|
+
* (external-cli-sync). Reads the CLI native store; imports when the external
|
|
4331
|
+
* lineage ROTATED (different refresh token) or its access token is still
|
|
4332
|
+
* valid. When the imported access token is already expired it refreshes once
|
|
4333
|
+
* with the rotated refresh token. A `not-rotated` outcome persists the
|
|
4334
|
+
* `external-not-rotated` warning on the (about-to-be-expired) account so the
|
|
4335
|
+
* UI can tell "genuine revocation" apart from a plain refresh failure.
|
|
4336
|
+
*/
|
|
4337
|
+
async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
|
|
4338
|
+
const markerOwner = this.safeReadMarker(provider);
|
|
4339
|
+
if (markerOwner && markerOwner !== capturedId) return false;
|
|
4340
|
+
const external = this.safeReadExternal(provider);
|
|
4341
|
+
const decision = decideExternalImport(captured, external);
|
|
4342
|
+
if (decision === "not-rotated") {
|
|
4343
|
+
captured.syncWarning = "external-not-rotated";
|
|
3492
4344
|
return false;
|
|
3493
4345
|
}
|
|
4346
|
+
if (decision !== "import" || !external) return false;
|
|
4347
|
+
let imported = buildImportedTokens(
|
|
4348
|
+
captured,
|
|
4349
|
+
external
|
|
4350
|
+
);
|
|
4351
|
+
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
|
|
4352
|
+
if (!accessStillValid) {
|
|
4353
|
+
try {
|
|
4354
|
+
const refreshed = await refreshWithToken(external.refreshToken);
|
|
4355
|
+
imported = {
|
|
4356
|
+
...imported,
|
|
4357
|
+
accessToken: refreshed.accessToken,
|
|
4358
|
+
refreshToken: refreshed.refreshToken ?? imported.refreshToken,
|
|
4359
|
+
expiresAt: refreshed.expiresAt,
|
|
4360
|
+
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4361
|
+
};
|
|
4362
|
+
if (refreshed.idToken) imported.idToken = refreshed.idToken;
|
|
4363
|
+
} catch {
|
|
4364
|
+
return false;
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
this.writeBackById(provider, capturedId, imported);
|
|
4368
|
+
this.resyncExternal(provider, capturedId, imported);
|
|
4369
|
+
return true;
|
|
4370
|
+
}
|
|
4371
|
+
/**
|
|
4372
|
+
* Marker-gated external write-back (external-cli-sync). After a successful
|
|
4373
|
+
* refresh of the account that OWNS the provider's native CLI store (imported
|
|
4374
|
+
* via `importExternalCliAccount`), push the rotated credential back into the
|
|
4375
|
+
* file — otherwise the daemon's refresh invalidates the single-use refresh
|
|
4376
|
+
* token and silently logs the bare CLI out. NON-FATAL: the internal store is
|
|
4377
|
+
* already persisted; a failed external write only leaves the file stale,
|
|
4378
|
+
* which the `external-divergent` warning surfaces.
|
|
4379
|
+
*/
|
|
4380
|
+
resyncExternal(provider, accountId, tokens) {
|
|
4381
|
+
try {
|
|
4382
|
+
this.externalCliStore.writeBack(provider, accountId, tokens);
|
|
4383
|
+
} catch {
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
/** Read the marker's owning account id, never letting an fs error escape. */
|
|
4387
|
+
safeReadMarker(provider) {
|
|
4388
|
+
try {
|
|
4389
|
+
return this.externalCliStore.readMarkerAccountId(provider);
|
|
4390
|
+
} catch {
|
|
4391
|
+
return void 0;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
/**
|
|
4395
|
+
* DAEMON-ONLY (admin import button): which providers have a usable external
|
|
4396
|
+
* CLI credential on THIS machine. Pure detection — reads the native files,
|
|
4397
|
+
* never mutates anything, never returns a token.
|
|
4398
|
+
*/
|
|
4399
|
+
async listExternalCliAvailability() {
|
|
4400
|
+
return {
|
|
4401
|
+
claude: Boolean(this.safeReadExternal("claude")?.accessToken),
|
|
4402
|
+
codex: Boolean(this.safeReadExternal("codex")?.accessToken)
|
|
4403
|
+
};
|
|
4404
|
+
}
|
|
4405
|
+
/**
|
|
4406
|
+
* DAEMON-ONLY (admin import button): import the external CLI's current login
|
|
4407
|
+
* as a NEW account (+ activate), and take MANAGED ownership of the native
|
|
4408
|
+
* store (marker) so subsequent refreshes write back — keeping the bare CLI
|
|
4409
|
+
* and the daemon on the same live credential instead of silently killing one
|
|
4410
|
+
* side's single-use refresh token.
|
|
4411
|
+
*/
|
|
4412
|
+
async importExternalCliAccount(provider, label) {
|
|
4413
|
+
const external = this.safeReadExternal(provider);
|
|
4414
|
+
if (!external?.accessToken) return { ok: false, reason: "no-credential" };
|
|
4415
|
+
const tokens = buildTokensFromExternal(provider, external);
|
|
4416
|
+
const result = await this.appendProviderAccount(provider, tokens, label);
|
|
4417
|
+
try {
|
|
4418
|
+
this.externalCliStore.writeMarker(provider, result.id);
|
|
4419
|
+
} catch {
|
|
4420
|
+
}
|
|
4421
|
+
return { ok: true, id: result.id };
|
|
3494
4422
|
}
|
|
3495
4423
|
/**
|
|
3496
4424
|
* Materialize a lazily-synthesized account id to disk (design D3). On a legacy
|
|
@@ -3574,6 +4502,18 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3574
4502
|
this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3575
4503
|
return result;
|
|
3576
4504
|
}
|
|
4505
|
+
/**
|
|
4506
|
+
* DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
|
|
4507
|
+
* rejects an unknown id. Label-only — no token material is read or written
|
|
4508
|
+
* (the secret-free invariant holds).
|
|
4509
|
+
*/
|
|
4510
|
+
async renameAccount(providerId, id, label) {
|
|
4511
|
+
const current = this.readConfig();
|
|
4512
|
+
const result = renameAccount(current, providerId, id, label);
|
|
4513
|
+
if (!result.ok) return result;
|
|
4514
|
+
this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4515
|
+
return result;
|
|
4516
|
+
}
|
|
3577
4517
|
/**
|
|
3578
4518
|
* DAEMON-ONLY CLEAR (design D1/D3, NOT on the port). Remove a single provider's
|
|
3579
4519
|
* block from `tokens.json` and re-persist (the strategies already tolerate an
|
|
@@ -3590,9 +4530,9 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3590
4530
|
* → `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
3591
4531
|
* write — incl. child 4's future refresh writes — lands encrypted. */
|
|
3592
4532
|
persist(config) {
|
|
3593
|
-
(0,
|
|
4533
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path7.dirname)(this.tokensPath), { recursive: true });
|
|
3594
4534
|
const encrypted = encryptTokens(config, this.box);
|
|
3595
|
-
(0,
|
|
4535
|
+
(0, import_node_fs12.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
|
|
3596
4536
|
}
|
|
3597
4537
|
/**
|
|
3598
4538
|
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
@@ -3608,10 +4548,10 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3608
4548
|
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
3609
4549
|
*/
|
|
3610
4550
|
readConfig() {
|
|
3611
|
-
if (!(0,
|
|
4551
|
+
if (!(0, import_node_fs12.existsSync)(this.tokensPath)) return { updatedAt: "" };
|
|
3612
4552
|
let parsed;
|
|
3613
4553
|
try {
|
|
3614
|
-
const raw = JSON.parse((0,
|
|
4554
|
+
const raw = JSON.parse((0, import_node_fs12.readFileSync)(this.tokensPath, "utf8"));
|
|
3615
4555
|
parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
3616
4556
|
} catch {
|
|
3617
4557
|
parsed = null;
|
|
@@ -3622,6 +4562,95 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
3622
4562
|
}
|
|
3623
4563
|
};
|
|
3624
4564
|
|
|
4565
|
+
// src/TokenRefreshScheduler.ts
|
|
4566
|
+
var REFRESH_LEAD_MS = 5 * 6e4;
|
|
4567
|
+
var SWEEP_INTERVAL_MS = 6e4;
|
|
4568
|
+
var OAUTH_PROVIDERS = ["claude", "codex", "gemini"];
|
|
4569
|
+
var TokenRefreshScheduler = class {
|
|
4570
|
+
constructor(store, logger, intervalMs = SWEEP_INTERVAL_MS, leadMs = REFRESH_LEAD_MS) {
|
|
4571
|
+
this.store = store;
|
|
4572
|
+
this.logger = logger;
|
|
4573
|
+
this.intervalMs = intervalMs;
|
|
4574
|
+
this.leadMs = leadMs;
|
|
4575
|
+
}
|
|
4576
|
+
store;
|
|
4577
|
+
logger;
|
|
4578
|
+
intervalMs;
|
|
4579
|
+
leadMs;
|
|
4580
|
+
timer = null;
|
|
4581
|
+
sweeping = false;
|
|
4582
|
+
/** Arm the sweep interval. Idempotent. The timer never holds the loop open. */
|
|
4583
|
+
start() {
|
|
4584
|
+
if (this.timer) return;
|
|
4585
|
+
this.timer = setInterval(() => void this.sweep(), this.intervalMs);
|
|
4586
|
+
this.timer.unref?.();
|
|
4587
|
+
}
|
|
4588
|
+
/** Clear the interval (daemon shutdown / test teardown). Idempotent. */
|
|
4589
|
+
dispose() {
|
|
4590
|
+
if (this.timer) {
|
|
4591
|
+
clearInterval(this.timer);
|
|
4592
|
+
this.timer = null;
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
/** One sweep over every account of every OAuth provider. Exposed for tests. */
|
|
4596
|
+
async sweep(now = Date.now()) {
|
|
4597
|
+
if (this.sweeping) return;
|
|
4598
|
+
this.sweeping = true;
|
|
4599
|
+
try {
|
|
4600
|
+
const config = await this.store.getFullConfig();
|
|
4601
|
+
for (const provider of OAUTH_PROVIDERS) {
|
|
4602
|
+
const activeId = getActiveAccount(config, provider)?.id;
|
|
4603
|
+
for (const account of listAccounts(config, provider)) {
|
|
4604
|
+
if (!this.needsRefresh(account.tokens, now)) continue;
|
|
4605
|
+
await this.refreshOne(provider, account.id, account.id === activeId);
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
} catch (error) {
|
|
4609
|
+
this.logger.warn("token-refresh sweep failed", {
|
|
4610
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4611
|
+
});
|
|
4612
|
+
} finally {
|
|
4613
|
+
this.sweeping = false;
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
/** Expiring within the lead window, refreshable, and not already dead. */
|
|
4617
|
+
needsRefresh(tokens, now) {
|
|
4618
|
+
const t = tokens;
|
|
4619
|
+
if (!t.refreshToken || t.status === "expired" || t.status === "error") return false;
|
|
4620
|
+
if (!t.expiresAt) return false;
|
|
4621
|
+
const expiresAt = Date.parse(t.expiresAt);
|
|
4622
|
+
return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
|
|
4623
|
+
}
|
|
4624
|
+
/** Refresh one account; failures are logged, never thrown (the store has
|
|
4625
|
+
* already flagged the account `expired`). */
|
|
4626
|
+
async refreshOne(provider, id, isActive) {
|
|
4627
|
+
try {
|
|
4628
|
+
const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
|
|
4629
|
+
if (!ok) {
|
|
4630
|
+
this.logger.warn("background token refresh failed", { provider, accountId: id });
|
|
4631
|
+
} else {
|
|
4632
|
+
this.logger.info("background token refresh succeeded", { provider, accountId: id });
|
|
4633
|
+
}
|
|
4634
|
+
} catch (error) {
|
|
4635
|
+
this.logger.warn("background token refresh threw", {
|
|
4636
|
+
provider,
|
|
4637
|
+
accountId: id,
|
|
4638
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
refreshActive(provider) {
|
|
4643
|
+
switch (provider) {
|
|
4644
|
+
case "claude":
|
|
4645
|
+
return this.store.refreshClaudeToken();
|
|
4646
|
+
case "codex":
|
|
4647
|
+
return this.store.refreshCodexToken();
|
|
4648
|
+
case "gemini":
|
|
4649
|
+
return this.store.refreshGeminiToken();
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
};
|
|
4653
|
+
|
|
3625
4654
|
// src/bootstrap.ts
|
|
3626
4655
|
function buildDaemon(config, paths) {
|
|
3627
4656
|
const logger = new ConsoleLogger();
|
|
@@ -3654,7 +4683,14 @@ function buildDaemon(config, paths) {
|
|
|
3654
4683
|
autoDisableStore.markAutoDisabled(keyId, status, at);
|
|
3655
4684
|
}
|
|
3656
4685
|
);
|
|
3657
|
-
const
|
|
4686
|
+
const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
|
|
4687
|
+
const pricingEngine = new import_usage.PricingEngine(pricingStore, logger);
|
|
4688
|
+
const usageEventStore = new JsonlUsageEventStore(
|
|
4689
|
+
defaultUsageEventsPath(paths.configPath),
|
|
4690
|
+
async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
|
|
4691
|
+
);
|
|
4692
|
+
const usageRecorder = new import_usage.UsageRecorder(usageEventStore, pricingEngine, logger);
|
|
4693
|
+
const providerProxy = (0, import_provider_proxy.getProviderProxy)({ llmConfig, apiKeyPool, usageRecorder });
|
|
3658
4694
|
llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
|
|
3659
4695
|
const outboundApiServer = (0, import_outbound_api4.getOutboundApiServer)({
|
|
3660
4696
|
db: keyDb,
|
|
@@ -3700,10 +4736,23 @@ function buildDaemon(config, paths) {
|
|
|
3700
4736
|
// the multi-account append (`appendProviderAccount`, import re-encrypts at-
|
|
3701
4737
|
// rest). Confined to the export/import handlers; never reached by a GET.
|
|
3702
4738
|
migrationCredentialStore: credentialStore,
|
|
4739
|
+
// Code CLI launch (dashboard parity): the external-terminal opener + PATH probe
|
|
4740
|
+
// default to the real implementations; tests inject spies so no window spawns.
|
|
4741
|
+
cliTerminalOpener: paths.cliTerminalOpener,
|
|
4742
|
+
cliPathProbe: paths.cliPathProbe,
|
|
4743
|
+
cliCommandRunner: paths.cliCommandRunner,
|
|
4744
|
+
// Usage/pricing admin surface (usage-pricing child): stats queries go
|
|
4745
|
+
// through the recorder facade, pricing mutations through the engine, and
|
|
4746
|
+
// the row DELETE through the concrete store (delete is store-local — the
|
|
4747
|
+
// core port stays frozen). None of these can reach key material.
|
|
4748
|
+
usageRecorder,
|
|
4749
|
+
pricingEngine,
|
|
4750
|
+
pricingStore,
|
|
3703
4751
|
// Use the DECRYPTED config so `admin.token` (if stored as `enc:`) is the
|
|
3704
4752
|
// plaintext bearer the AdminServer's constant-time compare expects (D4).
|
|
3705
4753
|
getAdminConfig: () => resolveAdminConfig(decryptedConfig.admin)
|
|
3706
4754
|
});
|
|
4755
|
+
const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
|
|
3707
4756
|
return {
|
|
3708
4757
|
logger,
|
|
3709
4758
|
llmConfig,
|
|
@@ -3716,7 +4765,11 @@ function buildDaemon(config, paths) {
|
|
|
3716
4765
|
credentialStore,
|
|
3717
4766
|
subscriptionRegistry,
|
|
3718
4767
|
subscriptionAccounts,
|
|
3719
|
-
|
|
4768
|
+
pricingStore,
|
|
4769
|
+
pricingEngine,
|
|
4770
|
+
usageRecorder,
|
|
4771
|
+
adminServer,
|
|
4772
|
+
tokenRefreshScheduler
|
|
3720
4773
|
};
|
|
3721
4774
|
}
|
|
3722
4775
|
|
|
@@ -3758,10 +4811,10 @@ function buildCliSpawnPlan(opts) {
|
|
|
3758
4811
|
};
|
|
3759
4812
|
}
|
|
3760
4813
|
function resolveInPathDefault(candidate) {
|
|
3761
|
-
const segments = (process.env["PATH"] ?? "").split(
|
|
4814
|
+
const segments = (process.env["PATH"] ?? "").split(import_node_path8.delimiter).filter(Boolean);
|
|
3762
4815
|
for (const seg of segments) {
|
|
3763
|
-
const full = (0,
|
|
3764
|
-
if ((0,
|
|
4816
|
+
const full = (0, import_node_path8.join)(seg, candidate);
|
|
4817
|
+
if ((0, import_node_fs13.existsSync)(full)) return full;
|
|
3765
4818
|
}
|
|
3766
4819
|
return null;
|
|
3767
4820
|
}
|
|
@@ -3801,9 +4854,10 @@ async function runLaunch(argv, deps) {
|
|
|
3801
4854
|
try {
|
|
3802
4855
|
await daemon.llmConfig.ready();
|
|
3803
4856
|
await daemon.providerProxy.start();
|
|
3804
|
-
} catch (
|
|
4857
|
+
} catch (err5) {
|
|
3805
4858
|
daemon.apiKeyPool.dispose();
|
|
3806
|
-
|
|
4859
|
+
daemon.tokenRefreshScheduler.dispose();
|
|
4860
|
+
throw err5;
|
|
3807
4861
|
}
|
|
3808
4862
|
let launch;
|
|
3809
4863
|
try {
|
|
@@ -3811,10 +4865,11 @@ async function runLaunch(argv, deps) {
|
|
|
3811
4865
|
providerId: values.provider,
|
|
3812
4866
|
model: values.model
|
|
3813
4867
|
});
|
|
3814
|
-
} catch (
|
|
4868
|
+
} catch (err5) {
|
|
3815
4869
|
await daemon.providerProxy.stop();
|
|
3816
4870
|
daemon.apiKeyPool.dispose();
|
|
3817
|
-
|
|
4871
|
+
daemon.tokenRefreshScheduler.dispose();
|
|
4872
|
+
throw err5;
|
|
3818
4873
|
}
|
|
3819
4874
|
try {
|
|
3820
4875
|
const plan = buildCliSpawnPlan({
|
|
@@ -3835,6 +4890,7 @@ async function runLaunch(argv, deps) {
|
|
|
3835
4890
|
launch.onSessionEnd();
|
|
3836
4891
|
await daemon.providerProxy.stop();
|
|
3837
4892
|
daemon.apiKeyPool.dispose();
|
|
4893
|
+
daemon.tokenRefreshScheduler.dispose();
|
|
3838
4894
|
}
|
|
3839
4895
|
}
|
|
3840
4896
|
async function buildLaunchConfig(cli, llmConfig, opts) {
|
|
@@ -3848,15 +4904,15 @@ async function buildLaunchConfig(cli, llmConfig, opts) {
|
|
|
3848
4904
|
};
|
|
3849
4905
|
switch (cli) {
|
|
3850
4906
|
case "claude":
|
|
3851
|
-
return (0,
|
|
4907
|
+
return (0, import_cli_launcher2.buildClaudeCliLaunchConfig)(common);
|
|
3852
4908
|
case "codex":
|
|
3853
|
-
return (0,
|
|
4909
|
+
return (0, import_cli_launcher2.buildCodexLaunchConfig)(common);
|
|
3854
4910
|
case "gemini":
|
|
3855
|
-
return (0,
|
|
4911
|
+
return (0, import_cli_launcher2.buildGeminiCliLaunchConfig)(common);
|
|
3856
4912
|
case "qwen":
|
|
3857
4913
|
case "copilot":
|
|
3858
4914
|
case "opencode":
|
|
3859
|
-
return (0,
|
|
4915
|
+
return (0, import_cli_launcher2.buildChatCliLaunchConfig)({ backendId: cli, ...common });
|
|
3860
4916
|
default: {
|
|
3861
4917
|
const _exhaustive = cli;
|
|
3862
4918
|
throw new Error(`Unsupported launch CLI: ${String(_exhaustive)}`);
|
|
@@ -3865,7 +4921,7 @@ async function buildLaunchConfig(cli, llmConfig, opts) {
|
|
|
3865
4921
|
}
|
|
3866
4922
|
function spawnCliInherit(plan) {
|
|
3867
4923
|
return new Promise((resolve, reject) => {
|
|
3868
|
-
const child = (0,
|
|
4924
|
+
const child = (0, import_node_child_process2.spawn)(plan.command, plan.args, {
|
|
3869
4925
|
stdio: "inherit",
|
|
3870
4926
|
env: plan.env,
|
|
3871
4927
|
cwd: plan.cwd,
|
|
@@ -3884,9 +4940,9 @@ function spawnCliInherit(plan) {
|
|
|
3884
4940
|
process.removeListener("SIGINT", onSignal);
|
|
3885
4941
|
process.removeListener("SIGTERM", onSignal);
|
|
3886
4942
|
};
|
|
3887
|
-
child.on("error", (
|
|
4943
|
+
child.on("error", (err5) => {
|
|
3888
4944
|
detach();
|
|
3889
|
-
if (
|
|
4945
|
+
if (err5.code === "ENOENT") {
|
|
3890
4946
|
reject(
|
|
3891
4947
|
new Error(
|
|
3892
4948
|
`launch: "${plan.command}" not found on PATH \u2014 install the CLI first.`
|
|
@@ -3894,7 +4950,7 @@ function spawnCliInherit(plan) {
|
|
|
3894
4950
|
);
|
|
3895
4951
|
return;
|
|
3896
4952
|
}
|
|
3897
|
-
reject(
|
|
4953
|
+
reject(err5);
|
|
3898
4954
|
});
|
|
3899
4955
|
child.on("exit", (code, signal) => {
|
|
3900
4956
|
detach();
|
|
@@ -3904,7 +4960,7 @@ function spawnCliInherit(plan) {
|
|
|
3904
4960
|
}
|
|
3905
4961
|
|
|
3906
4962
|
// src/commands/login.ts
|
|
3907
|
-
var
|
|
4963
|
+
var import_node_child_process3 = require("child_process");
|
|
3908
4964
|
var import_node_readline = require("readline");
|
|
3909
4965
|
var import_node_util4 = require("util");
|
|
3910
4966
|
var import_subscriptions5 = require("@omnicross/subscriptions");
|
|
@@ -4055,7 +5111,7 @@ function openBrowser(url) {
|
|
|
4055
5111
|
return new Promise((resolve) => {
|
|
4056
5112
|
try {
|
|
4057
5113
|
const { command, args } = buildOpenBrowserCommand(process.platform, url);
|
|
4058
|
-
const child = (0,
|
|
5114
|
+
const child = (0, import_node_child_process3.spawn)(command, args, { stdio: "ignore", detached: true });
|
|
4059
5115
|
child.on("error", () => resolve(false));
|
|
4060
5116
|
child.unref();
|
|
4061
5117
|
resolve(true);
|
|
@@ -4075,7 +5131,7 @@ function promptPaste(prompt) {
|
|
|
4075
5131
|
}
|
|
4076
5132
|
|
|
4077
5133
|
// src/commands/providers.ts
|
|
4078
|
-
var
|
|
5134
|
+
var import_node_crypto10 = require("crypto");
|
|
4079
5135
|
var import_node_util5 = require("util");
|
|
4080
5136
|
async function runProviders(argv) {
|
|
4081
5137
|
const { values, positionals } = (0, import_node_util5.parseArgs)({
|
|
@@ -4197,7 +5253,7 @@ function providersAddKey(configPath, providerId, opts) {
|
|
|
4197
5253
|
const cfg = loadConfig(configPath);
|
|
4198
5254
|
const row = cfg.providers.find((p) => p.id === providerId);
|
|
4199
5255
|
if (!row) throw new Error(`providers add-key: unknown provider '${providerId}'`);
|
|
4200
|
-
const entry = { id: (0,
|
|
5256
|
+
const entry = { id: (0, import_node_crypto10.randomUUID)(), apiKey: opts.key };
|
|
4201
5257
|
if (opts.label) entry.label = opts.label;
|
|
4202
5258
|
if (opts.weight !== void 0) {
|
|
4203
5259
|
const w = Number(opts.weight);
|
|
@@ -4225,7 +5281,7 @@ function providersRmKey(configPath, providerId, keyId) {
|
|
|
4225
5281
|
}
|
|
4226
5282
|
|
|
4227
5283
|
// src/commands/secrets.ts
|
|
4228
|
-
var
|
|
5284
|
+
var import_node_fs14 = require("fs");
|
|
4229
5285
|
var import_node_util6 = require("util");
|
|
4230
5286
|
async function runSecrets(argv) {
|
|
4231
5287
|
const { values, positionals } = (0, import_node_util6.parseArgs)({
|
|
@@ -4297,7 +5353,7 @@ function secretsStatus(args) {
|
|
|
4297
5353
|
reportField("admin.token", cfg.admin.token);
|
|
4298
5354
|
}
|
|
4299
5355
|
const tokensPath = defaultTokensPath(args.config);
|
|
4300
|
-
if ((0,
|
|
5356
|
+
if ((0, import_node_fs14.existsSync)(tokensPath)) {
|
|
4301
5357
|
console.info(`Secret status for ${tokensPath}:`);
|
|
4302
5358
|
reportTokenFields(tokensPath);
|
|
4303
5359
|
}
|
|
@@ -4337,7 +5393,7 @@ function secretsRotate(args) {
|
|
|
4337
5393
|
const tokensPath = defaultTokensPath(args.config);
|
|
4338
5394
|
try {
|
|
4339
5395
|
cfg = loadConfig(args.config);
|
|
4340
|
-
if ((0,
|
|
5396
|
+
if ((0, import_node_fs14.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, oldBox);
|
|
4341
5397
|
} finally {
|
|
4342
5398
|
setSecretBox(null);
|
|
4343
5399
|
}
|
|
@@ -4366,28 +5422,28 @@ function secretsDecrypt(args) {
|
|
|
4366
5422
|
let tokensPlain = null;
|
|
4367
5423
|
try {
|
|
4368
5424
|
cfg = loadConfig(args.config);
|
|
4369
|
-
if ((0,
|
|
5425
|
+
if ((0, import_node_fs14.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
|
|
4370
5426
|
} finally {
|
|
4371
5427
|
setSecretBox(null);
|
|
4372
5428
|
}
|
|
4373
5429
|
saveConfig(args.config, cfg);
|
|
4374
5430
|
if (tokensPlain) {
|
|
4375
|
-
(0,
|
|
5431
|
+
(0, import_node_fs14.writeFileSync)(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
|
|
4376
5432
|
}
|
|
4377
5433
|
console.info(`Decrypted secrets to plaintext in ${args.config}` + tokensSuffix(args.config));
|
|
4378
5434
|
}
|
|
4379
|
-
function readRawConfig(
|
|
5435
|
+
function readRawConfig(path2) {
|
|
4380
5436
|
let parsed;
|
|
4381
5437
|
try {
|
|
4382
|
-
parsed = JSON.parse((0,
|
|
5438
|
+
parsed = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
|
|
4383
5439
|
} catch {
|
|
4384
|
-
throw new Error(`secrets: cannot read or parse '${
|
|
5440
|
+
throw new Error(`secrets: cannot read or parse '${path2}'`);
|
|
4385
5441
|
}
|
|
4386
5442
|
return validateConfig(parsed);
|
|
4387
5443
|
}
|
|
4388
|
-
function readRawJson(
|
|
5444
|
+
function readRawJson(path2) {
|
|
4389
5445
|
try {
|
|
4390
|
-
const parsed = JSON.parse((0,
|
|
5446
|
+
const parsed = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
|
|
4391
5447
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
4392
5448
|
return parsed;
|
|
4393
5449
|
}
|
|
@@ -4397,7 +5453,7 @@ function readRawJson(path) {
|
|
|
4397
5453
|
}
|
|
4398
5454
|
function encryptTokensFileInPlace(configPath, box) {
|
|
4399
5455
|
const tokensPath = defaultTokensPath(configPath);
|
|
4400
|
-
if (!(0,
|
|
5456
|
+
if (!(0, import_node_fs14.existsSync)(tokensPath)) return;
|
|
4401
5457
|
const plain = decryptTokensFile(tokensPath, box);
|
|
4402
5458
|
writeTokensEncrypted(tokensPath, plain, box);
|
|
4403
5459
|
}
|
|
@@ -4410,7 +5466,7 @@ function writeTokensEncrypted(tokensPath, plain, box) {
|
|
|
4410
5466
|
{ updatedAt: "", ...plain },
|
|
4411
5467
|
box
|
|
4412
5468
|
);
|
|
4413
|
-
(0,
|
|
5469
|
+
(0, import_node_fs14.writeFileSync)(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
|
|
4414
5470
|
}
|
|
4415
5471
|
var TOKEN_FIELDS2 = {
|
|
4416
5472
|
claude: ["accessToken", "refreshToken"],
|
|
@@ -4433,7 +5489,7 @@ function walkTokens(raw, fn) {
|
|
|
4433
5489
|
return next;
|
|
4434
5490
|
}
|
|
4435
5491
|
function tokensSuffix(configPath) {
|
|
4436
|
-
return (0,
|
|
5492
|
+
return (0, import_node_fs14.existsSync)(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
|
|
4437
5493
|
}
|
|
4438
5494
|
|
|
4439
5495
|
// src/commands/start.ts
|
|
@@ -4475,6 +5531,7 @@ async function runStart(argv) {
|
|
|
4475
5531
|
await daemon.adminServer.start();
|
|
4476
5532
|
dashboardUrl = daemon.adminServer.getStatus().url;
|
|
4477
5533
|
}
|
|
5534
|
+
daemon.tokenRefreshScheduler.start();
|
|
4478
5535
|
const status = daemon.outboundApiServer.getStatus();
|
|
4479
5536
|
console.info("omnicross daemon is running.");
|
|
4480
5537
|
if (dashboardUrl) console.info(` dashboard : ${dashboardUrl}`);
|
|
@@ -4493,6 +5550,32 @@ async function runStart(argv) {
|
|
|
4493
5550
|
console.info(" dashboard: localhost-default; --no-dashboard to disable; admin.token for a bearer gate");
|
|
4494
5551
|
}
|
|
4495
5552
|
console.info("Press Ctrl+C to stop.");
|
|
5553
|
+
return { dashboardUrl };
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
// src/commands/ui.ts
|
|
5557
|
+
async function runUi(argv, deps) {
|
|
5558
|
+
const start = deps?.start ?? runStart;
|
|
5559
|
+
const open = deps?.openBrowser ?? openBrowser;
|
|
5560
|
+
const noOpen = argv.includes("--no-open");
|
|
5561
|
+
const startArgv = argv.filter((a) => a !== "--no-open");
|
|
5562
|
+
if (!resolveUiDist()) {
|
|
5563
|
+
console.warn(
|
|
5564
|
+
"omnicross ui: Control Panel assets not found (@omnicross/ui has no built dist). The daemon will still start, but /ui will 404. Install @omnicross/ui (or build it: npm run build -w @omnicross/ui), or set OMNICROSS_UI_DIST."
|
|
5565
|
+
);
|
|
5566
|
+
}
|
|
5567
|
+
const { dashboardUrl } = await start(startArgv);
|
|
5568
|
+
if (!dashboardUrl) {
|
|
5569
|
+
console.warn("omnicross ui: the admin dashboard is disabled \u2014 no UI to open.");
|
|
5570
|
+
return;
|
|
5571
|
+
}
|
|
5572
|
+
const uiUrl = `${dashboardUrl}/ui/`;
|
|
5573
|
+
console.info(`Control Panel: ${uiUrl}`);
|
|
5574
|
+
if (noOpen) return;
|
|
5575
|
+
const launched = await open(uiUrl).catch(() => false);
|
|
5576
|
+
if (!launched) {
|
|
5577
|
+
console.info("(Could not open a browser automatically \u2014 open the URL above manually.)");
|
|
5578
|
+
}
|
|
4496
5579
|
}
|
|
4497
5580
|
|
|
4498
5581
|
// src/cli.ts
|
|
@@ -4500,6 +5583,8 @@ var USAGE = `omnicross \u2014 standalone @omnicross/core daemon
|
|
|
4500
5583
|
|
|
4501
5584
|
Usage:
|
|
4502
5585
|
omnicross start --config <path> Boot the daemon (BYO-key serving).
|
|
5586
|
+
omnicross ui --config <path> [--no-open] Boot the daemon + open the Control Panel
|
|
5587
|
+
(the web UI at <dashboard>/ui/) in a browser.
|
|
4503
5588
|
omnicross keys add <name> --config <p> Mint a named API key (shown once).
|
|
4504
5589
|
omnicross keys list --config <p> List stored keys (no secrets).
|
|
4505
5590
|
omnicross keys revoke <id> --config <p> Revoke a key.
|
|
@@ -4525,6 +5610,9 @@ async function main() {
|
|
|
4525
5610
|
case "start":
|
|
4526
5611
|
await runStart(rest);
|
|
4527
5612
|
return;
|
|
5613
|
+
case "ui":
|
|
5614
|
+
await runUi(rest);
|
|
5615
|
+
return;
|
|
4528
5616
|
case "keys":
|
|
4529
5617
|
await runKeys(rest);
|
|
4530
5618
|
return;
|
|
@@ -4556,7 +5644,7 @@ async function main() {
|
|
|
4556
5644
|
process.exitCode = 1;
|
|
4557
5645
|
}
|
|
4558
5646
|
}
|
|
4559
|
-
main().catch((
|
|
4560
|
-
console.error(
|
|
5647
|
+
main().catch((err5) => {
|
|
5648
|
+
console.error(err5 instanceof Error ? err5.message : String(err5));
|
|
4561
5649
|
process.exitCode = 1;
|
|
4562
5650
|
});
|