@omnicross/daemon 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2425 -660
- package/dist/cli.js +2416 -618
- package/dist/index.cjs +2286 -622
- package/dist/index.d.cts +452 -189
- package/dist/index.d.ts +452 -189
- package/dist/index.js +2273 -576
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -53,16 +53,18 @@ __export(src_exports, {
|
|
|
53
53
|
module.exports = __toCommonJS(src_exports);
|
|
54
54
|
|
|
55
55
|
// src/bootstrap.ts
|
|
56
|
-
var
|
|
56
|
+
var import_node_fs22 = require("fs");
|
|
57
57
|
var import_audit_types = require("@omnicross/contracts/audit-types");
|
|
58
58
|
var import_billing_types = require("@omnicross/contracts/billing-types");
|
|
59
59
|
var import_GeminiCodeAssistProjectResolver = require("@omnicross/core/auth/GeminiCodeAssistProjectResolver");
|
|
60
60
|
var import_ApiKeyPoolService = require("@omnicross/core/completion/ApiKeyPoolService");
|
|
61
61
|
var import_outbound_api4 = require("@omnicross/core/outbound-api");
|
|
62
62
|
var import_subscriptionRegistryPort = require("@omnicross/core/outbound-api/subscriptionRegistryPort");
|
|
63
|
-
var
|
|
64
|
-
var
|
|
65
|
-
var
|
|
63
|
+
var import_SubscriptionAccountHealth3 = require("@omnicross/core/pipeline/SubscriptionAccountHealth");
|
|
64
|
+
var import_AccountAllowanceStore4 = require("@omnicross/core/pipeline/AccountAllowanceStore");
|
|
65
|
+
var import_AccountAllowanceScheduling4 = require("@omnicross/core/pipeline/AccountAllowanceScheduling");
|
|
66
|
+
var import_upstreamFetch8 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
67
|
+
var import_SubscriptionIdentityStore3 = require("@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore");
|
|
66
68
|
var import_gemini_code_assist_resolver = require("@omnicross/core/ports/gemini-code-assist-resolver");
|
|
67
69
|
var import_provider_proxy = require("@omnicross/core/provider-proxy");
|
|
68
70
|
var import_outbound_api5 = require("@omnicross/core/outbound-api");
|
|
@@ -174,8 +176,448 @@ function handleCodexOAuthStatus(sessionId, deps) {
|
|
|
174
176
|
return { status: 200, body: { state: s.status, ...s.error ? { message: s.error } : {} } };
|
|
175
177
|
}
|
|
176
178
|
|
|
179
|
+
// src/allowance/AccountAllowanceService.ts
|
|
180
|
+
var import_AccountAllowanceStore2 = require("@omnicross/core/pipeline/AccountAllowanceStore");
|
|
181
|
+
var import_AccountAllowanceScheduling = require("@omnicross/core/pipeline/AccountAllowanceScheduling");
|
|
182
|
+
|
|
183
|
+
// src/allowance/ClaudeAllowanceCollector.ts
|
|
184
|
+
var import_AccountAllowanceStore = require("@omnicross/core/pipeline/AccountAllowanceStore");
|
|
185
|
+
var import_upstreamFetch = require("@omnicross/core/pipeline/upstreamFetch");
|
|
186
|
+
var import_fingerprintHeaders = require("@omnicross/core/provider-proxy/identity/fingerprintHeaders");
|
|
187
|
+
var import_SubscriptionIdentityStore = require("@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore");
|
|
188
|
+
var CLAUDE_ALLOWANCE_CACHE_MS = 5 * 6e4;
|
|
189
|
+
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
190
|
+
function finitePercent(value) {
|
|
191
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
192
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
193
|
+
return Number.isFinite(number) && number >= 0 && number <= 100 ? number : null;
|
|
194
|
+
}
|
|
195
|
+
function isoInstant(value) {
|
|
196
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
197
|
+
const time = Date.parse(value);
|
|
198
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : void 0;
|
|
199
|
+
}
|
|
200
|
+
function secondsUntil(instant, now) {
|
|
201
|
+
if (!instant) return void 0;
|
|
202
|
+
return Math.max(0, Math.floor((Date.parse(instant) - now) / 1e3));
|
|
203
|
+
}
|
|
204
|
+
function windowFromPayload(id, payload, now) {
|
|
205
|
+
const usedPercent = finitePercent(payload?.utilization);
|
|
206
|
+
const resetsAt = isoInstant(payload?.resets_at);
|
|
207
|
+
const isSonnet = id === "seven-day-sonnet";
|
|
208
|
+
const isFiveHour = id === "five-hour";
|
|
209
|
+
return {
|
|
210
|
+
id,
|
|
211
|
+
label: isFiveHour ? "5 hours" : isSonnet ? "7 days \xB7 Sonnet" : "7 days",
|
|
212
|
+
scope: isSonnet ? "model-family" : "all",
|
|
213
|
+
modelFamily: isSonnet ? "sonnet" : void 0,
|
|
214
|
+
usedPercent,
|
|
215
|
+
windowMinutes: isFiveHour ? 5 * 60 : 7 * 24 * 60,
|
|
216
|
+
resetsAt,
|
|
217
|
+
remainingSeconds: secondsUntil(resetsAt, now),
|
|
218
|
+
state: usedPercent !== null || resetsAt ? "fresh" : "unavailable"
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function emptyClaudeWindows(state) {
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
id: "five-hour",
|
|
225
|
+
label: "5 hours",
|
|
226
|
+
scope: "all",
|
|
227
|
+
usedPercent: null,
|
|
228
|
+
windowMinutes: 5 * 60,
|
|
229
|
+
state
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "seven-day",
|
|
233
|
+
label: "7 days",
|
|
234
|
+
scope: "all",
|
|
235
|
+
usedPercent: null,
|
|
236
|
+
windowMinutes: 7 * 24 * 60,
|
|
237
|
+
state
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "seven-day-sonnet",
|
|
241
|
+
label: "7 days \xB7 Sonnet",
|
|
242
|
+
scope: "model-family",
|
|
243
|
+
modelFamily: "sonnet",
|
|
244
|
+
usedPercent: null,
|
|
245
|
+
windowMinutes: 7 * 24 * 60,
|
|
246
|
+
state
|
|
247
|
+
}
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
function hasHeader(headers, name) {
|
|
251
|
+
const wanted = name.toLowerCase();
|
|
252
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === wanted);
|
|
253
|
+
}
|
|
254
|
+
var ClaudeAllowanceCollector = class {
|
|
255
|
+
constructor(credentials, store = (0, import_AccountAllowanceStore.getSharedAccountAllowanceStore)(), fetchImpl = (url, init, accountId) => (0, import_upstreamFetch.fetchUpstream)(url, init, { providerId: "claude", accountId }), identityStore = (0, import_SubscriptionIdentityStore.getSharedIdentityStore)(), now = Date.now) {
|
|
256
|
+
this.credentials = credentials;
|
|
257
|
+
this.store = store;
|
|
258
|
+
this.fetchImpl = fetchImpl;
|
|
259
|
+
this.identityStore = identityStore;
|
|
260
|
+
this.now = now;
|
|
261
|
+
}
|
|
262
|
+
credentials;
|
|
263
|
+
store;
|
|
264
|
+
fetchImpl;
|
|
265
|
+
identityStore;
|
|
266
|
+
now;
|
|
267
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
268
|
+
async collectMany(accounts, options = {}) {
|
|
269
|
+
const settled = await Promise.allSettled(accounts.map((account) => this.collect(account, options)));
|
|
270
|
+
return settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
271
|
+
}
|
|
272
|
+
collect(account, options = {}) {
|
|
273
|
+
const now = this.now();
|
|
274
|
+
const unsupported = account.tokens.isSetupToken || account.tokens.authMethod !== "oauth";
|
|
275
|
+
if (unsupported) {
|
|
276
|
+
const existing = this.store.get("claude", account.id, now);
|
|
277
|
+
if (existing?.windows.every((window) => window.state === "unsupported")) return Promise.resolve(existing);
|
|
278
|
+
const snapshot = this.unsupportedSnapshot(account.id, now);
|
|
279
|
+
this.store.set(snapshot);
|
|
280
|
+
return Promise.resolve(snapshot);
|
|
281
|
+
}
|
|
282
|
+
const cached = this.store.get("claude", account.id, now);
|
|
283
|
+
if (!options.force && cached && this.isCacheValid(cached, now, options.refreshAheadMs)) return Promise.resolve(cached);
|
|
284
|
+
const running = this.inFlight.get(account.id);
|
|
285
|
+
if (running) return running;
|
|
286
|
+
const promise = this.fetchAccount(account.id).catch(() => this.failureSnapshot(account.id, "claude_usage_request_failed", this.now())).finally(() => this.inFlight.delete(account.id));
|
|
287
|
+
this.inFlight.set(account.id, promise);
|
|
288
|
+
return promise;
|
|
289
|
+
}
|
|
290
|
+
isCacheValid(snapshot, now, refreshAheadMs) {
|
|
291
|
+
if (snapshot.source !== "oauth-usage-api") return false;
|
|
292
|
+
if (snapshot.windows.every((window) => window.state === "unsupported")) return true;
|
|
293
|
+
const expiresAt = snapshot.expiresAt ? Date.parse(snapshot.expiresAt) : 0;
|
|
294
|
+
const ahead = typeof refreshAheadMs === "number" && Number.isFinite(refreshAheadMs) ? Math.max(0, refreshAheadMs) : 0;
|
|
295
|
+
return Number.isFinite(expiresAt) && expiresAt > now + ahead;
|
|
296
|
+
}
|
|
297
|
+
async fetchAccount(accountId) {
|
|
298
|
+
let token = await this.credentials.getAccessTokenForAccount("claude", accountId);
|
|
299
|
+
if (!token) return this.failureSnapshot(accountId, "claude_usage_token_unavailable", this.now());
|
|
300
|
+
let response = await this.request(accountId, token);
|
|
301
|
+
if (response.status === 401) {
|
|
302
|
+
const refreshed = await this.credentials.refreshAccountToken("claude", accountId);
|
|
303
|
+
if (!refreshed) return this.failureSnapshot(accountId, "claude_usage_unauthorized", this.now());
|
|
304
|
+
token = await this.credentials.getAccessTokenForAccount("claude", accountId);
|
|
305
|
+
if (!token) return this.failureSnapshot(accountId, "claude_usage_token_unavailable", this.now());
|
|
306
|
+
response = await this.request(accountId, token);
|
|
307
|
+
}
|
|
308
|
+
if (response.status === 403) {
|
|
309
|
+
const snapshot2 = this.unsupportedSnapshot(accountId, this.now(), "claude_usage_unsupported");
|
|
310
|
+
this.store.set(snapshot2);
|
|
311
|
+
return snapshot2;
|
|
312
|
+
}
|
|
313
|
+
if (!response.ok) {
|
|
314
|
+
return this.failureSnapshot(accountId, "claude_usage_http_error", this.now());
|
|
315
|
+
}
|
|
316
|
+
let payload;
|
|
317
|
+
try {
|
|
318
|
+
payload = await response.json();
|
|
319
|
+
} catch {
|
|
320
|
+
return this.failureSnapshot(accountId, "claude_usage_invalid_response", this.now());
|
|
321
|
+
}
|
|
322
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
323
|
+
return this.failureSnapshot(accountId, "claude_usage_invalid_response", this.now());
|
|
324
|
+
}
|
|
325
|
+
const now = this.now();
|
|
326
|
+
const usage = payload;
|
|
327
|
+
const snapshot = {
|
|
328
|
+
providerId: "claude",
|
|
329
|
+
accountId,
|
|
330
|
+
source: "oauth-usage-api",
|
|
331
|
+
observedAt: new Date(now).toISOString(),
|
|
332
|
+
expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
|
|
333
|
+
windows: [
|
|
334
|
+
windowFromPayload("five-hour", usage.five_hour, now),
|
|
335
|
+
windowFromPayload("seven-day", usage.seven_day, now),
|
|
336
|
+
windowFromPayload("seven-day-sonnet", usage.seven_day_sonnet, now)
|
|
337
|
+
]
|
|
338
|
+
};
|
|
339
|
+
this.store.set(snapshot);
|
|
340
|
+
return snapshot;
|
|
341
|
+
}
|
|
342
|
+
request(accountId, token) {
|
|
343
|
+
const headers = {
|
|
344
|
+
Authorization: `Bearer ${token}`,
|
|
345
|
+
Accept: "application/json",
|
|
346
|
+
"Content-Type": "application/json",
|
|
347
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
348
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
349
|
+
};
|
|
350
|
+
(0, import_fingerprintHeaders.applyFingerprint)(this.identityStore, headers, "claude", accountId, void 0);
|
|
351
|
+
if (!hasHeader(headers, "user-agent")) {
|
|
352
|
+
headers["User-Agent"] = "claude-cli/2.0.53 (external, cli)";
|
|
353
|
+
}
|
|
354
|
+
return this.fetchImpl(CLAUDE_USAGE_URL, {
|
|
355
|
+
method: "GET",
|
|
356
|
+
headers,
|
|
357
|
+
signal: AbortSignal.timeout(15e3)
|
|
358
|
+
}, accountId);
|
|
359
|
+
}
|
|
360
|
+
failureSnapshot(accountId, code, now) {
|
|
361
|
+
const existing = this.store.get("claude", accountId, now);
|
|
362
|
+
const snapshot = existing ? {
|
|
363
|
+
...existing,
|
|
364
|
+
expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
|
|
365
|
+
windows: existing.windows.map((window) => ({
|
|
366
|
+
...window,
|
|
367
|
+
state: window.state === "unsupported" ? "unsupported" : window.usedPercent !== null || window.resetsAt ? "stale" : "unavailable"
|
|
368
|
+
})),
|
|
369
|
+
lastErrorCode: code
|
|
370
|
+
} : {
|
|
371
|
+
providerId: "claude",
|
|
372
|
+
accountId,
|
|
373
|
+
source: "oauth-usage-api",
|
|
374
|
+
observedAt: new Date(now).toISOString(),
|
|
375
|
+
expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
|
|
376
|
+
windows: emptyClaudeWindows("unavailable"),
|
|
377
|
+
lastErrorCode: code
|
|
378
|
+
};
|
|
379
|
+
this.store.set(snapshot);
|
|
380
|
+
return snapshot;
|
|
381
|
+
}
|
|
382
|
+
unsupportedSnapshot(accountId, now, code = "claude_usage_unsupported_auth") {
|
|
383
|
+
return {
|
|
384
|
+
providerId: "claude",
|
|
385
|
+
accountId,
|
|
386
|
+
source: "oauth-usage-api",
|
|
387
|
+
observedAt: new Date(now).toISOString(),
|
|
388
|
+
windows: emptyClaudeWindows("unsupported"),
|
|
389
|
+
lastErrorCode: code
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/allowance/AccountAllowanceService.ts
|
|
395
|
+
function codexUnavailable(accountId, now) {
|
|
396
|
+
return {
|
|
397
|
+
providerId: "codex",
|
|
398
|
+
accountId,
|
|
399
|
+
source: "response-headers",
|
|
400
|
+
observedAt: new Date(now).toISOString(),
|
|
401
|
+
windows: [
|
|
402
|
+
{ id: "primary", label: "Primary", scope: "all", usedPercent: null, state: "unavailable" },
|
|
403
|
+
{ id: "secondary", label: "Secondary", scope: "all", usedPercent: null, state: "unavailable" }
|
|
404
|
+
],
|
|
405
|
+
lastErrorCode: "codex_allowance_not_observed"
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
var AccountAllowanceService = class {
|
|
409
|
+
constructor(credentials, store = (0, import_AccountAllowanceStore2.getSharedAccountAllowanceStore)(), collector, now = Date.now) {
|
|
410
|
+
this.credentials = credentials;
|
|
411
|
+
this.store = store;
|
|
412
|
+
this.now = now;
|
|
413
|
+
this.claudeCollector = collector ?? new ClaudeAllowanceCollector(credentials, store);
|
|
414
|
+
}
|
|
415
|
+
credentials;
|
|
416
|
+
store;
|
|
417
|
+
now;
|
|
418
|
+
claudeCollector;
|
|
419
|
+
/**
|
|
420
|
+
* Read all/filtered snapshots. Claude's five-minute cache is refreshed lazily;
|
|
421
|
+
* Codex remains passive and reports not-observed until a real model response.
|
|
422
|
+
*/
|
|
423
|
+
async list(filter = {}) {
|
|
424
|
+
const config = await this.credentials.getFullConfig();
|
|
425
|
+
this.store.pruneToKnownAccounts([
|
|
426
|
+
...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
|
|
427
|
+
...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
|
|
428
|
+
]);
|
|
429
|
+
const wantsClaude = !filter.providerId || filter.providerId === "claude";
|
|
430
|
+
const claudeAccounts = (config.claudeAccounts ?? []).filter(
|
|
431
|
+
(account) => !filter.accountId || account.id === filter.accountId
|
|
432
|
+
);
|
|
433
|
+
if (wantsClaude) await this.claudeCollector.collectMany(claudeAccounts);
|
|
434
|
+
const wantsCodex = !filter.providerId || filter.providerId === "codex";
|
|
435
|
+
const codexAccounts = (config.codexAccounts ?? []).filter(
|
|
436
|
+
(account) => !filter.accountId || account.id === filter.accountId
|
|
437
|
+
);
|
|
438
|
+
if (wantsCodex) {
|
|
439
|
+
for (const account of codexAccounts) {
|
|
440
|
+
if (!this.store.get("codex", account.id)) this.store.set(codexUnavailable(account.id, this.now()));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const known = /* @__PURE__ */ new Set();
|
|
444
|
+
if (wantsClaude) for (const account of claudeAccounts) known.add(`claude\0${account.id}`);
|
|
445
|
+
if (wantsCodex) for (const account of codexAccounts) known.add(`codex\0${account.id}`);
|
|
446
|
+
return this.store.list(filter).filter((snapshot) => known.has(`${snapshot.providerId}\0${snapshot.accountId}`));
|
|
447
|
+
}
|
|
448
|
+
/** Force-refresh Claude usage for one account or every stored Claude account. */
|
|
449
|
+
async refreshClaude(accountId) {
|
|
450
|
+
const config = await this.credentials.getFullConfig();
|
|
451
|
+
this.store.pruneToKnownAccounts([
|
|
452
|
+
...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
|
|
453
|
+
...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
|
|
454
|
+
]);
|
|
455
|
+
const accounts = (config.claudeAccounts ?? []).filter(
|
|
456
|
+
(account) => !accountId || account.id === accountId
|
|
457
|
+
);
|
|
458
|
+
return this.claudeCollector.collectMany(accounts, { force: true });
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Keep Claude snapshots warm for allowance-aware routing. This deliberately
|
|
462
|
+
* excludes Codex (whose quota is learned from real response headers) and
|
|
463
|
+
* preserves the collector's cache + per-account in-flight coalescing.
|
|
464
|
+
*/
|
|
465
|
+
async maintainClaudeCache(refreshAheadMs) {
|
|
466
|
+
const config = await this.credentials.getFullConfig();
|
|
467
|
+
this.store.pruneToKnownAccounts([
|
|
468
|
+
...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
|
|
469
|
+
...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
|
|
470
|
+
]);
|
|
471
|
+
await this.claudeCollector.collectMany(config.claudeAccounts ?? [], { refreshAheadMs });
|
|
472
|
+
}
|
|
473
|
+
/** Remove a cache row as soon as an account is deleted by the admin path. */
|
|
474
|
+
removeAccountSnapshot(providerId, accountId) {
|
|
475
|
+
this.store.delete(providerId, accountId);
|
|
476
|
+
}
|
|
477
|
+
/** Remove all allowance rows for a provider block that was deleted. */
|
|
478
|
+
removeProviderSnapshots(providerId) {
|
|
479
|
+
for (const snapshot of this.store.list({ providerId })) {
|
|
480
|
+
this.store.delete(snapshot.providerId, snapshot.accountId);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/** Secret-free policy diagnostics for the settings/accounts UI. */
|
|
484
|
+
getSchedulingStatus() {
|
|
485
|
+
const scheduling = (0, import_AccountAllowanceScheduling.getSharedAccountAllowanceScheduling)();
|
|
486
|
+
return { config: scheduling.getConfig(), history: scheduling.getHistory() };
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// src/allowance/ClaudeAllowanceRefreshScheduler.ts
|
|
491
|
+
var CLAUDE_ALLOWANCE_CHECK_INTERVAL_MS = 6e4;
|
|
492
|
+
var CLAUDE_ALLOWANCE_REFRESH_AHEAD_MS = 9e4;
|
|
493
|
+
var ClaudeAllowanceRefreshScheduler = class {
|
|
494
|
+
constructor(service, logger, intervalMs = CLAUDE_ALLOWANCE_CHECK_INTERVAL_MS, refreshAheadMs = CLAUDE_ALLOWANCE_REFRESH_AHEAD_MS) {
|
|
495
|
+
this.service = service;
|
|
496
|
+
this.logger = logger;
|
|
497
|
+
this.intervalMs = intervalMs;
|
|
498
|
+
this.refreshAheadMs = refreshAheadMs;
|
|
499
|
+
}
|
|
500
|
+
service;
|
|
501
|
+
logger;
|
|
502
|
+
intervalMs;
|
|
503
|
+
refreshAheadMs;
|
|
504
|
+
timer = null;
|
|
505
|
+
started = false;
|
|
506
|
+
enabled = false;
|
|
507
|
+
sweeping = false;
|
|
508
|
+
/**
|
|
509
|
+
* Apply live server policy. Once started, enable/disable changes arm or disarm
|
|
510
|
+
* immediately; the initial enabled sweep is fire-and-forget.
|
|
511
|
+
*/
|
|
512
|
+
configure(config) {
|
|
513
|
+
const nextEnabled = config?.enabled === true;
|
|
514
|
+
if (this.enabled === nextEnabled) return;
|
|
515
|
+
this.enabled = nextEnabled;
|
|
516
|
+
if (!this.started) return;
|
|
517
|
+
if (nextEnabled) {
|
|
518
|
+
this.arm();
|
|
519
|
+
void this.sweep();
|
|
520
|
+
} else {
|
|
521
|
+
this.disarm();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** Start the lifecycle. Disabled policy remains completely inert. */
|
|
525
|
+
start() {
|
|
526
|
+
if (this.started) return;
|
|
527
|
+
this.started = true;
|
|
528
|
+
if (!this.enabled) return;
|
|
529
|
+
this.arm();
|
|
530
|
+
void this.sweep();
|
|
531
|
+
}
|
|
532
|
+
/** Stop all future checks. Idempotent and safe during an in-flight refresh. */
|
|
533
|
+
dispose() {
|
|
534
|
+
this.started = false;
|
|
535
|
+
this.disarm();
|
|
536
|
+
}
|
|
537
|
+
/** One non-overlapping cache-maintenance pass. Exposed for focused tests. */
|
|
538
|
+
async sweep() {
|
|
539
|
+
if (!this.enabled || this.sweeping) return;
|
|
540
|
+
this.sweeping = true;
|
|
541
|
+
try {
|
|
542
|
+
await this.service.maintainClaudeCache(this.refreshAheadMs);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
this.logger.warn("Claude allowance background refresh failed", {
|
|
545
|
+
error: error instanceof Error ? error.message : String(error)
|
|
546
|
+
});
|
|
547
|
+
} finally {
|
|
548
|
+
this.sweeping = false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
arm() {
|
|
552
|
+
if (this.timer) return;
|
|
553
|
+
this.timer = setInterval(() => void this.sweep(), this.intervalMs);
|
|
554
|
+
this.timer.unref?.();
|
|
555
|
+
}
|
|
556
|
+
disarm() {
|
|
557
|
+
if (this.timer) clearInterval(this.timer);
|
|
558
|
+
this.timer = null;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// src/allowance/JsonAccountAllowancePersistence.ts
|
|
563
|
+
var import_node_crypto2 = require("crypto");
|
|
564
|
+
var import_node_fs = require("fs");
|
|
565
|
+
var import_node_path = require("path");
|
|
566
|
+
var import_AccountAllowanceStore3 = require("@omnicross/core/pipeline/AccountAllowanceStore");
|
|
567
|
+
var ACCOUNT_ALLOWANCE_CACHE_VERSION = 1;
|
|
568
|
+
var MAX_PERSISTED_ALLOWANCE_SNAPSHOTS = 256;
|
|
569
|
+
var MAX_ALLOWANCE_CACHE_BYTES = 1e6;
|
|
570
|
+
var JsonAccountAllowancePersistence = class {
|
|
571
|
+
constructor(cachePath) {
|
|
572
|
+
this.cachePath = cachePath;
|
|
573
|
+
}
|
|
574
|
+
cachePath;
|
|
575
|
+
/** Read only the `snapshots` payload; all row validation remains defensive. */
|
|
576
|
+
load() {
|
|
577
|
+
if (!(0, import_node_fs.existsSync)(this.cachePath)) return [];
|
|
578
|
+
try {
|
|
579
|
+
if ((0, import_node_fs.statSync)(this.cachePath).size > MAX_ALLOWANCE_CACHE_BYTES) return [];
|
|
580
|
+
const raw = (0, import_node_fs.readFileSync)(this.cachePath, "utf8");
|
|
581
|
+
if (!raw.trim()) return [];
|
|
582
|
+
const parsed = JSON.parse(raw);
|
|
583
|
+
if (Array.isArray(parsed)) return parsed;
|
|
584
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
|
|
585
|
+
const file = parsed;
|
|
586
|
+
return file.version === ACCOUNT_ALLOWANCE_CACHE_VERSION && Array.isArray(file.snapshots) ? file.snapshots : [];
|
|
587
|
+
} catch {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/** Replace the file atomically; the target remains intact if replacement fails. */
|
|
592
|
+
save(snapshots) {
|
|
593
|
+
const rows = [];
|
|
594
|
+
for (const snapshot of snapshots) {
|
|
595
|
+
const normalized = (0, import_AccountAllowanceStore3.normalizeAccountAllowanceSnapshot)(snapshot);
|
|
596
|
+
if (!normalized) continue;
|
|
597
|
+
rows.push(normalized);
|
|
598
|
+
if (rows.length >= MAX_PERSISTED_ALLOWANCE_SNAPSHOTS) break;
|
|
599
|
+
}
|
|
600
|
+
const file = {
|
|
601
|
+
version: ACCOUNT_ALLOWANCE_CACHE_VERSION,
|
|
602
|
+
snapshots: rows
|
|
603
|
+
};
|
|
604
|
+
const serialized = JSON.stringify(file, null, 2) + "\n";
|
|
605
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_ALLOWANCE_CACHE_BYTES) {
|
|
606
|
+
throw new Error("account allowance cache exceeds its size limit");
|
|
607
|
+
}
|
|
608
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(this.cachePath), { recursive: true });
|
|
609
|
+
const temporaryPath = `${this.cachePath}.${process.pid}.${(0, import_node_crypto2.randomUUID)()}.tmp`;
|
|
610
|
+
try {
|
|
611
|
+
(0, import_node_fs.writeFileSync)(temporaryPath, serialized, { encoding: "utf8", flag: "wx" });
|
|
612
|
+
(0, import_node_fs.renameSync)(temporaryPath, this.cachePath);
|
|
613
|
+
} finally {
|
|
614
|
+
(0, import_node_fs.rmSync)(temporaryPath, { force: true });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
177
619
|
// src/admin/AdminServer.ts
|
|
178
|
-
var
|
|
620
|
+
var import_node_crypto9 = require("crypto");
|
|
179
621
|
var import_node_http2 = __toESM(require("http"), 1);
|
|
180
622
|
var import_health_logging_types = require("@omnicross/contracts/health-logging-types");
|
|
181
623
|
|
|
@@ -194,16 +636,16 @@ function intParam(value) {
|
|
|
194
636
|
}
|
|
195
637
|
function handleAuditQuery(req, res, reader) {
|
|
196
638
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
197
|
-
const
|
|
639
|
+
const query2 = {};
|
|
198
640
|
const keyId = url.searchParams.get("keyId");
|
|
199
|
-
if (keyId && keyId.trim())
|
|
641
|
+
if (keyId && keyId.trim()) query2.keyId = keyId.trim();
|
|
200
642
|
const from = intParam(url.searchParams.get("from"));
|
|
201
|
-
if (from !== void 0)
|
|
643
|
+
if (from !== void 0) query2.from = from;
|
|
202
644
|
const to = intParam(url.searchParams.get("to"));
|
|
203
|
-
if (to !== void 0)
|
|
645
|
+
if (to !== void 0) query2.to = to;
|
|
204
646
|
const limit = intParam(url.searchParams.get("limit"));
|
|
205
|
-
if (limit !== void 0)
|
|
206
|
-
const records = reader ? reader(
|
|
647
|
+
if (limit !== void 0) query2.limit = limit;
|
|
648
|
+
const records = reader ? reader(query2) : [];
|
|
207
649
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
208
650
|
res.end(JSON.stringify({ records }));
|
|
209
651
|
}
|
|
@@ -275,19 +717,19 @@ function resetWebhookRuntimeForTests() {
|
|
|
275
717
|
|
|
276
718
|
// src/admin/webhookTestApi.ts
|
|
277
719
|
function readJsonBody(req) {
|
|
278
|
-
return new Promise((
|
|
720
|
+
return new Promise((resolve2) => {
|
|
279
721
|
const chunks = [];
|
|
280
722
|
req.on("data", (c) => chunks.push(c));
|
|
281
723
|
req.on("end", () => {
|
|
282
724
|
try {
|
|
283
725
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
284
726
|
const parsed = raw ? JSON.parse(raw) : {};
|
|
285
|
-
|
|
727
|
+
resolve2(parsed && typeof parsed === "object" ? parsed : {});
|
|
286
728
|
} catch {
|
|
287
|
-
|
|
729
|
+
resolve2({});
|
|
288
730
|
}
|
|
289
731
|
});
|
|
290
|
-
req.on("error", () =>
|
|
732
|
+
req.on("error", () => resolve2({}));
|
|
291
733
|
});
|
|
292
734
|
}
|
|
293
735
|
async function handleWebhookTest(req, res) {
|
|
@@ -306,13 +748,15 @@ async function handleWebhookTest(req, res) {
|
|
|
306
748
|
// src/admin/adminApi.ts
|
|
307
749
|
var import_node_http = __toESM(require("http"), 1);
|
|
308
750
|
var import_outbound_api2 = require("@omnicross/core/outbound-api");
|
|
309
|
-
var
|
|
751
|
+
var import_AccountAllowanceScheduling2 = require("@omnicross/core/pipeline/AccountAllowanceScheduling");
|
|
752
|
+
var import_SubscriptionAccountHealth = require("@omnicross/core/pipeline/SubscriptionAccountHealth");
|
|
753
|
+
var import_upstreamFetch3 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
310
754
|
|
|
311
755
|
// src/config.ts
|
|
312
|
-
var
|
|
756
|
+
var import_node_fs3 = require("fs");
|
|
313
757
|
|
|
314
758
|
// src/secrets/envelope.ts
|
|
315
|
-
var
|
|
759
|
+
var import_node_crypto3 = require("crypto");
|
|
316
760
|
var ENVELOPE_PREFIX = "enc:";
|
|
317
761
|
var ENVELOPE_VERSION = "v1";
|
|
318
762
|
var KEY_BYTES = 32;
|
|
@@ -342,8 +786,8 @@ function encryptValue(plain, key) {
|
|
|
342
786
|
if (key.length !== KEY_BYTES) {
|
|
343
787
|
throw new Error(`secret key must be ${KEY_BYTES} bytes`);
|
|
344
788
|
}
|
|
345
|
-
const iv = (0,
|
|
346
|
-
const cipher = (0,
|
|
789
|
+
const iv = (0, import_node_crypto3.randomBytes)(IV_BYTES);
|
|
790
|
+
const cipher = (0, import_node_crypto3.createCipheriv)("aes-256-gcm", key, iv);
|
|
347
791
|
const ciphertext = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
348
792
|
const tag = cipher.getAuthTag();
|
|
349
793
|
return [
|
|
@@ -360,21 +804,21 @@ function decryptValue(envelope, key) {
|
|
|
360
804
|
throw new Error(`secret key must be ${KEY_BYTES} bytes`);
|
|
361
805
|
}
|
|
362
806
|
const { iv, tag, ciphertext } = parseEnvelope(envelope);
|
|
363
|
-
const decipher = (0,
|
|
807
|
+
const decipher = (0, import_node_crypto3.createDecipheriv)("aes-256-gcm", key, iv);
|
|
364
808
|
decipher.setAuthTag(tag);
|
|
365
809
|
const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
366
810
|
return plain.toString("utf8");
|
|
367
811
|
}
|
|
368
812
|
|
|
369
813
|
// src/secrets/masterKey.ts
|
|
370
|
-
var
|
|
371
|
-
var
|
|
814
|
+
var import_node_crypto4 = require("crypto");
|
|
815
|
+
var import_node_fs2 = require("fs");
|
|
372
816
|
var import_node_os = require("os");
|
|
373
|
-
var
|
|
817
|
+
var import_node_path2 = require("path");
|
|
374
818
|
var MASTER_KEY_ENV = "OMNICROSS_MASTER_KEY";
|
|
375
819
|
var KEY_BYTES2 = 32;
|
|
376
820
|
function defaultMasterKeyPath() {
|
|
377
|
-
return (0,
|
|
821
|
+
return (0, import_node_path2.join)((0, import_node_os.homedir)(), ".omnicross", "master.key");
|
|
378
822
|
}
|
|
379
823
|
function decodeEnvKey(raw) {
|
|
380
824
|
const trimmed = raw.trim();
|
|
@@ -390,7 +834,7 @@ function decodeEnvKey(raw) {
|
|
|
390
834
|
return buf;
|
|
391
835
|
}
|
|
392
836
|
function readKeyFile(path2) {
|
|
393
|
-
const raw = (0,
|
|
837
|
+
const raw = (0, import_node_fs2.readFileSync)(path2);
|
|
394
838
|
if (raw.length === KEY_BYTES2) return raw;
|
|
395
839
|
const text = raw.toString("utf8").trim();
|
|
396
840
|
if (/^[0-9a-fA-F]{64}$/.test(text)) return Buffer.from(text, "hex");
|
|
@@ -401,11 +845,11 @@ function readKeyFile(path2) {
|
|
|
401
845
|
);
|
|
402
846
|
}
|
|
403
847
|
function generateKeyFile(path2) {
|
|
404
|
-
const key = (0,
|
|
405
|
-
(0,
|
|
406
|
-
(0,
|
|
848
|
+
const key = (0, import_node_crypto4.randomBytes)(KEY_BYTES2);
|
|
849
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path2), { recursive: true });
|
|
850
|
+
(0, import_node_fs2.writeFileSync)(path2, key, { mode: 384 });
|
|
407
851
|
try {
|
|
408
|
-
(0,
|
|
852
|
+
(0, import_node_fs2.chmodSync)(path2, 384);
|
|
409
853
|
} catch {
|
|
410
854
|
}
|
|
411
855
|
return key;
|
|
@@ -416,7 +860,7 @@ function resolveMasterKey(options = {}) {
|
|
|
416
860
|
return decodeEnvKey(envRaw);
|
|
417
861
|
}
|
|
418
862
|
const keyFilePath = options.keyFilePath ?? defaultMasterKeyPath();
|
|
419
|
-
if ((0,
|
|
863
|
+
if ((0, import_node_fs2.existsSync)(keyFilePath)) {
|
|
420
864
|
return readKeyFile(keyFilePath);
|
|
421
865
|
}
|
|
422
866
|
return generateKeyFile(keyFilePath);
|
|
@@ -695,7 +1139,19 @@ function validateLogging(raw) {
|
|
|
695
1139
|
if (typeof l["file"] === "string" && l["file"].length > 0) out.file = l["file"];
|
|
696
1140
|
return out.level !== void 0 || out.format !== void 0 || out.file !== void 0 ? out : void 0;
|
|
697
1141
|
}
|
|
698
|
-
var VALID_FORMATS = [
|
|
1142
|
+
var VALID_FORMATS = [
|
|
1143
|
+
"openai",
|
|
1144
|
+
"anthropic",
|
|
1145
|
+
"gemini",
|
|
1146
|
+
"openai-response"
|
|
1147
|
+
];
|
|
1148
|
+
var FORMAT_AXIS_TRANSFORMERS = [
|
|
1149
|
+
"openai",
|
|
1150
|
+
"anthropic",
|
|
1151
|
+
"gemini",
|
|
1152
|
+
"openai-response",
|
|
1153
|
+
"gemini-code-assist"
|
|
1154
|
+
];
|
|
699
1155
|
function validateApiKeys(raw) {
|
|
700
1156
|
if (!Array.isArray(raw)) return void 0;
|
|
701
1157
|
const out = [];
|
|
@@ -800,6 +1256,33 @@ function validateApiModes(raw) {
|
|
|
800
1256
|
}
|
|
801
1257
|
return out.length > 0 ? out : void 0;
|
|
802
1258
|
}
|
|
1259
|
+
function transformerEntryName(entry) {
|
|
1260
|
+
return typeof entry === "string" ? entry : entry[0];
|
|
1261
|
+
}
|
|
1262
|
+
function migrateFormatAxis(apiFormat, transformer) {
|
|
1263
|
+
const use = transformer?.use;
|
|
1264
|
+
if (!use || use.length === 0) return { apiFormat, transformer };
|
|
1265
|
+
const hasFormatEntry = use.some((e) => FORMAT_AXIS_TRANSFORMERS.includes(transformerEntryName(e)));
|
|
1266
|
+
if (!hasFormatEntry) return { apiFormat, transformer };
|
|
1267
|
+
let migratedFormat = apiFormat;
|
|
1268
|
+
if (apiFormat === "openai") {
|
|
1269
|
+
const promoted = use.map(transformerEntryName).find((n) => VALID_FORMATS.includes(n));
|
|
1270
|
+
if (promoted) migratedFormat = promoted;
|
|
1271
|
+
}
|
|
1272
|
+
const rest = use.filter((e) => !FORMAT_AXIS_TRANSFORMERS.includes(transformerEntryName(e)));
|
|
1273
|
+
const next = {};
|
|
1274
|
+
let kept = false;
|
|
1275
|
+
if (rest.length > 0) {
|
|
1276
|
+
next.use = rest;
|
|
1277
|
+
kept = true;
|
|
1278
|
+
}
|
|
1279
|
+
for (const key of Object.keys(transformer)) {
|
|
1280
|
+
if (key === "use") continue;
|
|
1281
|
+
next[key] = transformer[key];
|
|
1282
|
+
kept = true;
|
|
1283
|
+
}
|
|
1284
|
+
return { apiFormat: migratedFormat, transformer: kept ? next : void 0 };
|
|
1285
|
+
}
|
|
803
1286
|
function validateProvider(raw, index) {
|
|
804
1287
|
if (!raw || typeof raw !== "object") {
|
|
805
1288
|
throw new Error(`config: providers[${index}] is not an object`);
|
|
@@ -830,10 +1313,14 @@ function validateProvider(raw, index) {
|
|
|
830
1313
|
const apiVersion = typeof p["apiVersion"] === "string" && p["apiVersion"].length > 0 ? p["apiVersion"] : void 0;
|
|
831
1314
|
const maxConcurrency = typeof p["maxConcurrency"] === "number" && Number.isFinite(p["maxConcurrency"]) ? p["maxConcurrency"] : void 0;
|
|
832
1315
|
const modelsEndpoint = typeof p["modelsEndpoint"] === "string" && p["modelsEndpoint"].length > 0 ? p["modelsEndpoint"] : void 0;
|
|
1316
|
+
const { apiFormat: migratedFormat, transformer: migratedTransformer } = migrateFormatAxis(
|
|
1317
|
+
apiFormat,
|
|
1318
|
+
validateTransformer(p["transformer"])
|
|
1319
|
+
);
|
|
833
1320
|
return {
|
|
834
1321
|
id,
|
|
835
1322
|
name,
|
|
836
|
-
apiFormat,
|
|
1323
|
+
apiFormat: migratedFormat,
|
|
837
1324
|
baseUrl,
|
|
838
1325
|
apiKey,
|
|
839
1326
|
models: Array.isArray(models) ? models.filter((m) => typeof m === "string") : void 0,
|
|
@@ -847,7 +1334,9 @@ function validateProvider(raw, index) {
|
|
|
847
1334
|
modelsEndpoint,
|
|
848
1335
|
// Provider transformer config (app-parity child 5): load-guard, collapse-to-
|
|
849
1336
|
// undefined; non-secret; ENFORCED via resolveTransformerChain (parity-2 child 2).
|
|
850
|
-
|
|
1337
|
+
// Format-axis entries are stripped by `migrateFormatAxis` — `use[]` is the
|
|
1338
|
+
// MODIFIER axis only.
|
|
1339
|
+
transformer: migratedTransformer,
|
|
851
1340
|
// Coding-plan endpoint (app-parity-2 child 3): load-guard, collapse-to-undefined.
|
|
852
1341
|
// SECRET-bearing (apiKey encrypted at rest); enforced by core's resolveProviderEndpoint.
|
|
853
1342
|
codingPlan: validateCodingPlan(p["codingPlan"]),
|
|
@@ -879,7 +1368,7 @@ function setSecretBox(box) {
|
|
|
879
1368
|
function loadConfig(path2) {
|
|
880
1369
|
let raw;
|
|
881
1370
|
try {
|
|
882
|
-
raw = (0,
|
|
1371
|
+
raw = (0, import_node_fs3.readFileSync)(path2, "utf8");
|
|
883
1372
|
} catch {
|
|
884
1373
|
throw new Error(`config: cannot read file at '${path2}'`);
|
|
885
1374
|
}
|
|
@@ -894,7 +1383,7 @@ function loadConfig(path2) {
|
|
|
894
1383
|
}
|
|
895
1384
|
function saveConfig(path2, cfg) {
|
|
896
1385
|
const toWrite = secretBox ? encryptConfigSecrets(cfg, secretBox) : cfg;
|
|
897
|
-
(0,
|
|
1386
|
+
(0, import_node_fs3.writeFileSync)(path2, JSON.stringify(toWrite, null, 2) + "\n", "utf8");
|
|
898
1387
|
}
|
|
899
1388
|
|
|
900
1389
|
// src/pool/resolveEnvKey.ts
|
|
@@ -906,6 +1395,698 @@ function resolveEnvKey(rawKey) {
|
|
|
906
1395
|
return rawKey;
|
|
907
1396
|
}
|
|
908
1397
|
|
|
1398
|
+
// src/integrations/IntegrationManager.ts
|
|
1399
|
+
var import_node_crypto5 = require("crypto");
|
|
1400
|
+
var import_node_fs5 = require("fs");
|
|
1401
|
+
var import_node_os2 = require("os");
|
|
1402
|
+
var import_node_path4 = require("path");
|
|
1403
|
+
var import_core = require("@omnicross/core");
|
|
1404
|
+
|
|
1405
|
+
// src/integrations/IntegrationStateStore.ts
|
|
1406
|
+
var import_node_fs4 = require("fs");
|
|
1407
|
+
var import_node_path3 = require("path");
|
|
1408
|
+
var EMPTY_STATE = { version: 1, clients: {} };
|
|
1409
|
+
var IntegrationStateStore = class {
|
|
1410
|
+
constructor(path2, box) {
|
|
1411
|
+
this.path = path2;
|
|
1412
|
+
this.box = box;
|
|
1413
|
+
}
|
|
1414
|
+
path;
|
|
1415
|
+
box;
|
|
1416
|
+
load() {
|
|
1417
|
+
if (!(0, import_node_fs4.existsSync)(this.path)) return { ...EMPTY_STATE, clients: {} };
|
|
1418
|
+
let raw;
|
|
1419
|
+
try {
|
|
1420
|
+
raw = JSON.parse((0, import_node_fs4.readFileSync)(this.path, "utf8"));
|
|
1421
|
+
} catch {
|
|
1422
|
+
throw new Error(`integration state '${this.path}' is not valid JSON`);
|
|
1423
|
+
}
|
|
1424
|
+
if (!isState(raw)) {
|
|
1425
|
+
throw new Error(`integration state '${this.path}' has an unsupported shape`);
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
version: 1,
|
|
1429
|
+
gatewayKey: raw.gatewayKey ? { ...raw.gatewayKey, secret: this.box.decryptMaybe(raw.gatewayKey.secret) } : void 0,
|
|
1430
|
+
clients: decryptClients(raw.clients, this.box)
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
save(state) {
|
|
1434
|
+
const encrypted = {
|
|
1435
|
+
version: 1,
|
|
1436
|
+
gatewayKey: state.gatewayKey ? { ...state.gatewayKey, secret: this.box.encrypt(state.gatewayKey.secret) } : void 0,
|
|
1437
|
+
clients: encryptClients(state.clients, this.box)
|
|
1438
|
+
};
|
|
1439
|
+
atomicWrite(this.path, JSON.stringify(encrypted, null, 2) + "\n");
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
function transformClients(clients, transform) {
|
|
1443
|
+
const out = {};
|
|
1444
|
+
for (const client of ["codex", "claude"]) {
|
|
1445
|
+
const row = clients[client];
|
|
1446
|
+
if (row) {
|
|
1447
|
+
out[client] = {
|
|
1448
|
+
...row,
|
|
1449
|
+
originalContent: transform(row.originalContent),
|
|
1450
|
+
credentialFile: row.credentialFile ? { ...row.credentialFile, originalContent: transform(row.credentialFile.originalContent) } : void 0
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return out;
|
|
1455
|
+
}
|
|
1456
|
+
function decryptClients(clients, box) {
|
|
1457
|
+
return transformClients(clients, (value) => box.decryptMaybe(value));
|
|
1458
|
+
}
|
|
1459
|
+
function encryptClients(clients, box) {
|
|
1460
|
+
return transformClients(clients, (value) => box.encrypt(value));
|
|
1461
|
+
}
|
|
1462
|
+
function isState(value) {
|
|
1463
|
+
if (!value || typeof value !== "object") return false;
|
|
1464
|
+
const row = value;
|
|
1465
|
+
if (row.version !== 1 || !row.clients || typeof row.clients !== "object") return false;
|
|
1466
|
+
if (row.gatewayKey !== void 0) {
|
|
1467
|
+
const key = row.gatewayKey;
|
|
1468
|
+
if (!key || typeof key !== "object" || typeof key.id !== "string" || typeof key.secret !== "string" || typeof key.createdAt !== "number") return false;
|
|
1469
|
+
}
|
|
1470
|
+
for (const client of ["codex", "claude"]) {
|
|
1471
|
+
const candidate = row.clients[client];
|
|
1472
|
+
if (candidate === void 0) continue;
|
|
1473
|
+
if (!isInstallRecord(candidate, client)) return false;
|
|
1474
|
+
}
|
|
1475
|
+
return true;
|
|
1476
|
+
}
|
|
1477
|
+
function isInstallRecord(value, client) {
|
|
1478
|
+
if (!value || typeof value !== "object") return false;
|
|
1479
|
+
const row = value;
|
|
1480
|
+
return row.client === client && typeof row.configPath === "string" && typeof row.originalExisted === "boolean" && typeof row.originalContent === "string" && typeof row.originalHash === "string" && typeof row.installedHash === "string" && typeof row.installedAt === "number" && typeof row.gatewayBaseUrl === "string" && (row.credentialFile === void 0 || isManagedFileRecord(row.credentialFile));
|
|
1481
|
+
}
|
|
1482
|
+
function isManagedFileRecord(value) {
|
|
1483
|
+
if (!value || typeof value !== "object") return false;
|
|
1484
|
+
const row = value;
|
|
1485
|
+
return typeof row.path === "string" && typeof row.originalExisted === "boolean" && typeof row.originalContent === "string" && typeof row.originalHash === "string" && typeof row.installedHash === "string";
|
|
1486
|
+
}
|
|
1487
|
+
function atomicWrite(path2, content) {
|
|
1488
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(path2), { recursive: true });
|
|
1489
|
+
const temp = `${path2}.tmp-${process.pid}-${Date.now()}`;
|
|
1490
|
+
(0, import_node_fs4.writeFileSync)(temp, content, { encoding: "utf8", mode: 384 });
|
|
1491
|
+
try {
|
|
1492
|
+
(0, import_node_fs4.renameSync)(temp, path2);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
try {
|
|
1495
|
+
(0, import_node_fs4.unlinkSync)(temp);
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
throw error;
|
|
1499
|
+
} finally {
|
|
1500
|
+
if ((0, import_node_fs4.existsSync)(path2)) {
|
|
1501
|
+
try {
|
|
1502
|
+
(0, import_node_fs4.chmodSync)(path2, 384);
|
|
1503
|
+
} catch {
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/integrations/configAdapters.ts
|
|
1510
|
+
var CODEX_BEGIN = "# >>> omnicross managed provider >>>";
|
|
1511
|
+
var CODEX_END = "# <<< omnicross managed provider <<<";
|
|
1512
|
+
var CODEX_PROVIDER = "omnicross";
|
|
1513
|
+
var CLAUDE_API_KEY_SENTINEL = "omnicross-gateway";
|
|
1514
|
+
function renderCodexConfig(input) {
|
|
1515
|
+
if (input.existing.includes(CODEX_BEGIN) || input.existing.includes(CODEX_END)) {
|
|
1516
|
+
throw new Error("Codex config contains an unmanaged/orphaned Omnicross marker");
|
|
1517
|
+
}
|
|
1518
|
+
if (/^\s*\[\s*model_providers\s*\.\s*["']?omnicross["']?\s*]/m.test(input.existing)) {
|
|
1519
|
+
throw new Error("Codex config already defines model_providers.omnicross");
|
|
1520
|
+
}
|
|
1521
|
+
const eol = input.existing.includes("\r\n") ? "\r\n" : "\n";
|
|
1522
|
+
const lines = input.existing.replace(/\r\n/g, "\n").split("\n");
|
|
1523
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
|
|
1524
|
+
const rootEnd = firstTable < 0 ? lines.length : firstTable;
|
|
1525
|
+
const assignments = {
|
|
1526
|
+
model_provider: [],
|
|
1527
|
+
preferred_auth_method: []
|
|
1528
|
+
};
|
|
1529
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
1530
|
+
if (/^\s*#/.test(lines[index])) continue;
|
|
1531
|
+
for (const key of Object.keys(assignments)) {
|
|
1532
|
+
if (new RegExp(`^\\s*${key}\\s*=`).test(lines[index])) assignments[key].push(index);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (assignments.model_provider.length > 1) {
|
|
1536
|
+
throw new Error("Codex config has duplicate top-level model_provider keys");
|
|
1537
|
+
}
|
|
1538
|
+
if (assignments.preferred_auth_method.length > 1) {
|
|
1539
|
+
throw new Error("Codex config has duplicate top-level preferred_auth_method keys");
|
|
1540
|
+
}
|
|
1541
|
+
const managedRoot = {
|
|
1542
|
+
model_provider: `model_provider = "${CODEX_PROVIDER}" # managed by Omnicross`,
|
|
1543
|
+
preferred_auth_method: 'preferred_auth_method = "apikey" # managed by Omnicross'
|
|
1544
|
+
};
|
|
1545
|
+
const missing = [];
|
|
1546
|
+
for (const key of Object.keys(assignments)) {
|
|
1547
|
+
const [index] = assignments[key];
|
|
1548
|
+
if (index === void 0) missing.push(managedRoot[key]);
|
|
1549
|
+
else lines[index] = managedRoot[key];
|
|
1550
|
+
}
|
|
1551
|
+
if (missing.length > 0) lines.splice(rootEnd, 0, ...missing, "");
|
|
1552
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1553
|
+
const base = lines.length > 0 ? `${lines.join("\n")}
|
|
1554
|
+
|
|
1555
|
+
` : "";
|
|
1556
|
+
const root = trimTrailingSlash(input.gatewayBaseUrl);
|
|
1557
|
+
const block = [
|
|
1558
|
+
CODEX_BEGIN,
|
|
1559
|
+
`[model_providers.${CODEX_PROVIDER}]`,
|
|
1560
|
+
'name = "Omnicross Local Gateway"',
|
|
1561
|
+
`base_url = ${tomlString(`${root}/v1`)}`,
|
|
1562
|
+
'wire_api = "responses"',
|
|
1563
|
+
"requires_openai_auth = true",
|
|
1564
|
+
"supports_websockets = false",
|
|
1565
|
+
CODEX_END,
|
|
1566
|
+
""
|
|
1567
|
+
].join("\n");
|
|
1568
|
+
return (base + block).replace(/\n/g, eol);
|
|
1569
|
+
}
|
|
1570
|
+
function renderCodexAuth(secret) {
|
|
1571
|
+
return JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: secret }, null, 2) + "\n";
|
|
1572
|
+
}
|
|
1573
|
+
function renderClaudeSettings(existing, gatewayBaseUrl, secret) {
|
|
1574
|
+
let parsed = {};
|
|
1575
|
+
if (existing.trim()) {
|
|
1576
|
+
try {
|
|
1577
|
+
parsed = JSON.parse(existing);
|
|
1578
|
+
} catch {
|
|
1579
|
+
throw new Error("Claude settings file is not valid JSON");
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if (!isPlainObject(parsed)) throw new Error("Claude settings root must be a JSON object");
|
|
1583
|
+
const settings = { ...parsed };
|
|
1584
|
+
const oldEnv = settings.env;
|
|
1585
|
+
if (oldEnv !== void 0 && !isPlainObject(oldEnv)) {
|
|
1586
|
+
throw new Error("Claude settings env field must be a JSON object");
|
|
1587
|
+
}
|
|
1588
|
+
settings.env = {
|
|
1589
|
+
...oldEnv,
|
|
1590
|
+
ANTHROPIC_BASE_URL: trimTrailingSlash(gatewayBaseUrl),
|
|
1591
|
+
ANTHROPIC_AUTH_TOKEN: secret,
|
|
1592
|
+
ANTHROPIC_API_KEY: CLAUDE_API_KEY_SENTINEL
|
|
1593
|
+
};
|
|
1594
|
+
return JSON.stringify(settings, null, 2) + "\n";
|
|
1595
|
+
}
|
|
1596
|
+
function restoreCodexBase(current, original) {
|
|
1597
|
+
const hasBegin = current.includes(CODEX_BEGIN);
|
|
1598
|
+
const hasEnd = current.includes(CODEX_END);
|
|
1599
|
+
if (hasBegin !== hasEnd) throw new Error("Codex config has an incomplete Omnicross managed block");
|
|
1600
|
+
const eol = current.includes("\r\n") ? "\r\n" : "\n";
|
|
1601
|
+
let normalized = current.replace(/\r\n/g, "\n");
|
|
1602
|
+
if (hasBegin) {
|
|
1603
|
+
const start = normalized.indexOf(CODEX_BEGIN);
|
|
1604
|
+
const endMarker = normalized.indexOf(CODEX_END, start);
|
|
1605
|
+
if (endMarker < 0) throw new Error("Codex config has an incomplete Omnicross managed block");
|
|
1606
|
+
const end = normalized.indexOf("\n", endMarker);
|
|
1607
|
+
normalized = normalized.slice(0, start) + (end < 0 ? "" : normalized.slice(end + 1));
|
|
1608
|
+
}
|
|
1609
|
+
const lines = normalized.split("\n");
|
|
1610
|
+
for (const key of ["model_provider", "preferred_auth_method"]) {
|
|
1611
|
+
const originalAssignment = rootAssignment(original, key);
|
|
1612
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
|
|
1613
|
+
const rootEnd = firstTable < 0 ? lines.length : firstTable;
|
|
1614
|
+
const managedIndex = lines.slice(0, rootEnd).findIndex(
|
|
1615
|
+
(line) => new RegExp(`^\\s*${key}\\s*=.*#\\s*managed by Omnicross\\s*$`).test(line)
|
|
1616
|
+
);
|
|
1617
|
+
if (managedIndex >= 0) {
|
|
1618
|
+
if (originalAssignment) lines[managedIndex] = originalAssignment;
|
|
1619
|
+
else lines.splice(managedIndex, 1);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return lines.join("\n").replace(/\n/g, eol);
|
|
1623
|
+
}
|
|
1624
|
+
function restoreClaudeBase(current, original, gatewayBaseUrl, secret) {
|
|
1625
|
+
const currentRoot = parseSettings(current);
|
|
1626
|
+
const originalRoot = parseSettings(original);
|
|
1627
|
+
const env = isPlainObject(currentRoot.env) ? { ...currentRoot.env } : {};
|
|
1628
|
+
const originalEnv = isPlainObject(originalRoot.env) ? originalRoot.env : {};
|
|
1629
|
+
const expected = {
|
|
1630
|
+
ANTHROPIC_BASE_URL: trimTrailingSlash(gatewayBaseUrl),
|
|
1631
|
+
ANTHROPIC_AUTH_TOKEN: secret,
|
|
1632
|
+
ANTHROPIC_API_KEY: CLAUDE_API_KEY_SENTINEL
|
|
1633
|
+
};
|
|
1634
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1635
|
+
if (env[key] !== value) continue;
|
|
1636
|
+
if (Object.prototype.hasOwnProperty.call(originalEnv, key)) env[key] = originalEnv[key];
|
|
1637
|
+
else delete env[key];
|
|
1638
|
+
}
|
|
1639
|
+
const next = { ...currentRoot };
|
|
1640
|
+
if (Object.keys(env).length > 0 || Object.prototype.hasOwnProperty.call(originalRoot, "env")) next.env = env;
|
|
1641
|
+
else delete next.env;
|
|
1642
|
+
return JSON.stringify(next, null, 2) + "\n";
|
|
1643
|
+
}
|
|
1644
|
+
function tomlString(value) {
|
|
1645
|
+
return JSON.stringify(value);
|
|
1646
|
+
}
|
|
1647
|
+
function trimTrailingSlash(value) {
|
|
1648
|
+
return value.replace(/\/+$/, "");
|
|
1649
|
+
}
|
|
1650
|
+
function isPlainObject(value) {
|
|
1651
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1652
|
+
}
|
|
1653
|
+
function parseSettings(value) {
|
|
1654
|
+
if (!value.trim()) return {};
|
|
1655
|
+
let parsed;
|
|
1656
|
+
try {
|
|
1657
|
+
parsed = JSON.parse(value);
|
|
1658
|
+
} catch {
|
|
1659
|
+
throw new Error("Claude settings file is not valid JSON");
|
|
1660
|
+
}
|
|
1661
|
+
if (!isPlainObject(parsed)) throw new Error("Claude settings root must be a JSON object");
|
|
1662
|
+
return parsed;
|
|
1663
|
+
}
|
|
1664
|
+
function rootAssignment(content, key) {
|
|
1665
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
1666
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
|
|
1667
|
+
const root = lines.slice(0, firstTable < 0 ? lines.length : firstTable);
|
|
1668
|
+
return root.find((line) => new RegExp(`^\\s*${key}\\s*=`).test(line) && !/^\s*#/.test(line));
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// src/integrations/IntegrationManager.ts
|
|
1672
|
+
var IntegrationConflictError = class extends Error {
|
|
1673
|
+
constructor(message) {
|
|
1674
|
+
super(message);
|
|
1675
|
+
this.name = "IntegrationConflictError";
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
var IntegrationManager = class {
|
|
1679
|
+
constructor(options) {
|
|
1680
|
+
this.options = options;
|
|
1681
|
+
assertLoopbackGatewayUrl(options.gatewayBaseUrl);
|
|
1682
|
+
this.homeDir = options.homeDir ?? (0, import_node_os2.homedir)();
|
|
1683
|
+
}
|
|
1684
|
+
options;
|
|
1685
|
+
homeDir;
|
|
1686
|
+
async listStatus() {
|
|
1687
|
+
const state = this.options.stateStore.load();
|
|
1688
|
+
const keyUsable = await this.isKeyUsable(state);
|
|
1689
|
+
return ["codex", "claude"].map((client) => this.statusFor(client, state, keyUsable));
|
|
1690
|
+
}
|
|
1691
|
+
async plan(client, configPath = this.defaultConfigPath(client)) {
|
|
1692
|
+
const state = this.options.stateStore.load();
|
|
1693
|
+
const record = state.clients[client];
|
|
1694
|
+
const target = record?.configPath ?? (0, import_node_path4.resolve)(configPath);
|
|
1695
|
+
const status = this.statusFor(client, state, await this.isKeyUsable(state));
|
|
1696
|
+
const changes = client === "codex" ? [
|
|
1697
|
+
"model_provider",
|
|
1698
|
+
"preferred_auth_method",
|
|
1699
|
+
"model_providers.omnicross",
|
|
1700
|
+
"auth.json.auth_mode",
|
|
1701
|
+
"auth.json.OPENAI_API_KEY"
|
|
1702
|
+
] : ["env.ANTHROPIC_BASE_URL", "env.ANTHROPIC_AUTH_TOKEN", "env.ANTHROPIC_API_KEY"];
|
|
1703
|
+
if (!record) return { client, configPath: target, action: "install", canApply: true, changes, warnings: [] };
|
|
1704
|
+
if (status.status === "enabled") {
|
|
1705
|
+
return { client, configPath: target, action: "none", canApply: true, changes: [], warnings: [] };
|
|
1706
|
+
}
|
|
1707
|
+
return {
|
|
1708
|
+
client,
|
|
1709
|
+
configPath: target,
|
|
1710
|
+
action: "repair",
|
|
1711
|
+
canApply: true,
|
|
1712
|
+
changes,
|
|
1713
|
+
warnings: ["Configuration changed after installation; repair preserves unrelated current settings."]
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
async install(client, configPath = this.defaultConfigPath(client)) {
|
|
1717
|
+
const target = (0, import_node_path4.resolve)(configPath);
|
|
1718
|
+
const state = this.options.stateStore.load();
|
|
1719
|
+
const existingRecord = state.clients[client];
|
|
1720
|
+
if (existingRecord) {
|
|
1721
|
+
const status = this.statusFor(client, state, await this.isKeyUsable(state));
|
|
1722
|
+
if (status.status === "enabled") return status;
|
|
1723
|
+
throw new IntegrationConflictError(
|
|
1724
|
+
`${client} integration configuration has drifted; restore or remove it before reinstalling`
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
const key = await this.ensureGatewayKey(state);
|
|
1728
|
+
const original = readOptional(target);
|
|
1729
|
+
const originalContent = original ?? "";
|
|
1730
|
+
const installed = this.renderInstalled(client, originalContent, key.secret);
|
|
1731
|
+
const credentialPath = client === "codex" ? this.codexAuthPathForConfig(target) : void 0;
|
|
1732
|
+
const originalCredential = credentialPath ? readOptional(credentialPath) : null;
|
|
1733
|
+
const installedCredential = credentialPath ? renderCodexAuth(key.secret) : void 0;
|
|
1734
|
+
const record = {
|
|
1735
|
+
client,
|
|
1736
|
+
configPath: target,
|
|
1737
|
+
originalExisted: original !== null,
|
|
1738
|
+
originalContent,
|
|
1739
|
+
originalHash: sha256(originalContent),
|
|
1740
|
+
installedHash: sha256(installed),
|
|
1741
|
+
installedAt: Date.now(),
|
|
1742
|
+
gatewayBaseUrl: this.options.gatewayBaseUrl,
|
|
1743
|
+
credentialFile: credentialPath && installedCredential ? managedFileRecord(credentialPath, originalCredential, installedCredential) : void 0
|
|
1744
|
+
};
|
|
1745
|
+
const prior = state.clients[client];
|
|
1746
|
+
state.clients[client] = record;
|
|
1747
|
+
this.options.stateStore.save(state);
|
|
1748
|
+
try {
|
|
1749
|
+
applyFileChangesWithRollback([
|
|
1750
|
+
{ path: target, content: installed },
|
|
1751
|
+
...credentialPath && installedCredential ? [{ path: credentialPath, content: installedCredential }] : []
|
|
1752
|
+
]);
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
if (prior) state.clients[client] = prior;
|
|
1755
|
+
else delete state.clients[client];
|
|
1756
|
+
this.options.stateStore.save(state);
|
|
1757
|
+
throw error;
|
|
1758
|
+
}
|
|
1759
|
+
return this.statusFor(client, state, true);
|
|
1760
|
+
}
|
|
1761
|
+
async repair(client) {
|
|
1762
|
+
const state = this.options.stateStore.load();
|
|
1763
|
+
const record = state.clients[client];
|
|
1764
|
+
if (!record) return this.install(client);
|
|
1765
|
+
const previouslyInstalledSecret = state.gatewayKey?.secret;
|
|
1766
|
+
const currentFile = readOptional(record.configPath);
|
|
1767
|
+
if (client === "claude" && currentFile !== null && !previouslyInstalledSecret) {
|
|
1768
|
+
throw new IntegrationConflictError(
|
|
1769
|
+
"Claude integration key state is missing; refusing to repair an ambiguous settings file"
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
const key = await this.ensureGatewayKey(state);
|
|
1773
|
+
const current = currentFile ?? record.originalContent;
|
|
1774
|
+
const base = client === "codex" ? restoreCodexBase(current, record.originalContent) : restoreClaudeBase(
|
|
1775
|
+
current,
|
|
1776
|
+
record.originalContent,
|
|
1777
|
+
record.gatewayBaseUrl,
|
|
1778
|
+
previouslyInstalledSecret ?? key.secret
|
|
1779
|
+
);
|
|
1780
|
+
const installed = this.renderInstalled(client, base, key.secret);
|
|
1781
|
+
const credentialPath = client === "codex" ? record.credentialFile?.path ?? this.codexAuthPathForConfig(record.configPath) : void 0;
|
|
1782
|
+
const currentCredential = credentialPath ? readOptional(credentialPath) : null;
|
|
1783
|
+
const originalCredential = record.credentialFile ? originalSnapshotForRepair(record.credentialFile, currentCredential) : currentCredential;
|
|
1784
|
+
const installedCredential = credentialPath ? renderCodexAuth(key.secret) : void 0;
|
|
1785
|
+
const prior = {
|
|
1786
|
+
...record,
|
|
1787
|
+
credentialFile: record.credentialFile ? { ...record.credentialFile } : void 0
|
|
1788
|
+
};
|
|
1789
|
+
Object.assign(record, {
|
|
1790
|
+
originalExisted: currentFile !== null || record.originalExisted,
|
|
1791
|
+
originalContent: base,
|
|
1792
|
+
originalHash: sha256(base),
|
|
1793
|
+
installedHash: sha256(installed),
|
|
1794
|
+
installedAt: Date.now(),
|
|
1795
|
+
gatewayBaseUrl: this.options.gatewayBaseUrl,
|
|
1796
|
+
credentialFile: credentialPath && installedCredential ? managedFileRecord(credentialPath, originalCredential, installedCredential) : void 0
|
|
1797
|
+
});
|
|
1798
|
+
this.options.stateStore.save(state);
|
|
1799
|
+
try {
|
|
1800
|
+
applyFileChangesWithRollback([
|
|
1801
|
+
{ path: record.configPath, content: installed },
|
|
1802
|
+
...credentialPath && installedCredential ? [{ path: credentialPath, content: installedCredential }] : []
|
|
1803
|
+
]);
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
state.clients[client] = prior;
|
|
1806
|
+
this.options.stateStore.save(state);
|
|
1807
|
+
throw error;
|
|
1808
|
+
}
|
|
1809
|
+
return this.statusFor(client, state, true);
|
|
1810
|
+
}
|
|
1811
|
+
async remove(client) {
|
|
1812
|
+
const state = this.options.stateStore.load();
|
|
1813
|
+
const record = state.clients[client];
|
|
1814
|
+
if (!record) return this.statusFor(client, state, await this.isKeyUsable(state));
|
|
1815
|
+
const files = [primaryManagedFile(record), ...record.credentialFile ? [record.credentialFile] : []];
|
|
1816
|
+
const currentFiles = files.map((file) => ({ file, current: readOptional(file.path) }));
|
|
1817
|
+
const dispositions = currentFiles.map(({ file, current }) => managedFileDisposition(file, current));
|
|
1818
|
+
if (dispositions.some((disposition) => disposition !== "installed" && disposition !== "restored")) {
|
|
1819
|
+
throw new IntegrationConflictError(
|
|
1820
|
+
`${client} configuration changed after Omnicross installed it; refusing to overwrite user edits`
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
const changes = currentFiles.flatMap(({ file }, index) => dispositions[index] === "installed" ? [{ path: file.path, content: file.originalExisted ? file.originalContent : null }] : []);
|
|
1824
|
+
applyFileChangesWithRollback(changes);
|
|
1825
|
+
delete state.clients[client];
|
|
1826
|
+
this.options.stateStore.save(state);
|
|
1827
|
+
return this.statusFor(client, state, await this.isKeyUsable(state));
|
|
1828
|
+
}
|
|
1829
|
+
async rotateGatewayKey() {
|
|
1830
|
+
const state = this.options.stateStore.load();
|
|
1831
|
+
const previousGatewayKey = state.gatewayKey;
|
|
1832
|
+
const oldKeyId = state.gatewayKey?.id;
|
|
1833
|
+
const claude = state.clients.claude;
|
|
1834
|
+
const codex = state.clients.codex;
|
|
1835
|
+
let nextClaude;
|
|
1836
|
+
let nextCodexAuth;
|
|
1837
|
+
if (claude) {
|
|
1838
|
+
const current = readOptional(claude.configPath);
|
|
1839
|
+
if (current === null || sha256(current) !== claude.installedHash) {
|
|
1840
|
+
throw new IntegrationConflictError("Claude configuration drift must be resolved before key rotation");
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
if (codex) {
|
|
1844
|
+
const current = readOptional(codex.configPath);
|
|
1845
|
+
if (current === null || sha256(current) !== codex.installedHash) {
|
|
1846
|
+
throw new IntegrationConflictError("Codex configuration drift must be resolved before key rotation");
|
|
1847
|
+
}
|
|
1848
|
+
if (!codex.credentialFile) {
|
|
1849
|
+
throw new IntegrationConflictError("Codex integration must be repaired before key rotation");
|
|
1850
|
+
}
|
|
1851
|
+
const currentAuth = readOptional(codex.credentialFile.path);
|
|
1852
|
+
if (currentAuth === null || sha256(currentAuth) !== codex.credentialFile.installedHash) {
|
|
1853
|
+
throw new IntegrationConflictError("Codex credential drift must be resolved before key rotation");
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const created = await (0, import_core.createIntegrationKey)(this.options.keyDb, "Omnicross native CLI integration");
|
|
1857
|
+
const nextGatewayKey = {
|
|
1858
|
+
id: created.id,
|
|
1859
|
+
secret: created.plaintextOnce,
|
|
1860
|
+
createdAt: created.createdAt
|
|
1861
|
+
};
|
|
1862
|
+
state.gatewayKey = nextGatewayKey;
|
|
1863
|
+
const previousClaudeHash = claude?.installedHash;
|
|
1864
|
+
const previousCodexAuthHash = codex?.credentialFile?.installedHash;
|
|
1865
|
+
if (claude) {
|
|
1866
|
+
const current = readOptional(claude.configPath) ?? "{}";
|
|
1867
|
+
nextClaude = renderClaudeSettings(current, this.options.gatewayBaseUrl, created.plaintextOnce);
|
|
1868
|
+
claude.installedHash = sha256(nextClaude);
|
|
1869
|
+
}
|
|
1870
|
+
if (codex?.credentialFile) {
|
|
1871
|
+
nextCodexAuth = renderCodexAuth(created.plaintextOnce);
|
|
1872
|
+
codex.credentialFile.installedHash = sha256(nextCodexAuth);
|
|
1873
|
+
}
|
|
1874
|
+
try {
|
|
1875
|
+
this.options.stateStore.save(state);
|
|
1876
|
+
applyFileChangesWithRollback([
|
|
1877
|
+
...codex?.credentialFile && nextCodexAuth !== void 0 ? [{ path: codex.credentialFile.path, content: nextCodexAuth }] : [],
|
|
1878
|
+
...claude && nextClaude !== void 0 ? [{ path: claude.configPath, content: nextClaude }] : []
|
|
1879
|
+
]);
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
state.gatewayKey = previousGatewayKey;
|
|
1882
|
+
if (claude && previousClaudeHash !== void 0) claude.installedHash = previousClaudeHash;
|
|
1883
|
+
if (codex?.credentialFile && previousCodexAuthHash !== void 0) {
|
|
1884
|
+
codex.credentialFile.installedHash = previousCodexAuthHash;
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
this.options.stateStore.save(state);
|
|
1888
|
+
} finally {
|
|
1889
|
+
await this.options.keyDb.outboundApiKeysRevoke(created.id);
|
|
1890
|
+
}
|
|
1891
|
+
throw error;
|
|
1892
|
+
}
|
|
1893
|
+
if (oldKeyId && oldKeyId !== created.id) await this.options.keyDb.outboundApiKeysRevoke(oldKeyId);
|
|
1894
|
+
return { keyId: created.id };
|
|
1895
|
+
}
|
|
1896
|
+
async getGatewayToken() {
|
|
1897
|
+
const state = this.options.stateStore.load();
|
|
1898
|
+
if (!state.gatewayKey || !await this.isKeyUsable(state)) {
|
|
1899
|
+
throw new Error("Omnicross integration key is missing or revoked; reinstall the CLI integration");
|
|
1900
|
+
}
|
|
1901
|
+
return state.gatewayKey.secret;
|
|
1902
|
+
}
|
|
1903
|
+
async ensureGatewayKey(state) {
|
|
1904
|
+
if (state.gatewayKey && await this.isKeyUsable(state)) return state.gatewayKey;
|
|
1905
|
+
const created = await (0, import_core.createIntegrationKey)(this.options.keyDb, "Omnicross native CLI integration");
|
|
1906
|
+
const previousGatewayKey = state.gatewayKey;
|
|
1907
|
+
const nextGatewayKey = {
|
|
1908
|
+
id: created.id,
|
|
1909
|
+
secret: created.plaintextOnce,
|
|
1910
|
+
createdAt: created.createdAt
|
|
1911
|
+
};
|
|
1912
|
+
state.gatewayKey = nextGatewayKey;
|
|
1913
|
+
try {
|
|
1914
|
+
this.options.stateStore.save(state);
|
|
1915
|
+
return nextGatewayKey;
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
state.gatewayKey = previousGatewayKey;
|
|
1918
|
+
try {
|
|
1919
|
+
await this.options.keyDb.outboundApiKeysRevoke(created.id);
|
|
1920
|
+
} catch {
|
|
1921
|
+
}
|
|
1922
|
+
throw error;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
async isKeyUsable(state) {
|
|
1926
|
+
if (!state.gatewayKey) return false;
|
|
1927
|
+
const rows = await this.options.keyDb.outboundApiKeysList();
|
|
1928
|
+
return rows.some((row) => row.id === state.gatewayKey?.id && row.enabled && row.revokedAt === null && row.kind === "integration");
|
|
1929
|
+
}
|
|
1930
|
+
statusFor(client, state, keyUsable) {
|
|
1931
|
+
const record = state.clients[client];
|
|
1932
|
+
if (!record) return { client, status: "not-installed", configPath: this.defaultConfigPath(client) };
|
|
1933
|
+
const current = readOptional(record.configPath);
|
|
1934
|
+
if (current === null) {
|
|
1935
|
+
return {
|
|
1936
|
+
client,
|
|
1937
|
+
status: "configuration-missing",
|
|
1938
|
+
configPath: record.configPath,
|
|
1939
|
+
installedAt: record.installedAt,
|
|
1940
|
+
gatewayBaseUrl: record.gatewayBaseUrl
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
if (sha256(current) !== record.installedHash) {
|
|
1944
|
+
return {
|
|
1945
|
+
client,
|
|
1946
|
+
status: "configuration-drift",
|
|
1947
|
+
configPath: record.configPath,
|
|
1948
|
+
installedAt: record.installedAt,
|
|
1949
|
+
gatewayBaseUrl: record.gatewayBaseUrl
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
if (client === "codex") {
|
|
1953
|
+
if (!record.credentialFile) {
|
|
1954
|
+
return {
|
|
1955
|
+
client,
|
|
1956
|
+
status: "configuration-drift",
|
|
1957
|
+
configPath: record.configPath,
|
|
1958
|
+
installedAt: record.installedAt,
|
|
1959
|
+
gatewayBaseUrl: record.gatewayBaseUrl,
|
|
1960
|
+
message: "Codex integration uses a legacy authentication layout and must be repaired."
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
const credential = readOptional(record.credentialFile.path);
|
|
1964
|
+
if (credential === null) {
|
|
1965
|
+
return {
|
|
1966
|
+
client,
|
|
1967
|
+
status: "configuration-missing",
|
|
1968
|
+
configPath: record.configPath,
|
|
1969
|
+
installedAt: record.installedAt,
|
|
1970
|
+
gatewayBaseUrl: record.gatewayBaseUrl,
|
|
1971
|
+
message: "Codex auth.json is missing."
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
if (sha256(credential) !== record.credentialFile.installedHash) {
|
|
1975
|
+
return {
|
|
1976
|
+
client,
|
|
1977
|
+
status: "configuration-drift",
|
|
1978
|
+
configPath: record.configPath,
|
|
1979
|
+
installedAt: record.installedAt,
|
|
1980
|
+
gatewayBaseUrl: record.gatewayBaseUrl,
|
|
1981
|
+
message: "Codex auth.json changed after installation."
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return {
|
|
1986
|
+
client,
|
|
1987
|
+
status: keyUsable ? "enabled" : "key-missing",
|
|
1988
|
+
configPath: record.configPath,
|
|
1989
|
+
installedAt: record.installedAt,
|
|
1990
|
+
gatewayBaseUrl: record.gatewayBaseUrl
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
defaultConfigPath(client) {
|
|
1994
|
+
return client === "codex" ? (0, import_node_path4.join)(this.homeDir, ".codex", "config.toml") : (0, import_node_path4.join)(this.homeDir, ".claude", "settings.json");
|
|
1995
|
+
}
|
|
1996
|
+
codexAuthPathForConfig(configPath) {
|
|
1997
|
+
return (0, import_node_path4.join)((0, import_node_path4.dirname)(configPath), "auth.json");
|
|
1998
|
+
}
|
|
1999
|
+
renderInstalled(client, base, secret) {
|
|
2000
|
+
if (client === "claude") {
|
|
2001
|
+
return renderClaudeSettings(base, this.options.gatewayBaseUrl, secret);
|
|
2002
|
+
}
|
|
2003
|
+
return renderCodexConfig({
|
|
2004
|
+
existing: base,
|
|
2005
|
+
gatewayBaseUrl: this.options.gatewayBaseUrl
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
function readOptional(path2) {
|
|
2010
|
+
return (0, import_node_fs5.existsSync)(path2) ? (0, import_node_fs5.readFileSync)(path2, "utf8") : null;
|
|
2011
|
+
}
|
|
2012
|
+
function sha256(value) {
|
|
2013
|
+
return (0, import_node_crypto5.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
2014
|
+
}
|
|
2015
|
+
function managedFileRecord(path2, original, installed) {
|
|
2016
|
+
const originalContent = original ?? "";
|
|
2017
|
+
return {
|
|
2018
|
+
path: path2,
|
|
2019
|
+
originalExisted: original !== null,
|
|
2020
|
+
originalContent,
|
|
2021
|
+
originalHash: sha256(originalContent),
|
|
2022
|
+
installedHash: sha256(installed)
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
function primaryManagedFile(record) {
|
|
2026
|
+
return {
|
|
2027
|
+
path: record.configPath,
|
|
2028
|
+
originalExisted: record.originalExisted,
|
|
2029
|
+
originalContent: record.originalContent,
|
|
2030
|
+
originalHash: record.originalHash,
|
|
2031
|
+
installedHash: record.installedHash
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
function managedFileDisposition(record, current) {
|
|
2035
|
+
if (current !== null && sha256(current) === record.installedHash) return "installed";
|
|
2036
|
+
const matchesOriginalExistence = record.originalExisted ? current !== null : current === null;
|
|
2037
|
+
if (matchesOriginalExistence && sha256(current ?? "") === record.originalHash) return "restored";
|
|
2038
|
+
return current === null ? "missing" : "drift";
|
|
2039
|
+
}
|
|
2040
|
+
function originalSnapshotForRepair(record, current) {
|
|
2041
|
+
const disposition = managedFileDisposition(record, current);
|
|
2042
|
+
if (disposition === "installed" || disposition === "restored") {
|
|
2043
|
+
return record.originalExisted ? record.originalContent : null;
|
|
2044
|
+
}
|
|
2045
|
+
return current;
|
|
2046
|
+
}
|
|
2047
|
+
function applyFileChangesWithRollback(changes) {
|
|
2048
|
+
if (changes.length === 0) return;
|
|
2049
|
+
const snapshots = changes.map((change) => ({ path: change.path, content: readOptional(change.path) }));
|
|
2050
|
+
try {
|
|
2051
|
+
for (const change of changes) writeOptional(change.path, change.content);
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
const rollbackFailures = [];
|
|
2054
|
+
for (const snapshot of [...snapshots].reverse()) {
|
|
2055
|
+
try {
|
|
2056
|
+
writeOptional(snapshot.path, snapshot.content);
|
|
2057
|
+
} catch {
|
|
2058
|
+
rollbackFailures.push(snapshot.path);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (rollbackFailures.length > 0) {
|
|
2062
|
+
throw new IntegrationConflictError(
|
|
2063
|
+
`CLI integration update failed and rollback could not restore: ${rollbackFailures.join(", ")}`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
throw error;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
function writeOptional(path2, content) {
|
|
2070
|
+
if (content !== null) {
|
|
2071
|
+
atomicWrite(path2, content);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if ((0, import_node_fs5.existsSync)(path2)) (0, import_node_fs5.unlinkSync)(path2);
|
|
2075
|
+
}
|
|
2076
|
+
function assertLoopbackGatewayUrl(value) {
|
|
2077
|
+
let url;
|
|
2078
|
+
try {
|
|
2079
|
+
url = new URL(value);
|
|
2080
|
+
} catch {
|
|
2081
|
+
throw new Error("gatewayBaseUrl must be a valid loopback URL");
|
|
2082
|
+
}
|
|
2083
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
2084
|
+
const literalLoopback = host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
2085
|
+
if (url.protocol !== "http:" || !literalLoopback || url.username || url.password || url.search || url.hash) {
|
|
2086
|
+
throw new Error("native CLI integrations require an unauthenticated literal HTTP loopback gateway URL");
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
909
2090
|
// src/preset-catalog.ts
|
|
910
2091
|
var presetsModule = __toESM(require("@omnicross/contracts/provider-presets"), 1);
|
|
911
2092
|
function normalizeCatalogModule(m) {
|
|
@@ -926,14 +2107,13 @@ function getCatalog() {
|
|
|
926
2107
|
|
|
927
2108
|
// src/preset-map.ts
|
|
928
2109
|
var EXCLUSION_REASONS = {
|
|
929
|
-
"openai-response": "daemon rows have no openai-response format; the Responses API needs a transformer chain that a BYO daemon provider row cannot express.",
|
|
930
2110
|
"azure-openai": "Azure needs an apiVersion + a deployment-name-as-model URL template + an empty baseUrl; a daemon provider row cannot express that shape."
|
|
931
2111
|
};
|
|
932
2112
|
var FORMAT_MAP = {
|
|
933
2113
|
openai: "openai",
|
|
934
2114
|
anthropic: "anthropic",
|
|
935
2115
|
google: "gemini",
|
|
936
|
-
"openai-response":
|
|
2116
|
+
"openai-response": "openai-response",
|
|
937
2117
|
"azure-openai": null
|
|
938
2118
|
};
|
|
939
2119
|
function resolveFormat(raw) {
|
|
@@ -1054,11 +2234,11 @@ function preserveOutboundProxySecrets(incoming, current) {
|
|
|
1054
2234
|
}
|
|
1055
2235
|
|
|
1056
2236
|
// src/proxy/upstreamProxyResolver.ts
|
|
1057
|
-
var
|
|
2237
|
+
var import_upstreamFetch2 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
1058
2238
|
var serverProxy;
|
|
1059
2239
|
function setServerProxyConfig(proxy) {
|
|
1060
2240
|
serverProxy = proxy;
|
|
1061
|
-
(0,
|
|
2241
|
+
(0, import_upstreamFetch2.bumpUpstreamProxyGeneration)();
|
|
1062
2242
|
}
|
|
1063
2243
|
function getServerProxyConfig() {
|
|
1064
2244
|
return serverProxy;
|
|
@@ -1138,6 +2318,67 @@ var VALID_PROVIDER_IDS = [
|
|
|
1138
2318
|
function asSubscriptionProviderId(id) {
|
|
1139
2319
|
return VALID_PROVIDER_IDS.includes(id) ? id : null;
|
|
1140
2320
|
}
|
|
2321
|
+
var ACCOUNT_PATCH_KEYS = /* @__PURE__ */ new Set(["label", "enabled", "priority", "group", "tags"]);
|
|
2322
|
+
function validateAccountMetadataPatch(body) {
|
|
2323
|
+
const keys = Object.keys(body);
|
|
2324
|
+
if (keys.length === 0 || keys.some((key) => !ACCOUNT_PATCH_KEYS.has(key))) return null;
|
|
2325
|
+
const patch = {};
|
|
2326
|
+
if ("label" in body) {
|
|
2327
|
+
if (typeof body["label"] !== "string" || body["label"].trim().length > 120) return null;
|
|
2328
|
+
patch.label = body["label"].trim();
|
|
2329
|
+
}
|
|
2330
|
+
if ("enabled" in body) {
|
|
2331
|
+
if (typeof body["enabled"] !== "boolean") return null;
|
|
2332
|
+
patch.enabled = body["enabled"];
|
|
2333
|
+
}
|
|
2334
|
+
if ("priority" in body) {
|
|
2335
|
+
const priority = body["priority"];
|
|
2336
|
+
if (typeof priority !== "number" || !Number.isFinite(priority) || priority < -1e4 || priority > 1e4) return null;
|
|
2337
|
+
patch.priority = priority;
|
|
2338
|
+
}
|
|
2339
|
+
if ("group" in body) {
|
|
2340
|
+
const group = body["group"];
|
|
2341
|
+
if (group !== null && typeof group !== "string") return null;
|
|
2342
|
+
const normalized = typeof group === "string" ? group.trim() : null;
|
|
2343
|
+
if (normalized !== null && normalized.length > 80) return null;
|
|
2344
|
+
patch.group = normalized || null;
|
|
2345
|
+
}
|
|
2346
|
+
if ("tags" in body) {
|
|
2347
|
+
const tags = body["tags"];
|
|
2348
|
+
if (!Array.isArray(tags) || tags.length > 20) return null;
|
|
2349
|
+
const normalized = tags.map((tag) => typeof tag === "string" ? tag.trim() : "");
|
|
2350
|
+
if (normalized.some((tag) => !tag || tag.length > 40)) return null;
|
|
2351
|
+
patch.tags = [...new Set(normalized)];
|
|
2352
|
+
}
|
|
2353
|
+
return patch;
|
|
2354
|
+
}
|
|
2355
|
+
function validateAccountBatchBody(body) {
|
|
2356
|
+
const action = body["action"];
|
|
2357
|
+
const rawAccounts = body["accounts"];
|
|
2358
|
+
if (!Array.isArray(rawAccounts) || rawAccounts.length < 1 || rawAccounts.length > 100) return null;
|
|
2359
|
+
if (action !== "enable" && action !== "disable" && action !== "set-group" && action !== "delete") return null;
|
|
2360
|
+
const refs = [];
|
|
2361
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2362
|
+
for (const raw of rawAccounts) {
|
|
2363
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2364
|
+
const row = raw;
|
|
2365
|
+
const providerId = typeof row["providerId"] === "string" ? asSubscriptionProviderId(row["providerId"]) : null;
|
|
2366
|
+
const accountId = typeof row["accountId"] === "string" ? row["accountId"].trim() : "";
|
|
2367
|
+
if (!providerId || !accountId || accountId.length > 200) return null;
|
|
2368
|
+
const key = `${providerId}\0${accountId}`;
|
|
2369
|
+
if (seen.has(key)) return null;
|
|
2370
|
+
seen.add(key);
|
|
2371
|
+
refs.push({ providerId, accountId });
|
|
2372
|
+
}
|
|
2373
|
+
if (action === "set-group") {
|
|
2374
|
+
const group = body["group"];
|
|
2375
|
+
if (group !== null && typeof group !== "string") return null;
|
|
2376
|
+
const normalized = typeof group === "string" ? group.trim() : null;
|
|
2377
|
+
if (normalized !== null && normalized.length > 80) return null;
|
|
2378
|
+
return { refs, mutation: { action, group: normalized || null } };
|
|
2379
|
+
}
|
|
2380
|
+
return { refs, mutation: { action } };
|
|
2381
|
+
}
|
|
1141
2382
|
var CLAUDE_AUTH_METHODS = /* @__PURE__ */ new Set(["oauth", "setup_token", "manual"]);
|
|
1142
2383
|
var OAUTH_AUTH_METHODS = /* @__PURE__ */ new Set(["oauth", "manual"]);
|
|
1143
2384
|
var TOKEN_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -1349,9 +2590,9 @@ async function exchangeGemini(code, codeVerifier, exchangeFetch) {
|
|
|
1349
2590
|
|
|
1350
2591
|
// src/admin/cliLaunch.ts
|
|
1351
2592
|
var import_node_child_process = require("child_process");
|
|
1352
|
-
var
|
|
1353
|
-
var
|
|
1354
|
-
var
|
|
2593
|
+
var import_node_crypto6 = require("crypto");
|
|
2594
|
+
var import_node_fs6 = require("fs");
|
|
2595
|
+
var import_node_path5 = require("path");
|
|
1355
2596
|
var import_cli_launcher = require("@omnicross/cli-launcher");
|
|
1356
2597
|
var LAUNCHABLE_CLIS = [
|
|
1357
2598
|
{ id: "claude", displayName: "Claude Code", command: "claude" },
|
|
@@ -1374,10 +2615,10 @@ function isLaunchCliId(id) {
|
|
|
1374
2615
|
return id !== void 0 && LAUNCHABLE_IDS.has(id);
|
|
1375
2616
|
}
|
|
1376
2617
|
function probeDefault(candidate) {
|
|
1377
|
-
const segments = (process.env["PATH"] ?? "").split(
|
|
2618
|
+
const segments = (process.env["PATH"] ?? "").split(import_node_path5.delimiter).filter(Boolean);
|
|
1378
2619
|
for (const seg of segments) {
|
|
1379
|
-
const full = (0,
|
|
1380
|
-
if ((0,
|
|
2620
|
+
const full = (0, import_node_path5.join)(seg, candidate);
|
|
2621
|
+
if ((0, import_node_fs6.existsSync)(full)) return full;
|
|
1381
2622
|
}
|
|
1382
2623
|
return null;
|
|
1383
2624
|
}
|
|
@@ -1464,10 +2705,10 @@ var sessions = /* @__PURE__ */ new Map();
|
|
|
1464
2705
|
function errBody(message) {
|
|
1465
2706
|
return { error: { type: "admin_api_error", message } };
|
|
1466
2707
|
}
|
|
1467
|
-
var defaultCommandRunner = (command) => new Promise((
|
|
2708
|
+
var defaultCommandRunner = (command) => new Promise((resolve2) => {
|
|
1468
2709
|
(0, import_node_child_process.exec)(command, { timeout: 18e4 }, (err5, _stdout, stderr) => {
|
|
1469
|
-
if (err5)
|
|
1470
|
-
else
|
|
2710
|
+
if (err5) resolve2({ ok: false, error: stderr.trim() || err5.message });
|
|
2711
|
+
else resolve2({ ok: true });
|
|
1471
2712
|
});
|
|
1472
2713
|
});
|
|
1473
2714
|
async function handleCliInstall(cli, runner = defaultCommandRunner) {
|
|
@@ -1529,7 +2770,7 @@ async function handleCliLaunch(cli, body, ctx) {
|
|
|
1529
2770
|
launch.onSessionEnd();
|
|
1530
2771
|
return { status: 500, body: errBody(err5 instanceof Error ? err5.message : "failed to open terminal") };
|
|
1531
2772
|
}
|
|
1532
|
-
const id = (0,
|
|
2773
|
+
const id = (0, import_node_crypto6.randomUUID)();
|
|
1533
2774
|
sessions.set(id, {
|
|
1534
2775
|
id,
|
|
1535
2776
|
cli,
|
|
@@ -1542,12 +2783,12 @@ async function handleCliLaunch(cli, body, ctx) {
|
|
|
1542
2783
|
}
|
|
1543
2784
|
|
|
1544
2785
|
// src/admin/auditConfigBody.ts
|
|
1545
|
-
var
|
|
2786
|
+
var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1546
2787
|
function validateAuditSegment(patch) {
|
|
1547
2788
|
const errors = [];
|
|
1548
2789
|
const audit = patch.audit;
|
|
1549
2790
|
if (audit === void 0) return errors;
|
|
1550
|
-
if (!
|
|
2791
|
+
if (!isPlainObject2(audit)) {
|
|
1551
2792
|
errors.push("audit must be an object");
|
|
1552
2793
|
return errors;
|
|
1553
2794
|
}
|
|
@@ -1569,12 +2810,12 @@ function validateAuditSegment(patch) {
|
|
|
1569
2810
|
|
|
1570
2811
|
// src/admin/billingConfigBody.ts
|
|
1571
2812
|
var BILLING_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
1572
|
-
var
|
|
2813
|
+
var isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1573
2814
|
function validateBillingSegment(patch) {
|
|
1574
2815
|
const errors = [];
|
|
1575
2816
|
const billing = patch.billing;
|
|
1576
2817
|
if (billing === void 0) return errors;
|
|
1577
|
-
if (!
|
|
2818
|
+
if (!isPlainObject3(billing)) {
|
|
1578
2819
|
errors.push("billing must be an object");
|
|
1579
2820
|
return errors;
|
|
1580
2821
|
}
|
|
@@ -1710,6 +2951,96 @@ function parseKeyPolicyBody(body) {
|
|
|
1710
2951
|
return { ok: true, policy };
|
|
1711
2952
|
}
|
|
1712
2953
|
|
|
2954
|
+
// src/admin/gatewayBindingBody.ts
|
|
2955
|
+
var ENDPOINTS = /* @__PURE__ */ new Set(["chat", "responses", "messages", "gemini"]);
|
|
2956
|
+
var TARGET_KINDS = /* @__PURE__ */ new Set(["account", "account-group", "account-pool", "provider"]);
|
|
2957
|
+
var FALLBACKS = /* @__PURE__ */ new Set(["next", "fail", "global"]);
|
|
2958
|
+
function isRecord(value) {
|
|
2959
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2960
|
+
}
|
|
2961
|
+
function nonBlank(value) {
|
|
2962
|
+
return typeof value === "string" && value.trim() !== "";
|
|
2963
|
+
}
|
|
2964
|
+
function validateStringArray(value, path2, errors) {
|
|
2965
|
+
if (!Array.isArray(value) || value.some((entry) => !nonBlank(entry))) {
|
|
2966
|
+
errors.push(`${path2} must be an array of non-empty strings`);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
function validateGatewayBindingsSegment(patch) {
|
|
2970
|
+
if (!Object.prototype.hasOwnProperty.call(patch, "bindings")) return [];
|
|
2971
|
+
const raw = patch.bindings;
|
|
2972
|
+
if (!Array.isArray(raw)) return ["bindings must be an array"];
|
|
2973
|
+
if (raw.length > 1e3) return ["bindings cannot contain more than 1000 entries"];
|
|
2974
|
+
const errors = [];
|
|
2975
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2976
|
+
raw.forEach((entry, index) => {
|
|
2977
|
+
const path2 = `bindings[${index}]`;
|
|
2978
|
+
if (!isRecord(entry)) {
|
|
2979
|
+
errors.push(`${path2} must be an object`);
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
if (!nonBlank(entry.id)) errors.push(`${path2}.id is required`);
|
|
2983
|
+
else if (ids.has(entry.id.trim())) errors.push(`${path2}.id must be unique`);
|
|
2984
|
+
else ids.add(entry.id.trim());
|
|
2985
|
+
if (!nonBlank(entry.name)) errors.push(`${path2}.name is required`);
|
|
2986
|
+
if (typeof entry.enabled !== "boolean") errors.push(`${path2}.enabled must be boolean`);
|
|
2987
|
+
if (!ENDPOINTS.has(String(entry.endpoint))) errors.push(`${path2}.endpoint is invalid`);
|
|
2988
|
+
if (!FALLBACKS.has(String(entry.fallback))) {
|
|
2989
|
+
errors.push(`${path2}.fallback must be next or fail`);
|
|
2990
|
+
}
|
|
2991
|
+
if (entry.priority !== void 0 && (typeof entry.priority !== "number" || !Number.isInteger(entry.priority) || entry.priority < 0 || entry.priority > 1e4)) {
|
|
2992
|
+
errors.push(`${path2}.priority must be an integer from 0 to 10000`);
|
|
2993
|
+
}
|
|
2994
|
+
if (entry.apiKeyIds !== void 0) validateStringArray(entry.apiKeyIds, `${path2}.apiKeyIds`, errors);
|
|
2995
|
+
if (entry.keyScope !== void 0 && entry.keyScope !== "all" && entry.keyScope !== "selected") {
|
|
2996
|
+
errors.push(`${path2}.keyScope must be all or selected`);
|
|
2997
|
+
}
|
|
2998
|
+
if (entry.modelMode !== void 0 && entry.modelMode !== "passthrough" && entry.modelMode !== "mapped") {
|
|
2999
|
+
errors.push(`${path2}.modelMode must be passthrough or mapped`);
|
|
3000
|
+
}
|
|
3001
|
+
if (entry.modelMappings !== void 0) {
|
|
3002
|
+
if (!Array.isArray(entry.modelMappings)) {
|
|
3003
|
+
errors.push(`${path2}.modelMappings must be an array`);
|
|
3004
|
+
} else if (entry.modelMappings.length > 100) {
|
|
3005
|
+
errors.push(`${path2}.modelMappings cannot contain more than 100 entries`);
|
|
3006
|
+
} else if (entry.modelMappings.some(
|
|
3007
|
+
(mapping) => !isRecord(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
|
|
3008
|
+
)) {
|
|
3009
|
+
errors.push(`${path2}.modelMappings must contain non-empty source and target strings`);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (!isRecord(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
|
|
3013
|
+
errors.push(`${path2}.target is invalid`);
|
|
3014
|
+
} else {
|
|
3015
|
+
if (!nonBlank(entry.target.providerId)) errors.push(`${path2}.target.providerId is required`);
|
|
3016
|
+
if (entry.target.kind === "account" && !nonBlank(entry.target.accountId)) {
|
|
3017
|
+
errors.push(`${path2}.target.accountId is required`);
|
|
3018
|
+
}
|
|
3019
|
+
if (entry.target.kind === "account-group" && !nonBlank(entry.target.group)) {
|
|
3020
|
+
errors.push(`${path2}.target.group is required`);
|
|
3021
|
+
}
|
|
3022
|
+
if (entry.target.kind === "provider" && entry.target.keyId !== void 0 && !nonBlank(entry.target.keyId)) {
|
|
3023
|
+
errors.push(`${path2}.target.keyId must be a non-empty string`);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
if (entry.modelMap !== void 0) {
|
|
3027
|
+
if (!isRecord(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
|
|
3028
|
+
errors.push(`${path2}.modelMap must contain string values`);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
if (entry.models !== void 0) validateStringArray(entry.models, `${path2}.models`, errors);
|
|
3032
|
+
if (entry.backgroundModelIds !== void 0) {
|
|
3033
|
+
validateStringArray(entry.backgroundModelIds, `${path2}.backgroundModelIds`, errors);
|
|
3034
|
+
}
|
|
3035
|
+
for (const field of ["defaultModel", "backgroundModel"]) {
|
|
3036
|
+
if (entry[field] !== void 0 && typeof entry[field] !== "string") {
|
|
3037
|
+
errors.push(`${path2}.${field} must be a string`);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
return errors;
|
|
3042
|
+
}
|
|
3043
|
+
|
|
1713
3044
|
// src/admin/voucherAdmin.ts
|
|
1714
3045
|
var import_outbound_api = require("@omnicross/core/outbound-api");
|
|
1715
3046
|
function writeJson(res, status, body) {
|
|
@@ -1720,15 +3051,15 @@ function writeErr(res, status, message) {
|
|
|
1720
3051
|
writeJson(res, status, { error: { type: "voucher_error", message } });
|
|
1721
3052
|
}
|
|
1722
3053
|
function readJsonBody2(req) {
|
|
1723
|
-
return new Promise((
|
|
3054
|
+
return new Promise((resolve2, reject) => {
|
|
1724
3055
|
const chunks = [];
|
|
1725
3056
|
req.on("data", (c) => chunks.push(c));
|
|
1726
3057
|
req.on("end", () => {
|
|
1727
3058
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1728
|
-
if (!raw.trim()) return
|
|
3059
|
+
if (!raw.trim()) return resolve2({});
|
|
1729
3060
|
try {
|
|
1730
3061
|
const parsed = JSON.parse(raw);
|
|
1731
|
-
|
|
3062
|
+
resolve2(parsed && typeof parsed === "object" ? parsed : {});
|
|
1732
3063
|
} catch {
|
|
1733
3064
|
reject(new Error("invalid-json"));
|
|
1734
3065
|
}
|
|
@@ -1818,12 +3149,12 @@ async function handleVoucher(req, res, method, rest, deps) {
|
|
|
1818
3149
|
// src/admin/webhookConfigBody.ts
|
|
1819
3150
|
var import_webhook_types = require("@omnicross/contracts/webhook-types");
|
|
1820
3151
|
var WEBHOOK_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
1821
|
-
var
|
|
3152
|
+
var isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1822
3153
|
function validateWebhookSegment(patch) {
|
|
1823
3154
|
const errors = [];
|
|
1824
3155
|
const webhook = patch.webhook;
|
|
1825
3156
|
if (webhook === void 0) return errors;
|
|
1826
|
-
if (!
|
|
3157
|
+
if (!isPlainObject4(webhook)) {
|
|
1827
3158
|
errors.push("webhook must be an object");
|
|
1828
3159
|
return errors;
|
|
1829
3160
|
}
|
|
@@ -1837,7 +3168,7 @@ function validateWebhookSegment(patch) {
|
|
|
1837
3168
|
}
|
|
1838
3169
|
const seenIds = /* @__PURE__ */ new Set();
|
|
1839
3170
|
for (const [i, raw] of (Array.isArray(destinations) ? destinations : []).entries()) {
|
|
1840
|
-
if (!
|
|
3171
|
+
if (!isPlainObject4(raw)) {
|
|
1841
3172
|
errors.push(`webhook.destinations[${i}] must be an object`);
|
|
1842
3173
|
continue;
|
|
1843
3174
|
}
|
|
@@ -1903,7 +3234,7 @@ function preserveWebhookSecrets(incoming, current) {
|
|
|
1903
3234
|
}
|
|
1904
3235
|
|
|
1905
3236
|
// src/audit/auditRuntime.ts
|
|
1906
|
-
var
|
|
3237
|
+
var import_node_path6 = require("path");
|
|
1907
3238
|
var import_auditSink = require("@omnicross/core/pipeline/auditSink");
|
|
1908
3239
|
var import_upstreamTrace = require("@omnicross/core/pipeline/upstreamTrace");
|
|
1909
3240
|
var writer = null;
|
|
@@ -1924,7 +3255,7 @@ function applyAuditConfig(config) {
|
|
|
1924
3255
|
sweeper.configure(config);
|
|
1925
3256
|
sweeper.start();
|
|
1926
3257
|
}
|
|
1927
|
-
(0, import_upstreamTrace.setUpstreamTracePath)(config.captureBodies ? (0,
|
|
3258
|
+
(0, import_upstreamTrace.setUpstreamTracePath)(config.captureBodies ? (0, import_node_path6.join)(auditDir, "upstream-trace.jsonl") : null);
|
|
1928
3259
|
} else {
|
|
1929
3260
|
(0, import_auditSink.setAuditCaptureConfig)(null);
|
|
1930
3261
|
(0, import_auditSink.setAuditSink)(null);
|
|
@@ -1982,7 +3313,7 @@ function resetBillingRuntimeForTests() {
|
|
|
1982
3313
|
}
|
|
1983
3314
|
|
|
1984
3315
|
// src/ports/account-multi.ts
|
|
1985
|
-
var
|
|
3316
|
+
var import_node_crypto7 = require("crypto");
|
|
1986
3317
|
var PROVIDER_KEYS = {
|
|
1987
3318
|
claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
|
|
1988
3319
|
codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
|
|
@@ -2048,7 +3379,7 @@ function migrateLazily(config) {
|
|
|
2048
3379
|
}
|
|
2049
3380
|
function addAccount(config, p, tokens, label) {
|
|
2050
3381
|
const accounts = [...getAccounts(config, p)];
|
|
2051
|
-
const id = (0,
|
|
3382
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
2052
3383
|
accounts.push({
|
|
2053
3384
|
id,
|
|
2054
3385
|
label: label ?? `Account ${accounts.length + 1}`,
|
|
@@ -2124,9 +3455,13 @@ function sanitizeAccounts(config, p) {
|
|
|
2124
3455
|
const activeId = getActiveId(config, p);
|
|
2125
3456
|
return accounts.map((a) => {
|
|
2126
3457
|
const t = a.tokens;
|
|
3458
|
+
const enabled = a.enabled !== false;
|
|
2127
3459
|
return {
|
|
2128
3460
|
id: a.id,
|
|
2129
3461
|
label: a.label,
|
|
3462
|
+
enabled,
|
|
3463
|
+
group: a.group?.trim() || p,
|
|
3464
|
+
tags: a.tags ?? [],
|
|
2130
3465
|
status: t.status ?? "unconfigured",
|
|
2131
3466
|
authMethod: t.authMethod,
|
|
2132
3467
|
subscriptionLevel: t.subscriptionLevel,
|
|
@@ -2135,6 +3470,8 @@ function sanitizeAccounts(config, p) {
|
|
|
2135
3470
|
isSetupToken: t.isSetupToken,
|
|
2136
3471
|
hasAccessToken: !!(t.accessToken || t.apiKey),
|
|
2137
3472
|
isActive: a.id === activeId,
|
|
3473
|
+
schedulable: enabled,
|
|
3474
|
+
errorMessage: sanitizeDiagnosticMessage(t.errorMessage),
|
|
2138
3475
|
// Scheduling metadata (subscription-account-scheduling): editable priority
|
|
2139
3476
|
// (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
|
|
2140
3477
|
priority: a.priority,
|
|
@@ -2149,6 +3486,54 @@ function sanitizeAccounts(config, p) {
|
|
|
2149
3486
|
};
|
|
2150
3487
|
});
|
|
2151
3488
|
}
|
|
3489
|
+
function sanitizeDiagnosticMessage(value) {
|
|
3490
|
+
if (!value) return void 0;
|
|
3491
|
+
const lower = value.toLowerCase();
|
|
3492
|
+
if (lower.includes("timeout") || lower.includes("timed out")) return "Credential operation timed out.";
|
|
3493
|
+
if (lower.includes("network") || lower.includes("fetch")) return "Credential network request failed.";
|
|
3494
|
+
if (lower.includes("401") || lower.includes("unauthorized") || lower.includes("revoked")) {
|
|
3495
|
+
return "Credential authorization was rejected.";
|
|
3496
|
+
}
|
|
3497
|
+
return "Credential operation failed.";
|
|
3498
|
+
}
|
|
3499
|
+
function patchAccountMetadata(config, p, id, patch) {
|
|
3500
|
+
const accounts = getAccounts(config, p);
|
|
3501
|
+
if (!accounts.some((account) => account.id === id)) return { ok: false };
|
|
3502
|
+
setAccounts(config, p, accounts.map((account) => {
|
|
3503
|
+
if (account.id !== id) return account;
|
|
3504
|
+
const next = { ...account };
|
|
3505
|
+
if (patch.label !== void 0) next.label = patch.label;
|
|
3506
|
+
if (patch.enabled !== void 0) next.enabled = patch.enabled;
|
|
3507
|
+
if (patch.priority !== void 0) next.priority = patch.priority;
|
|
3508
|
+
if (patch.group !== void 0) {
|
|
3509
|
+
if (patch.group === null || patch.group === "") delete next.group;
|
|
3510
|
+
else next.group = patch.group;
|
|
3511
|
+
}
|
|
3512
|
+
if (patch.tags !== void 0) next.tags = patch.tags;
|
|
3513
|
+
return next;
|
|
3514
|
+
}));
|
|
3515
|
+
return { ok: true };
|
|
3516
|
+
}
|
|
3517
|
+
function batchManageAccounts(config, refs, mutation) {
|
|
3518
|
+
for (const ref of refs) {
|
|
3519
|
+
if (!getAccounts(config, ref.providerId).some((account) => account.id === ref.accountId)) {
|
|
3520
|
+
return { ok: false, missing: ref };
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
for (const ref of refs) {
|
|
3524
|
+
if (mutation.action === "delete") {
|
|
3525
|
+
removeAccount(config, ref.providerId, ref.accountId);
|
|
3526
|
+
} else {
|
|
3527
|
+
patchAccountMetadata(
|
|
3528
|
+
config,
|
|
3529
|
+
ref.providerId,
|
|
3530
|
+
ref.accountId,
|
|
3531
|
+
mutation.action === "set-group" ? { group: mutation.group } : { enabled: mutation.action === "enable" }
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
return { ok: true, affected: refs.length };
|
|
3536
|
+
}
|
|
2152
3537
|
function renameAccount(config, p, id, label) {
|
|
2153
3538
|
const accounts = getAccounts(config, p);
|
|
2154
3539
|
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
@@ -2238,7 +3623,7 @@ function clearProvider(config, p) {
|
|
|
2238
3623
|
var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
|
|
2239
3624
|
|
|
2240
3625
|
// src/migration/packCodec.ts
|
|
2241
|
-
var
|
|
3626
|
+
var import_node_crypto8 = require("crypto");
|
|
2242
3627
|
var PACK_MAGIC = "OMCXPACK";
|
|
2243
3628
|
var PACK_VERSION = 1;
|
|
2244
3629
|
var KDF_ALGORITHM = "scrypt";
|
|
@@ -2276,17 +3661,17 @@ function fromB64Url(s) {
|
|
|
2276
3661
|
return Buffer.from(s, "base64url").toString("utf8");
|
|
2277
3662
|
}
|
|
2278
3663
|
function deriveKey(passphrase, salt, N, r, p) {
|
|
2279
|
-
return (0,
|
|
3664
|
+
return (0, import_node_crypto8.scryptSync)(passphrase, salt, KEY_BYTES3, { N, r, p, maxmem: SCRYPT_MAXMEM });
|
|
2280
3665
|
}
|
|
2281
3666
|
function aadFor(magic, version, kdf) {
|
|
2282
3667
|
return Buffer.from(`${magic}|${version}|${kdf}`, "utf8");
|
|
2283
3668
|
}
|
|
2284
3669
|
function sealPack(bundleJson, passphrase) {
|
|
2285
3670
|
assertPassphraseStrength(passphrase);
|
|
2286
|
-
const salt = (0,
|
|
2287
|
-
const iv = (0,
|
|
3671
|
+
const salt = (0, import_node_crypto8.randomBytes)(SCRYPT_SALT_BYTES);
|
|
3672
|
+
const iv = (0, import_node_crypto8.randomBytes)(IV_BYTES2);
|
|
2288
3673
|
const key = deriveKey(passphrase, salt, SCRYPT_N, SCRYPT_R, SCRYPT_P);
|
|
2289
|
-
const cipher = (0,
|
|
3674
|
+
const cipher = (0, import_node_crypto8.createCipheriv)("aes-256-gcm", key, iv);
|
|
2290
3675
|
cipher.setAAD(aadFor(PACK_MAGIC, PACK_VERSION, KDF_ALGORITHM));
|
|
2291
3676
|
const ciphertext = Buffer.concat([cipher.update(bundleJson, "utf8"), cipher.final()]);
|
|
2292
3677
|
const tag = cipher.getAuthTag();
|
|
@@ -2334,7 +3719,7 @@ function openPack(packString, passphrase) {
|
|
|
2334
3719
|
throw new PackAuthError("migration pack is malformed (invalid iv/tag length)");
|
|
2335
3720
|
}
|
|
2336
3721
|
const key = deriveKey(passphrase, salt, header.N, header.r, header.p);
|
|
2337
|
-
const decipher = (0,
|
|
3722
|
+
const decipher = (0, import_node_crypto8.createDecipheriv)("aes-256-gcm", key, iv);
|
|
2338
3723
|
decipher.setAAD(aadFor(PACK_MAGIC, PACK_VERSION, KDF_ALGORITHM));
|
|
2339
3724
|
decipher.setAuthTag(tag);
|
|
2340
3725
|
try {
|
|
@@ -2486,9 +3871,9 @@ function parseFiniteInt(raw) {
|
|
|
2486
3871
|
const n = Number(raw);
|
|
2487
3872
|
return Number.isFinite(n) && Number.isInteger(n) ? n : null;
|
|
2488
3873
|
}
|
|
2489
|
-
function parseRange(
|
|
2490
|
-
const startTs = parseFiniteInt(
|
|
2491
|
-
const endTs = parseFiniteInt(
|
|
3874
|
+
function parseRange(query2) {
|
|
3875
|
+
const startTs = parseFiniteInt(query2.get("startTs"));
|
|
3876
|
+
const endTs = parseFiniteInt(query2.get("endTs"));
|
|
2492
3877
|
if (startTs === null || endTs === null) {
|
|
2493
3878
|
return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
|
|
2494
3879
|
}
|
|
@@ -2501,8 +3886,8 @@ var BUCKET_SPAN_MS = {
|
|
|
2501
3886
|
month: 28 * 864e5
|
|
2502
3887
|
};
|
|
2503
3888
|
var MAX_TIMESERIES_BUCKETS = 2e3;
|
|
2504
|
-
async function handleUsageGet(view,
|
|
2505
|
-
const range = parseRange(
|
|
3889
|
+
async function handleUsageGet(view, query2, deps) {
|
|
3890
|
+
const range = parseRange(query2);
|
|
2506
3891
|
if (!isRange(range)) return range;
|
|
2507
3892
|
switch (view) {
|
|
2508
3893
|
case "totals":
|
|
@@ -2510,7 +3895,7 @@ async function handleUsageGet(view, query, deps) {
|
|
|
2510
3895
|
case "by-model":
|
|
2511
3896
|
return { status: 200, body: await deps.usageRecorder.getByModel(range) };
|
|
2512
3897
|
case "timeseries": {
|
|
2513
|
-
const bucket =
|
|
3898
|
+
const bucket = query2.get("bucket");
|
|
2514
3899
|
if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
|
|
2515
3900
|
return err4(400, "bucket must be one of 'hour', 'day', 'month'");
|
|
2516
3901
|
}
|
|
@@ -2596,9 +3981,9 @@ async function handlePricingUpsert(body, deps) {
|
|
|
2596
3981
|
const entry = await deps.pricingEngine.upsertManual(input);
|
|
2597
3982
|
return { status: 200, body: { entry } };
|
|
2598
3983
|
}
|
|
2599
|
-
async function handlePricingDelete(
|
|
2600
|
-
const providerId =
|
|
2601
|
-
const modelId =
|
|
3984
|
+
async function handlePricingDelete(query2, deps) {
|
|
3985
|
+
const providerId = query2.get("providerId")?.trim() ?? "";
|
|
3986
|
+
const modelId = query2.get("modelId")?.trim() ?? "";
|
|
2602
3987
|
if (!providerId || !modelId) {
|
|
2603
3988
|
return err4(400, "delete requires providerId and modelId query params");
|
|
2604
3989
|
}
|
|
@@ -2615,7 +4000,8 @@ async function handlePricingFetchLatest(deps) {
|
|
|
2615
4000
|
appliedCount: result.applied.length,
|
|
2616
4001
|
conflicts: result.conflicts,
|
|
2617
4002
|
fetchedAt: result.fetchedAt,
|
|
2618
|
-
sourceUrl: result.sourceUrl
|
|
4003
|
+
sourceUrl: result.sourceUrl,
|
|
4004
|
+
sources: result.sources
|
|
2619
4005
|
}
|
|
2620
4006
|
};
|
|
2621
4007
|
} catch (e) {
|
|
@@ -2663,12 +4049,80 @@ async function handlePricingResolveConflicts(body, deps) {
|
|
|
2663
4049
|
return { status: 200, body: { ...resolution, staleCount } };
|
|
2664
4050
|
}
|
|
2665
4051
|
|
|
4052
|
+
// src/admin/accountAllowanceApi.ts
|
|
4053
|
+
function writeJson2(res, status, body) {
|
|
4054
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
4055
|
+
res.end(JSON.stringify(body));
|
|
4056
|
+
}
|
|
4057
|
+
function writeError(res, status, message) {
|
|
4058
|
+
writeJson2(res, status, { error: { type: "account_allowance_error", message } });
|
|
4059
|
+
}
|
|
4060
|
+
function readJson(req) {
|
|
4061
|
+
return new Promise((resolve2, reject) => {
|
|
4062
|
+
const chunks = [];
|
|
4063
|
+
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
4064
|
+
req.on("end", () => {
|
|
4065
|
+
try {
|
|
4066
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
4067
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
4068
|
+
resolve2(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
|
|
4069
|
+
} catch (error) {
|
|
4070
|
+
reject(error);
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
4073
|
+
req.on("error", reject);
|
|
4074
|
+
});
|
|
4075
|
+
}
|
|
4076
|
+
function query(req) {
|
|
4077
|
+
const raw = req.url ?? "";
|
|
4078
|
+
const index = raw.indexOf("?");
|
|
4079
|
+
return new URLSearchParams(index >= 0 ? raw.slice(index + 1) : "");
|
|
4080
|
+
}
|
|
4081
|
+
function allowanceProvider(value) {
|
|
4082
|
+
if (!value) return void 0;
|
|
4083
|
+
return value === "claude" || value === "codex" ? value : null;
|
|
4084
|
+
}
|
|
4085
|
+
async function handleAccountAllowanceApi(req, res, method, rest, service) {
|
|
4086
|
+
if (!service) return writeError(res, 501, "account allowance service is not available");
|
|
4087
|
+
if (method === "GET" && rest.length === 1 && rest[0] === "scheduling") {
|
|
4088
|
+
if (!service.getSchedulingStatus) {
|
|
4089
|
+
return writeError(res, 501, "allowance scheduling diagnostics are not available");
|
|
4090
|
+
}
|
|
4091
|
+
return writeJson2(res, 200, { scheduling: service.getSchedulingStatus() });
|
|
4092
|
+
}
|
|
4093
|
+
if (method === "GET") {
|
|
4094
|
+
const params = query(req);
|
|
4095
|
+
const pathProvider = rest.length >= 2 ? rest[0] : null;
|
|
4096
|
+
const providerId = allowanceProvider(pathProvider ?? params.get("providerId") ?? params.get("provider"));
|
|
4097
|
+
if (providerId === null) return writeError(res, 400, "providerId must be claude or codex");
|
|
4098
|
+
const accountId = rest.length >= 2 ? rest[1] : params.get("accountId") ?? void 0;
|
|
4099
|
+
const allowances = await service.list({ providerId, accountId });
|
|
4100
|
+
return writeJson2(res, 200, { allowances });
|
|
4101
|
+
}
|
|
4102
|
+
if (method === "POST" && rest[0] === "refresh") {
|
|
4103
|
+
const body = await readJson(req);
|
|
4104
|
+
const requestedProvider = allowanceProvider(
|
|
4105
|
+
typeof body["providerId"] === "string" ? body["providerId"] : "claude"
|
|
4106
|
+
);
|
|
4107
|
+
if (requestedProvider !== "claude") {
|
|
4108
|
+
return writeError(res, 400, "only Claude allowances support explicit refresh");
|
|
4109
|
+
}
|
|
4110
|
+
const accountId = typeof body["accountId"] === "string" && body["accountId"].trim() ? body["accountId"].trim() : void 0;
|
|
4111
|
+
const allowances = await service.refreshClaude(accountId);
|
|
4112
|
+
if (accountId && allowances.length === 0) {
|
|
4113
|
+
return writeError(res, 404, `Claude account '${accountId}' not found`);
|
|
4114
|
+
}
|
|
4115
|
+
return writeJson2(res, 200, { allowances });
|
|
4116
|
+
}
|
|
4117
|
+
return writeError(res, 405, `method ${method} not allowed on account allowances`);
|
|
4118
|
+
}
|
|
4119
|
+
|
|
2666
4120
|
// src/admin/adminApi.ts
|
|
2667
4121
|
function readBody(req) {
|
|
2668
|
-
return new Promise((
|
|
4122
|
+
return new Promise((resolve2, reject) => {
|
|
2669
4123
|
const chunks = [];
|
|
2670
4124
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
2671
|
-
req.on("end", () =>
|
|
4125
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
2672
4126
|
req.on("error", reject);
|
|
2673
4127
|
});
|
|
2674
4128
|
}
|
|
@@ -2682,12 +4136,12 @@ async function readJsonBody3(req) {
|
|
|
2682
4136
|
return {};
|
|
2683
4137
|
}
|
|
2684
4138
|
}
|
|
2685
|
-
function
|
|
4139
|
+
function writeJson3(res, status, body) {
|
|
2686
4140
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
2687
4141
|
res.end(JSON.stringify(body));
|
|
2688
4142
|
}
|
|
2689
4143
|
function writeJsonError(res, status, message) {
|
|
2690
|
-
|
|
4144
|
+
writeJson3(res, status, { error: { type: "admin_api_error", message } });
|
|
2691
4145
|
}
|
|
2692
4146
|
function maskProviderApiKey(apiKey) {
|
|
2693
4147
|
if (!apiKey) return "";
|
|
@@ -2704,6 +4158,9 @@ function toKeyInfo(row) {
|
|
|
2704
4158
|
createdAt: row.createdAt,
|
|
2705
4159
|
lastUsedAt: row.lastUsedAt,
|
|
2706
4160
|
revoked: row.revokedAt !== null,
|
|
4161
|
+
kind: row.kind,
|
|
4162
|
+
allowedEndpoints: row.allowedEndpoints,
|
|
4163
|
+
loopbackOnly: row.loopbackOnly,
|
|
2707
4164
|
maxConcurrency: row.maxConcurrency,
|
|
2708
4165
|
// Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
|
|
2709
4166
|
// the UI reads them to render + pre-fill the policy editor.
|
|
@@ -2789,6 +4246,8 @@ async function handleAdminApi(req, res, path2, deps) {
|
|
|
2789
4246
|
return await handleAccounts(req, res, method, rest, deps);
|
|
2790
4247
|
case "cli":
|
|
2791
4248
|
return await handleCli(req, res, method, rest, deps);
|
|
4249
|
+
case "integrations":
|
|
4250
|
+
return await handleIntegrations(req, res, method, rest, deps);
|
|
2792
4251
|
case "status":
|
|
2793
4252
|
return await handleStatus(res, method, deps);
|
|
2794
4253
|
case "playground":
|
|
@@ -2816,7 +4275,7 @@ function requestQuery(req) {
|
|
|
2816
4275
|
return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
|
|
2817
4276
|
}
|
|
2818
4277
|
function writeResult(res, result) {
|
|
2819
|
-
|
|
4278
|
+
writeJson3(res, result.status, result.body);
|
|
2820
4279
|
}
|
|
2821
4280
|
async function handleUsage(req, res, method, rest, deps) {
|
|
2822
4281
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
|
|
@@ -2825,7 +4284,7 @@ async function handleUsage(req, res, method, rest, deps) {
|
|
|
2825
4284
|
async function handleDashboardRoute(res, method, deps) {
|
|
2826
4285
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
|
|
2827
4286
|
const result = await handleDashboard(deps);
|
|
2828
|
-
return
|
|
4287
|
+
return writeJson3(res, result.status, result.body);
|
|
2829
4288
|
}
|
|
2830
4289
|
async function handlePricing(req, res, method, rest, deps) {
|
|
2831
4290
|
if (rest.length === 0) {
|
|
@@ -2858,13 +4317,13 @@ async function handleMigrationExport(req, res, method, deps) {
|
|
|
2858
4317
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
|
|
2859
4318
|
const body = await readJsonBody3(req);
|
|
2860
4319
|
const result = await handleExport(body, migrationDeps(deps));
|
|
2861
|
-
return
|
|
4320
|
+
return writeJson3(res, result.status, result.body);
|
|
2862
4321
|
}
|
|
2863
4322
|
async function handleMigrationImport(req, res, method, deps) {
|
|
2864
4323
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
|
|
2865
4324
|
const body = await readJsonBody3(req);
|
|
2866
4325
|
const result = await handleImport(body, migrationDeps(deps));
|
|
2867
|
-
return
|
|
4326
|
+
return writeJson3(res, result.status, result.body);
|
|
2868
4327
|
}
|
|
2869
4328
|
async function handleProviders(req, res, method, rest, deps) {
|
|
2870
4329
|
const cfg = loadConfig(deps.configPath);
|
|
@@ -2895,10 +4354,10 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2895
4354
|
if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
|
|
2896
4355
|
const row = cfg.providers.find((p) => p.id === rest[0]);
|
|
2897
4356
|
if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
|
|
2898
|
-
return
|
|
4357
|
+
return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
|
|
2899
4358
|
}
|
|
2900
4359
|
if (method === "GET") {
|
|
2901
|
-
return
|
|
4360
|
+
return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
|
|
2902
4361
|
}
|
|
2903
4362
|
if (method === "POST") {
|
|
2904
4363
|
const body = await readJsonBody3(req);
|
|
@@ -2909,7 +4368,7 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2909
4368
|
}
|
|
2910
4369
|
cfg.providers.push(provider);
|
|
2911
4370
|
persistProviders(cfg, deps);
|
|
2912
|
-
return
|
|
4371
|
+
return writeJson3(res, 201, { provider: toProviderView(provider) });
|
|
2913
4372
|
}
|
|
2914
4373
|
const id = rest[0];
|
|
2915
4374
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -2922,12 +4381,12 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2922
4381
|
if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
|
|
2923
4382
|
cfg.providers[idx] = updated;
|
|
2924
4383
|
persistProviders(cfg, deps);
|
|
2925
|
-
return
|
|
4384
|
+
return writeJson3(res, 200, { provider: toProviderView(updated) });
|
|
2926
4385
|
}
|
|
2927
4386
|
if (method === "DELETE") {
|
|
2928
4387
|
cfg.providers.splice(idx, 1);
|
|
2929
4388
|
persistProviders(cfg, deps);
|
|
2930
|
-
return
|
|
4389
|
+
return writeJson3(res, 200, { ok: true });
|
|
2931
4390
|
}
|
|
2932
4391
|
return writeJsonError(res, 405, `method ${method} not allowed on providers`);
|
|
2933
4392
|
}
|
|
@@ -2960,14 +4419,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
|
|
|
2960
4419
|
}
|
|
2961
4420
|
cfg.providers = reordered;
|
|
2962
4421
|
persistProviders(cfg, deps);
|
|
2963
|
-
return
|
|
4422
|
+
return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
|
|
2964
4423
|
}
|
|
2965
4424
|
async function handleDiscoverModels(res, id, cfg) {
|
|
2966
4425
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
2967
4426
|
const row = cfg.providers.find((p) => p.id === id);
|
|
2968
4427
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
2969
|
-
if (row.apiFormat !== "openai") {
|
|
2970
|
-
return
|
|
4428
|
+
if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
|
|
4429
|
+
return writeJson3(res, 200, { models: [], unsupportedFormat: true });
|
|
2971
4430
|
}
|
|
2972
4431
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
2973
4432
|
const base = row.baseUrl.replace(/\/+$/, "");
|
|
@@ -2975,7 +4434,7 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
2975
4434
|
try {
|
|
2976
4435
|
const headers = { Accept: "application/json" };
|
|
2977
4436
|
if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
2978
|
-
const response = await (0,
|
|
4437
|
+
const response = await (0, import_upstreamFetch3.fetchUpstream)(url, { method: "GET", headers }, { providerId: "byo" });
|
|
2979
4438
|
if (!response.ok) {
|
|
2980
4439
|
const text = await response.text().catch(() => "");
|
|
2981
4440
|
let message = text.slice(0, 300);
|
|
@@ -2984,17 +4443,17 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
2984
4443
|
message = parsed?.error?.message || parsed?.message || message;
|
|
2985
4444
|
} catch {
|
|
2986
4445
|
}
|
|
2987
|
-
return
|
|
4446
|
+
return writeJson3(res, 200, {
|
|
2988
4447
|
models: [],
|
|
2989
4448
|
error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
|
|
2990
4449
|
});
|
|
2991
4450
|
}
|
|
2992
4451
|
const data = await response.json();
|
|
2993
4452
|
const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
|
|
2994
|
-
return
|
|
4453
|
+
return writeJson3(res, 200, { models });
|
|
2995
4454
|
} catch (err5) {
|
|
2996
4455
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
2997
|
-
return
|
|
4456
|
+
return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
|
|
2998
4457
|
}
|
|
2999
4458
|
}
|
|
3000
4459
|
async function handleTestModel(req, res, id, cfg) {
|
|
@@ -3005,13 +4464,13 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3005
4464
|
const model = typeof body["model"] === "string" ? body["model"].trim() : "";
|
|
3006
4465
|
if (!model) return writeJsonError(res, 400, "test requires a { model } string");
|
|
3007
4466
|
if (row.apiFormat === "gemini") {
|
|
3008
|
-
return
|
|
4467
|
+
return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
|
|
3009
4468
|
}
|
|
3010
4469
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
3011
4470
|
if (!resolvedKey) {
|
|
3012
|
-
return
|
|
4471
|
+
return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
|
|
3013
4472
|
}
|
|
3014
|
-
|
|
4473
|
+
let url = row.baseUrl.replace(/\/+$/, "");
|
|
3015
4474
|
const prompt = "Reply with the single word: OK.";
|
|
3016
4475
|
const headers = { "Content-Type": "application/json" };
|
|
3017
4476
|
let payload;
|
|
@@ -3019,6 +4478,10 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3019
4478
|
headers["x-api-key"] = resolvedKey;
|
|
3020
4479
|
headers["anthropic-version"] = "2023-06-01";
|
|
3021
4480
|
payload = { model, max_tokens: 16, messages: [{ role: "user", content: prompt }] };
|
|
4481
|
+
} else if (row.apiFormat === "openai-response") {
|
|
4482
|
+
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
4483
|
+
if (!/\/responses$/.test(url)) url = `${url}/v1/responses`;
|
|
4484
|
+
payload = { model, max_output_tokens: 16, stream: false, input: prompt };
|
|
3022
4485
|
} else {
|
|
3023
4486
|
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
3024
4487
|
payload = {
|
|
@@ -3030,7 +4493,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3030
4493
|
}
|
|
3031
4494
|
const startedAt = Date.now();
|
|
3032
4495
|
try {
|
|
3033
|
-
const response = await (0,
|
|
4496
|
+
const response = await (0, import_upstreamFetch3.fetchUpstream)(
|
|
3034
4497
|
url,
|
|
3035
4498
|
{ method: "POST", headers, body: JSON.stringify(payload) },
|
|
3036
4499
|
{ providerId: "byo" }
|
|
@@ -3044,9 +4507,9 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3044
4507
|
message = parsed?.error?.message || parsed?.message || message;
|
|
3045
4508
|
} catch {
|
|
3046
4509
|
}
|
|
3047
|
-
return
|
|
4510
|
+
return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
|
|
3048
4511
|
}
|
|
3049
|
-
return
|
|
4512
|
+
return writeJson3(res, 200, {
|
|
3050
4513
|
ok: true,
|
|
3051
4514
|
status: response.status,
|
|
3052
4515
|
latencyMs,
|
|
@@ -3054,7 +4517,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3054
4517
|
});
|
|
3055
4518
|
} catch (err5) {
|
|
3056
4519
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
3057
|
-
return
|
|
4520
|
+
return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
|
|
3058
4521
|
}
|
|
3059
4522
|
}
|
|
3060
4523
|
function extractSampleText(text, apiFormat) {
|
|
@@ -3095,7 +4558,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
|
|
|
3095
4558
|
const row = cfg.providers.find((p) => p.id === id);
|
|
3096
4559
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
3097
4560
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3098
|
-
return
|
|
4561
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3099
4562
|
}
|
|
3100
4563
|
function parsePoolKeyInput(body, existing) {
|
|
3101
4564
|
const out = {};
|
|
@@ -3126,7 +4589,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
|
|
|
3126
4589
|
row.apiKeys = [...row.apiKeys ?? [], entry];
|
|
3127
4590
|
persistProviders(cfg, deps);
|
|
3128
4591
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3129
|
-
return
|
|
4592
|
+
return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3130
4593
|
}
|
|
3131
4594
|
async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
3132
4595
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3146,7 +4609,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
3146
4609
|
row.apiKeys[keyIdx] = entry;
|
|
3147
4610
|
persistProviders(cfg, deps);
|
|
3148
4611
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3149
|
-
return
|
|
4612
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3150
4613
|
}
|
|
3151
4614
|
async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
3152
4615
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3160,7 +4623,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
|
3160
4623
|
if (row.apiKeys.length === 0) row.apiKeys = void 0;
|
|
3161
4624
|
persistProviders(cfg, deps);
|
|
3162
4625
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3163
|
-
return
|
|
4626
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3164
4627
|
}
|
|
3165
4628
|
async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
3166
4629
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3174,7 +4637,7 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
3174
4637
|
row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
|
|
3175
4638
|
persistProviders(cfg, deps);
|
|
3176
4639
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3177
|
-
return
|
|
4640
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3178
4641
|
}
|
|
3179
4642
|
function parseApiKeysInput(raw, existing) {
|
|
3180
4643
|
if (!Array.isArray(raw)) return existing;
|
|
@@ -3292,7 +4755,9 @@ function parseProviderInput(body, existing) {
|
|
|
3292
4755
|
const baseUrl = body["baseUrl"];
|
|
3293
4756
|
if (!id) return null;
|
|
3294
4757
|
const name = typeof body["name"] === "string" && body["name"].length > 0 ? body["name"] : body["name"] === null ? void 0 : existing?.name;
|
|
3295
|
-
if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini")
|
|
4758
|
+
if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini" && apiFormat !== "openai-response") {
|
|
4759
|
+
return null;
|
|
4760
|
+
}
|
|
3296
4761
|
if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
|
|
3297
4762
|
const rawKey = body["apiKey"];
|
|
3298
4763
|
let apiKey = typeof rawKey === "string" && rawKey.length > 0 ? rawKey : existing?.apiKey ?? "";
|
|
@@ -3314,10 +4779,11 @@ function parseProviderInput(body, existing) {
|
|
|
3314
4779
|
apiKey = mode.apiKey;
|
|
3315
4780
|
}
|
|
3316
4781
|
}
|
|
4782
|
+
const migrated = migrateFormatAxis(apiFormat, transformer);
|
|
3317
4783
|
return {
|
|
3318
4784
|
id,
|
|
3319
4785
|
name,
|
|
3320
|
-
apiFormat,
|
|
4786
|
+
apiFormat: migrated.apiFormat,
|
|
3321
4787
|
baseUrl: baseUrl.trim(),
|
|
3322
4788
|
apiKey,
|
|
3323
4789
|
models,
|
|
@@ -3328,7 +4794,7 @@ function parseProviderInput(body, existing) {
|
|
|
3328
4794
|
apiVersion,
|
|
3329
4795
|
maxConcurrency,
|
|
3330
4796
|
modelsEndpoint,
|
|
3331
|
-
transformer,
|
|
4797
|
+
transformer: migrated.transformer,
|
|
3332
4798
|
codingPlan,
|
|
3333
4799
|
apiModes,
|
|
3334
4800
|
selectedApiModeId
|
|
@@ -3345,13 +4811,13 @@ function handlePresets(res, method) {
|
|
|
3345
4811
|
baseUrl: p.baseUrl,
|
|
3346
4812
|
models: p.models
|
|
3347
4813
|
}));
|
|
3348
|
-
return
|
|
4814
|
+
return writeJson3(res, 200, { presets, excluded });
|
|
3349
4815
|
}
|
|
3350
4816
|
async function handleKeys(req, res, method, rest, deps) {
|
|
3351
4817
|
if (method === "GET" && rest.length === 0) {
|
|
3352
4818
|
const rows = await deps.keyDb.outboundApiKeysList();
|
|
3353
4819
|
const reader = deps.keySpendReader;
|
|
3354
|
-
if (!reader) return
|
|
4820
|
+
if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
|
|
3355
4821
|
const now = Date.now();
|
|
3356
4822
|
const keys = await Promise.all(
|
|
3357
4823
|
rows.map(async (row) => {
|
|
@@ -3363,13 +4829,13 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3363
4829
|
return info;
|
|
3364
4830
|
})
|
|
3365
4831
|
);
|
|
3366
|
-
return
|
|
4832
|
+
return writeJson3(res, 200, { keys });
|
|
3367
4833
|
}
|
|
3368
4834
|
if (method === "POST" && rest.length === 0) {
|
|
3369
4835
|
const body = await readJsonBody3(req);
|
|
3370
4836
|
const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
|
|
3371
4837
|
const created = await (0, import_outbound_api2.createNamedKey)(deps.keyDb, name);
|
|
3372
|
-
return
|
|
4838
|
+
return writeJson3(res, 201, {
|
|
3373
4839
|
id: created.id,
|
|
3374
4840
|
name: created.name,
|
|
3375
4841
|
keyPrefix: created.keyPrefix,
|
|
@@ -3381,13 +4847,13 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3381
4847
|
const action = rest[1];
|
|
3382
4848
|
if (method === "POST" && id && action === "revoke") {
|
|
3383
4849
|
const ok = await deps.keyDb.outboundApiKeysRevoke(id);
|
|
3384
|
-
return
|
|
4850
|
+
return writeJson3(res, ok ? 200 : 404, { ok });
|
|
3385
4851
|
}
|
|
3386
4852
|
if (method === "POST" && id && action === "enabled") {
|
|
3387
4853
|
const body = await readJsonBody3(req);
|
|
3388
4854
|
const enabled = body["enabled"] === true;
|
|
3389
4855
|
const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
|
|
3390
|
-
return
|
|
4856
|
+
return writeJson3(res, ok ? 200 : 404, { ok, enabled });
|
|
3391
4857
|
}
|
|
3392
4858
|
if (method === "POST" && id && action === "max-concurrency") {
|
|
3393
4859
|
const body = await readJsonBody3(req);
|
|
@@ -3405,14 +4871,14 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3405
4871
|
);
|
|
3406
4872
|
}
|
|
3407
4873
|
const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
|
|
3408
|
-
return
|
|
4874
|
+
return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
|
|
3409
4875
|
}
|
|
3410
4876
|
if (method === "POST" && id && action === "policy") {
|
|
3411
4877
|
const body = await readJsonBody3(req);
|
|
3412
4878
|
const parsed = parseKeyPolicyBody(body);
|
|
3413
4879
|
if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
|
|
3414
4880
|
const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
|
|
3415
|
-
return
|
|
4881
|
+
return writeJson3(res, ok ? 200 : 404, { ok });
|
|
3416
4882
|
}
|
|
3417
4883
|
return writeJsonError(res, 405, `method ${method} not allowed on keys`);
|
|
3418
4884
|
}
|
|
@@ -3423,10 +4889,10 @@ function validateQueueSegments(patch) {
|
|
|
3423
4889
|
errors.push(`${label} must be a number ${min}..${max}`);
|
|
3424
4890
|
}
|
|
3425
4891
|
};
|
|
3426
|
-
const
|
|
4892
|
+
const isPlainObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3427
4893
|
const umq = patch.userMessageQueue;
|
|
3428
4894
|
if (umq !== void 0) {
|
|
3429
|
-
if (!
|
|
4895
|
+
if (!isPlainObject5(umq)) {
|
|
3430
4896
|
errors.push("userMessageQueue must be an object");
|
|
3431
4897
|
} else {
|
|
3432
4898
|
if (typeof umq.enabled !== "boolean") {
|
|
@@ -3438,7 +4904,7 @@ function validateQueueSegments(patch) {
|
|
|
3438
4904
|
}
|
|
3439
4905
|
const cq = patch.concurrencyQueue;
|
|
3440
4906
|
if (cq !== void 0) {
|
|
3441
|
-
if (!
|
|
4907
|
+
if (!isPlainObject5(cq)) {
|
|
3442
4908
|
errors.push("concurrencyQueue must be an object");
|
|
3443
4909
|
} else {
|
|
3444
4910
|
checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
|
|
@@ -3448,7 +4914,7 @@ function validateQueueSegments(patch) {
|
|
|
3448
4914
|
}
|
|
3449
4915
|
const ah = patch.accountHealth;
|
|
3450
4916
|
if (ah !== void 0) {
|
|
3451
|
-
if (!
|
|
4917
|
+
if (!isPlainObject5(ah)) {
|
|
3452
4918
|
errors.push("accountHealth must be an object");
|
|
3453
4919
|
} else {
|
|
3454
4920
|
if (typeof ah.overloadCooldownEnabled !== "boolean") {
|
|
@@ -3459,6 +4925,31 @@ function validateQueueSegments(patch) {
|
|
|
3459
4925
|
}
|
|
3460
4926
|
return errors;
|
|
3461
4927
|
}
|
|
4928
|
+
function validateAllowanceSchedulingSegment(patch) {
|
|
4929
|
+
const value = patch.allowanceScheduling;
|
|
4930
|
+
if (value === void 0) return [];
|
|
4931
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4932
|
+
return ["allowanceScheduling must be an object"];
|
|
4933
|
+
}
|
|
4934
|
+
const allowance = value;
|
|
4935
|
+
const errors = [];
|
|
4936
|
+
const checkNumber = (field, min, max) => {
|
|
4937
|
+
const candidate = allowance[field];
|
|
4938
|
+
if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate < min || candidate > max) {
|
|
4939
|
+
errors.push(`allowanceScheduling.${field} must be a number ${min}..${max}`);
|
|
4940
|
+
}
|
|
4941
|
+
};
|
|
4942
|
+
if (typeof allowance.enabled !== "boolean") {
|
|
4943
|
+
errors.push("allowanceScheduling.enabled must be a boolean");
|
|
4944
|
+
}
|
|
4945
|
+
checkNumber("demoteAtPercent", 0, 100);
|
|
4946
|
+
checkNumber("pauseAtPercent", 0, 100);
|
|
4947
|
+
checkNumber("priorityPenalty", 1, 1e3);
|
|
4948
|
+
if (typeof allowance.demoteAtPercent === "number" && typeof allowance.pauseAtPercent === "number" && allowance.pauseAtPercent < allowance.demoteAtPercent) {
|
|
4949
|
+
errors.push("allowanceScheduling.pauseAtPercent must be >= demoteAtPercent");
|
|
4950
|
+
}
|
|
4951
|
+
return errors;
|
|
4952
|
+
}
|
|
3462
4953
|
async function handleServer(req, res, method, deps) {
|
|
3463
4954
|
if (method === "GET") {
|
|
3464
4955
|
const config = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
|
|
@@ -3466,7 +4957,7 @@ async function handleServer(req, res, method, deps) {
|
|
|
3466
4957
|
if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
|
|
3467
4958
|
if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
|
|
3468
4959
|
if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
|
|
3469
|
-
return
|
|
4960
|
+
return writeJson3(res, 200, { server });
|
|
3470
4961
|
}
|
|
3471
4962
|
if (method === "PUT") {
|
|
3472
4963
|
const patch = await readJsonBody3(req);
|
|
@@ -3474,6 +4965,18 @@ async function handleServer(req, res, method, deps) {
|
|
|
3474
4965
|
if (queueErrors.length > 0) {
|
|
3475
4966
|
return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
|
|
3476
4967
|
}
|
|
4968
|
+
const allowanceErrors = validateAllowanceSchedulingSegment(patch);
|
|
4969
|
+
if (allowanceErrors.length > 0) {
|
|
4970
|
+
return writeJsonError(
|
|
4971
|
+
res,
|
|
4972
|
+
400,
|
|
4973
|
+
`invalid allowance scheduling config: ${allowanceErrors.join("; ")}`
|
|
4974
|
+
);
|
|
4975
|
+
}
|
|
4976
|
+
const bindingErrors = validateGatewayBindingsSegment(patch);
|
|
4977
|
+
if (bindingErrors.length > 0) {
|
|
4978
|
+
return writeJsonError(res, 400, `invalid gateway bindings: ${bindingErrors.join("; ")}`);
|
|
4979
|
+
}
|
|
3477
4980
|
const webhookErrors = validateWebhookSegment(patch);
|
|
3478
4981
|
if (webhookErrors.length > 0) {
|
|
3479
4982
|
return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
|
|
@@ -3497,69 +5000,116 @@ async function handleServer(req, res, method, deps) {
|
|
|
3497
5000
|
if (patch.billing) {
|
|
3498
5001
|
effectivePatch = { ...effectivePatch, billing: preserveBillingSecret(patch.billing, current.billing) };
|
|
3499
5002
|
}
|
|
3500
|
-
const merged = (0, import_outbound_api2.mergeServerConfig)(current, effectivePatch);
|
|
3501
|
-
await (0, import_outbound_api2.saveServerConfig)(deps.settingsStore, merged);
|
|
3502
|
-
setServerProxyConfig(merged.proxy);
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
networkBinding: merged.networkBinding,
|
|
3522
|
-
endpoints: merged.endpoints,
|
|
3523
|
-
port: merged.port,
|
|
3524
|
-
userMessageQueue: merged.userMessageQueue,
|
|
3525
|
-
concurrencyQueue: merged.concurrencyQueue,
|
|
3526
|
-
// voucher-redemption #9: hot-apply the voucher flag so enabling the product
|
|
3527
|
-
// takes effect without a restart.
|
|
3528
|
-
voucher: merged.voucher
|
|
3529
|
-
});
|
|
3530
|
-
} catch (err5) {
|
|
3531
|
-
const missing = incompleteConfigMissing(err5);
|
|
3532
|
-
if (missing) {
|
|
3533
|
-
return writeJson2(res, 200, {
|
|
3534
|
-
server: merged,
|
|
3535
|
-
error: { code: "incomplete-model-config", missing }
|
|
3536
|
-
});
|
|
3537
|
-
}
|
|
3538
|
-
throw err5;
|
|
3539
|
-
}
|
|
3540
|
-
return writeJson2(res, 200, { server: merged });
|
|
5003
|
+
const merged = (0, import_outbound_api2.mergeServerConfig)(current, effectivePatch);
|
|
5004
|
+
await (0, import_outbound_api2.saveServerConfig)(deps.settingsStore, merged);
|
|
5005
|
+
setServerProxyConfig(merged.proxy);
|
|
5006
|
+
(0, import_AccountAllowanceScheduling2.getSharedAccountAllowanceScheduling)().configure(merged.allowanceScheduling);
|
|
5007
|
+
deps.allowanceRefreshScheduler?.configure(merged.allowanceScheduling);
|
|
5008
|
+
applyWebhookConfig(merged.webhook);
|
|
5009
|
+
applyAuditConfig(merged.audit);
|
|
5010
|
+
applyBillingConfig(merged.billing);
|
|
5011
|
+
await deps.outboundApiServer.applyConfig({
|
|
5012
|
+
enabled: merged.enabled,
|
|
5013
|
+
networkBinding: merged.networkBinding,
|
|
5014
|
+
endpoints: merged.endpoints,
|
|
5015
|
+
bindings: merged.bindings,
|
|
5016
|
+
port: merged.port,
|
|
5017
|
+
userMessageQueue: merged.userMessageQueue,
|
|
5018
|
+
concurrencyQueue: merged.concurrencyQueue,
|
|
5019
|
+
// voucher-redemption #9: hot-apply the voucher flag so enabling the product
|
|
5020
|
+
// takes effect without a restart.
|
|
5021
|
+
voucher: merged.voucher
|
|
5022
|
+
});
|
|
5023
|
+
return writeJson3(res, 200, { server: merged });
|
|
3541
5024
|
}
|
|
3542
5025
|
return writeJsonError(res, 405, `method ${method} not allowed on server`);
|
|
3543
5026
|
}
|
|
3544
|
-
function incompleteConfigMissing(err5) {
|
|
3545
|
-
if (typeof err5 !== "object" || err5 === null) return null;
|
|
3546
|
-
const missing = err5.missing;
|
|
3547
|
-
return Array.isArray(missing) ? missing : null;
|
|
3548
|
-
}
|
|
3549
5027
|
async function handleAccounts(req, res, method, rest, deps) {
|
|
5028
|
+
if (rest[0] === "allowances") {
|
|
5029
|
+
return handleAccountAllowanceApi(
|
|
5030
|
+
req,
|
|
5031
|
+
res,
|
|
5032
|
+
method,
|
|
5033
|
+
rest.slice(1),
|
|
5034
|
+
deps.accountAllowanceService
|
|
5035
|
+
);
|
|
5036
|
+
}
|
|
3550
5037
|
if (method === "GET" && rest.length === 0) {
|
|
3551
5038
|
const accounts = await deps.subscriptionAccounts.listAll();
|
|
3552
5039
|
const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
3553
5040
|
const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
|
|
3554
|
-
return
|
|
5041
|
+
return writeJson3(res, 200, { accounts, providerAccounts, externalCli });
|
|
5042
|
+
}
|
|
5043
|
+
if (method === "POST" && rest[0] === "batch" && rest.length === 1) {
|
|
5044
|
+
const body = await readJsonBody3(req);
|
|
5045
|
+
const parsed = validateAccountBatchBody(body);
|
|
5046
|
+
if (!parsed) return writeJsonError(res, 400, "invalid account batch request");
|
|
5047
|
+
const result = await deps.subscriptionTokenWriter.batchManageAccounts(parsed.refs, parsed.mutation);
|
|
5048
|
+
if (!result.ok) {
|
|
5049
|
+
return writeJsonError(
|
|
5050
|
+
res,
|
|
5051
|
+
404,
|
|
5052
|
+
`account '${result.missing.accountId}' not found for provider '${result.missing.providerId}'`
|
|
5053
|
+
);
|
|
5054
|
+
}
|
|
5055
|
+
if (parsed.mutation.action === "delete") {
|
|
5056
|
+
for (const ref of parsed.refs) {
|
|
5057
|
+
deps.accountAllowanceService?.removeAccountSnapshot?.(ref.providerId, ref.accountId);
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
return writeJson3(res, 200, { ok: true, affected: result.affected });
|
|
3555
5061
|
}
|
|
3556
5062
|
if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
|
|
3557
5063
|
const result = handleCodexOAuthStatus(rest[2], deps);
|
|
3558
|
-
return
|
|
5064
|
+
return writeJson3(res, result.status, result.body);
|
|
3559
5065
|
}
|
|
3560
5066
|
if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
|
|
3561
5067
|
const result = handleCodexOAuthCancel(rest[2], deps);
|
|
3562
|
-
return
|
|
5068
|
+
return writeJson3(res, result.status, result.body);
|
|
5069
|
+
}
|
|
5070
|
+
if (method === "GET" && rest.length === 3 && rest[2] === "diagnostics") {
|
|
5071
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5072
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5073
|
+
const accountId = rest[1];
|
|
5074
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5075
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5076
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5077
|
+
}
|
|
5078
|
+
const health2 = (0, import_SubscriptionAccountHealth.getSharedAccountHealth)().getDiagnostics({ providerId, accountId });
|
|
5079
|
+
const allowance = deps.accountAllowanceService?.getSchedulingStatus?.()?.history.filter((entry) => entry.providerId === providerId && entry.accountId === accountId).map((entry) => ({
|
|
5080
|
+
kind: "allowance-policy",
|
|
5081
|
+
at: Date.parse(entry.decidedAt),
|
|
5082
|
+
providerId: entry.providerId,
|
|
5083
|
+
accountId: entry.accountId,
|
|
5084
|
+
action: entry.action,
|
|
5085
|
+
reason: entry.reason,
|
|
5086
|
+
usedPercent: entry.usedPercent,
|
|
5087
|
+
resumeAt: entry.resumeAt
|
|
5088
|
+
})) ?? [];
|
|
5089
|
+
const diagnostics = [...health2, ...allowance].sort((left, right) => right.at - left.at).slice(0, 200);
|
|
5090
|
+
return writeJson3(res, 200, { diagnostics });
|
|
5091
|
+
}
|
|
5092
|
+
if (method === "GET" && rest.length === 3 && rest[2] === "events") {
|
|
5093
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5094
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5095
|
+
const accountId = rest[1];
|
|
5096
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5097
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5098
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5099
|
+
}
|
|
5100
|
+
const snapshot = deps.accountProbeService?.getAllHistory().find((entry) => entry.providerId === providerId && entry.accountId === accountId);
|
|
5101
|
+
const diagnostics = (0, import_SubscriptionAccountHealth.getSharedAccountHealth)().getDiagnostics({ providerId, accountId });
|
|
5102
|
+
return writeJson3(res, 200, { events: snapshot?.records ?? [], diagnostics });
|
|
5103
|
+
}
|
|
5104
|
+
if (method === "PATCH" && rest.length === 2) {
|
|
5105
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5106
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5107
|
+
const body = await readJsonBody3(req);
|
|
5108
|
+
const patch = validateAccountMetadataPatch(body);
|
|
5109
|
+
if (!patch) return writeJsonError(res, 400, "invalid account metadata patch");
|
|
5110
|
+
const result = await deps.subscriptionTokenWriter.patchAccountMetadata(providerId, rest[1], patch);
|
|
5111
|
+
if (!result.ok) return writeJsonError(res, 404, `account '${rest[1]}' not found`);
|
|
5112
|
+
return writeJson3(res, 200, { ok: true });
|
|
3563
5113
|
}
|
|
3564
5114
|
if (method === "PUT" || method === "POST" || method === "DELETE") {
|
|
3565
5115
|
const providerId = asSubscriptionProviderId(rest[0]);
|
|
@@ -3568,12 +5118,12 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3568
5118
|
}
|
|
3569
5119
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
|
|
3570
5120
|
const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
|
|
3571
|
-
return
|
|
5121
|
+
return writeJson3(res, result.status, result.body);
|
|
3572
5122
|
}
|
|
3573
5123
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
|
|
3574
5124
|
const body2 = await readJsonBody3(req);
|
|
3575
5125
|
const result = await handleOAuthComplete(providerId, body2, deps);
|
|
3576
|
-
return
|
|
5126
|
+
return writeJson3(res, result.status, result.body);
|
|
3577
5127
|
}
|
|
3578
5128
|
if (method === "POST" && rest[1] === "accounts") {
|
|
3579
5129
|
const body2 = await readJsonBody3(req);
|
|
@@ -3584,7 +5134,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3584
5134
|
const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
|
|
3585
5135
|
await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
|
|
3586
5136
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3587
|
-
return
|
|
5137
|
+
return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
|
|
3588
5138
|
}
|
|
3589
5139
|
if (method === "POST" && rest[1] === "import-external") {
|
|
3590
5140
|
if (providerId !== "claude" && providerId !== "codex") {
|
|
@@ -3597,7 +5147,13 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3597
5147
|
return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
|
|
3598
5148
|
}
|
|
3599
5149
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3600
|
-
return
|
|
5150
|
+
return writeJson3(res, 200, {
|
|
5151
|
+
ok: true,
|
|
5152
|
+
account: status2 ?? void 0,
|
|
5153
|
+
nativeCredentialMode: result.nativeCredentialMode,
|
|
5154
|
+
refreshWritesNativeCredentials: result.refreshWritesNativeCredentials,
|
|
5155
|
+
message: "Imported a read-only copy. Omnicross does not manage the native CLI credential file and future refreshes do not write it."
|
|
5156
|
+
});
|
|
3601
5157
|
}
|
|
3602
5158
|
if (method === "POST" && rest[1] === "refresh") {
|
|
3603
5159
|
if (providerId === "opencodego") {
|
|
@@ -3606,7 +5162,17 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3606
5162
|
const writer2 = deps.subscriptionTokenWriter;
|
|
3607
5163
|
const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
|
|
3608
5164
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3609
|
-
return
|
|
5165
|
+
return writeJson3(res, 200, { ok, account: status2 ?? void 0 });
|
|
5166
|
+
}
|
|
5167
|
+
if (method === "POST" && rest.length === 3 && rest[2] === "test") {
|
|
5168
|
+
const accountId = rest[1];
|
|
5169
|
+
if (!deps.accountProbeService) return writeJsonError(res, 501, "account probe service unavailable");
|
|
5170
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5171
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5172
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5173
|
+
}
|
|
5174
|
+
const result = await deps.accountProbeService.probeAccount(providerId, accountId);
|
|
5175
|
+
return writeJson3(res, 200, { ok: result.ok, marked: result.marked });
|
|
3610
5176
|
}
|
|
3611
5177
|
if (method === "POST" && rest[2] === "label") {
|
|
3612
5178
|
const accountId = rest[1];
|
|
@@ -3614,7 +5180,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3614
5180
|
const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
|
|
3615
5181
|
const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
|
|
3616
5182
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3617
|
-
return
|
|
5183
|
+
return writeJson3(res, 200, { ok: true });
|
|
3618
5184
|
}
|
|
3619
5185
|
if (method === "POST" && rest[2] === "priority") {
|
|
3620
5186
|
const accountId = rest[1];
|
|
@@ -3626,7 +5192,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3626
5192
|
}
|
|
3627
5193
|
const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
|
|
3628
5194
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3629
|
-
return
|
|
5195
|
+
return writeJson3(res, 200, { ok: true });
|
|
3630
5196
|
}
|
|
3631
5197
|
if (method === "POST" && rest[2] === "proxy") {
|
|
3632
5198
|
const accountId = rest[1];
|
|
@@ -3639,7 +5205,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3639
5205
|
}
|
|
3640
5206
|
const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
|
|
3641
5207
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3642
|
-
return
|
|
5208
|
+
return writeJson3(res, 200, { ok: true });
|
|
3643
5209
|
}
|
|
3644
5210
|
if (method === "POST" && rest[2] === "supported-models") {
|
|
3645
5211
|
const accountId = rest[1];
|
|
@@ -3648,7 +5214,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3648
5214
|
if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
|
|
3649
5215
|
const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
|
|
3650
5216
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3651
|
-
return
|
|
5217
|
+
return writeJson3(res, 200, { ok: true });
|
|
3652
5218
|
}
|
|
3653
5219
|
if (method === "PUT" && rest[1] === "active") {
|
|
3654
5220
|
const body2 = await readJsonBody3(req);
|
|
@@ -3656,17 +5222,22 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3656
5222
|
if (!id) return writeJsonError(res, 400, "active switch requires { id }");
|
|
3657
5223
|
const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
|
|
3658
5224
|
if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
|
|
3659
|
-
return
|
|
5225
|
+
return writeJson3(res, 200, { ok: true });
|
|
3660
5226
|
}
|
|
3661
|
-
if (method === "DELETE" && rest.length
|
|
5227
|
+
if (method === "DELETE" && rest.length === 2) {
|
|
3662
5228
|
const accountId = rest[1];
|
|
3663
5229
|
const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
|
|
3664
5230
|
if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3665
|
-
|
|
5231
|
+
deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
|
|
5232
|
+
return writeJson3(res, 200, { ok: true });
|
|
3666
5233
|
}
|
|
3667
|
-
if (method === "DELETE") {
|
|
5234
|
+
if (method === "DELETE" && rest.length === 1) {
|
|
3668
5235
|
await deps.subscriptionTokenWriter.clearProvider(providerId);
|
|
3669
|
-
|
|
5236
|
+
deps.accountAllowanceService?.removeProviderSnapshots?.(providerId);
|
|
5237
|
+
return writeJson3(res, 200, { ok: true });
|
|
5238
|
+
}
|
|
5239
|
+
if (method === "DELETE") {
|
|
5240
|
+
return writeJsonError(res, 405, "method DELETE not allowed on this accounts path");
|
|
3670
5241
|
}
|
|
3671
5242
|
const body = await readJsonBody3(req);
|
|
3672
5243
|
const config = validateTokenBody(providerId, body);
|
|
@@ -3675,22 +5246,22 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3675
5246
|
}
|
|
3676
5247
|
await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
|
|
3677
5248
|
const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3678
|
-
return
|
|
5249
|
+
return writeJson3(res, 200, status ? { account: status } : { ok: true });
|
|
3679
5250
|
}
|
|
3680
5251
|
return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
|
|
3681
5252
|
}
|
|
3682
5253
|
async function handleCli(req, res, method, rest, deps) {
|
|
3683
5254
|
if (method === "GET" && rest.length === 0) {
|
|
3684
5255
|
const result = handleCliList(process.platform, deps.cliPathProbe);
|
|
3685
|
-
return
|
|
5256
|
+
return writeJson3(res, result.status, result.body);
|
|
3686
5257
|
}
|
|
3687
5258
|
if (method === "GET" && rest[0] === "sessions") {
|
|
3688
5259
|
const result = handleCliSessions();
|
|
3689
|
-
return
|
|
5260
|
+
return writeJson3(res, result.status, result.body);
|
|
3690
5261
|
}
|
|
3691
5262
|
if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
|
|
3692
5263
|
const result = handleCliStop(rest[1]);
|
|
3693
|
-
return
|
|
5264
|
+
return writeJson3(res, result.status, result.body);
|
|
3694
5265
|
}
|
|
3695
5266
|
if (method === "POST" && rest[1] === "install") {
|
|
3696
5267
|
const cli = rest[0];
|
|
@@ -3698,7 +5269,7 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
3698
5269
|
return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
|
|
3699
5270
|
}
|
|
3700
5271
|
const result = await handleCliInstall(cli, deps.cliCommandRunner);
|
|
3701
|
-
return
|
|
5272
|
+
return writeJson3(res, result.status, result.body);
|
|
3702
5273
|
}
|
|
3703
5274
|
if (method === "POST" && rest[1] === "launch") {
|
|
3704
5275
|
const cli = rest[0];
|
|
@@ -3713,28 +5284,100 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
3713
5284
|
opener: deps.cliTerminalOpener,
|
|
3714
5285
|
probe: deps.cliPathProbe
|
|
3715
5286
|
});
|
|
3716
|
-
return
|
|
5287
|
+
return writeJson3(res, result.status, result.body);
|
|
3717
5288
|
}
|
|
3718
5289
|
return writeJsonError(res, 405, `method ${method} not allowed on cli`);
|
|
3719
5290
|
}
|
|
5291
|
+
async function handleIntegrations(req, res, method, rest, deps) {
|
|
5292
|
+
const factory = deps.integrationManagerFactory;
|
|
5293
|
+
if (!factory) return writeJsonError(res, 501, "native CLI integration is not available");
|
|
5294
|
+
const manager = factory();
|
|
5295
|
+
try {
|
|
5296
|
+
if (method === "GET" && rest.length === 0) {
|
|
5297
|
+
return writeJson3(res, 200, {
|
|
5298
|
+
integrations: await manager.listStatus(),
|
|
5299
|
+
gateway: deps.outboundApiServer.getStatus()
|
|
5300
|
+
});
|
|
5301
|
+
}
|
|
5302
|
+
if (method === "POST" && rest.length === 1 && rest[0] === "rotate") {
|
|
5303
|
+
await manager.rotateGatewayKey();
|
|
5304
|
+
return writeJson3(res, 200, { ok: true, integrations: await manager.listStatus() });
|
|
5305
|
+
}
|
|
5306
|
+
const client = rest[0];
|
|
5307
|
+
if (!isIntegrationClient(client)) {
|
|
5308
|
+
return writeJsonError(res, 400, `unknown integration client '${client ?? ""}'`);
|
|
5309
|
+
}
|
|
5310
|
+
if (method === "POST" && rest[1] === "plan") {
|
|
5311
|
+
const body = await readJsonBody3(req);
|
|
5312
|
+
const configPath = body.configPath;
|
|
5313
|
+
if (configPath !== void 0 && typeof configPath !== "string") {
|
|
5314
|
+
return writeJsonError(res, 400, "configPath must be a string");
|
|
5315
|
+
}
|
|
5316
|
+
const plan = await manager.plan(client, configPath);
|
|
5317
|
+
return writeJson3(res, 200, { plan });
|
|
5318
|
+
}
|
|
5319
|
+
if (method === "POST" && (rest[1] === "install" || rest[1] === "apply")) {
|
|
5320
|
+
const body = await readJsonBody3(req);
|
|
5321
|
+
const configPath = body.configPath;
|
|
5322
|
+
if (configPath !== void 0 && typeof configPath !== "string") {
|
|
5323
|
+
return writeJsonError(res, 400, "configPath must be a string");
|
|
5324
|
+
}
|
|
5325
|
+
const status = await manager.install(client, configPath);
|
|
5326
|
+
return writeJson3(res, 200, { integration: status });
|
|
5327
|
+
}
|
|
5328
|
+
if (method === "POST" && rest[1] === "repair") {
|
|
5329
|
+
const status = await manager.repair(client);
|
|
5330
|
+
return writeJson3(res, 200, { integration: status });
|
|
5331
|
+
}
|
|
5332
|
+
if (method === "DELETE" && rest.length === 1 || method === "POST" && rest[1] === "remove") {
|
|
5333
|
+
const status = await manager.remove(client);
|
|
5334
|
+
return writeJson3(res, 200, { integration: status });
|
|
5335
|
+
}
|
|
5336
|
+
return writeJsonError(res, 405, `method ${method} not allowed on integrations`);
|
|
5337
|
+
} catch (error) {
|
|
5338
|
+
if (error instanceof IntegrationConflictError) {
|
|
5339
|
+
return writeJsonError(res, 409, error.message);
|
|
5340
|
+
}
|
|
5341
|
+
throw error;
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
function isIntegrationClient(value) {
|
|
5345
|
+
return value === "codex" || value === "claude";
|
|
5346
|
+
}
|
|
3720
5347
|
async function handleStatus(res, method, deps) {
|
|
3721
5348
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
|
|
3722
5349
|
const status = deps.outboundApiServer.getStatus();
|
|
3723
5350
|
const serverConfig = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
|
|
3724
|
-
const endpoints =
|
|
3725
|
-
|
|
3726
|
-
|
|
5351
|
+
const endpoints = ["chat", "responses", "messages", "gemini"].map((endpoint) => {
|
|
5352
|
+
const routes = (serverConfig.bindings ?? []).filter((binding) => binding.enabled && binding.endpoint === endpoint).map((binding) => (0, import_outbound_api2.gatewayBindingToEndpointConfig)(binding));
|
|
5353
|
+
const useSubscription = routes.some((route) => route.useSubscription);
|
|
5354
|
+
if ((0, import_outbound_api2.isKindMappedEndpoint)(endpoint)) {
|
|
5355
|
+
const kinds = {};
|
|
5356
|
+
for (const route of routes) {
|
|
5357
|
+
for (const [kind, ref] of Object.entries(route.modelMap ?? {})) {
|
|
5358
|
+
if (ref?.trim() && !kinds[kind]) kinds[kind] = ref;
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
return { endpoint, kinds, useSubscription };
|
|
3727
5362
|
}
|
|
3728
|
-
if (
|
|
3729
|
-
return {
|
|
5363
|
+
if (endpoint === "chat") {
|
|
5364
|
+
return {
|
|
5365
|
+
endpoint,
|
|
5366
|
+
models: [...new Set(routes.flatMap((route) => route.models ?? []))],
|
|
5367
|
+
useSubscription
|
|
5368
|
+
};
|
|
3730
5369
|
}
|
|
3731
|
-
return {
|
|
5370
|
+
return {
|
|
5371
|
+
endpoint,
|
|
5372
|
+
model: routes.find((route) => route.defaultModel?.trim())?.defaultModel ?? "",
|
|
5373
|
+
useSubscription
|
|
5374
|
+
};
|
|
3732
5375
|
});
|
|
3733
5376
|
if (status.running) {
|
|
3734
5377
|
const queueStatus = deps.outboundApiServer.getQueueStatus();
|
|
3735
|
-
return
|
|
5378
|
+
return writeJson3(res, 200, { ...status, endpoints, queueStatus });
|
|
3736
5379
|
}
|
|
3737
|
-
return
|
|
5380
|
+
return writeJson3(res, 200, { ...status, endpoints });
|
|
3738
5381
|
}
|
|
3739
5382
|
function resolvePlaygroundPath(endpoint, body) {
|
|
3740
5383
|
switch (endpoint) {
|
|
@@ -3760,16 +5403,16 @@ async function handlePlayground(req, res, method, deps) {
|
|
|
3760
5403
|
const payload = body["body"];
|
|
3761
5404
|
const status = deps.outboundApiServer.getStatus();
|
|
3762
5405
|
if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
|
|
3763
|
-
const path2 = resolvePlaygroundPath(endpoint,
|
|
5406
|
+
const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
|
|
3764
5407
|
if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
|
|
3765
5408
|
const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
3766
5409
|
await proxyToOutbound(res, status.port, path2, key, upstreamBody);
|
|
3767
5410
|
}
|
|
3768
|
-
function
|
|
5411
|
+
function isRecord2(v) {
|
|
3769
5412
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3770
5413
|
}
|
|
3771
5414
|
function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
3772
|
-
return new Promise((
|
|
5415
|
+
return new Promise((resolve2) => {
|
|
3773
5416
|
const upstream = import_node_http.default.request(
|
|
3774
5417
|
{
|
|
3775
5418
|
host: "127.0.0.1",
|
|
@@ -3790,14 +5433,14 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
|
3790
5433
|
proxRes.on("data", (chunk) => res.write(chunk));
|
|
3791
5434
|
proxRes.on("end", () => {
|
|
3792
5435
|
res.end();
|
|
3793
|
-
|
|
5436
|
+
resolve2();
|
|
3794
5437
|
});
|
|
3795
5438
|
}
|
|
3796
5439
|
);
|
|
3797
5440
|
upstream.on("error", (err5) => {
|
|
3798
5441
|
if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
|
|
3799
5442
|
else res.end();
|
|
3800
|
-
|
|
5443
|
+
resolve2();
|
|
3801
5444
|
});
|
|
3802
5445
|
upstream.write(body);
|
|
3803
5446
|
upstream.end();
|
|
@@ -3805,10 +5448,10 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
|
3805
5448
|
}
|
|
3806
5449
|
|
|
3807
5450
|
// src/admin/uiStatic.ts
|
|
3808
|
-
var
|
|
5451
|
+
var import_node_fs7 = require("fs");
|
|
3809
5452
|
var import_promises = require("fs/promises");
|
|
3810
5453
|
var import_node_module = require("module");
|
|
3811
|
-
var
|
|
5454
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
3812
5455
|
var import_meta = {};
|
|
3813
5456
|
var CONTENT_TYPES = {
|
|
3814
5457
|
".html": "text/html; charset=utf-8",
|
|
@@ -3829,13 +5472,13 @@ var CONTENT_TYPES = {
|
|
|
3829
5472
|
function resolveUiDist() {
|
|
3830
5473
|
const fromEnv = process.env["OMNICROSS_UI_DIST"];
|
|
3831
5474
|
if (fromEnv) {
|
|
3832
|
-
return (0,
|
|
5475
|
+
return (0, import_node_fs7.existsSync)(import_node_path7.default.join(fromEnv, "index.html")) ? import_node_path7.default.resolve(fromEnv) : null;
|
|
3833
5476
|
}
|
|
3834
5477
|
try {
|
|
3835
5478
|
const req = (0, import_node_module.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta.url);
|
|
3836
5479
|
const pkgJson = req.resolve("@omnicross/ui/package.json");
|
|
3837
|
-
const dist =
|
|
3838
|
-
return (0,
|
|
5480
|
+
const dist = import_node_path7.default.join(import_node_path7.default.dirname(pkgJson), "dist");
|
|
5481
|
+
return (0, import_node_fs7.existsSync)(import_node_path7.default.join(dist, "index.html")) ? dist : null;
|
|
3839
5482
|
} catch {
|
|
3840
5483
|
return null;
|
|
3841
5484
|
}
|
|
@@ -3877,16 +5520,16 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
3877
5520
|
res.end(JSON.stringify({ error: { type: "bad_request", message: "invalid path" } }));
|
|
3878
5521
|
return true;
|
|
3879
5522
|
}
|
|
3880
|
-
const filePath =
|
|
3881
|
-
if (filePath !== uiDist && !filePath.startsWith(uiDist +
|
|
5523
|
+
const filePath = import_node_path7.default.resolve(uiDist, rel === "" ? "index.html" : rel);
|
|
5524
|
+
if (filePath !== uiDist && !filePath.startsWith(uiDist + import_node_path7.default.sep)) {
|
|
3882
5525
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
3883
5526
|
res.end(JSON.stringify({ error: { type: "forbidden", message: "path outside ui root" } }));
|
|
3884
5527
|
return true;
|
|
3885
5528
|
}
|
|
3886
5529
|
let target = filePath;
|
|
3887
|
-
if (!(0,
|
|
3888
|
-
if (
|
|
3889
|
-
target =
|
|
5530
|
+
if (!(0, import_node_fs7.existsSync)(target) || (0, import_node_fs7.statSync)(target).isDirectory()) {
|
|
5531
|
+
if (import_node_path7.default.extname(rel) === "") {
|
|
5532
|
+
target = import_node_path7.default.join(uiDist, "index.html");
|
|
3890
5533
|
} else {
|
|
3891
5534
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
3892
5535
|
res.end(JSON.stringify({ error: { type: "not_found", message: "no such ui asset" } }));
|
|
@@ -3894,14 +5537,14 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
3894
5537
|
}
|
|
3895
5538
|
}
|
|
3896
5539
|
const body = await (0, import_promises.readFile)(target);
|
|
3897
|
-
const type = CONTENT_TYPES[
|
|
5540
|
+
const type = CONTENT_TYPES[import_node_path7.default.extname(target).toLowerCase()] ?? "application/octet-stream";
|
|
3898
5541
|
res.writeHead(200, { "Content-Type": type, "Content-Length": body.length });
|
|
3899
5542
|
res.end(req.method === "HEAD" ? void 0 : body);
|
|
3900
5543
|
return true;
|
|
3901
5544
|
}
|
|
3902
5545
|
|
|
3903
5546
|
// src/admin/version.ts
|
|
3904
|
-
var DAEMON_VERSION = true ? "0.1.
|
|
5547
|
+
var DAEMON_VERSION = true ? "0.1.6" : "0.0.0-dev";
|
|
3905
5548
|
|
|
3906
5549
|
// src/admin/AdminServer.ts
|
|
3907
5550
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
@@ -3940,14 +5583,14 @@ var AdminServer = class {
|
|
|
3940
5583
|
}
|
|
3941
5584
|
/** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
|
|
3942
5585
|
listen(bindAddr, port) {
|
|
3943
|
-
return new Promise((
|
|
5586
|
+
return new Promise((resolve2, reject) => {
|
|
3944
5587
|
const server = import_node_http2.default.createServer((req, res) => {
|
|
3945
5588
|
this.onRequest(req, res);
|
|
3946
5589
|
});
|
|
3947
5590
|
const onError = (err5) => {
|
|
3948
5591
|
if (err5.code === "EADDRINUSE" && port !== 0) {
|
|
3949
5592
|
server.removeListener("error", onError);
|
|
3950
|
-
this.listen(bindAddr, 0).then(
|
|
5593
|
+
this.listen(bindAddr, 0).then(resolve2, reject);
|
|
3951
5594
|
return;
|
|
3952
5595
|
}
|
|
3953
5596
|
reject(err5);
|
|
@@ -3959,7 +5602,7 @@ var AdminServer = class {
|
|
|
3959
5602
|
server.removeListener("error", onError);
|
|
3960
5603
|
server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
|
|
3961
5604
|
this.server = server;
|
|
3962
|
-
|
|
5605
|
+
resolve2(addr.port);
|
|
3963
5606
|
} else {
|
|
3964
5607
|
reject(new Error("Failed to get admin server address"));
|
|
3965
5608
|
}
|
|
@@ -4040,8 +5683,8 @@ var AdminServer = class {
|
|
|
4040
5683
|
if (!server) return;
|
|
4041
5684
|
this.server = null;
|
|
4042
5685
|
this.boundPort = 0;
|
|
4043
|
-
return new Promise((
|
|
4044
|
-
server.close(() =>
|
|
5686
|
+
return new Promise((resolve2) => {
|
|
5687
|
+
server.close(() => resolve2());
|
|
4045
5688
|
});
|
|
4046
5689
|
}
|
|
4047
5690
|
/** A live status snapshot. */
|
|
@@ -4057,7 +5700,7 @@ function constantTimeEquals(a, b) {
|
|
|
4057
5700
|
const bufA = Buffer.from(a, "utf8");
|
|
4058
5701
|
const bufB = Buffer.from(b, "utf8");
|
|
4059
5702
|
if (bufA.length !== bufB.length) return false;
|
|
4060
|
-
return (0,
|
|
5703
|
+
return (0, import_node_crypto9.timingSafeEqual)(bufA, bufB);
|
|
4061
5704
|
}
|
|
4062
5705
|
|
|
4063
5706
|
// src/admin/health.ts
|
|
@@ -4106,7 +5749,7 @@ function buildHealthReport(deps) {
|
|
|
4106
5749
|
}
|
|
4107
5750
|
|
|
4108
5751
|
// src/admin/oauthSessions.ts
|
|
4109
|
-
var
|
|
5752
|
+
var import_node_crypto10 = __toESM(require("crypto"), 1);
|
|
4110
5753
|
var DEFAULT_OAUTH_SESSION_TTL_MS = 10 * 60 * 1e3;
|
|
4111
5754
|
var OAuthSessionStore = class {
|
|
4112
5755
|
constructor(ttlMs = DEFAULT_OAUTH_SESSION_TTL_MS) {
|
|
@@ -4120,7 +5763,7 @@ var OAuthSessionStore = class {
|
|
|
4120
5763
|
*/
|
|
4121
5764
|
put(session) {
|
|
4122
5765
|
this.sweep();
|
|
4123
|
-
const sessionId =
|
|
5766
|
+
const sessionId = import_node_crypto10.default.randomBytes(24).toString("base64url");
|
|
4124
5767
|
this.sessions.set(sessionId, { ...session, createdAt: Date.now() });
|
|
4125
5768
|
return sessionId;
|
|
4126
5769
|
}
|
|
@@ -4157,7 +5800,7 @@ function pageHtml(message) {
|
|
|
4157
5800
|
return `<!doctype html><meta charset="utf-8"><title>omnicross login</title><body style="font-family:sans-serif;padding:2rem"><h2>${message}</h2><p>You can close this window and return to the terminal.</p></body>`;
|
|
4158
5801
|
}
|
|
4159
5802
|
function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
|
|
4160
|
-
return new Promise((
|
|
5803
|
+
return new Promise((resolve2, reject) => {
|
|
4161
5804
|
let settled = false;
|
|
4162
5805
|
const finish = (server2, fn) => {
|
|
4163
5806
|
if (settled) return;
|
|
@@ -4188,7 +5831,7 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal
|
|
|
4188
5831
|
}
|
|
4189
5832
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4190
5833
|
res.end(pageHtml("Login complete."));
|
|
4191
|
-
finish(server, () =>
|
|
5834
|
+
finish(server, () => resolve2(code));
|
|
4192
5835
|
});
|
|
4193
5836
|
const abort = () => finish(server, () => reject(new Error("login: cancelled")));
|
|
4194
5837
|
if (signal?.aborted) {
|
|
@@ -4283,32 +5926,43 @@ function createPoolKeysLoader(getProviderRow, autoDisabled) {
|
|
|
4283
5926
|
}
|
|
4284
5927
|
|
|
4285
5928
|
// src/commands/paths.ts
|
|
4286
|
-
var
|
|
5929
|
+
var import_node_path8 = require("path");
|
|
4287
5930
|
function defaultVouchersPath(configPath) {
|
|
4288
|
-
return (0,
|
|
5931
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "vouchers.json");
|
|
5932
|
+
}
|
|
5933
|
+
function defaultIntegrationsPath(configPath) {
|
|
5934
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "integrations.json");
|
|
4289
5935
|
}
|
|
4290
5936
|
function defaultPricingPath(configPath) {
|
|
4291
|
-
return (0,
|
|
5937
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "pricing.json");
|
|
5938
|
+
}
|
|
5939
|
+
function defaultPricingRefreshStatePath(configPath) {
|
|
5940
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "pricing-refresh.json");
|
|
5941
|
+
}
|
|
5942
|
+
function defaultAccountAllowancePath(configPath) {
|
|
5943
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "allowance-cache.json");
|
|
4292
5944
|
}
|
|
4293
5945
|
function defaultUsageEventsPath(configPath) {
|
|
4294
|
-
return (0,
|
|
5946
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "usage-events.jsonl");
|
|
4295
5947
|
}
|
|
4296
5948
|
function defaultAuditDir(configPath) {
|
|
4297
|
-
return (0,
|
|
5949
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "audit");
|
|
4298
5950
|
}
|
|
4299
5951
|
function defaultBillingDir(configPath) {
|
|
4300
|
-
return (0,
|
|
5952
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "billing");
|
|
4301
5953
|
}
|
|
4302
5954
|
|
|
4303
5955
|
// src/ports/ConfigFileProviderConfigSource.ts
|
|
4304
|
-
var
|
|
5956
|
+
var import_core2 = require("@omnicross/core");
|
|
4305
5957
|
var EMPTY_CHAIN = {
|
|
4306
5958
|
providerTransformers: [],
|
|
4307
5959
|
modelTransformers: []
|
|
4308
5960
|
};
|
|
4309
5961
|
var FORMAT_TRANSFORMER = {
|
|
5962
|
+
openai: "openai",
|
|
4310
5963
|
anthropic: "anthropic",
|
|
4311
|
-
gemini: "gemini"
|
|
5964
|
+
gemini: "gemini",
|
|
5965
|
+
"openai-response": "openai-response"
|
|
4312
5966
|
};
|
|
4313
5967
|
var ConfigFileProviderConfigSource = class {
|
|
4314
5968
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -4324,8 +5978,8 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4324
5978
|
reloadHook;
|
|
4325
5979
|
constructor(config) {
|
|
4326
5980
|
for (const p of config.providers) this.providers.set(p.id, p);
|
|
4327
|
-
this.transformerService = new
|
|
4328
|
-
void (0,
|
|
5981
|
+
this.transformerService = new import_core2.TransformerService();
|
|
5982
|
+
void (0, import_core2.registerBuiltinTransformers)(this.transformerService);
|
|
4329
5983
|
}
|
|
4330
5984
|
// ── Reload hook (key-pool design D4) ───────────────────────────────────────
|
|
4331
5985
|
/**
|
|
@@ -4346,7 +6000,7 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4346
6000
|
}
|
|
4347
6001
|
/** Await the built-in transformer registration (tests await this before dispatch). */
|
|
4348
6002
|
async ready() {
|
|
4349
|
-
await (0,
|
|
6003
|
+
await (0, import_core2.registerBuiltinTransformers)(this.transformerService);
|
|
4350
6004
|
}
|
|
4351
6005
|
// ── Hot-reload seam (admin dashboard, RT3 design D6) ───────────────────────
|
|
4352
6006
|
/**
|
|
@@ -4377,7 +6031,7 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4377
6031
|
}
|
|
4378
6032
|
async getMainTransformer(providerId) {
|
|
4379
6033
|
const row = this.providers.get(providerId);
|
|
4380
|
-
if (!row
|
|
6034
|
+
if (!row) return null;
|
|
4381
6035
|
const name = FORMAT_TRANSFORMER[row.apiFormat];
|
|
4382
6036
|
const instances = this.transformerService.resolveTransformerReferences([name]);
|
|
4383
6037
|
return instances[0] ?? null;
|
|
@@ -4387,11 +6041,8 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4387
6041
|
if (!row) return EMPTY_CHAIN;
|
|
4388
6042
|
const customRefs = row.transformer?.use ?? [];
|
|
4389
6043
|
if (customRefs.length === 0) return EMPTY_CHAIN;
|
|
4390
|
-
const formatName = row.apiFormat === "openai" ? void 0 : FORMAT_TRANSFORMER[row.apiFormat];
|
|
4391
|
-
const effectiveRefs = formatName ? customRefs.filter((ref) => (typeof ref === "string" ? ref : ref[0]) !== formatName) : customRefs;
|
|
4392
|
-
if (effectiveRefs.length === 0) return EMPTY_CHAIN;
|
|
4393
6044
|
return {
|
|
4394
|
-
providerTransformers: this.transformerService.resolveTransformerReferences(
|
|
6045
|
+
providerTransformers: this.transformerService.resolveTransformerReferences(customRefs),
|
|
4395
6046
|
modelTransformers: []
|
|
4396
6047
|
};
|
|
4397
6048
|
}
|
|
@@ -4422,7 +6073,7 @@ function resolvePreferredApiKey(row) {
|
|
|
4422
6073
|
}
|
|
4423
6074
|
function toLLMProvider(row) {
|
|
4424
6075
|
const apiFormat = row.apiFormat === "gemini" ? "google" : row.apiFormat;
|
|
4425
|
-
const transformer =
|
|
6076
|
+
const transformer = { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
|
|
4426
6077
|
const allModels = row.models ?? [];
|
|
4427
6078
|
const models = row.modelConfigs ? allModels.filter((id) => row.modelConfigs.find((c) => c.id === id)?.enabled !== false) : allModels;
|
|
4428
6079
|
return {
|
|
@@ -4458,7 +6109,7 @@ function toLLMProvider(row) {
|
|
|
4458
6109
|
}
|
|
4459
6110
|
|
|
4460
6111
|
// src/ports/ConfigurableLogger.ts
|
|
4461
|
-
var
|
|
6112
|
+
var import_node_fs8 = require("fs");
|
|
4462
6113
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
4463
6114
|
var RESERVED_JSON_KEYS = /* @__PURE__ */ new Set(["ts", "level", "msg", "error"]);
|
|
4464
6115
|
var ConfigurableLogger = class {
|
|
@@ -4492,7 +6143,7 @@ var ConfigurableLogger = class {
|
|
|
4492
6143
|
const stream = this.fileStream;
|
|
4493
6144
|
this.fileStream = null;
|
|
4494
6145
|
if (!stream) return Promise.resolve();
|
|
4495
|
-
return new Promise((
|
|
6146
|
+
return new Promise((resolve2) => stream.end(() => resolve2()));
|
|
4496
6147
|
}
|
|
4497
6148
|
emit(level, message, error, meta) {
|
|
4498
6149
|
if (LEVEL_ORDER[level] > this.threshold) return;
|
|
@@ -4534,7 +6185,7 @@ var ConfigurableLogger = class {
|
|
|
4534
6185
|
if (this.fileDisabled || !this.filePath) return null;
|
|
4535
6186
|
if (this.fileStream) return this.fileStream;
|
|
4536
6187
|
try {
|
|
4537
|
-
const stream = (0,
|
|
6188
|
+
const stream = (0, import_node_fs8.createWriteStream)(this.filePath, { flags: "a" });
|
|
4538
6189
|
stream.on("error", () => {
|
|
4539
6190
|
this.fileDisabled = true;
|
|
4540
6191
|
this.fileStream = null;
|
|
@@ -4603,7 +6254,7 @@ function safeStringify(value) {
|
|
|
4603
6254
|
}
|
|
4604
6255
|
|
|
4605
6256
|
// src/ports/JsonApiServerSettingsStore.ts
|
|
4606
|
-
var
|
|
6257
|
+
var import_node_fs9 = require("fs");
|
|
4607
6258
|
var import_outbound_api3 = require("@omnicross/core/outbound-api");
|
|
4608
6259
|
var JsonApiServerSettingsStore = class {
|
|
4609
6260
|
/**
|
|
@@ -4630,7 +6281,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
4630
6281
|
if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return;
|
|
4631
6282
|
const file = this.readFile();
|
|
4632
6283
|
file.server = this.encryptSecrets(value);
|
|
4633
|
-
(0,
|
|
6284
|
+
(0, import_node_fs9.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
4634
6285
|
}
|
|
4635
6286
|
/** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
|
|
4636
6287
|
encryptSecrets(config) {
|
|
@@ -4653,7 +6304,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
4653
6304
|
/** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
|
|
4654
6305
|
readFile() {
|
|
4655
6306
|
try {
|
|
4656
|
-
const raw = (0,
|
|
6307
|
+
const raw = (0, import_node_fs9.readFileSync)(this.configPath, "utf8");
|
|
4657
6308
|
const parsed = JSON.parse(raw);
|
|
4658
6309
|
if (parsed && typeof parsed === "object") return parsed;
|
|
4659
6310
|
} catch {
|
|
@@ -4663,8 +6314,8 @@ var JsonApiServerSettingsStore = class {
|
|
|
4663
6314
|
};
|
|
4664
6315
|
|
|
4665
6316
|
// src/ports/JsonlUsageEventStore.ts
|
|
4666
|
-
var
|
|
4667
|
-
var
|
|
6317
|
+
var import_node_crypto11 = require("crypto");
|
|
6318
|
+
var import_node_fs10 = require("fs");
|
|
4668
6319
|
var JsonlUsageEventStore = class {
|
|
4669
6320
|
constructor(eventsPath, isPriced) {
|
|
4670
6321
|
this.eventsPath = eventsPath;
|
|
@@ -4676,10 +6327,10 @@ var JsonlUsageEventStore = class {
|
|
|
4676
6327
|
async insert(input) {
|
|
4677
6328
|
const row = {
|
|
4678
6329
|
...input,
|
|
4679
|
-
id: (0,
|
|
6330
|
+
id: (0, import_node_crypto11.randomUUID)(),
|
|
4680
6331
|
ts: input.ts ?? Date.now()
|
|
4681
6332
|
};
|
|
4682
|
-
(0,
|
|
6333
|
+
(0, import_node_fs10.appendFileSync)(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
|
|
4683
6334
|
return row.id;
|
|
4684
6335
|
}
|
|
4685
6336
|
async getTotals(range) {
|
|
@@ -4774,15 +6425,15 @@ var JsonlUsageEventStore = class {
|
|
|
4774
6425
|
* Used to lazily seed the outbound key-policy spend tracker (once per key). A
|
|
4775
6426
|
* key with no attributed events yields all zeros.
|
|
4776
6427
|
*/
|
|
4777
|
-
async getSpendByKey(
|
|
6428
|
+
async getSpendByKey(query2) {
|
|
4778
6429
|
let totalUsd = 0;
|
|
4779
6430
|
let dailyUsd = 0;
|
|
4780
6431
|
let weeklyUsd = 0;
|
|
4781
|
-
for (const row of this.readRows({ startTs: 0, endTs:
|
|
4782
|
-
if (row.apiKeyId !==
|
|
6432
|
+
for (const row of this.readRows({ startTs: 0, endTs: query2.endTs })) {
|
|
6433
|
+
if (row.apiKeyId !== query2.apiKeyId) continue;
|
|
4783
6434
|
totalUsd += row.costUsd;
|
|
4784
|
-
if (row.ts >=
|
|
4785
|
-
if (row.ts >=
|
|
6435
|
+
if (row.ts >= query2.dayStartTs) dailyUsd += row.costUsd;
|
|
6436
|
+
if (row.ts >= query2.weekStartTs) weeklyUsd += row.costUsd;
|
|
4786
6437
|
}
|
|
4787
6438
|
return { totalUsd, dailyUsd, weeklyUsd };
|
|
4788
6439
|
}
|
|
@@ -4867,10 +6518,10 @@ var JsonlUsageEventStore = class {
|
|
|
4867
6518
|
}
|
|
4868
6519
|
/** Parse every line, skipping malformed/torn lines defensively. */
|
|
4869
6520
|
readAllRows() {
|
|
4870
|
-
if (!(0,
|
|
6521
|
+
if (!(0, import_node_fs10.existsSync)(this.eventsPath)) return [];
|
|
4871
6522
|
let raw;
|
|
4872
6523
|
try {
|
|
4873
|
-
raw = (0,
|
|
6524
|
+
raw = (0, import_node_fs10.readFileSync)(this.eventsPath, "utf8");
|
|
4874
6525
|
} catch {
|
|
4875
6526
|
return [];
|
|
4876
6527
|
}
|
|
@@ -4954,7 +6605,7 @@ function isUsageEventRecord(parsed) {
|
|
|
4954
6605
|
}
|
|
4955
6606
|
|
|
4956
6607
|
// src/ports/JsonOutboundKeyDb.ts
|
|
4957
|
-
var
|
|
6608
|
+
var import_node_fs11 = require("fs");
|
|
4958
6609
|
var JsonOutboundKeyDb = class {
|
|
4959
6610
|
constructor(keysPath) {
|
|
4960
6611
|
this.keysPath = keysPath;
|
|
@@ -4980,7 +6631,10 @@ var JsonOutboundKeyDb = class {
|
|
|
4980
6631
|
enabled: true,
|
|
4981
6632
|
createdAt: input.createdAt ?? Date.now(),
|
|
4982
6633
|
lastUsedAt: null,
|
|
4983
|
-
revokedAt: null
|
|
6634
|
+
revokedAt: null,
|
|
6635
|
+
kind: input.kind,
|
|
6636
|
+
allowedEndpoints: input.allowedEndpoints,
|
|
6637
|
+
loopbackOnly: input.loopbackOnly
|
|
4984
6638
|
};
|
|
4985
6639
|
rows.push(row);
|
|
4986
6640
|
this.writeRows(rows);
|
|
@@ -5057,16 +6711,16 @@ var JsonOutboundKeyDb = class {
|
|
|
5057
6711
|
}
|
|
5058
6712
|
/** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5059
6713
|
readRows() {
|
|
5060
|
-
if (!(0,
|
|
6714
|
+
if (!(0, import_node_fs11.existsSync)(this.keysPath)) return [];
|
|
5061
6715
|
try {
|
|
5062
|
-
const parsed = JSON.parse((0,
|
|
6716
|
+
const parsed = JSON.parse((0, import_node_fs11.readFileSync)(this.keysPath, "utf8"));
|
|
5063
6717
|
return Array.isArray(parsed) ? parsed : [];
|
|
5064
6718
|
} catch {
|
|
5065
6719
|
return [];
|
|
5066
6720
|
}
|
|
5067
6721
|
}
|
|
5068
6722
|
writeRows(rows) {
|
|
5069
|
-
(0,
|
|
6723
|
+
(0, import_node_fs11.writeFileSync)(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
5070
6724
|
}
|
|
5071
6725
|
};
|
|
5072
6726
|
function applyPolicyField(row, field, value) {
|
|
@@ -5076,12 +6730,29 @@ function applyPolicyField(row, field, value) {
|
|
|
5076
6730
|
}
|
|
5077
6731
|
|
|
5078
6732
|
// src/ports/JsonPricingStore.ts
|
|
5079
|
-
var
|
|
6733
|
+
var import_node_fs12 = require("fs");
|
|
6734
|
+
var import_node_crypto12 = require("crypto");
|
|
5080
6735
|
var JsonPricingStore = class {
|
|
5081
6736
|
constructor(pricingPath) {
|
|
5082
6737
|
this.pricingPath = pricingPath;
|
|
5083
6738
|
}
|
|
5084
6739
|
pricingPath;
|
|
6740
|
+
/**
|
|
6741
|
+
* Return whether the durable snapshot can actually serve at least one price.
|
|
6742
|
+
*
|
|
6743
|
+
* This intentionally checks the file itself instead of relying on refresh
|
|
6744
|
+
* metadata: a recent `lastSuccessAt` must not hide a deleted, truncated, or
|
|
6745
|
+
* otherwise unusable pricing table after a crash or manual file edit.
|
|
6746
|
+
*/
|
|
6747
|
+
hasUsableSnapshot() {
|
|
6748
|
+
if (!(0, import_node_fs12.existsSync)(this.pricingPath)) return false;
|
|
6749
|
+
try {
|
|
6750
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(this.pricingPath, "utf8"));
|
|
6751
|
+
return Array.isArray(parsed) && parsed.some(isUsablePricingRow);
|
|
6752
|
+
} catch {
|
|
6753
|
+
return false;
|
|
6754
|
+
}
|
|
6755
|
+
}
|
|
5085
6756
|
async getAll() {
|
|
5086
6757
|
return this.readRows();
|
|
5087
6758
|
}
|
|
@@ -5094,17 +6765,17 @@ var JsonPricingStore = class {
|
|
|
5094
6765
|
*/
|
|
5095
6766
|
async upsert(input, asUserEdit) {
|
|
5096
6767
|
const rows = this.readRows();
|
|
5097
|
-
const entry = this.applyUpsert(rows, input, asUserEdit);
|
|
6768
|
+
const entry = this.applyUpsert(rows, input, asUserEdit, "litellm");
|
|
5098
6769
|
this.writeRows(rows);
|
|
5099
6770
|
return entry;
|
|
5100
6771
|
}
|
|
5101
6772
|
/**
|
|
5102
6773
|
* Apply a batch fetched from a pricing source. Rows whose local copy is
|
|
5103
6774
|
* user-edited are NOT applied — they come back as `{ current, incoming }`
|
|
5104
|
-
* conflicts; everything else is upserted
|
|
5105
|
-
* for the whole batch.
|
|
6775
|
+
* conflicts; everything else is upserted with the supplied automatic source.
|
|
6776
|
+
* ONE file write for the whole batch.
|
|
5106
6777
|
*/
|
|
5107
|
-
async bulkApplyFromSource(entries) {
|
|
6778
|
+
async bulkApplyFromSource(entries, source = "litellm") {
|
|
5108
6779
|
const rows = this.readRows();
|
|
5109
6780
|
const applied = [];
|
|
5110
6781
|
const conflicts = [];
|
|
@@ -5120,7 +6791,8 @@ var JsonPricingStore = class {
|
|
|
5120
6791
|
rows,
|
|
5121
6792
|
incoming,
|
|
5122
6793
|
/* asUserEdit */
|
|
5123
|
-
false
|
|
6794
|
+
false,
|
|
6795
|
+
source
|
|
5124
6796
|
));
|
|
5125
6797
|
}
|
|
5126
6798
|
if (applied.length > 0) this.writeRows(rows);
|
|
@@ -5143,7 +6815,8 @@ var JsonPricingStore = class {
|
|
|
5143
6815
|
rows,
|
|
5144
6816
|
r.incoming,
|
|
5145
6817
|
/* asUserEdit */
|
|
5146
|
-
false
|
|
6818
|
+
false,
|
|
6819
|
+
"litellm"
|
|
5147
6820
|
);
|
|
5148
6821
|
overwrittenCount += 1;
|
|
5149
6822
|
}
|
|
@@ -5164,7 +6837,7 @@ var JsonPricingStore = class {
|
|
|
5164
6837
|
return true;
|
|
5165
6838
|
}
|
|
5166
6839
|
/** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
|
|
5167
|
-
applyUpsert(rows, input, asUserEdit) {
|
|
6840
|
+
applyUpsert(rows, input, asUserEdit, automaticSource) {
|
|
5168
6841
|
const now = Date.now();
|
|
5169
6842
|
const entry = {
|
|
5170
6843
|
providerId: input.providerId,
|
|
@@ -5173,7 +6846,7 @@ var JsonPricingStore = class {
|
|
|
5173
6846
|
outputPricePer1m: input.outputPricePer1m,
|
|
5174
6847
|
cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
|
|
5175
6848
|
cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
|
|
5176
|
-
source: asUserEdit ? "user" :
|
|
6849
|
+
source: asUserEdit ? "user" : automaticSource,
|
|
5177
6850
|
userEdited: asUserEdit,
|
|
5178
6851
|
editedAt: asUserEdit ? now : null,
|
|
5179
6852
|
updatedAt: now
|
|
@@ -5187,21 +6860,142 @@ var JsonPricingStore = class {
|
|
|
5187
6860
|
}
|
|
5188
6861
|
/** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5189
6862
|
readRows() {
|
|
5190
|
-
if (!(0,
|
|
6863
|
+
if (!(0, import_node_fs12.existsSync)(this.pricingPath)) return [];
|
|
5191
6864
|
try {
|
|
5192
|
-
const parsed = JSON.parse((0,
|
|
6865
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(this.pricingPath, "utf8"));
|
|
5193
6866
|
return Array.isArray(parsed) ? parsed : [];
|
|
5194
6867
|
} catch {
|
|
5195
6868
|
return [];
|
|
5196
6869
|
}
|
|
5197
6870
|
}
|
|
5198
6871
|
writeRows(rows) {
|
|
5199
|
-
|
|
6872
|
+
const temporaryPath = `${this.pricingPath}.${process.pid}.${(0, import_node_crypto12.randomUUID)()}.tmp`;
|
|
6873
|
+
try {
|
|
6874
|
+
(0, import_node_fs12.writeFileSync)(temporaryPath, JSON.stringify(rows, null, 2) + "\n", {
|
|
6875
|
+
encoding: "utf8",
|
|
6876
|
+
flag: "wx"
|
|
6877
|
+
});
|
|
6878
|
+
this.replaceFile(temporaryPath);
|
|
6879
|
+
} finally {
|
|
6880
|
+
(0, import_node_fs12.rmSync)(temporaryPath, { force: true });
|
|
6881
|
+
}
|
|
6882
|
+
}
|
|
6883
|
+
/** Isolated for deterministic failure testing; never removes the target. */
|
|
6884
|
+
replaceFile(temporaryPath) {
|
|
6885
|
+
(0, import_node_fs12.renameSync)(temporaryPath, this.pricingPath);
|
|
6886
|
+
}
|
|
6887
|
+
};
|
|
6888
|
+
function isUsablePricingRow(value) {
|
|
6889
|
+
if (!value || typeof value !== "object") return false;
|
|
6890
|
+
const row = value;
|
|
6891
|
+
return typeof row.providerId === "string" && row.providerId.length > 0 && typeof row.modelId === "string" && row.modelId.length > 0 && typeof row.inputPricePer1m === "number" && Number.isFinite(row.inputPricePer1m) && typeof row.outputPricePer1m === "number" && Number.isFinite(row.outputPricePer1m);
|
|
6892
|
+
}
|
|
6893
|
+
|
|
6894
|
+
// src/pricing/PricingRefreshScheduler.ts
|
|
6895
|
+
var import_node_fs13 = require("fs");
|
|
6896
|
+
var EMPTY_STATE2 = {
|
|
6897
|
+
lastAttemptAt: null,
|
|
6898
|
+
lastSuccessAt: null,
|
|
6899
|
+
lastError: null,
|
|
6900
|
+
sources: []
|
|
6901
|
+
};
|
|
6902
|
+
var PricingRefreshScheduler = class {
|
|
6903
|
+
constructor(engine, catalog2, statePath, logger, options = {}) {
|
|
6904
|
+
this.engine = engine;
|
|
6905
|
+
this.catalog = catalog2;
|
|
6906
|
+
this.statePath = statePath;
|
|
6907
|
+
this.logger = logger;
|
|
6908
|
+
this.staleAfterMs = options.staleAfterMs ?? 24 * 60 * 60 * 1e3;
|
|
6909
|
+
this.intervalMs = options.intervalMs ?? 60 * 60 * 1e3;
|
|
6910
|
+
this.now = options.now ?? Date.now;
|
|
6911
|
+
}
|
|
6912
|
+
engine;
|
|
6913
|
+
catalog;
|
|
6914
|
+
statePath;
|
|
6915
|
+
logger;
|
|
6916
|
+
staleAfterMs;
|
|
6917
|
+
intervalMs;
|
|
6918
|
+
now;
|
|
6919
|
+
timer = null;
|
|
6920
|
+
inFlight = null;
|
|
6921
|
+
/** Fire one stale check immediately and arm an unref'ed periodic check. */
|
|
6922
|
+
start() {
|
|
6923
|
+
if (this.timer) return;
|
|
6924
|
+
void this.refreshIfStale();
|
|
6925
|
+
this.timer = setInterval(() => void this.refreshIfStale(), this.intervalMs);
|
|
6926
|
+
this.timer.unref?.();
|
|
6927
|
+
}
|
|
6928
|
+
dispose() {
|
|
6929
|
+
if (this.timer) clearInterval(this.timer);
|
|
6930
|
+
this.timer = null;
|
|
6931
|
+
}
|
|
6932
|
+
getState() {
|
|
6933
|
+
if (!(0, import_node_fs13.existsSync)(this.statePath)) return { ...EMPTY_STATE2, sources: [] };
|
|
6934
|
+
try {
|
|
6935
|
+
const value = JSON.parse((0, import_node_fs13.readFileSync)(this.statePath, "utf8"));
|
|
6936
|
+
return {
|
|
6937
|
+
lastAttemptAt: finiteOrNull(value.lastAttemptAt),
|
|
6938
|
+
lastSuccessAt: finiteOrNull(value.lastSuccessAt),
|
|
6939
|
+
lastError: typeof value.lastError === "string" ? value.lastError : null,
|
|
6940
|
+
sources: Array.isArray(value.sources) ? value.sources : []
|
|
6941
|
+
};
|
|
6942
|
+
} catch {
|
|
6943
|
+
return { ...EMPTY_STATE2, sources: [] };
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6946
|
+
/** Public for admin/manual tests; concurrent checks share one promise. */
|
|
6947
|
+
refreshIfStale(force = false) {
|
|
6948
|
+
if (this.inFlight) return this.inFlight;
|
|
6949
|
+
const state = this.getState();
|
|
6950
|
+
if (!force && this.catalog.hasUsableSnapshot() && state.lastSuccessAt !== null && this.now() - state.lastSuccessAt < this.staleAfterMs) {
|
|
6951
|
+
return Promise.resolve();
|
|
6952
|
+
}
|
|
6953
|
+
const task = this.runRefresh(state);
|
|
6954
|
+
this.inFlight = task;
|
|
6955
|
+
return task.finally(() => {
|
|
6956
|
+
if (this.inFlight === task) this.inFlight = null;
|
|
6957
|
+
});
|
|
6958
|
+
}
|
|
6959
|
+
async runRefresh(previous) {
|
|
6960
|
+
const lastAttemptAt = this.now();
|
|
6961
|
+
try {
|
|
6962
|
+
const result = await this.engine.fetchLatestFromSource();
|
|
6963
|
+
const failed = result.sources.filter((source) => source.status === "failed");
|
|
6964
|
+
const complete = failed.length === 0;
|
|
6965
|
+
this.writeState({
|
|
6966
|
+
lastAttemptAt,
|
|
6967
|
+
// A partial refresh keeps useful rows, but remains stale so the failed
|
|
6968
|
+
// source is retried on the next hourly check instead of 24 hours later.
|
|
6969
|
+
lastSuccessAt: complete ? this.now() : previous.lastSuccessAt,
|
|
6970
|
+
lastError: complete ? null : failed.map((source) => `${source.source}: ${source.error ?? "failed"}`).join("; "),
|
|
6971
|
+
sources: result.sources
|
|
6972
|
+
});
|
|
6973
|
+
} catch (error) {
|
|
6974
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6975
|
+
this.writeState({
|
|
6976
|
+
lastAttemptAt,
|
|
6977
|
+
lastSuccessAt: previous.lastSuccessAt,
|
|
6978
|
+
lastError: message,
|
|
6979
|
+
sources: previous.sources
|
|
6980
|
+
});
|
|
6981
|
+
this.logger.warn("[PricingRefreshScheduler] background refresh failed; cached prices retained", {
|
|
6982
|
+
error: message
|
|
6983
|
+
});
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
writeState(state) {
|
|
6987
|
+
const temporaryPath = `${this.statePath}.tmp`;
|
|
6988
|
+
(0, import_node_fs13.writeFileSync)(temporaryPath, `${JSON.stringify(state, null, 2)}
|
|
6989
|
+
`, "utf8");
|
|
6990
|
+
(0, import_node_fs13.renameSync)(temporaryPath, this.statePath);
|
|
5200
6991
|
}
|
|
5201
6992
|
};
|
|
6993
|
+
function finiteOrNull(value) {
|
|
6994
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6995
|
+
}
|
|
5202
6996
|
|
|
5203
6997
|
// src/ports/JsonVoucherDb.ts
|
|
5204
|
-
var
|
|
6998
|
+
var import_node_fs14 = require("fs");
|
|
5205
6999
|
var JsonVoucherDb = class {
|
|
5206
7000
|
constructor(vouchersPath) {
|
|
5207
7001
|
this.vouchersPath = vouchersPath;
|
|
@@ -5279,55 +7073,32 @@ var JsonVoucherDb = class {
|
|
|
5279
7073
|
}
|
|
5280
7074
|
/** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5281
7075
|
readRows() {
|
|
5282
|
-
if (!(0,
|
|
7076
|
+
if (!(0, import_node_fs14.existsSync)(this.vouchersPath)) return [];
|
|
5283
7077
|
try {
|
|
5284
|
-
const parsed = JSON.parse((0,
|
|
7078
|
+
const parsed = JSON.parse((0, import_node_fs14.readFileSync)(this.vouchersPath, "utf8"));
|
|
5285
7079
|
return Array.isArray(parsed) ? parsed : [];
|
|
5286
7080
|
} catch {
|
|
5287
7081
|
return [];
|
|
5288
7082
|
}
|
|
5289
7083
|
}
|
|
5290
7084
|
writeRows(rows) {
|
|
5291
|
-
(0,
|
|
7085
|
+
(0, import_node_fs14.writeFileSync)(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
5292
7086
|
}
|
|
5293
7087
|
};
|
|
5294
7088
|
|
|
5295
7089
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5296
|
-
var
|
|
5297
|
-
var
|
|
5298
|
-
var
|
|
5299
|
-
var
|
|
5300
|
-
var
|
|
7090
|
+
var import_node_fs16 = require("fs");
|
|
7091
|
+
var import_node_path10 = require("path");
|
|
7092
|
+
var import_SubscriptionAccountHealth2 = require("@omnicross/core/pipeline/SubscriptionAccountHealth");
|
|
7093
|
+
var import_AccountAllowanceScheduling3 = require("@omnicross/core/pipeline/AccountAllowanceScheduling");
|
|
7094
|
+
var import_upstreamFetch4 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
7095
|
+
var import_SubscriptionIdentityStore2 = require("@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore");
|
|
5301
7096
|
var import_subscriptions3 = require("@omnicross/subscriptions");
|
|
5302
7097
|
|
|
5303
7098
|
// src/ports/account-sync.ts
|
|
5304
|
-
var IMPORT_EXPIRY_MARGIN_MS = 6e4;
|
|
5305
7099
|
function viewOf(tokens) {
|
|
5306
7100
|
return tokens;
|
|
5307
7101
|
}
|
|
5308
|
-
function decideExternalImport(captured, external, now = Date.now()) {
|
|
5309
|
-
if (!external?.accessToken) return "no-credential";
|
|
5310
|
-
const capturedRt = viewOf(captured).refreshToken;
|
|
5311
|
-
const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
|
|
5312
|
-
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
|
|
5313
|
-
return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
|
|
5314
|
-
}
|
|
5315
|
-
function buildImportedTokens(captured, external) {
|
|
5316
|
-
const imported = {
|
|
5317
|
-
...captured,
|
|
5318
|
-
accessToken: external.accessToken,
|
|
5319
|
-
status: "authorized",
|
|
5320
|
-
errorMessage: void 0,
|
|
5321
|
-
syncWarning: void 0,
|
|
5322
|
-
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5323
|
-
};
|
|
5324
|
-
if (external.refreshToken) imported.refreshToken = external.refreshToken;
|
|
5325
|
-
if (external.expiresAt) imported.expiresAt = external.expiresAt;
|
|
5326
|
-
else delete imported.expiresAt;
|
|
5327
|
-
if (external.idToken) imported.idToken = external.idToken;
|
|
5328
|
-
if (external.scopes) imported.scopes = external.scopes;
|
|
5329
|
-
return imported;
|
|
5330
|
-
}
|
|
5331
7102
|
function buildTokensFromExternal(provider, external) {
|
|
5332
7103
|
const base = {
|
|
5333
7104
|
authMethod: "oauth",
|
|
@@ -5348,14 +7119,6 @@ function buildTokensFromExternal(provider, external) {
|
|
|
5348
7119
|
if (external.idToken) tokens.idToken = external.idToken;
|
|
5349
7120
|
return tokens;
|
|
5350
7121
|
}
|
|
5351
|
-
function isExternalDivergent(stored, external) {
|
|
5352
|
-
if (!external?.accessToken || !external.refreshToken) return false;
|
|
5353
|
-
const view = viewOf(stored);
|
|
5354
|
-
if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
|
|
5355
|
-
const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
|
|
5356
|
-
const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
|
|
5357
|
-
return !Number.isFinite(storedExp) || externalExp > storedExp;
|
|
5358
|
-
}
|
|
5359
7122
|
function findDuplicateCredentialIds(accounts) {
|
|
5360
7123
|
const byCredential = /* @__PURE__ */ new Map();
|
|
5361
7124
|
for (const account of accounts) {
|
|
@@ -5374,11 +7137,11 @@ function findDuplicateCredentialIds(accounts) {
|
|
|
5374
7137
|
}
|
|
5375
7138
|
|
|
5376
7139
|
// src/ports/external-cli-credentials.ts
|
|
5377
|
-
var
|
|
5378
|
-
var
|
|
5379
|
-
var
|
|
5380
|
-
function externalStorePath(provider, home = (0,
|
|
5381
|
-
return provider === "claude" ? (0,
|
|
7140
|
+
var import_node_fs15 = require("fs");
|
|
7141
|
+
var import_node_os3 = require("os");
|
|
7142
|
+
var import_node_path9 = require("path");
|
|
7143
|
+
function externalStorePath(provider, home = (0, import_node_os3.homedir)()) {
|
|
7144
|
+
return provider === "claude" ? (0, import_node_path9.join)(home, ".claude", ".credentials.json") : (0, import_node_path9.join)(home, ".codex", "auth.json");
|
|
5382
7145
|
}
|
|
5383
7146
|
function decodeJwtExpiryMs(token) {
|
|
5384
7147
|
try {
|
|
@@ -5425,12 +7188,12 @@ function parseCodexTokensEnvelope(raw) {
|
|
|
5425
7188
|
}
|
|
5426
7189
|
return parsed;
|
|
5427
7190
|
}
|
|
5428
|
-
function readExternalCliCredentials(provider, home = (0,
|
|
7191
|
+
function readExternalCliCredentials(provider, home = (0, import_node_os3.homedir)()) {
|
|
5429
7192
|
const path2 = externalStorePath(provider, home);
|
|
5430
|
-
if (!(0,
|
|
7193
|
+
if (!(0, import_node_fs15.existsSync)(path2)) return null;
|
|
5431
7194
|
let raw;
|
|
5432
7195
|
try {
|
|
5433
|
-
const parsed = JSON.parse((0,
|
|
7196
|
+
const parsed = JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
|
|
5434
7197
|
raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
5435
7198
|
} catch {
|
|
5436
7199
|
return null;
|
|
@@ -5438,84 +7201,6 @@ function readExternalCliCredentials(provider, home = (0, import_node_os2.homedir
|
|
|
5438
7201
|
return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
|
|
5439
7202
|
}
|
|
5440
7203
|
|
|
5441
|
-
// src/ports/external-cli-store.ts
|
|
5442
|
-
var import_node_fs12 = require("fs");
|
|
5443
|
-
var import_node_os3 = require("os");
|
|
5444
|
-
var import_node_path7 = require("path");
|
|
5445
|
-
function markerPath(provider, home) {
|
|
5446
|
-
return `${externalStorePath(provider, home)}.omnicross-managed`;
|
|
5447
|
-
}
|
|
5448
|
-
function backupPath(provider, home) {
|
|
5449
|
-
return `${externalStorePath(provider, home)}.omnicross-backup`;
|
|
5450
|
-
}
|
|
5451
|
-
function buildClaudeOAuthEnvelope(tokens) {
|
|
5452
|
-
if (!tokens.accessToken) return null;
|
|
5453
|
-
const envelope = { accessToken: tokens.accessToken };
|
|
5454
|
-
if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
|
|
5455
|
-
if (tokens.expiresAt) {
|
|
5456
|
-
const ms = Date.parse(tokens.expiresAt);
|
|
5457
|
-
if (Number.isFinite(ms)) envelope.expiresAt = ms;
|
|
5458
|
-
}
|
|
5459
|
-
if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
|
|
5460
|
-
return envelope;
|
|
5461
|
-
}
|
|
5462
|
-
function buildCodexTokensEnvelope(tokens) {
|
|
5463
|
-
if (!tokens.accessToken && !tokens.idToken) return null;
|
|
5464
|
-
const envelope = { access_token: tokens.accessToken ?? "" };
|
|
5465
|
-
if (tokens.idToken) envelope.id_token = tokens.idToken;
|
|
5466
|
-
if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
|
|
5467
|
-
return envelope;
|
|
5468
|
-
}
|
|
5469
|
-
function readExistingObject(path2) {
|
|
5470
|
-
if (!(0, import_node_fs12.existsSync)(path2)) return {};
|
|
5471
|
-
try {
|
|
5472
|
-
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
|
|
5473
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5474
|
-
} catch {
|
|
5475
|
-
return {};
|
|
5476
|
-
}
|
|
5477
|
-
}
|
|
5478
|
-
function writeAtomic(path2, content) {
|
|
5479
|
-
(0, import_node_fs12.mkdirSync)((0, import_node_path7.dirname)(path2), { recursive: true });
|
|
5480
|
-
const temp = `${path2}.omnicross-tmp`;
|
|
5481
|
-
(0, import_node_fs12.writeFileSync)(temp, content, "utf8");
|
|
5482
|
-
(0, import_node_fs12.renameSync)(temp, path2);
|
|
5483
|
-
}
|
|
5484
|
-
function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
|
|
5485
|
-
return {
|
|
5486
|
-
readMarkerAccountId(provider) {
|
|
5487
|
-
const path2 = markerPath(provider, home);
|
|
5488
|
-
if (!(0, import_node_fs12.existsSync)(path2)) return void 0;
|
|
5489
|
-
try {
|
|
5490
|
-
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
|
|
5491
|
-
return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
|
|
5492
|
-
} catch {
|
|
5493
|
-
return void 0;
|
|
5494
|
-
}
|
|
5495
|
-
},
|
|
5496
|
-
writeMarker(provider, accountId) {
|
|
5497
|
-
writeAtomic(
|
|
5498
|
-
markerPath(provider, home),
|
|
5499
|
-
JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
5500
|
-
);
|
|
5501
|
-
},
|
|
5502
|
-
writeBack(provider, accountId, tokens) {
|
|
5503
|
-
const owner = this.readMarkerAccountId(provider);
|
|
5504
|
-
if (owner !== accountId) return false;
|
|
5505
|
-
const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
|
|
5506
|
-
if (!envelope) return false;
|
|
5507
|
-
const storePath = externalStorePath(provider, home);
|
|
5508
|
-
if ((0, import_node_fs12.existsSync)(storePath) && !(0, import_node_fs12.existsSync)(backupPath(provider, home))) {
|
|
5509
|
-
(0, import_node_fs12.copyFileSync)(storePath, backupPath(provider, home));
|
|
5510
|
-
}
|
|
5511
|
-
const existing = readExistingObject(storePath);
|
|
5512
|
-
const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
|
|
5513
|
-
writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
|
|
5514
|
-
return true;
|
|
5515
|
-
}
|
|
5516
|
-
};
|
|
5517
|
-
}
|
|
5518
|
-
|
|
5519
7204
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5520
7205
|
var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
|
|
5521
7206
|
var JsonSubscriptionCredentialStore = class {
|
|
@@ -5528,32 +7213,30 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5528
7213
|
* proxy-aware {@link fetchUpstream} that threads the
|
|
5529
7214
|
* `{ providerId, accountId }` ctx (upstream-proxy M1) so a
|
|
5530
7215
|
* per-account/per-provider proxy is honored on refresh exactly
|
|
5531
|
-
* as on relay
|
|
7216
|
+
* as on relay refresh egresses from the SAME proxy IP as the
|
|
5532
7217
|
* account's traffic. NOT used by any read/write path.
|
|
5533
7218
|
*/
|
|
5534
|
-
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials
|
|
7219
|
+
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials) {
|
|
5535
7220
|
this.tokensPath = tokensPath;
|
|
5536
7221
|
this.box = box;
|
|
5537
7222
|
this.fetchImpl = fetchImpl;
|
|
5538
7223
|
this.externalCliReader = externalCliReader;
|
|
5539
|
-
this.externalCliStore = externalCliStore;
|
|
5540
7224
|
}
|
|
5541
7225
|
tokensPath;
|
|
5542
7226
|
box;
|
|
5543
7227
|
fetchImpl;
|
|
5544
7228
|
externalCliReader;
|
|
5545
|
-
externalCliStore;
|
|
5546
7229
|
/**
|
|
5547
7230
|
* The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
|
|
5548
7231
|
* TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
|
|
5549
7232
|
* through {@link fetchUpstream} with the account's `{ providerId, accountId }`
|
|
5550
|
-
* ctx so the per-account/provider proxy applies. `@internal`
|
|
7233
|
+
* ctx so the per-account/provider proxy applies. `@internal` also a test seam.
|
|
5551
7234
|
*/
|
|
5552
7235
|
buildRefreshFetch(providerId, accountId) {
|
|
5553
|
-
return this.fetchImpl ?? ((url, init) => (0,
|
|
7236
|
+
return this.fetchImpl ?? ((url, init) => (0, import_upstreamFetch4.fetchUpstream)(url, init, { providerId, accountId }));
|
|
5554
7237
|
}
|
|
5555
7238
|
/**
|
|
5556
|
-
* In-flight refresh coalescing
|
|
7239
|
+
* In-flight refresh coalescing. OAuth refresh tokens are
|
|
5557
7240
|
* SINGLE-USE: two concurrent refreshes of one account each spend the same
|
|
5558
7241
|
* token and the loser bricks a healthy account. Every refresh entry point
|
|
5559
7242
|
* (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
|
|
@@ -5568,13 +7251,13 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5568
7251
|
return run;
|
|
5569
7252
|
}
|
|
5570
7253
|
/** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
|
|
5571
|
-
* file is absent/corrupt). This is the hot read
|
|
7254
|
+
* file is absent/corrupt). This is the hot read the codex / gemini auth
|
|
5572
7255
|
* strategies pull `accessToken` / `expiresAt` / `status` from it. */
|
|
5573
7256
|
async getFullConfig() {
|
|
5574
7257
|
return this.readConfig();
|
|
5575
7258
|
}
|
|
5576
7259
|
/** Current Claude OAuth access token, or `null` when none is stored. No inline
|
|
5577
|
-
* refresh here
|
|
7260
|
+
* refresh here the lead-window / 401-retry refresh is driven by the
|
|
5578
7261
|
* subscription auth strategy, which calls `refreshClaudeToken` (now real). */
|
|
5579
7262
|
async getValidClaudeAccessToken() {
|
|
5580
7263
|
return this.readConfig().claude?.accessToken ?? null;
|
|
@@ -5599,13 +7282,14 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5599
7282
|
/**
|
|
5600
7283
|
* DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
|
|
5601
7284
|
* each provider's accounts to the secret-free `SubscriptionAccountSanitized`
|
|
5602
|
-
* shape (id/label/status/expiresAt/hasAccessToken/isActive)
|
|
7285
|
+
* shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
|
|
5603
7286
|
* Used by the admin accounts GET (secret-IN-never-OUT).
|
|
5604
7287
|
*/
|
|
5605
7288
|
async listSanitizedAccounts() {
|
|
5606
7289
|
const config = this.readConfig();
|
|
5607
|
-
const health2 = (0,
|
|
5608
|
-
const
|
|
7290
|
+
const health2 = (0, import_SubscriptionAccountHealth2.getSharedAccountHealth)();
|
|
7291
|
+
const allowanceScheduling = (0, import_AccountAllowanceScheduling3.getSharedAccountAllowanceScheduling)();
|
|
7292
|
+
const identityStore = (0, import_SubscriptionIdentityStore2.getSharedIdentityStore)();
|
|
5609
7293
|
const fingerprintOn = identityStore.isEnabled();
|
|
5610
7294
|
const now = Date.now();
|
|
5611
7295
|
const out = {};
|
|
@@ -5614,7 +7298,13 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5614
7298
|
if (sanitized.length === 0) continue;
|
|
5615
7299
|
for (const account of sanitized) {
|
|
5616
7300
|
const status = health2.getStatus(provider, account.id, now);
|
|
7301
|
+
const allowance = allowanceScheduling.preview(provider, account.id, account.priority ?? 50, now);
|
|
5617
7302
|
account.health = status.state;
|
|
7303
|
+
account.schedulable = account.enabled && status.state === "healthy" && allowance.schedulable;
|
|
7304
|
+
account.allowanceAction = allowance.action;
|
|
7305
|
+
account.allowanceEffectivePriority = allowance.effectivePriority;
|
|
7306
|
+
account.allowanceUsedPercent = allowance.usedPercent;
|
|
7307
|
+
account.allowanceResumeAt = allowance.resumeAt;
|
|
5618
7308
|
account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
|
|
5619
7309
|
if (fingerprintOn && provider === "claude") {
|
|
5620
7310
|
account.identityCaptured = identityStore.hasIdentity(provider, account.id);
|
|
@@ -5622,31 +7312,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5622
7312
|
account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
|
|
5623
7313
|
}
|
|
5624
7314
|
}
|
|
5625
|
-
out[provider] = this.
|
|
7315
|
+
out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
|
|
5626
7316
|
}
|
|
5627
7317
|
return out;
|
|
5628
7318
|
}
|
|
5629
7319
|
/**
|
|
5630
|
-
* List-time credential
|
|
5631
|
-
*
|
|
5632
|
-
* credential
|
|
5633
|
-
* rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
|
|
5634
|
-
* a failed refresh (`external-not-rotated`) takes precedence — it is the most
|
|
5635
|
-
* actionable state.
|
|
7320
|
+
* List-time managed-credential conflict warnings. Computed, not persisted:
|
|
7321
|
+
* `duplicate-token` is projected when two accounts of one provider share a
|
|
7322
|
+
* credential. This deliberately does not inspect either native CLI file.
|
|
5636
7323
|
*/
|
|
5637
|
-
|
|
7324
|
+
attachDuplicateWarnings(config, provider, sanitized) {
|
|
5638
7325
|
const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
|
|
5639
|
-
let divergentId;
|
|
5640
|
-
if (provider === "claude" || provider === "codex") {
|
|
5641
|
-
const active = getActiveAccount(config, provider);
|
|
5642
|
-
if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
|
|
5643
|
-
divergentId = active.id;
|
|
5644
|
-
}
|
|
5645
|
-
}
|
|
5646
|
-
if (duplicates.size === 0 && !divergentId) return sanitized;
|
|
5647
7326
|
return sanitized.map((account) => {
|
|
5648
|
-
const computed =
|
|
5649
|
-
|
|
7327
|
+
const computed = duplicates.has(account.id) ? "duplicate-token" : void 0;
|
|
7328
|
+
const persisted = account.syncWarning === "duplicate-token" ? account.syncWarning : void 0;
|
|
7329
|
+
if (!persisted && !computed) {
|
|
7330
|
+
const { syncWarning: _obsoleteWarning, ...withoutWarning } = account;
|
|
7331
|
+
return withoutWarning;
|
|
7332
|
+
}
|
|
7333
|
+
return { ...account, syncWarning: persisted ?? computed };
|
|
5650
7334
|
});
|
|
5651
7335
|
}
|
|
5652
7336
|
/** Read the external CLI store, never letting an fs/parse error escape. */
|
|
@@ -5659,11 +7343,11 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5659
7343
|
}
|
|
5660
7344
|
/**
|
|
5661
7345
|
* Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
|
|
5662
|
-
* the block has no refresh_token (setup-token / manual)
|
|
7346
|
+
* the block has no refresh_token (setup-token / manual) no upstream call, the
|
|
5663
7347
|
* block is untouched. Otherwise mint via the shared claude refresh flow and
|
|
5664
7348
|
* write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
|
|
5665
|
-
* On failure
|
|
5666
|
-
* errorMessage
|
|
7349
|
+
* On failure status:expired +
|
|
7350
|
+
* errorMessage `false`.
|
|
5667
7351
|
*/
|
|
5668
7352
|
async refreshClaudeToken() {
|
|
5669
7353
|
return this.coalesce("claude:active", async () => {
|
|
@@ -5688,19 +7372,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5688
7372
|
syncWarning: void 0
|
|
5689
7373
|
};
|
|
5690
7374
|
this.writeBackById("claude", capturedId, next);
|
|
5691
|
-
this.resyncExternal("claude", capturedId, next);
|
|
5692
7375
|
return true;
|
|
5693
7376
|
} catch (error) {
|
|
5694
|
-
if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
|
|
5695
|
-
const r = await import_subscriptions3.claudeOAuth.refreshAccessToken(rt, refreshFetch);
|
|
5696
|
-
return {
|
|
5697
|
-
accessToken: r.accessToken,
|
|
5698
|
-
refreshToken: r.refreshToken,
|
|
5699
|
-
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5700
|
-
};
|
|
5701
|
-
})) {
|
|
5702
|
-
return true;
|
|
5703
|
-
}
|
|
5704
7377
|
this.markExpiredById("claude", capturedId, claude, error);
|
|
5705
7378
|
return false;
|
|
5706
7379
|
}
|
|
@@ -5735,20 +7408,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5735
7408
|
syncWarning: void 0
|
|
5736
7409
|
};
|
|
5737
7410
|
this.writeBackById("codex", capturedId, next);
|
|
5738
|
-
this.resyncExternal("codex", capturedId, next);
|
|
5739
7411
|
return true;
|
|
5740
7412
|
} catch (error) {
|
|
5741
|
-
if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
|
|
5742
|
-
const r = await import_subscriptions3.codexOAuth.refreshAccessToken(rt, refreshFetch);
|
|
5743
|
-
return {
|
|
5744
|
-
accessToken: r.accessToken,
|
|
5745
|
-
refreshToken: r.refreshToken,
|
|
5746
|
-
idToken: r.idToken,
|
|
5747
|
-
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5748
|
-
};
|
|
5749
|
-
})) {
|
|
5750
|
-
return true;
|
|
5751
|
-
}
|
|
5752
7413
|
this.markExpiredById("codex", capturedId, codex, error);
|
|
5753
7414
|
return false;
|
|
5754
7415
|
}
|
|
@@ -5791,11 +7452,10 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5791
7452
|
});
|
|
5792
7453
|
}
|
|
5793
7454
|
/**
|
|
5794
|
-
* Refresh a SPECIFIC account by id (background scheduler sweep
|
|
5795
|
-
*
|
|
5796
|
-
*
|
|
5797
|
-
*
|
|
5798
|
-
* failure flags ONLY that account `expired`.
|
|
7455
|
+
* Refresh a SPECIFIC managed account by id (background scheduler sweep and
|
|
7456
|
+
* account-pool resolution). It uses only that account's stored refresh
|
|
7457
|
+
* token. Coalesced per `provider:id`; on failure flags ONLY that account
|
|
7458
|
+
* `expired`.
|
|
5799
7459
|
*/
|
|
5800
7460
|
async refreshAccountById(provider, id) {
|
|
5801
7461
|
return this.coalesce(`${provider}:${id}`, async () => {
|
|
@@ -5809,7 +7469,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5809
7469
|
const next = {
|
|
5810
7470
|
...captured,
|
|
5811
7471
|
accessToken: refreshed.accessToken,
|
|
5812
|
-
// Gemini's refresh response omits a new refresh token
|
|
7472
|
+
// Gemini's refresh response omits a new refresh token keep the captured.
|
|
5813
7473
|
refreshToken: refreshed.refreshToken ?? captured.refreshToken,
|
|
5814
7474
|
expiresAt: refreshed.expiresAt,
|
|
5815
7475
|
status: "authorized",
|
|
@@ -5819,7 +7479,6 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5819
7479
|
};
|
|
5820
7480
|
if (refreshed.idToken) next.idToken = refreshed.idToken;
|
|
5821
7481
|
this.writeBackById(provider, id, next);
|
|
5822
|
-
if (provider !== "gemini") this.resyncExternal(provider, id, next);
|
|
5823
7482
|
return true;
|
|
5824
7483
|
} catch (error) {
|
|
5825
7484
|
this.markExpiredById(provider, id, captured, error);
|
|
@@ -5827,7 +7486,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5827
7486
|
}
|
|
5828
7487
|
});
|
|
5829
7488
|
}
|
|
5830
|
-
//
|
|
7489
|
+
// By-id account-pool surface (subscription-account-scheduling, design D6)
|
|
5831
7490
|
/**
|
|
5832
7491
|
* Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
|
|
5833
7492
|
* provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
|
|
@@ -5860,7 +7519,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5860
7519
|
/**
|
|
5861
7520
|
* Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
|
|
5862
7521
|
* `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
|
|
5863
|
-
*
|
|
7522
|
+
* `false` (no refresh affordance).
|
|
5864
7523
|
*/
|
|
5865
7524
|
async refreshAccountToken(providerId, accountId) {
|
|
5866
7525
|
if (providerId === "opencodego") return false;
|
|
@@ -5882,7 +7541,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5882
7541
|
* (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
|
|
5883
7542
|
* whitelisted fingerprint headers; the token mirror is untouched); a no-op for
|
|
5884
7543
|
* an unknown id. Called by the identity store's persistence port on a first-seen
|
|
5885
|
-
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller
|
|
7544
|
+
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
|
|
5886
7545
|
* store's port wrapper swallows a rejection so the relay hot path is unaffected.
|
|
5887
7546
|
*/
|
|
5888
7547
|
async setAccountIdentity(providerId, accountId, identity) {
|
|
@@ -5907,7 +7566,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5907
7566
|
* DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
|
|
5908
7567
|
* the port). Passing `undefined` clears the override. Write-only password: when
|
|
5909
7568
|
* the incoming structured proxy omits the password but the account already had
|
|
5910
|
-
* one, the current (decrypted) password is preserved
|
|
7569
|
+
* one, the current (decrypted) password is preserved editing host/port never
|
|
5911
7570
|
* wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
|
|
5912
7571
|
*/
|
|
5913
7572
|
async setAccountProxy(providerId, accountId, proxy) {
|
|
@@ -5942,75 +7601,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5942
7601
|
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5943
7602
|
};
|
|
5944
7603
|
}
|
|
5945
|
-
/**
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
* UI can tell "genuine revocation" apart from a plain refresh failure.
|
|
5953
|
-
*/
|
|
5954
|
-
async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
|
|
5955
|
-
const markerOwner = this.safeReadMarker(provider);
|
|
5956
|
-
if (markerOwner && markerOwner !== capturedId) return false;
|
|
5957
|
-
const external = this.safeReadExternal(provider);
|
|
5958
|
-
const decision = decideExternalImport(captured, external);
|
|
5959
|
-
if (decision === "not-rotated") {
|
|
5960
|
-
captured.syncWarning = "external-not-rotated";
|
|
5961
|
-
return false;
|
|
5962
|
-
}
|
|
5963
|
-
if (decision !== "import" || !external) return false;
|
|
5964
|
-
let imported = buildImportedTokens(
|
|
5965
|
-
captured,
|
|
5966
|
-
external
|
|
5967
|
-
);
|
|
5968
|
-
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
|
|
5969
|
-
if (!accessStillValid) {
|
|
5970
|
-
try {
|
|
5971
|
-
const refreshed = await refreshWithToken(external.refreshToken);
|
|
5972
|
-
imported = {
|
|
5973
|
-
...imported,
|
|
5974
|
-
accessToken: refreshed.accessToken,
|
|
5975
|
-
refreshToken: refreshed.refreshToken ?? imported.refreshToken,
|
|
5976
|
-
expiresAt: refreshed.expiresAt,
|
|
5977
|
-
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5978
|
-
};
|
|
5979
|
-
if (refreshed.idToken) imported.idToken = refreshed.idToken;
|
|
5980
|
-
} catch {
|
|
5981
|
-
return false;
|
|
5982
|
-
}
|
|
5983
|
-
}
|
|
5984
|
-
this.writeBackById(provider, capturedId, imported);
|
|
5985
|
-
this.resyncExternal(provider, capturedId, imported);
|
|
5986
|
-
return true;
|
|
5987
|
-
}
|
|
5988
|
-
/**
|
|
5989
|
-
* Marker-gated external write-back (external-cli-sync). After a successful
|
|
5990
|
-
* refresh of the account that OWNS the provider's native CLI store (imported
|
|
5991
|
-
* via `importExternalCliAccount`), push the rotated credential back into the
|
|
5992
|
-
* file — otherwise the daemon's refresh invalidates the single-use refresh
|
|
5993
|
-
* token and silently logs the bare CLI out. NON-FATAL: the internal store is
|
|
5994
|
-
* already persisted; a failed external write only leaves the file stale,
|
|
5995
|
-
* which the `external-divergent` warning surfaces.
|
|
5996
|
-
*/
|
|
5997
|
-
resyncExternal(provider, accountId, tokens) {
|
|
5998
|
-
try {
|
|
5999
|
-
this.externalCliStore.writeBack(provider, accountId, tokens);
|
|
6000
|
-
} catch {
|
|
6001
|
-
}
|
|
7604
|
+
/** Atomically patch one account's non-secret management metadata. */
|
|
7605
|
+
async patchAccountMetadata(providerId, accountId, patch) {
|
|
7606
|
+
const config = this.readConfig();
|
|
7607
|
+
const result = patchAccountMetadata(config, providerId, accountId, patch);
|
|
7608
|
+
if (!result.ok) return result;
|
|
7609
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7610
|
+
return result;
|
|
6002
7611
|
}
|
|
6003
|
-
/**
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
7612
|
+
/** Validate every target, then persist one all-or-nothing batch mutation. */
|
|
7613
|
+
async batchManageAccounts(refs, mutation) {
|
|
7614
|
+
const config = this.readConfig();
|
|
7615
|
+
const result = batchManageAccounts(config, refs, mutation);
|
|
7616
|
+
if (!result.ok) return result;
|
|
7617
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7618
|
+
return result;
|
|
6010
7619
|
}
|
|
6011
7620
|
/**
|
|
6012
7621
|
* DAEMON-ONLY (admin import button): which providers have a usable external
|
|
6013
|
-
* CLI credential on THIS machine. Pure detection
|
|
7622
|
+
* CLI credential on THIS machine. Pure detection reads the native files,
|
|
6014
7623
|
* never mutates anything, never returns a token.
|
|
6015
7624
|
*/
|
|
6016
7625
|
async listExternalCliAvailability() {
|
|
@@ -6021,21 +7630,22 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6021
7630
|
}
|
|
6022
7631
|
/**
|
|
6023
7632
|
* DAEMON-ONLY (admin import button): import the external CLI's current login
|
|
6024
|
-
* as a NEW account (+ activate)
|
|
6025
|
-
*
|
|
6026
|
-
*
|
|
6027
|
-
*
|
|
7633
|
+
* as a NEW account (+ activate). This is a COPY-ONLY import: Omnicross never
|
|
7634
|
+
* claims, writes, moves, restores, or deletes the native CLI credential file
|
|
7635
|
+
* or any legacy `.omnicross-managed` marker/backup beside it. Subsequent
|
|
7636
|
+
* refreshes persist only Omnicross's encrypted token store.
|
|
6028
7637
|
*/
|
|
6029
7638
|
async importExternalCliAccount(provider, label) {
|
|
6030
7639
|
const external = this.safeReadExternal(provider);
|
|
6031
7640
|
if (!external?.accessToken) return { ok: false, reason: "no-credential" };
|
|
6032
7641
|
const tokens = buildTokensFromExternal(provider, external);
|
|
6033
7642
|
const result = await this.appendProviderAccount(provider, tokens, label);
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
7643
|
+
return {
|
|
7644
|
+
ok: true,
|
|
7645
|
+
id: result.id,
|
|
7646
|
+
nativeCredentialMode: "read-only",
|
|
7647
|
+
refreshWritesNativeCredentials: false
|
|
7648
|
+
};
|
|
6039
7649
|
}
|
|
6040
7650
|
/**
|
|
6041
7651
|
* Materialize a lazily-synthesized account id to disk (design D3). On a legacy
|
|
@@ -6068,7 +7678,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6068
7678
|
this.writeBackById(providerId, capturedId, {
|
|
6069
7679
|
...block,
|
|
6070
7680
|
status: "expired",
|
|
6071
|
-
errorMessage
|
|
7681
|
+
errorMessage,
|
|
7682
|
+
syncWarning: "syncWarning" in block && block.syncWarning === "duplicate-token" ? "duplicate-token" : void 0
|
|
6072
7683
|
});
|
|
6073
7684
|
}
|
|
6074
7685
|
/**
|
|
@@ -6077,7 +7688,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6077
7688
|
* `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
|
|
6078
7689
|
* OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
|
|
6079
7690
|
* tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
|
|
6080
|
-
* so a first-ever write still produces a valid config. No cache
|
|
7691
|
+
* so a first-ever write still produces a valid config. No cache the next read
|
|
6081
7692
|
* sees this write.
|
|
6082
7693
|
*/
|
|
6083
7694
|
async writeProviderTokens(providerId, config) {
|
|
@@ -6087,7 +7698,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6087
7698
|
}
|
|
6088
7699
|
/**
|
|
6089
7700
|
* DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
|
|
6090
|
-
* (optional label) and set it active, then re-derive the mirror
|
|
7701
|
+
* (optional label) and set it active, then re-derive the mirror used by
|
|
6091
7702
|
* `omnicross login <provider> --label` to add an account instead of overwriting.
|
|
6092
7703
|
*/
|
|
6093
7704
|
async appendProviderAccount(providerId, config, label) {
|
|
@@ -6121,7 +7732,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6121
7732
|
}
|
|
6122
7733
|
/**
|
|
6123
7734
|
* DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
|
|
6124
|
-
* rejects an unknown id. Label-only
|
|
7735
|
+
* rejects an unknown id. Label-only no token material is read or written
|
|
6125
7736
|
* (the secret-free invariant holds).
|
|
6126
7737
|
*/
|
|
6127
7738
|
async renameAccount(providerId, id, label) {
|
|
@@ -6144,12 +7755,12 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6144
7755
|
}
|
|
6145
7756
|
/** Write the merged config to disk as pretty JSON (mkdir parent if needed).
|
|
6146
7757
|
* Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
|
|
6147
|
-
*
|
|
6148
|
-
* write
|
|
7758
|
+
* `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
7759
|
+
* write incl. child 4's future refresh writes lands encrypted. */
|
|
6149
7760
|
persist(config) {
|
|
6150
|
-
(0,
|
|
7761
|
+
(0, import_node_fs16.mkdirSync)((0, import_node_path10.dirname)(this.tokensPath), { recursive: true });
|
|
6151
7762
|
const encrypted = encryptTokens(config, this.box);
|
|
6152
|
-
(0,
|
|
7763
|
+
(0, import_node_fs16.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
|
|
6153
7764
|
}
|
|
6154
7765
|
/**
|
|
6155
7766
|
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
@@ -6157,18 +7768,18 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6157
7768
|
* subscription bearer path is byte-identical).
|
|
6158
7769
|
*
|
|
6159
7770
|
* The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
|
|
6160
|
-
* file
|
|
7771
|
+
* file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
|
|
6161
7772
|
* wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
|
|
6162
|
-
* box's clear, secret-free error (secrets spec "
|
|
6163
|
-
* SHALL fail-fast, SHALL NOT
|
|
6164
|
-
* tokens" and silently send the WRONG bearer upstream
|
|
7773
|
+
* box's clear, secret-free error (secrets spec "/ UX":
|
|
7774
|
+
* SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
|
|
7775
|
+
* tokens" and silently send the WRONG bearer upstream 401). Mirrors
|
|
6165
7776
|
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
6166
7777
|
*/
|
|
6167
7778
|
readConfig() {
|
|
6168
|
-
if (!(0,
|
|
7779
|
+
if (!(0, import_node_fs16.existsSync)(this.tokensPath)) return { updatedAt: "" };
|
|
6169
7780
|
let parsed;
|
|
6170
7781
|
try {
|
|
6171
|
-
const raw = JSON.parse((0,
|
|
7782
|
+
const raw = JSON.parse((0, import_node_fs16.readFileSync)(this.tokensPath, "utf8"));
|
|
6172
7783
|
parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
6173
7784
|
} catch {
|
|
6174
7785
|
parsed = null;
|
|
@@ -6180,7 +7791,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6180
7791
|
};
|
|
6181
7792
|
|
|
6182
7793
|
// src/AccountHealthProbeScheduler.ts
|
|
6183
|
-
var
|
|
7794
|
+
var import_upstreamFetch5 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
6184
7795
|
|
|
6185
7796
|
// src/probe/ProbeStrategy.ts
|
|
6186
7797
|
var PROVIDER_PROBE_PLANS = {
|
|
@@ -6223,7 +7834,7 @@ var AccountHealthProbeScheduler = class {
|
|
|
6223
7834
|
this.logger = logger;
|
|
6224
7835
|
this.config = config;
|
|
6225
7836
|
this.now = opts.now ?? Date.now;
|
|
6226
|
-
this.fetchImpl = opts.fetchImpl ??
|
|
7837
|
+
this.fetchImpl = opts.fetchImpl ?? import_upstreamFetch5.fetchUpstream;
|
|
6227
7838
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6228
7839
|
this.planFor = opts.planFor ?? probePlanFor;
|
|
6229
7840
|
}
|
|
@@ -6496,8 +8107,8 @@ var AccountHealthSweeper = class {
|
|
|
6496
8107
|
};
|
|
6497
8108
|
|
|
6498
8109
|
// src/audit/AuditPruneSweeper.ts
|
|
6499
|
-
var
|
|
6500
|
-
var
|
|
8110
|
+
var import_node_fs17 = require("fs");
|
|
8111
|
+
var import_node_path11 = require("path");
|
|
6501
8112
|
|
|
6502
8113
|
// src/audit/auditFiles.ts
|
|
6503
8114
|
var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
@@ -6571,16 +8182,16 @@ var AuditPruneSweeper = class {
|
|
|
6571
8182
|
if (!this.config.enabled || this.sweeping) return 0;
|
|
6572
8183
|
this.sweeping = true;
|
|
6573
8184
|
try {
|
|
6574
|
-
if (!(0,
|
|
8185
|
+
if (!(0, import_node_fs17.existsSync)(this.auditDir)) return 0;
|
|
6575
8186
|
const today = new Date(this.now());
|
|
6576
8187
|
const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
|
|
6577
8188
|
const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
|
|
6578
8189
|
let removed = 0;
|
|
6579
|
-
for (const file of (0,
|
|
8190
|
+
for (const file of (0, import_node_fs17.readdirSync)(this.auditDir)) {
|
|
6580
8191
|
const dateMs = auditFileDateMs(file);
|
|
6581
8192
|
if (dateMs === null || dateMs >= cutoff) continue;
|
|
6582
8193
|
try {
|
|
6583
|
-
(0,
|
|
8194
|
+
(0, import_node_fs17.unlinkSync)((0, import_node_path11.join)(this.auditDir, file));
|
|
6584
8195
|
removed += 1;
|
|
6585
8196
|
} catch (error) {
|
|
6586
8197
|
this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
|
|
@@ -6603,26 +8214,26 @@ var AuditPruneSweeper = class {
|
|
|
6603
8214
|
};
|
|
6604
8215
|
|
|
6605
8216
|
// src/audit/auditReader.ts
|
|
6606
|
-
var
|
|
6607
|
-
var
|
|
8217
|
+
var import_node_fs18 = require("fs");
|
|
8218
|
+
var import_node_path12 = require("path");
|
|
6608
8219
|
var DEFAULT_LIMIT = 200;
|
|
6609
8220
|
var MAX_LIMIT = 2e3;
|
|
6610
|
-
function readAuditRecords(auditDir2,
|
|
6611
|
-
if (!(0,
|
|
8221
|
+
function readAuditRecords(auditDir2, query2 = {}) {
|
|
8222
|
+
if (!(0, import_node_fs18.existsSync)(auditDir2)) return [];
|
|
6612
8223
|
let files;
|
|
6613
8224
|
try {
|
|
6614
|
-
files = (0,
|
|
8225
|
+
files = (0, import_node_fs18.readdirSync)(auditDir2).filter((f) => AUDIT_FILE_RE.test(f));
|
|
6615
8226
|
} catch {
|
|
6616
8227
|
return [];
|
|
6617
8228
|
}
|
|
6618
|
-
const from = typeof
|
|
6619
|
-
const to = typeof
|
|
6620
|
-
const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(
|
|
8229
|
+
const from = typeof query2.from === "number" ? query2.from : -Infinity;
|
|
8230
|
+
const to = typeof query2.to === "number" ? query2.to : Infinity;
|
|
8231
|
+
const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query2.limit ?? DEFAULT_LIMIT)));
|
|
6621
8232
|
const matched = [];
|
|
6622
8233
|
for (const file of files.sort().reverse()) {
|
|
6623
8234
|
let raw;
|
|
6624
8235
|
try {
|
|
6625
|
-
raw = (0,
|
|
8236
|
+
raw = (0, import_node_fs18.readFileSync)((0, import_node_path12.join)(auditDir2, file), "utf8");
|
|
6626
8237
|
} catch {
|
|
6627
8238
|
continue;
|
|
6628
8239
|
}
|
|
@@ -6636,7 +8247,7 @@ function readAuditRecords(auditDir2, query = {}) {
|
|
|
6636
8247
|
continue;
|
|
6637
8248
|
}
|
|
6638
8249
|
if (!isAuditRecord(rec)) continue;
|
|
6639
|
-
if (
|
|
8250
|
+
if (query2.keyId !== void 0 && rec.keyId !== query2.keyId) continue;
|
|
6640
8251
|
if (rec.ts < from || rec.ts > to) continue;
|
|
6641
8252
|
matched.push(rec);
|
|
6642
8253
|
}
|
|
@@ -6651,8 +8262,8 @@ function isAuditRecord(value) {
|
|
|
6651
8262
|
}
|
|
6652
8263
|
|
|
6653
8264
|
// src/audit/AuditWriter.ts
|
|
6654
|
-
var
|
|
6655
|
-
var
|
|
8265
|
+
var import_node_fs19 = require("fs");
|
|
8266
|
+
var import_node_path13 = require("path");
|
|
6656
8267
|
var AuditWriter = class {
|
|
6657
8268
|
constructor(auditDir2, logger, defer = (fn) => setTimeout(fn, 0)) {
|
|
6658
8269
|
this.auditDir = auditDir2;
|
|
@@ -6685,19 +8296,19 @@ var AuditWriter = class {
|
|
|
6685
8296
|
*/
|
|
6686
8297
|
appendNow(record) {
|
|
6687
8298
|
if (!this.dirEnsured) {
|
|
6688
|
-
(0,
|
|
8299
|
+
(0, import_node_fs19.mkdirSync)(this.auditDir, { recursive: true });
|
|
6689
8300
|
this.dirEnsured = true;
|
|
6690
8301
|
}
|
|
6691
|
-
const file = (0,
|
|
6692
|
-
(0,
|
|
8302
|
+
const file = (0, import_node_path13.join)(this.auditDir, auditFileName(record.ts));
|
|
8303
|
+
(0, import_node_fs19.appendFileSync)(file, JSON.stringify(record) + "\n", "utf8");
|
|
6693
8304
|
}
|
|
6694
8305
|
};
|
|
6695
8306
|
|
|
6696
8307
|
// src/billing/BillingPublisher.ts
|
|
6697
|
-
var
|
|
6698
|
-
var
|
|
6699
|
-
var
|
|
6700
|
-
var
|
|
8308
|
+
var import_node_fs20 = require("fs");
|
|
8309
|
+
var import_node_crypto13 = require("crypto");
|
|
8310
|
+
var import_node_path14 = require("path");
|
|
8311
|
+
var import_upstreamFetch6 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
6701
8312
|
|
|
6702
8313
|
// src/billing/billingFiles.ts
|
|
6703
8314
|
var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
@@ -6720,7 +8331,7 @@ var BillingPublisher = class {
|
|
|
6720
8331
|
constructor(billingDir, logger, opts = {}) {
|
|
6721
8332
|
this.billingDir = billingDir;
|
|
6722
8333
|
this.logger = logger;
|
|
6723
|
-
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0,
|
|
8334
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0, import_upstreamFetch6.fetchUpstream)(url, init));
|
|
6724
8335
|
this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
|
|
6725
8336
|
this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
|
|
6726
8337
|
this.now = opts.now ?? Date.now;
|
|
@@ -6767,8 +8378,8 @@ var BillingPublisher = class {
|
|
|
6767
8378
|
*/
|
|
6768
8379
|
appendNow(event) {
|
|
6769
8380
|
this.ensureDir();
|
|
6770
|
-
const file = (0,
|
|
6771
|
-
(0,
|
|
8381
|
+
const file = (0, import_node_path14.join)(this.billingDir, billingFileName(event.ts));
|
|
8382
|
+
(0, import_node_fs20.appendFileSync)(file, JSON.stringify(event) + "\n", "utf8");
|
|
6772
8383
|
}
|
|
6773
8384
|
/**
|
|
6774
8385
|
* One best-effort delivery attempt for an event ALREADY in the ledger. POSTs the
|
|
@@ -6785,7 +8396,7 @@ var BillingPublisher = class {
|
|
|
6785
8396
|
const headers = { "Content-Type": "application/json" };
|
|
6786
8397
|
const secret = this.config?.secret;
|
|
6787
8398
|
if (secret) {
|
|
6788
|
-
const hmac = (0,
|
|
8399
|
+
const hmac = (0, import_node_crypto13.createHmac)("sha256", secret).update(body).digest("hex");
|
|
6789
8400
|
headers["X-Omnicross-Billing-Signature"] = `sha256=${hmac}`;
|
|
6790
8401
|
}
|
|
6791
8402
|
const res = await this.fetchImpl(endpoint, {
|
|
@@ -6817,8 +8428,8 @@ var BillingPublisher = class {
|
|
|
6817
8428
|
markDelivered(event) {
|
|
6818
8429
|
try {
|
|
6819
8430
|
this.ensureDir();
|
|
6820
|
-
const file = (0,
|
|
6821
|
-
(0,
|
|
8431
|
+
const file = (0, import_node_path14.join)(this.billingDir, deliveredFileName(event.ts));
|
|
8432
|
+
(0, import_node_fs20.appendFileSync)(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
|
|
6822
8433
|
} catch (error) {
|
|
6823
8434
|
this.logger.warn("[BillingPublisher] failed to append delivery marker", {
|
|
6824
8435
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -6827,20 +8438,20 @@ var BillingPublisher = class {
|
|
|
6827
8438
|
}
|
|
6828
8439
|
ensureDir() {
|
|
6829
8440
|
if (this.dirEnsured) return;
|
|
6830
|
-
(0,
|
|
8441
|
+
(0, import_node_fs20.mkdirSync)(this.billingDir, { recursive: true });
|
|
6831
8442
|
this.dirEnsured = true;
|
|
6832
8443
|
}
|
|
6833
8444
|
};
|
|
6834
8445
|
|
|
6835
8446
|
// src/billing/billingReader.ts
|
|
6836
|
-
var
|
|
6837
|
-
var
|
|
8447
|
+
var import_node_fs21 = require("fs");
|
|
8448
|
+
var import_node_path15 = require("path");
|
|
6838
8449
|
function readBillingLedger(billingDir) {
|
|
6839
8450
|
const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
|
|
6840
|
-
if (!(0,
|
|
8451
|
+
if (!(0, import_node_fs21.existsSync)(billingDir)) return view;
|
|
6841
8452
|
let files;
|
|
6842
8453
|
try {
|
|
6843
|
-
files = (0,
|
|
8454
|
+
files = (0, import_node_fs21.readdirSync)(billingDir);
|
|
6844
8455
|
} catch {
|
|
6845
8456
|
return view;
|
|
6846
8457
|
}
|
|
@@ -6871,7 +8482,7 @@ function readBillingStatus(billingDir) {
|
|
|
6871
8482
|
function parseLines(dir, file) {
|
|
6872
8483
|
let raw;
|
|
6873
8484
|
try {
|
|
6874
|
-
raw = (0,
|
|
8485
|
+
raw = (0, import_node_fs21.readFileSync)((0, import_node_path15.join)(dir, file), "utf8");
|
|
6875
8486
|
} catch {
|
|
6876
8487
|
return [];
|
|
6877
8488
|
}
|
|
@@ -7026,8 +8637,9 @@ var TokenRefreshScheduler = class {
|
|
|
7026
8637
|
const expiresAt = Date.parse(t.expiresAt);
|
|
7027
8638
|
return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
|
|
7028
8639
|
}
|
|
7029
|
-
/** Refresh one account; failures are logged, never thrown
|
|
7030
|
-
*
|
|
8640
|
+
/** Refresh one managed account; failures are logged, never thrown. The
|
|
8641
|
+
* store marks only the targeted account `expired` on a failed refresh.
|
|
8642
|
+
*/
|
|
7031
8643
|
async refreshOne(provider, id, isActive) {
|
|
7032
8644
|
try {
|
|
7033
8645
|
const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
|
|
@@ -7057,8 +8669,8 @@ var TokenRefreshScheduler = class {
|
|
|
7057
8669
|
};
|
|
7058
8670
|
|
|
7059
8671
|
// src/webhook/WebhookDispatcher.ts
|
|
7060
|
-
var
|
|
7061
|
-
var
|
|
8672
|
+
var import_node_crypto14 = require("crypto");
|
|
8673
|
+
var import_upstreamFetch7 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
7062
8674
|
var WEBHOOK_MAX_ATTEMPTS = 3;
|
|
7063
8675
|
var WEBHOOK_QUEUE_MAX = 1e3;
|
|
7064
8676
|
var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
|
|
@@ -7078,7 +8690,7 @@ var WebhookDispatcher = class {
|
|
|
7078
8690
|
sleep;
|
|
7079
8691
|
now;
|
|
7080
8692
|
constructor(opts = {}) {
|
|
7081
|
-
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0,
|
|
8693
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0, import_upstreamFetch7.fetchUpstream)(url, init));
|
|
7082
8694
|
this.logger = opts.logger;
|
|
7083
8695
|
this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
|
|
7084
8696
|
this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
|
|
@@ -7189,7 +8801,7 @@ function buildCustom(event, dest) {
|
|
|
7189
8801
|
const body = JSON.stringify(event);
|
|
7190
8802
|
const headers = {};
|
|
7191
8803
|
if (dest.secret) {
|
|
7192
|
-
const hmac = (0,
|
|
8804
|
+
const hmac = (0, import_node_crypto14.createHmac)("sha256", dest.secret).update(body).digest("hex");
|
|
7193
8805
|
headers["X-Omnicross-Signature"] = `sha256=${hmac}`;
|
|
7194
8806
|
}
|
|
7195
8807
|
return { body, headers };
|
|
@@ -7204,7 +8816,7 @@ function buildFeishu(event, dest, nowMs) {
|
|
|
7204
8816
|
const stringToSign = `${timestamp}
|
|
7205
8817
|
${dest.secret}`;
|
|
7206
8818
|
payload["timestamp"] = timestamp;
|
|
7207
|
-
payload["sign"] = (0,
|
|
8819
|
+
payload["sign"] = (0, import_node_crypto14.createHmac)("sha256", stringToSign).digest("base64");
|
|
7208
8820
|
}
|
|
7209
8821
|
return { body: JSON.stringify(payload), headers: {} };
|
|
7210
8822
|
}
|
|
@@ -7232,11 +8844,32 @@ function buildDaemon(config, paths) {
|
|
|
7232
8844
|
setSecretBox(secretBox3);
|
|
7233
8845
|
setSecretBox2(secretBox3);
|
|
7234
8846
|
const decryptedConfig = decryptConfigSecrets(config, secretBox3);
|
|
8847
|
+
const accountAllowanceStore = new import_AccountAllowanceStore4.AccountAllowanceStore(
|
|
8848
|
+
Date.now,
|
|
8849
|
+
void 0,
|
|
8850
|
+
new JsonAccountAllowancePersistence(defaultAccountAllowancePath(paths.configPath))
|
|
8851
|
+
);
|
|
8852
|
+
(0, import_AccountAllowanceStore4.setSharedAccountAllowanceStore)(accountAllowanceStore);
|
|
8853
|
+
(0, import_AccountAllowanceScheduling4.getSharedAccountAllowanceScheduling)().configure(
|
|
8854
|
+
(0, import_outbound_api4.normalizeServerConfig)(decryptedConfig.server).allowanceScheduling
|
|
8855
|
+
);
|
|
7235
8856
|
const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
|
|
7236
8857
|
const keyDb = new JsonOutboundKeyDb(paths.keysPath);
|
|
7237
8858
|
const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
|
|
7238
8859
|
const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
|
|
8860
|
+
const integrationStateStore = new IntegrationStateStore(
|
|
8861
|
+
defaultIntegrationsPath(paths.configPath),
|
|
8862
|
+
secretBox3
|
|
8863
|
+
);
|
|
7239
8864
|
const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
|
|
8865
|
+
const accountAllowanceService = new AccountAllowanceService(credentialStore, accountAllowanceStore);
|
|
8866
|
+
const claudeAllowanceRefreshScheduler = new ClaudeAllowanceRefreshScheduler(
|
|
8867
|
+
accountAllowanceService,
|
|
8868
|
+
logger
|
|
8869
|
+
);
|
|
8870
|
+
claudeAllowanceRefreshScheduler.configure(
|
|
8871
|
+
(0, import_outbound_api4.normalizeServerConfig)(decryptedConfig.server).allowanceScheduling
|
|
8872
|
+
);
|
|
7240
8873
|
const subscriptionAccounts = new import_subscriptions4.SubscriptionAccountService(credentialStore);
|
|
7241
8874
|
(0, import_subscriptions4.setSubscriptionAccountService)(subscriptionAccounts);
|
|
7242
8875
|
const subscriptionRegistry = new import_subscriptions4.SubscriptionProviderRegistry(
|
|
@@ -7245,7 +8878,7 @@ function buildDaemon(config, paths) {
|
|
|
7245
8878
|
);
|
|
7246
8879
|
(0, import_subscriptions4.setSubscriptionProviderRegistry)(subscriptionRegistry);
|
|
7247
8880
|
setServerProxyConfig(decryptedConfig.server?.proxy);
|
|
7248
|
-
(0,
|
|
8881
|
+
(0, import_upstreamFetch8.setUpstreamProxyResolver)(
|
|
7249
8882
|
createUpstreamProxyResolver({
|
|
7250
8883
|
getAccountProxy: (providerId, accountId) => credentialStore.getAccountProxy(providerId, accountId)
|
|
7251
8884
|
})
|
|
@@ -7265,7 +8898,17 @@ function buildDaemon(config, paths) {
|
|
|
7265
8898
|
}
|
|
7266
8899
|
);
|
|
7267
8900
|
const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
|
|
7268
|
-
const pricingEngine = new import_usage.PricingEngine(pricingStore, logger
|
|
8901
|
+
const pricingEngine = new import_usage.PricingEngine(pricingStore, logger, {
|
|
8902
|
+
// Catalog egress follows the same global/env proxy policy as every other
|
|
8903
|
+
// daemon upstream call; no provider/account override applies here.
|
|
8904
|
+
fetchImpl: ((input, init) => (0, import_upstreamFetch8.fetchUpstream)(String(input), init ?? {}))
|
|
8905
|
+
});
|
|
8906
|
+
const pricingRefreshScheduler = new PricingRefreshScheduler(
|
|
8907
|
+
pricingEngine,
|
|
8908
|
+
pricingStore,
|
|
8909
|
+
defaultPricingRefreshStatePath(paths.configPath),
|
|
8910
|
+
logger
|
|
8911
|
+
);
|
|
7269
8912
|
const usageEventStore = new JsonlUsageEventStore(
|
|
7270
8913
|
defaultUsageEventsPath(paths.configPath),
|
|
7271
8914
|
async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
|
|
@@ -7278,7 +8921,7 @@ function buildDaemon(config, paths) {
|
|
|
7278
8921
|
llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
|
|
7279
8922
|
const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
|
|
7280
8923
|
credentialStore,
|
|
7281
|
-
(0,
|
|
8924
|
+
(0, import_SubscriptionAccountHealth3.getSharedAccountHealth)(),
|
|
7282
8925
|
logger,
|
|
7283
8926
|
import_outbound_api4.DEFAULT_ACCOUNT_PROBE
|
|
7284
8927
|
);
|
|
@@ -7326,6 +8969,9 @@ function buildDaemon(config, paths) {
|
|
|
7326
8969
|
settingsStore,
|
|
7327
8970
|
outboundApiServer,
|
|
7328
8971
|
subscriptionAccounts,
|
|
8972
|
+
accountAllowanceService,
|
|
8973
|
+
allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
|
|
8974
|
+
accountProbeService: accountHealthProbeScheduler,
|
|
7329
8975
|
// Least-authority token WRITER (design D4) — the concrete credential store
|
|
7330
8976
|
// exposes `writeProviderTokens` / `clearProvider` as daemon-only methods (NOT
|
|
7331
8977
|
// on the `SubscriptionCredentialStore` port). The admin API sees ONLY these two
|
|
@@ -7346,7 +8992,7 @@ function buildDaemon(config, paths) {
|
|
|
7346
8992
|
// inject a mock so no real token endpoint is hit.
|
|
7347
8993
|
// upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
|
|
7348
8994
|
// helper so interactive login honors a configured proxy (global/env layers).
|
|
7349
|
-
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => (0,
|
|
8995
|
+
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => (0, import_upstreamFetch8.fetchUpstream)(url, init)),
|
|
7350
8996
|
subscriptionAccountAppender: credentialStore,
|
|
7351
8997
|
// Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
|
|
7352
8998
|
// + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
|
|
@@ -7364,6 +9010,16 @@ function buildDaemon(config, paths) {
|
|
|
7364
9010
|
cliTerminalOpener: paths.cliTerminalOpener,
|
|
7365
9011
|
cliPathProbe: paths.cliPathProbe,
|
|
7366
9012
|
cliCommandRunner: paths.cliCommandRunner,
|
|
9013
|
+
integrationManagerFactory: () => {
|
|
9014
|
+
const live = outboundApiServer.getStatus();
|
|
9015
|
+
const port = live.port || decryptedConfig.server?.port || import_outbound_api4.DEFAULT_OUTBOUND_PORT;
|
|
9016
|
+
return new IntegrationManager({
|
|
9017
|
+
configPath: paths.configPath,
|
|
9018
|
+
gatewayBaseUrl: live.loopbackUrl ?? `http://127.0.0.1:${port}`,
|
|
9019
|
+
keyDb,
|
|
9020
|
+
stateStore: integrationStateStore
|
|
9021
|
+
});
|
|
9022
|
+
},
|
|
7367
9023
|
// Usage/pricing admin surface (usage-pricing child): stats queries go
|
|
7368
9024
|
// through the recorder facade, pricing mutations through the engine, and
|
|
7369
9025
|
// the row DELETE through the concrete store (delete is store-local — the
|
|
@@ -7388,16 +9044,16 @@ function buildDaemon(config, paths) {
|
|
|
7388
9044
|
// date-rotated audit store. Bound to the store dir here so the AdminServer
|
|
7389
9045
|
// carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
|
|
7390
9046
|
// NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
|
|
7391
|
-
auditReader: (
|
|
9047
|
+
auditReader: (query2) => readAuditRecords(auditDir2, query2),
|
|
7392
9048
|
// billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
|
|
7393
9049
|
// secret-free total/delivered/pending counts of the durable ledger.
|
|
7394
9050
|
billingStatusReader: () => readBillingStatus(billingDir)
|
|
7395
9051
|
});
|
|
7396
9052
|
const webhookDispatcher = new WebhookDispatcher({
|
|
7397
9053
|
logger,
|
|
7398
|
-
fetchImpl: (url, init) => (0,
|
|
9054
|
+
fetchImpl: (url, init) => (0, import_upstreamFetch8.fetchUpstream)(url, init)
|
|
7399
9055
|
});
|
|
7400
|
-
setWebhookRuntime(webhookDispatcher, (0,
|
|
9056
|
+
setWebhookRuntime(webhookDispatcher, (0, import_SubscriptionAccountHealth3.getSharedAccountHealth)());
|
|
7401
9057
|
const auditWriter = new AuditWriter(auditDir2, logger);
|
|
7402
9058
|
const auditPruneSweeper = new AuditPruneSweeper(auditDir2, logger, import_audit_types.DEFAULT_AUDIT_CONFIG);
|
|
7403
9059
|
setAuditRuntime(auditWriter, auditPruneSweeper, auditDir2);
|
|
@@ -7412,7 +9068,7 @@ function buildDaemon(config, paths) {
|
|
|
7412
9068
|
const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
|
|
7413
9069
|
const accountHealthSweeper = new AccountHealthSweeper(
|
|
7414
9070
|
credentialStore,
|
|
7415
|
-
(0,
|
|
9071
|
+
(0, import_SubscriptionAccountHealth3.getSharedAccountHealth)(),
|
|
7416
9072
|
logger
|
|
7417
9073
|
);
|
|
7418
9074
|
return {
|
|
@@ -7427,8 +9083,11 @@ function buildDaemon(config, paths) {
|
|
|
7427
9083
|
credentialStore,
|
|
7428
9084
|
subscriptionRegistry,
|
|
7429
9085
|
subscriptionAccounts,
|
|
9086
|
+
accountAllowanceService,
|
|
9087
|
+
claudeAllowanceRefreshScheduler,
|
|
7430
9088
|
pricingStore,
|
|
7431
9089
|
pricingEngine,
|
|
9090
|
+
pricingRefreshScheduler,
|
|
7432
9091
|
usageRecorder,
|
|
7433
9092
|
adminServer,
|
|
7434
9093
|
tokenRefreshScheduler,
|
|
@@ -7447,7 +9106,7 @@ function resetDaemonSingletonsForTests() {
|
|
|
7447
9106
|
(0, import_subscriptionRegistryPort.setSubscriptionRegistryForOutbound)(null);
|
|
7448
9107
|
(0, import_subscriptions4.setSubscriptionProviderRegistry)(null);
|
|
7449
9108
|
(0, import_subscriptions4.setSubscriptionAccountService)(null);
|
|
7450
|
-
(0,
|
|
9109
|
+
(0, import_upstreamFetch8.setUpstreamProxyResolver)(null);
|
|
7451
9110
|
setServerProxyConfig(void 0);
|
|
7452
9111
|
(0, import_gemini_code_assist_resolver.setGeminiCodeAssistResolver)(null);
|
|
7453
9112
|
setSecretBox(null);
|
|
@@ -7455,12 +9114,14 @@ function resetDaemonSingletonsForTests() {
|
|
|
7455
9114
|
resetWebhookRuntimeForTests();
|
|
7456
9115
|
resetAuditRuntimeForTests();
|
|
7457
9116
|
resetBillingRuntimeForTests();
|
|
7458
|
-
(0,
|
|
9117
|
+
(0, import_SubscriptionIdentityStore3.__resetSharedIdentityStoreForTests)();
|
|
9118
|
+
(0, import_AccountAllowanceStore4.__resetSharedAccountAllowanceStoreForTests)();
|
|
9119
|
+
(0, import_AccountAllowanceScheduling4.__resetSharedAccountAllowanceSchedulingForTests)();
|
|
7459
9120
|
}
|
|
7460
9121
|
function isTokensStoreReadable(tokensPath) {
|
|
7461
9122
|
try {
|
|
7462
|
-
if (!(0,
|
|
7463
|
-
(0,
|
|
9123
|
+
if (!(0, import_node_fs22.existsSync)(tokensPath)) return true;
|
|
9124
|
+
(0, import_node_fs22.accessSync)(tokensPath, import_node_fs22.constants.R_OK);
|
|
7464
9125
|
return true;
|
|
7465
9126
|
} catch {
|
|
7466
9127
|
return false;
|
|
@@ -7506,6 +9167,9 @@ function inferApiFormat(provider) {
|
|
|
7506
9167
|
if (hay.includes("generativelanguage") || hay.includes("gemini") || hay.includes("google")) {
|
|
7507
9168
|
return { format: "gemini", ambiguous: false };
|
|
7508
9169
|
}
|
|
9170
|
+
if (hay.includes("/responses")) {
|
|
9171
|
+
return { format: "openai-response", ambiguous: false };
|
|
9172
|
+
}
|
|
7509
9173
|
if (hay.includes("openai") || hay.includes("/v1") || hay.includes("chat/completions")) {
|
|
7510
9174
|
return { format: "openai", ambiguous: false };
|
|
7511
9175
|
}
|