@omnicross/daemon 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2438 -667
- package/dist/cli.js +2430 -626
- package/dist/index.cjs +2302 -630
- package/dist/index.d.cts +452 -189
- package/dist/index.d.ts +452 -189
- package/dist/index.js +2289 -584
- package/package.json +1 -1
package/dist/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,12 +3234,16 @@ function preserveWebhookSecrets(incoming, current) {
|
|
|
1903
3234
|
}
|
|
1904
3235
|
|
|
1905
3236
|
// src/audit/auditRuntime.ts
|
|
3237
|
+
var import_node_path6 = require("path");
|
|
1906
3238
|
var import_auditSink = require("@omnicross/core/pipeline/auditSink");
|
|
3239
|
+
var import_upstreamTrace = require("@omnicross/core/pipeline/upstreamTrace");
|
|
1907
3240
|
var writer = null;
|
|
1908
3241
|
var sweeper = null;
|
|
1909
|
-
|
|
3242
|
+
var auditDir = "";
|
|
3243
|
+
function setAuditRuntime(w, s, dir) {
|
|
1910
3244
|
writer = w;
|
|
1911
3245
|
sweeper = s;
|
|
3246
|
+
auditDir = dir;
|
|
1912
3247
|
}
|
|
1913
3248
|
function applyAuditConfig(config) {
|
|
1914
3249
|
const enabled = config?.enabled === true && writer !== null;
|
|
@@ -1920,9 +3255,11 @@ function applyAuditConfig(config) {
|
|
|
1920
3255
|
sweeper.configure(config);
|
|
1921
3256
|
sweeper.start();
|
|
1922
3257
|
}
|
|
3258
|
+
(0, import_upstreamTrace.setUpstreamTracePath)(config.captureBodies ? (0, import_node_path6.join)(auditDir, "upstream-trace.jsonl") : null);
|
|
1923
3259
|
} else {
|
|
1924
3260
|
(0, import_auditSink.setAuditCaptureConfig)(null);
|
|
1925
3261
|
(0, import_auditSink.setAuditSink)(null);
|
|
3262
|
+
(0, import_upstreamTrace.setUpstreamTracePath)(null);
|
|
1926
3263
|
if (sweeper) {
|
|
1927
3264
|
if (config) sweeper.configure(config);
|
|
1928
3265
|
sweeper.dispose();
|
|
@@ -1932,9 +3269,11 @@ function applyAuditConfig(config) {
|
|
|
1932
3269
|
function resetAuditRuntimeForTests() {
|
|
1933
3270
|
(0, import_auditSink.setAuditCaptureConfig)(null);
|
|
1934
3271
|
(0, import_auditSink.setAuditSink)(null);
|
|
3272
|
+
(0, import_upstreamTrace.setUpstreamTracePath)(null);
|
|
1935
3273
|
if (sweeper) sweeper.dispose();
|
|
1936
3274
|
writer = null;
|
|
1937
3275
|
sweeper = null;
|
|
3276
|
+
auditDir = "";
|
|
1938
3277
|
}
|
|
1939
3278
|
|
|
1940
3279
|
// src/billing/billingRuntime.ts
|
|
@@ -1974,7 +3313,7 @@ function resetBillingRuntimeForTests() {
|
|
|
1974
3313
|
}
|
|
1975
3314
|
|
|
1976
3315
|
// src/ports/account-multi.ts
|
|
1977
|
-
var
|
|
3316
|
+
var import_node_crypto7 = require("crypto");
|
|
1978
3317
|
var PROVIDER_KEYS = {
|
|
1979
3318
|
claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
|
|
1980
3319
|
codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
|
|
@@ -2040,7 +3379,7 @@ function migrateLazily(config) {
|
|
|
2040
3379
|
}
|
|
2041
3380
|
function addAccount(config, p, tokens, label) {
|
|
2042
3381
|
const accounts = [...getAccounts(config, p)];
|
|
2043
|
-
const id = (0,
|
|
3382
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
2044
3383
|
accounts.push({
|
|
2045
3384
|
id,
|
|
2046
3385
|
label: label ?? `Account ${accounts.length + 1}`,
|
|
@@ -2116,9 +3455,13 @@ function sanitizeAccounts(config, p) {
|
|
|
2116
3455
|
const activeId = getActiveId(config, p);
|
|
2117
3456
|
return accounts.map((a) => {
|
|
2118
3457
|
const t = a.tokens;
|
|
3458
|
+
const enabled = a.enabled !== false;
|
|
2119
3459
|
return {
|
|
2120
3460
|
id: a.id,
|
|
2121
3461
|
label: a.label,
|
|
3462
|
+
enabled,
|
|
3463
|
+
group: a.group?.trim() || p,
|
|
3464
|
+
tags: a.tags ?? [],
|
|
2122
3465
|
status: t.status ?? "unconfigured",
|
|
2123
3466
|
authMethod: t.authMethod,
|
|
2124
3467
|
subscriptionLevel: t.subscriptionLevel,
|
|
@@ -2127,6 +3470,8 @@ function sanitizeAccounts(config, p) {
|
|
|
2127
3470
|
isSetupToken: t.isSetupToken,
|
|
2128
3471
|
hasAccessToken: !!(t.accessToken || t.apiKey),
|
|
2129
3472
|
isActive: a.id === activeId,
|
|
3473
|
+
schedulable: enabled,
|
|
3474
|
+
errorMessage: sanitizeDiagnosticMessage(t.errorMessage),
|
|
2130
3475
|
// Scheduling metadata (subscription-account-scheduling): editable priority
|
|
2131
3476
|
// (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
|
|
2132
3477
|
priority: a.priority,
|
|
@@ -2141,6 +3486,54 @@ function sanitizeAccounts(config, p) {
|
|
|
2141
3486
|
};
|
|
2142
3487
|
});
|
|
2143
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
|
+
}
|
|
2144
3537
|
function renameAccount(config, p, id, label) {
|
|
2145
3538
|
const accounts = getAccounts(config, p);
|
|
2146
3539
|
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
@@ -2230,7 +3623,7 @@ function clearProvider(config, p) {
|
|
|
2230
3623
|
var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
|
|
2231
3624
|
|
|
2232
3625
|
// src/migration/packCodec.ts
|
|
2233
|
-
var
|
|
3626
|
+
var import_node_crypto8 = require("crypto");
|
|
2234
3627
|
var PACK_MAGIC = "OMCXPACK";
|
|
2235
3628
|
var PACK_VERSION = 1;
|
|
2236
3629
|
var KDF_ALGORITHM = "scrypt";
|
|
@@ -2268,17 +3661,17 @@ function fromB64Url(s) {
|
|
|
2268
3661
|
return Buffer.from(s, "base64url").toString("utf8");
|
|
2269
3662
|
}
|
|
2270
3663
|
function deriveKey(passphrase, salt, N, r, p) {
|
|
2271
|
-
return (0,
|
|
3664
|
+
return (0, import_node_crypto8.scryptSync)(passphrase, salt, KEY_BYTES3, { N, r, p, maxmem: SCRYPT_MAXMEM });
|
|
2272
3665
|
}
|
|
2273
3666
|
function aadFor(magic, version, kdf) {
|
|
2274
3667
|
return Buffer.from(`${magic}|${version}|${kdf}`, "utf8");
|
|
2275
3668
|
}
|
|
2276
3669
|
function sealPack(bundleJson, passphrase) {
|
|
2277
3670
|
assertPassphraseStrength(passphrase);
|
|
2278
|
-
const salt = (0,
|
|
2279
|
-
const iv = (0,
|
|
3671
|
+
const salt = (0, import_node_crypto8.randomBytes)(SCRYPT_SALT_BYTES);
|
|
3672
|
+
const iv = (0, import_node_crypto8.randomBytes)(IV_BYTES2);
|
|
2280
3673
|
const key = deriveKey(passphrase, salt, SCRYPT_N, SCRYPT_R, SCRYPT_P);
|
|
2281
|
-
const cipher = (0,
|
|
3674
|
+
const cipher = (0, import_node_crypto8.createCipheriv)("aes-256-gcm", key, iv);
|
|
2282
3675
|
cipher.setAAD(aadFor(PACK_MAGIC, PACK_VERSION, KDF_ALGORITHM));
|
|
2283
3676
|
const ciphertext = Buffer.concat([cipher.update(bundleJson, "utf8"), cipher.final()]);
|
|
2284
3677
|
const tag = cipher.getAuthTag();
|
|
@@ -2326,7 +3719,7 @@ function openPack(packString, passphrase) {
|
|
|
2326
3719
|
throw new PackAuthError("migration pack is malformed (invalid iv/tag length)");
|
|
2327
3720
|
}
|
|
2328
3721
|
const key = deriveKey(passphrase, salt, header.N, header.r, header.p);
|
|
2329
|
-
const decipher = (0,
|
|
3722
|
+
const decipher = (0, import_node_crypto8.createDecipheriv)("aes-256-gcm", key, iv);
|
|
2330
3723
|
decipher.setAAD(aadFor(PACK_MAGIC, PACK_VERSION, KDF_ALGORITHM));
|
|
2331
3724
|
decipher.setAuthTag(tag);
|
|
2332
3725
|
try {
|
|
@@ -2478,9 +3871,9 @@ function parseFiniteInt(raw) {
|
|
|
2478
3871
|
const n = Number(raw);
|
|
2479
3872
|
return Number.isFinite(n) && Number.isInteger(n) ? n : null;
|
|
2480
3873
|
}
|
|
2481
|
-
function parseRange(
|
|
2482
|
-
const startTs = parseFiniteInt(
|
|
2483
|
-
const endTs = parseFiniteInt(
|
|
3874
|
+
function parseRange(query2) {
|
|
3875
|
+
const startTs = parseFiniteInt(query2.get("startTs"));
|
|
3876
|
+
const endTs = parseFiniteInt(query2.get("endTs"));
|
|
2484
3877
|
if (startTs === null || endTs === null) {
|
|
2485
3878
|
return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
|
|
2486
3879
|
}
|
|
@@ -2493,8 +3886,8 @@ var BUCKET_SPAN_MS = {
|
|
|
2493
3886
|
month: 28 * 864e5
|
|
2494
3887
|
};
|
|
2495
3888
|
var MAX_TIMESERIES_BUCKETS = 2e3;
|
|
2496
|
-
async function handleUsageGet(view,
|
|
2497
|
-
const range = parseRange(
|
|
3889
|
+
async function handleUsageGet(view, query2, deps) {
|
|
3890
|
+
const range = parseRange(query2);
|
|
2498
3891
|
if (!isRange(range)) return range;
|
|
2499
3892
|
switch (view) {
|
|
2500
3893
|
case "totals":
|
|
@@ -2502,7 +3895,7 @@ async function handleUsageGet(view, query, deps) {
|
|
|
2502
3895
|
case "by-model":
|
|
2503
3896
|
return { status: 200, body: await deps.usageRecorder.getByModel(range) };
|
|
2504
3897
|
case "timeseries": {
|
|
2505
|
-
const bucket =
|
|
3898
|
+
const bucket = query2.get("bucket");
|
|
2506
3899
|
if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
|
|
2507
3900
|
return err4(400, "bucket must be one of 'hour', 'day', 'month'");
|
|
2508
3901
|
}
|
|
@@ -2588,9 +3981,9 @@ async function handlePricingUpsert(body, deps) {
|
|
|
2588
3981
|
const entry = await deps.pricingEngine.upsertManual(input);
|
|
2589
3982
|
return { status: 200, body: { entry } };
|
|
2590
3983
|
}
|
|
2591
|
-
async function handlePricingDelete(
|
|
2592
|
-
const providerId =
|
|
2593
|
-
const modelId =
|
|
3984
|
+
async function handlePricingDelete(query2, deps) {
|
|
3985
|
+
const providerId = query2.get("providerId")?.trim() ?? "";
|
|
3986
|
+
const modelId = query2.get("modelId")?.trim() ?? "";
|
|
2594
3987
|
if (!providerId || !modelId) {
|
|
2595
3988
|
return err4(400, "delete requires providerId and modelId query params");
|
|
2596
3989
|
}
|
|
@@ -2607,7 +4000,8 @@ async function handlePricingFetchLatest(deps) {
|
|
|
2607
4000
|
appliedCount: result.applied.length,
|
|
2608
4001
|
conflicts: result.conflicts,
|
|
2609
4002
|
fetchedAt: result.fetchedAt,
|
|
2610
|
-
sourceUrl: result.sourceUrl
|
|
4003
|
+
sourceUrl: result.sourceUrl,
|
|
4004
|
+
sources: result.sources
|
|
2611
4005
|
}
|
|
2612
4006
|
};
|
|
2613
4007
|
} catch (e) {
|
|
@@ -2655,12 +4049,80 @@ async function handlePricingResolveConflicts(body, deps) {
|
|
|
2655
4049
|
return { status: 200, body: { ...resolution, staleCount } };
|
|
2656
4050
|
}
|
|
2657
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
|
+
|
|
2658
4120
|
// src/admin/adminApi.ts
|
|
2659
4121
|
function readBody(req) {
|
|
2660
|
-
return new Promise((
|
|
4122
|
+
return new Promise((resolve2, reject) => {
|
|
2661
4123
|
const chunks = [];
|
|
2662
4124
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
2663
|
-
req.on("end", () =>
|
|
4125
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
2664
4126
|
req.on("error", reject);
|
|
2665
4127
|
});
|
|
2666
4128
|
}
|
|
@@ -2674,12 +4136,12 @@ async function readJsonBody3(req) {
|
|
|
2674
4136
|
return {};
|
|
2675
4137
|
}
|
|
2676
4138
|
}
|
|
2677
|
-
function
|
|
4139
|
+
function writeJson3(res, status, body) {
|
|
2678
4140
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
2679
4141
|
res.end(JSON.stringify(body));
|
|
2680
4142
|
}
|
|
2681
4143
|
function writeJsonError(res, status, message) {
|
|
2682
|
-
|
|
4144
|
+
writeJson3(res, status, { error: { type: "admin_api_error", message } });
|
|
2683
4145
|
}
|
|
2684
4146
|
function maskProviderApiKey(apiKey) {
|
|
2685
4147
|
if (!apiKey) return "";
|
|
@@ -2696,6 +4158,9 @@ function toKeyInfo(row) {
|
|
|
2696
4158
|
createdAt: row.createdAt,
|
|
2697
4159
|
lastUsedAt: row.lastUsedAt,
|
|
2698
4160
|
revoked: row.revokedAt !== null,
|
|
4161
|
+
kind: row.kind,
|
|
4162
|
+
allowedEndpoints: row.allowedEndpoints,
|
|
4163
|
+
loopbackOnly: row.loopbackOnly,
|
|
2699
4164
|
maxConcurrency: row.maxConcurrency,
|
|
2700
4165
|
// Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
|
|
2701
4166
|
// the UI reads them to render + pre-fill the policy editor.
|
|
@@ -2781,6 +4246,8 @@ async function handleAdminApi(req, res, path2, deps) {
|
|
|
2781
4246
|
return await handleAccounts(req, res, method, rest, deps);
|
|
2782
4247
|
case "cli":
|
|
2783
4248
|
return await handleCli(req, res, method, rest, deps);
|
|
4249
|
+
case "integrations":
|
|
4250
|
+
return await handleIntegrations(req, res, method, rest, deps);
|
|
2784
4251
|
case "status":
|
|
2785
4252
|
return await handleStatus(res, method, deps);
|
|
2786
4253
|
case "playground":
|
|
@@ -2808,7 +4275,7 @@ function requestQuery(req) {
|
|
|
2808
4275
|
return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
|
|
2809
4276
|
}
|
|
2810
4277
|
function writeResult(res, result) {
|
|
2811
|
-
|
|
4278
|
+
writeJson3(res, result.status, result.body);
|
|
2812
4279
|
}
|
|
2813
4280
|
async function handleUsage(req, res, method, rest, deps) {
|
|
2814
4281
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
|
|
@@ -2817,7 +4284,7 @@ async function handleUsage(req, res, method, rest, deps) {
|
|
|
2817
4284
|
async function handleDashboardRoute(res, method, deps) {
|
|
2818
4285
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
|
|
2819
4286
|
const result = await handleDashboard(deps);
|
|
2820
|
-
return
|
|
4287
|
+
return writeJson3(res, result.status, result.body);
|
|
2821
4288
|
}
|
|
2822
4289
|
async function handlePricing(req, res, method, rest, deps) {
|
|
2823
4290
|
if (rest.length === 0) {
|
|
@@ -2850,13 +4317,13 @@ async function handleMigrationExport(req, res, method, deps) {
|
|
|
2850
4317
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
|
|
2851
4318
|
const body = await readJsonBody3(req);
|
|
2852
4319
|
const result = await handleExport(body, migrationDeps(deps));
|
|
2853
|
-
return
|
|
4320
|
+
return writeJson3(res, result.status, result.body);
|
|
2854
4321
|
}
|
|
2855
4322
|
async function handleMigrationImport(req, res, method, deps) {
|
|
2856
4323
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
|
|
2857
4324
|
const body = await readJsonBody3(req);
|
|
2858
4325
|
const result = await handleImport(body, migrationDeps(deps));
|
|
2859
|
-
return
|
|
4326
|
+
return writeJson3(res, result.status, result.body);
|
|
2860
4327
|
}
|
|
2861
4328
|
async function handleProviders(req, res, method, rest, deps) {
|
|
2862
4329
|
const cfg = loadConfig(deps.configPath);
|
|
@@ -2887,10 +4354,10 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2887
4354
|
if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
|
|
2888
4355
|
const row = cfg.providers.find((p) => p.id === rest[0]);
|
|
2889
4356
|
if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
|
|
2890
|
-
return
|
|
4357
|
+
return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
|
|
2891
4358
|
}
|
|
2892
4359
|
if (method === "GET") {
|
|
2893
|
-
return
|
|
4360
|
+
return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
|
|
2894
4361
|
}
|
|
2895
4362
|
if (method === "POST") {
|
|
2896
4363
|
const body = await readJsonBody3(req);
|
|
@@ -2901,7 +4368,7 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2901
4368
|
}
|
|
2902
4369
|
cfg.providers.push(provider);
|
|
2903
4370
|
persistProviders(cfg, deps);
|
|
2904
|
-
return
|
|
4371
|
+
return writeJson3(res, 201, { provider: toProviderView(provider) });
|
|
2905
4372
|
}
|
|
2906
4373
|
const id = rest[0];
|
|
2907
4374
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -2914,12 +4381,12 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2914
4381
|
if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
|
|
2915
4382
|
cfg.providers[idx] = updated;
|
|
2916
4383
|
persistProviders(cfg, deps);
|
|
2917
|
-
return
|
|
4384
|
+
return writeJson3(res, 200, { provider: toProviderView(updated) });
|
|
2918
4385
|
}
|
|
2919
4386
|
if (method === "DELETE") {
|
|
2920
4387
|
cfg.providers.splice(idx, 1);
|
|
2921
4388
|
persistProviders(cfg, deps);
|
|
2922
|
-
return
|
|
4389
|
+
return writeJson3(res, 200, { ok: true });
|
|
2923
4390
|
}
|
|
2924
4391
|
return writeJsonError(res, 405, `method ${method} not allowed on providers`);
|
|
2925
4392
|
}
|
|
@@ -2952,14 +4419,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
|
|
|
2952
4419
|
}
|
|
2953
4420
|
cfg.providers = reordered;
|
|
2954
4421
|
persistProviders(cfg, deps);
|
|
2955
|
-
return
|
|
4422
|
+
return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
|
|
2956
4423
|
}
|
|
2957
4424
|
async function handleDiscoverModels(res, id, cfg) {
|
|
2958
4425
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
2959
4426
|
const row = cfg.providers.find((p) => p.id === id);
|
|
2960
4427
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
2961
|
-
if (row.apiFormat !== "openai") {
|
|
2962
|
-
return
|
|
4428
|
+
if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
|
|
4429
|
+
return writeJson3(res, 200, { models: [], unsupportedFormat: true });
|
|
2963
4430
|
}
|
|
2964
4431
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
2965
4432
|
const base = row.baseUrl.replace(/\/+$/, "");
|
|
@@ -2967,7 +4434,7 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
2967
4434
|
try {
|
|
2968
4435
|
const headers = { Accept: "application/json" };
|
|
2969
4436
|
if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
2970
|
-
const response = await (0,
|
|
4437
|
+
const response = await (0, import_upstreamFetch3.fetchUpstream)(url, { method: "GET", headers }, { providerId: "byo" });
|
|
2971
4438
|
if (!response.ok) {
|
|
2972
4439
|
const text = await response.text().catch(() => "");
|
|
2973
4440
|
let message = text.slice(0, 300);
|
|
@@ -2976,17 +4443,17 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
2976
4443
|
message = parsed?.error?.message || parsed?.message || message;
|
|
2977
4444
|
} catch {
|
|
2978
4445
|
}
|
|
2979
|
-
return
|
|
4446
|
+
return writeJson3(res, 200, {
|
|
2980
4447
|
models: [],
|
|
2981
4448
|
error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
|
|
2982
4449
|
});
|
|
2983
4450
|
}
|
|
2984
4451
|
const data = await response.json();
|
|
2985
4452
|
const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
|
|
2986
|
-
return
|
|
4453
|
+
return writeJson3(res, 200, { models });
|
|
2987
4454
|
} catch (err5) {
|
|
2988
4455
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
2989
|
-
return
|
|
4456
|
+
return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
|
|
2990
4457
|
}
|
|
2991
4458
|
}
|
|
2992
4459
|
async function handleTestModel(req, res, id, cfg) {
|
|
@@ -2997,13 +4464,13 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
2997
4464
|
const model = typeof body["model"] === "string" ? body["model"].trim() : "";
|
|
2998
4465
|
if (!model) return writeJsonError(res, 400, "test requires a { model } string");
|
|
2999
4466
|
if (row.apiFormat === "gemini") {
|
|
3000
|
-
return
|
|
4467
|
+
return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
|
|
3001
4468
|
}
|
|
3002
4469
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
3003
4470
|
if (!resolvedKey) {
|
|
3004
|
-
return
|
|
4471
|
+
return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
|
|
3005
4472
|
}
|
|
3006
|
-
|
|
4473
|
+
let url = row.baseUrl.replace(/\/+$/, "");
|
|
3007
4474
|
const prompt = "Reply with the single word: OK.";
|
|
3008
4475
|
const headers = { "Content-Type": "application/json" };
|
|
3009
4476
|
let payload;
|
|
@@ -3011,6 +4478,10 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3011
4478
|
headers["x-api-key"] = resolvedKey;
|
|
3012
4479
|
headers["anthropic-version"] = "2023-06-01";
|
|
3013
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 };
|
|
3014
4485
|
} else {
|
|
3015
4486
|
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
3016
4487
|
payload = {
|
|
@@ -3022,7 +4493,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3022
4493
|
}
|
|
3023
4494
|
const startedAt = Date.now();
|
|
3024
4495
|
try {
|
|
3025
|
-
const response = await (0,
|
|
4496
|
+
const response = await (0, import_upstreamFetch3.fetchUpstream)(
|
|
3026
4497
|
url,
|
|
3027
4498
|
{ method: "POST", headers, body: JSON.stringify(payload) },
|
|
3028
4499
|
{ providerId: "byo" }
|
|
@@ -3036,9 +4507,9 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3036
4507
|
message = parsed?.error?.message || parsed?.message || message;
|
|
3037
4508
|
} catch {
|
|
3038
4509
|
}
|
|
3039
|
-
return
|
|
4510
|
+
return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
|
|
3040
4511
|
}
|
|
3041
|
-
return
|
|
4512
|
+
return writeJson3(res, 200, {
|
|
3042
4513
|
ok: true,
|
|
3043
4514
|
status: response.status,
|
|
3044
4515
|
latencyMs,
|
|
@@ -3046,7 +4517,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3046
4517
|
});
|
|
3047
4518
|
} catch (err5) {
|
|
3048
4519
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
3049
|
-
return
|
|
4520
|
+
return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
|
|
3050
4521
|
}
|
|
3051
4522
|
}
|
|
3052
4523
|
function extractSampleText(text, apiFormat) {
|
|
@@ -3087,7 +4558,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
|
|
|
3087
4558
|
const row = cfg.providers.find((p) => p.id === id);
|
|
3088
4559
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
3089
4560
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3090
|
-
return
|
|
4561
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3091
4562
|
}
|
|
3092
4563
|
function parsePoolKeyInput(body, existing) {
|
|
3093
4564
|
const out = {};
|
|
@@ -3118,7 +4589,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
|
|
|
3118
4589
|
row.apiKeys = [...row.apiKeys ?? [], entry];
|
|
3119
4590
|
persistProviders(cfg, deps);
|
|
3120
4591
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3121
|
-
return
|
|
4592
|
+
return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3122
4593
|
}
|
|
3123
4594
|
async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
3124
4595
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3138,7 +4609,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
3138
4609
|
row.apiKeys[keyIdx] = entry;
|
|
3139
4610
|
persistProviders(cfg, deps);
|
|
3140
4611
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3141
|
-
return
|
|
4612
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3142
4613
|
}
|
|
3143
4614
|
async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
3144
4615
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3152,7 +4623,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
|
3152
4623
|
if (row.apiKeys.length === 0) row.apiKeys = void 0;
|
|
3153
4624
|
persistProviders(cfg, deps);
|
|
3154
4625
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3155
|
-
return
|
|
4626
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3156
4627
|
}
|
|
3157
4628
|
async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
3158
4629
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3166,7 +4637,7 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
3166
4637
|
row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
|
|
3167
4638
|
persistProviders(cfg, deps);
|
|
3168
4639
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3169
|
-
return
|
|
4640
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3170
4641
|
}
|
|
3171
4642
|
function parseApiKeysInput(raw, existing) {
|
|
3172
4643
|
if (!Array.isArray(raw)) return existing;
|
|
@@ -3284,7 +4755,9 @@ function parseProviderInput(body, existing) {
|
|
|
3284
4755
|
const baseUrl = body["baseUrl"];
|
|
3285
4756
|
if (!id) return null;
|
|
3286
4757
|
const name = typeof body["name"] === "string" && body["name"].length > 0 ? body["name"] : body["name"] === null ? void 0 : existing?.name;
|
|
3287
|
-
if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini")
|
|
4758
|
+
if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini" && apiFormat !== "openai-response") {
|
|
4759
|
+
return null;
|
|
4760
|
+
}
|
|
3288
4761
|
if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
|
|
3289
4762
|
const rawKey = body["apiKey"];
|
|
3290
4763
|
let apiKey = typeof rawKey === "string" && rawKey.length > 0 ? rawKey : existing?.apiKey ?? "";
|
|
@@ -3306,10 +4779,11 @@ function parseProviderInput(body, existing) {
|
|
|
3306
4779
|
apiKey = mode.apiKey;
|
|
3307
4780
|
}
|
|
3308
4781
|
}
|
|
4782
|
+
const migrated = migrateFormatAxis(apiFormat, transformer);
|
|
3309
4783
|
return {
|
|
3310
4784
|
id,
|
|
3311
4785
|
name,
|
|
3312
|
-
apiFormat,
|
|
4786
|
+
apiFormat: migrated.apiFormat,
|
|
3313
4787
|
baseUrl: baseUrl.trim(),
|
|
3314
4788
|
apiKey,
|
|
3315
4789
|
models,
|
|
@@ -3320,7 +4794,7 @@ function parseProviderInput(body, existing) {
|
|
|
3320
4794
|
apiVersion,
|
|
3321
4795
|
maxConcurrency,
|
|
3322
4796
|
modelsEndpoint,
|
|
3323
|
-
transformer,
|
|
4797
|
+
transformer: migrated.transformer,
|
|
3324
4798
|
codingPlan,
|
|
3325
4799
|
apiModes,
|
|
3326
4800
|
selectedApiModeId
|
|
@@ -3337,13 +4811,13 @@ function handlePresets(res, method) {
|
|
|
3337
4811
|
baseUrl: p.baseUrl,
|
|
3338
4812
|
models: p.models
|
|
3339
4813
|
}));
|
|
3340
|
-
return
|
|
4814
|
+
return writeJson3(res, 200, { presets, excluded });
|
|
3341
4815
|
}
|
|
3342
4816
|
async function handleKeys(req, res, method, rest, deps) {
|
|
3343
4817
|
if (method === "GET" && rest.length === 0) {
|
|
3344
4818
|
const rows = await deps.keyDb.outboundApiKeysList();
|
|
3345
4819
|
const reader = deps.keySpendReader;
|
|
3346
|
-
if (!reader) return
|
|
4820
|
+
if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
|
|
3347
4821
|
const now = Date.now();
|
|
3348
4822
|
const keys = await Promise.all(
|
|
3349
4823
|
rows.map(async (row) => {
|
|
@@ -3355,13 +4829,13 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3355
4829
|
return info;
|
|
3356
4830
|
})
|
|
3357
4831
|
);
|
|
3358
|
-
return
|
|
4832
|
+
return writeJson3(res, 200, { keys });
|
|
3359
4833
|
}
|
|
3360
4834
|
if (method === "POST" && rest.length === 0) {
|
|
3361
4835
|
const body = await readJsonBody3(req);
|
|
3362
4836
|
const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
|
|
3363
4837
|
const created = await (0, import_outbound_api2.createNamedKey)(deps.keyDb, name);
|
|
3364
|
-
return
|
|
4838
|
+
return writeJson3(res, 201, {
|
|
3365
4839
|
id: created.id,
|
|
3366
4840
|
name: created.name,
|
|
3367
4841
|
keyPrefix: created.keyPrefix,
|
|
@@ -3373,13 +4847,13 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3373
4847
|
const action = rest[1];
|
|
3374
4848
|
if (method === "POST" && id && action === "revoke") {
|
|
3375
4849
|
const ok = await deps.keyDb.outboundApiKeysRevoke(id);
|
|
3376
|
-
return
|
|
4850
|
+
return writeJson3(res, ok ? 200 : 404, { ok });
|
|
3377
4851
|
}
|
|
3378
4852
|
if (method === "POST" && id && action === "enabled") {
|
|
3379
4853
|
const body = await readJsonBody3(req);
|
|
3380
4854
|
const enabled = body["enabled"] === true;
|
|
3381
4855
|
const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
|
|
3382
|
-
return
|
|
4856
|
+
return writeJson3(res, ok ? 200 : 404, { ok, enabled });
|
|
3383
4857
|
}
|
|
3384
4858
|
if (method === "POST" && id && action === "max-concurrency") {
|
|
3385
4859
|
const body = await readJsonBody3(req);
|
|
@@ -3397,14 +4871,14 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3397
4871
|
);
|
|
3398
4872
|
}
|
|
3399
4873
|
const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
|
|
3400
|
-
return
|
|
4874
|
+
return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
|
|
3401
4875
|
}
|
|
3402
4876
|
if (method === "POST" && id && action === "policy") {
|
|
3403
4877
|
const body = await readJsonBody3(req);
|
|
3404
4878
|
const parsed = parseKeyPolicyBody(body);
|
|
3405
4879
|
if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
|
|
3406
4880
|
const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
|
|
3407
|
-
return
|
|
4881
|
+
return writeJson3(res, ok ? 200 : 404, { ok });
|
|
3408
4882
|
}
|
|
3409
4883
|
return writeJsonError(res, 405, `method ${method} not allowed on keys`);
|
|
3410
4884
|
}
|
|
@@ -3415,10 +4889,10 @@ function validateQueueSegments(patch) {
|
|
|
3415
4889
|
errors.push(`${label} must be a number ${min}..${max}`);
|
|
3416
4890
|
}
|
|
3417
4891
|
};
|
|
3418
|
-
const
|
|
4892
|
+
const isPlainObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3419
4893
|
const umq = patch.userMessageQueue;
|
|
3420
4894
|
if (umq !== void 0) {
|
|
3421
|
-
if (!
|
|
4895
|
+
if (!isPlainObject5(umq)) {
|
|
3422
4896
|
errors.push("userMessageQueue must be an object");
|
|
3423
4897
|
} else {
|
|
3424
4898
|
if (typeof umq.enabled !== "boolean") {
|
|
@@ -3430,7 +4904,7 @@ function validateQueueSegments(patch) {
|
|
|
3430
4904
|
}
|
|
3431
4905
|
const cq = patch.concurrencyQueue;
|
|
3432
4906
|
if (cq !== void 0) {
|
|
3433
|
-
if (!
|
|
4907
|
+
if (!isPlainObject5(cq)) {
|
|
3434
4908
|
errors.push("concurrencyQueue must be an object");
|
|
3435
4909
|
} else {
|
|
3436
4910
|
checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
|
|
@@ -3440,7 +4914,7 @@ function validateQueueSegments(patch) {
|
|
|
3440
4914
|
}
|
|
3441
4915
|
const ah = patch.accountHealth;
|
|
3442
4916
|
if (ah !== void 0) {
|
|
3443
|
-
if (!
|
|
4917
|
+
if (!isPlainObject5(ah)) {
|
|
3444
4918
|
errors.push("accountHealth must be an object");
|
|
3445
4919
|
} else {
|
|
3446
4920
|
if (typeof ah.overloadCooldownEnabled !== "boolean") {
|
|
@@ -3451,6 +4925,31 @@ function validateQueueSegments(patch) {
|
|
|
3451
4925
|
}
|
|
3452
4926
|
return errors;
|
|
3453
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
|
+
}
|
|
3454
4953
|
async function handleServer(req, res, method, deps) {
|
|
3455
4954
|
if (method === "GET") {
|
|
3456
4955
|
const config = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
|
|
@@ -3458,7 +4957,7 @@ async function handleServer(req, res, method, deps) {
|
|
|
3458
4957
|
if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
|
|
3459
4958
|
if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
|
|
3460
4959
|
if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
|
|
3461
|
-
return
|
|
4960
|
+
return writeJson3(res, 200, { server });
|
|
3462
4961
|
}
|
|
3463
4962
|
if (method === "PUT") {
|
|
3464
4963
|
const patch = await readJsonBody3(req);
|
|
@@ -3466,6 +4965,18 @@ async function handleServer(req, res, method, deps) {
|
|
|
3466
4965
|
if (queueErrors.length > 0) {
|
|
3467
4966
|
return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
|
|
3468
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
|
+
}
|
|
3469
4980
|
const webhookErrors = validateWebhookSegment(patch);
|
|
3470
4981
|
if (webhookErrors.length > 0) {
|
|
3471
4982
|
return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
|
|
@@ -3487,71 +4998,118 @@ async function handleServer(req, res, method, deps) {
|
|
|
3487
4998
|
effectivePatch = { ...effectivePatch, webhook: preserveWebhookSecrets(patch.webhook, current.webhook) };
|
|
3488
4999
|
}
|
|
3489
5000
|
if (patch.billing) {
|
|
3490
|
-
effectivePatch = { ...effectivePatch, billing: preserveBillingSecret(patch.billing, current.billing) };
|
|
3491
|
-
}
|
|
3492
|
-
const merged = (0, import_outbound_api2.mergeServerConfig)(current, effectivePatch);
|
|
3493
|
-
await (0, import_outbound_api2.saveServerConfig)(deps.settingsStore, merged);
|
|
3494
|
-
setServerProxyConfig(merged.proxy);
|
|
3495
|
-
applyWebhookConfig(merged.webhook);
|
|
3496
|
-
applyAuditConfig(merged.audit);
|
|
3497
|
-
applyBillingConfig(merged.billing);
|
|
3498
|
-
if (merged.enabled) {
|
|
3499
|
-
const missing = (0, import_outbound_api2.validateServerModelConfig)(merged);
|
|
3500
|
-
if (missing.length > 0) {
|
|
3501
|
-
if (deps.outboundApiServer.getStatus().running) {
|
|
3502
|
-
await deps.outboundApiServer.stop();
|
|
3503
|
-
}
|
|
3504
|
-
return writeJson2(res, 200, {
|
|
3505
|
-
server: merged,
|
|
3506
|
-
error: { code: "incomplete-model-config", missing }
|
|
3507
|
-
});
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
try {
|
|
3511
|
-
await deps.outboundApiServer.applyConfig({
|
|
3512
|
-
enabled: merged.enabled,
|
|
3513
|
-
networkBinding: merged.networkBinding,
|
|
3514
|
-
endpoints: merged.endpoints,
|
|
3515
|
-
port: merged.port,
|
|
3516
|
-
userMessageQueue: merged.userMessageQueue,
|
|
3517
|
-
concurrencyQueue: merged.concurrencyQueue,
|
|
3518
|
-
// voucher-redemption #9: hot-apply the voucher flag so enabling the product
|
|
3519
|
-
// takes effect without a restart.
|
|
3520
|
-
voucher: merged.voucher
|
|
3521
|
-
});
|
|
3522
|
-
} catch (err5) {
|
|
3523
|
-
const missing = incompleteConfigMissing(err5);
|
|
3524
|
-
if (missing) {
|
|
3525
|
-
return writeJson2(res, 200, {
|
|
3526
|
-
server: merged,
|
|
3527
|
-
error: { code: "incomplete-model-config", missing }
|
|
3528
|
-
});
|
|
3529
|
-
}
|
|
3530
|
-
throw err5;
|
|
5001
|
+
effectivePatch = { ...effectivePatch, billing: preserveBillingSecret(patch.billing, current.billing) };
|
|
3531
5002
|
}
|
|
3532
|
-
|
|
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 });
|
|
3533
5024
|
}
|
|
3534
5025
|
return writeJsonError(res, 405, `method ${method} not allowed on server`);
|
|
3535
5026
|
}
|
|
3536
|
-
function incompleteConfigMissing(err5) {
|
|
3537
|
-
if (typeof err5 !== "object" || err5 === null) return null;
|
|
3538
|
-
const missing = err5.missing;
|
|
3539
|
-
return Array.isArray(missing) ? missing : null;
|
|
3540
|
-
}
|
|
3541
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
|
+
}
|
|
3542
5037
|
if (method === "GET" && rest.length === 0) {
|
|
3543
5038
|
const accounts = await deps.subscriptionAccounts.listAll();
|
|
3544
5039
|
const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
3545
5040
|
const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
|
|
3546
|
-
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 });
|
|
3547
5061
|
}
|
|
3548
5062
|
if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
|
|
3549
5063
|
const result = handleCodexOAuthStatus(rest[2], deps);
|
|
3550
|
-
return
|
|
5064
|
+
return writeJson3(res, result.status, result.body);
|
|
3551
5065
|
}
|
|
3552
5066
|
if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
|
|
3553
5067
|
const result = handleCodexOAuthCancel(rest[2], deps);
|
|
3554
|
-
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 });
|
|
3555
5113
|
}
|
|
3556
5114
|
if (method === "PUT" || method === "POST" || method === "DELETE") {
|
|
3557
5115
|
const providerId = asSubscriptionProviderId(rest[0]);
|
|
@@ -3560,12 +5118,12 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3560
5118
|
}
|
|
3561
5119
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
|
|
3562
5120
|
const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
|
|
3563
|
-
return
|
|
5121
|
+
return writeJson3(res, result.status, result.body);
|
|
3564
5122
|
}
|
|
3565
5123
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
|
|
3566
5124
|
const body2 = await readJsonBody3(req);
|
|
3567
5125
|
const result = await handleOAuthComplete(providerId, body2, deps);
|
|
3568
|
-
return
|
|
5126
|
+
return writeJson3(res, result.status, result.body);
|
|
3569
5127
|
}
|
|
3570
5128
|
if (method === "POST" && rest[1] === "accounts") {
|
|
3571
5129
|
const body2 = await readJsonBody3(req);
|
|
@@ -3576,7 +5134,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3576
5134
|
const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
|
|
3577
5135
|
await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
|
|
3578
5136
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3579
|
-
return
|
|
5137
|
+
return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
|
|
3580
5138
|
}
|
|
3581
5139
|
if (method === "POST" && rest[1] === "import-external") {
|
|
3582
5140
|
if (providerId !== "claude" && providerId !== "codex") {
|
|
@@ -3589,7 +5147,13 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3589
5147
|
return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
|
|
3590
5148
|
}
|
|
3591
5149
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3592
|
-
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
|
+
});
|
|
3593
5157
|
}
|
|
3594
5158
|
if (method === "POST" && rest[1] === "refresh") {
|
|
3595
5159
|
if (providerId === "opencodego") {
|
|
@@ -3598,7 +5162,17 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3598
5162
|
const writer2 = deps.subscriptionTokenWriter;
|
|
3599
5163
|
const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
|
|
3600
5164
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3601
|
-
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 });
|
|
3602
5176
|
}
|
|
3603
5177
|
if (method === "POST" && rest[2] === "label") {
|
|
3604
5178
|
const accountId = rest[1];
|
|
@@ -3606,7 +5180,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3606
5180
|
const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
|
|
3607
5181
|
const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
|
|
3608
5182
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3609
|
-
return
|
|
5183
|
+
return writeJson3(res, 200, { ok: true });
|
|
3610
5184
|
}
|
|
3611
5185
|
if (method === "POST" && rest[2] === "priority") {
|
|
3612
5186
|
const accountId = rest[1];
|
|
@@ -3618,7 +5192,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3618
5192
|
}
|
|
3619
5193
|
const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
|
|
3620
5194
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3621
|
-
return
|
|
5195
|
+
return writeJson3(res, 200, { ok: true });
|
|
3622
5196
|
}
|
|
3623
5197
|
if (method === "POST" && rest[2] === "proxy") {
|
|
3624
5198
|
const accountId = rest[1];
|
|
@@ -3631,7 +5205,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3631
5205
|
}
|
|
3632
5206
|
const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
|
|
3633
5207
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3634
|
-
return
|
|
5208
|
+
return writeJson3(res, 200, { ok: true });
|
|
3635
5209
|
}
|
|
3636
5210
|
if (method === "POST" && rest[2] === "supported-models") {
|
|
3637
5211
|
const accountId = rest[1];
|
|
@@ -3640,7 +5214,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3640
5214
|
if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
|
|
3641
5215
|
const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
|
|
3642
5216
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3643
|
-
return
|
|
5217
|
+
return writeJson3(res, 200, { ok: true });
|
|
3644
5218
|
}
|
|
3645
5219
|
if (method === "PUT" && rest[1] === "active") {
|
|
3646
5220
|
const body2 = await readJsonBody3(req);
|
|
@@ -3648,17 +5222,22 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3648
5222
|
if (!id) return writeJsonError(res, 400, "active switch requires { id }");
|
|
3649
5223
|
const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
|
|
3650
5224
|
if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
|
|
3651
|
-
return
|
|
5225
|
+
return writeJson3(res, 200, { ok: true });
|
|
3652
5226
|
}
|
|
3653
|
-
if (method === "DELETE" && rest.length
|
|
5227
|
+
if (method === "DELETE" && rest.length === 2) {
|
|
3654
5228
|
const accountId = rest[1];
|
|
3655
5229
|
const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
|
|
3656
5230
|
if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3657
|
-
|
|
5231
|
+
deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
|
|
5232
|
+
return writeJson3(res, 200, { ok: true });
|
|
3658
5233
|
}
|
|
3659
|
-
if (method === "DELETE") {
|
|
5234
|
+
if (method === "DELETE" && rest.length === 1) {
|
|
3660
5235
|
await deps.subscriptionTokenWriter.clearProvider(providerId);
|
|
3661
|
-
|
|
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");
|
|
3662
5241
|
}
|
|
3663
5242
|
const body = await readJsonBody3(req);
|
|
3664
5243
|
const config = validateTokenBody(providerId, body);
|
|
@@ -3667,22 +5246,22 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3667
5246
|
}
|
|
3668
5247
|
await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
|
|
3669
5248
|
const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3670
|
-
return
|
|
5249
|
+
return writeJson3(res, 200, status ? { account: status } : { ok: true });
|
|
3671
5250
|
}
|
|
3672
5251
|
return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
|
|
3673
5252
|
}
|
|
3674
5253
|
async function handleCli(req, res, method, rest, deps) {
|
|
3675
5254
|
if (method === "GET" && rest.length === 0) {
|
|
3676
5255
|
const result = handleCliList(process.platform, deps.cliPathProbe);
|
|
3677
|
-
return
|
|
5256
|
+
return writeJson3(res, result.status, result.body);
|
|
3678
5257
|
}
|
|
3679
5258
|
if (method === "GET" && rest[0] === "sessions") {
|
|
3680
5259
|
const result = handleCliSessions();
|
|
3681
|
-
return
|
|
5260
|
+
return writeJson3(res, result.status, result.body);
|
|
3682
5261
|
}
|
|
3683
5262
|
if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
|
|
3684
5263
|
const result = handleCliStop(rest[1]);
|
|
3685
|
-
return
|
|
5264
|
+
return writeJson3(res, result.status, result.body);
|
|
3686
5265
|
}
|
|
3687
5266
|
if (method === "POST" && rest[1] === "install") {
|
|
3688
5267
|
const cli = rest[0];
|
|
@@ -3690,7 +5269,7 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
3690
5269
|
return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
|
|
3691
5270
|
}
|
|
3692
5271
|
const result = await handleCliInstall(cli, deps.cliCommandRunner);
|
|
3693
|
-
return
|
|
5272
|
+
return writeJson3(res, result.status, result.body);
|
|
3694
5273
|
}
|
|
3695
5274
|
if (method === "POST" && rest[1] === "launch") {
|
|
3696
5275
|
const cli = rest[0];
|
|
@@ -3705,28 +5284,100 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
3705
5284
|
opener: deps.cliTerminalOpener,
|
|
3706
5285
|
probe: deps.cliPathProbe
|
|
3707
5286
|
});
|
|
3708
|
-
return
|
|
5287
|
+
return writeJson3(res, result.status, result.body);
|
|
3709
5288
|
}
|
|
3710
5289
|
return writeJsonError(res, 405, `method ${method} not allowed on cli`);
|
|
3711
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
|
+
}
|
|
3712
5347
|
async function handleStatus(res, method, deps) {
|
|
3713
5348
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
|
|
3714
5349
|
const status = deps.outboundApiServer.getStatus();
|
|
3715
5350
|
const serverConfig = await (0, import_outbound_api2.loadServerConfig)(deps.settingsStore);
|
|
3716
|
-
const endpoints =
|
|
3717
|
-
|
|
3718
|
-
|
|
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 };
|
|
3719
5362
|
}
|
|
3720
|
-
if (
|
|
3721
|
-
return {
|
|
5363
|
+
if (endpoint === "chat") {
|
|
5364
|
+
return {
|
|
5365
|
+
endpoint,
|
|
5366
|
+
models: [...new Set(routes.flatMap((route) => route.models ?? []))],
|
|
5367
|
+
useSubscription
|
|
5368
|
+
};
|
|
3722
5369
|
}
|
|
3723
|
-
return {
|
|
5370
|
+
return {
|
|
5371
|
+
endpoint,
|
|
5372
|
+
model: routes.find((route) => route.defaultModel?.trim())?.defaultModel ?? "",
|
|
5373
|
+
useSubscription
|
|
5374
|
+
};
|
|
3724
5375
|
});
|
|
3725
5376
|
if (status.running) {
|
|
3726
5377
|
const queueStatus = deps.outboundApiServer.getQueueStatus();
|
|
3727
|
-
return
|
|
5378
|
+
return writeJson3(res, 200, { ...status, endpoints, queueStatus });
|
|
3728
5379
|
}
|
|
3729
|
-
return
|
|
5380
|
+
return writeJson3(res, 200, { ...status, endpoints });
|
|
3730
5381
|
}
|
|
3731
5382
|
function resolvePlaygroundPath(endpoint, body) {
|
|
3732
5383
|
switch (endpoint) {
|
|
@@ -3752,16 +5403,16 @@ async function handlePlayground(req, res, method, deps) {
|
|
|
3752
5403
|
const payload = body["body"];
|
|
3753
5404
|
const status = deps.outboundApiServer.getStatus();
|
|
3754
5405
|
if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
|
|
3755
|
-
const path2 = resolvePlaygroundPath(endpoint,
|
|
5406
|
+
const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
|
|
3756
5407
|
if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
|
|
3757
5408
|
const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
3758
5409
|
await proxyToOutbound(res, status.port, path2, key, upstreamBody);
|
|
3759
5410
|
}
|
|
3760
|
-
function
|
|
5411
|
+
function isRecord2(v) {
|
|
3761
5412
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3762
5413
|
}
|
|
3763
5414
|
function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
3764
|
-
return new Promise((
|
|
5415
|
+
return new Promise((resolve2) => {
|
|
3765
5416
|
const upstream = import_node_http.default.request(
|
|
3766
5417
|
{
|
|
3767
5418
|
host: "127.0.0.1",
|
|
@@ -3782,14 +5433,14 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
|
3782
5433
|
proxRes.on("data", (chunk) => res.write(chunk));
|
|
3783
5434
|
proxRes.on("end", () => {
|
|
3784
5435
|
res.end();
|
|
3785
|
-
|
|
5436
|
+
resolve2();
|
|
3786
5437
|
});
|
|
3787
5438
|
}
|
|
3788
5439
|
);
|
|
3789
5440
|
upstream.on("error", (err5) => {
|
|
3790
5441
|
if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
|
|
3791
5442
|
else res.end();
|
|
3792
|
-
|
|
5443
|
+
resolve2();
|
|
3793
5444
|
});
|
|
3794
5445
|
upstream.write(body);
|
|
3795
5446
|
upstream.end();
|
|
@@ -3797,10 +5448,10 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
|
3797
5448
|
}
|
|
3798
5449
|
|
|
3799
5450
|
// src/admin/uiStatic.ts
|
|
3800
|
-
var
|
|
5451
|
+
var import_node_fs7 = require("fs");
|
|
3801
5452
|
var import_promises = require("fs/promises");
|
|
3802
5453
|
var import_node_module = require("module");
|
|
3803
|
-
var
|
|
5454
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
3804
5455
|
var import_meta = {};
|
|
3805
5456
|
var CONTENT_TYPES = {
|
|
3806
5457
|
".html": "text/html; charset=utf-8",
|
|
@@ -3821,13 +5472,13 @@ var CONTENT_TYPES = {
|
|
|
3821
5472
|
function resolveUiDist() {
|
|
3822
5473
|
const fromEnv = process.env["OMNICROSS_UI_DIST"];
|
|
3823
5474
|
if (fromEnv) {
|
|
3824
|
-
return (0,
|
|
5475
|
+
return (0, import_node_fs7.existsSync)(import_node_path7.default.join(fromEnv, "index.html")) ? import_node_path7.default.resolve(fromEnv) : null;
|
|
3825
5476
|
}
|
|
3826
5477
|
try {
|
|
3827
5478
|
const req = (0, import_node_module.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta.url);
|
|
3828
5479
|
const pkgJson = req.resolve("@omnicross/ui/package.json");
|
|
3829
|
-
const dist =
|
|
3830
|
-
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;
|
|
3831
5482
|
} catch {
|
|
3832
5483
|
return null;
|
|
3833
5484
|
}
|
|
@@ -3869,16 +5520,16 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
3869
5520
|
res.end(JSON.stringify({ error: { type: "bad_request", message: "invalid path" } }));
|
|
3870
5521
|
return true;
|
|
3871
5522
|
}
|
|
3872
|
-
const filePath =
|
|
3873
|
-
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)) {
|
|
3874
5525
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
3875
5526
|
res.end(JSON.stringify({ error: { type: "forbidden", message: "path outside ui root" } }));
|
|
3876
5527
|
return true;
|
|
3877
5528
|
}
|
|
3878
5529
|
let target = filePath;
|
|
3879
|
-
if (!(0,
|
|
3880
|
-
if (
|
|
3881
|
-
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");
|
|
3882
5533
|
} else {
|
|
3883
5534
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
3884
5535
|
res.end(JSON.stringify({ error: { type: "not_found", message: "no such ui asset" } }));
|
|
@@ -3886,14 +5537,14 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
3886
5537
|
}
|
|
3887
5538
|
}
|
|
3888
5539
|
const body = await (0, import_promises.readFile)(target);
|
|
3889
|
-
const type = CONTENT_TYPES[
|
|
5540
|
+
const type = CONTENT_TYPES[import_node_path7.default.extname(target).toLowerCase()] ?? "application/octet-stream";
|
|
3890
5541
|
res.writeHead(200, { "Content-Type": type, "Content-Length": body.length });
|
|
3891
5542
|
res.end(req.method === "HEAD" ? void 0 : body);
|
|
3892
5543
|
return true;
|
|
3893
5544
|
}
|
|
3894
5545
|
|
|
3895
5546
|
// src/admin/version.ts
|
|
3896
|
-
var DAEMON_VERSION = true ? "0.1.
|
|
5547
|
+
var DAEMON_VERSION = true ? "0.1.6" : "0.0.0-dev";
|
|
3897
5548
|
|
|
3898
5549
|
// src/admin/AdminServer.ts
|
|
3899
5550
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
@@ -3932,14 +5583,14 @@ var AdminServer = class {
|
|
|
3932
5583
|
}
|
|
3933
5584
|
/** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
|
|
3934
5585
|
listen(bindAddr, port) {
|
|
3935
|
-
return new Promise((
|
|
5586
|
+
return new Promise((resolve2, reject) => {
|
|
3936
5587
|
const server = import_node_http2.default.createServer((req, res) => {
|
|
3937
5588
|
this.onRequest(req, res);
|
|
3938
5589
|
});
|
|
3939
5590
|
const onError = (err5) => {
|
|
3940
5591
|
if (err5.code === "EADDRINUSE" && port !== 0) {
|
|
3941
5592
|
server.removeListener("error", onError);
|
|
3942
|
-
this.listen(bindAddr, 0).then(
|
|
5593
|
+
this.listen(bindAddr, 0).then(resolve2, reject);
|
|
3943
5594
|
return;
|
|
3944
5595
|
}
|
|
3945
5596
|
reject(err5);
|
|
@@ -3951,7 +5602,7 @@ var AdminServer = class {
|
|
|
3951
5602
|
server.removeListener("error", onError);
|
|
3952
5603
|
server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
|
|
3953
5604
|
this.server = server;
|
|
3954
|
-
|
|
5605
|
+
resolve2(addr.port);
|
|
3955
5606
|
} else {
|
|
3956
5607
|
reject(new Error("Failed to get admin server address"));
|
|
3957
5608
|
}
|
|
@@ -4032,8 +5683,8 @@ var AdminServer = class {
|
|
|
4032
5683
|
if (!server) return;
|
|
4033
5684
|
this.server = null;
|
|
4034
5685
|
this.boundPort = 0;
|
|
4035
|
-
return new Promise((
|
|
4036
|
-
server.close(() =>
|
|
5686
|
+
return new Promise((resolve2) => {
|
|
5687
|
+
server.close(() => resolve2());
|
|
4037
5688
|
});
|
|
4038
5689
|
}
|
|
4039
5690
|
/** A live status snapshot. */
|
|
@@ -4049,7 +5700,7 @@ function constantTimeEquals(a, b) {
|
|
|
4049
5700
|
const bufA = Buffer.from(a, "utf8");
|
|
4050
5701
|
const bufB = Buffer.from(b, "utf8");
|
|
4051
5702
|
if (bufA.length !== bufB.length) return false;
|
|
4052
|
-
return (0,
|
|
5703
|
+
return (0, import_node_crypto9.timingSafeEqual)(bufA, bufB);
|
|
4053
5704
|
}
|
|
4054
5705
|
|
|
4055
5706
|
// src/admin/health.ts
|
|
@@ -4098,7 +5749,7 @@ function buildHealthReport(deps) {
|
|
|
4098
5749
|
}
|
|
4099
5750
|
|
|
4100
5751
|
// src/admin/oauthSessions.ts
|
|
4101
|
-
var
|
|
5752
|
+
var import_node_crypto10 = __toESM(require("crypto"), 1);
|
|
4102
5753
|
var DEFAULT_OAUTH_SESSION_TTL_MS = 10 * 60 * 1e3;
|
|
4103
5754
|
var OAuthSessionStore = class {
|
|
4104
5755
|
constructor(ttlMs = DEFAULT_OAUTH_SESSION_TTL_MS) {
|
|
@@ -4112,7 +5763,7 @@ var OAuthSessionStore = class {
|
|
|
4112
5763
|
*/
|
|
4113
5764
|
put(session) {
|
|
4114
5765
|
this.sweep();
|
|
4115
|
-
const sessionId =
|
|
5766
|
+
const sessionId = import_node_crypto10.default.randomBytes(24).toString("base64url");
|
|
4116
5767
|
this.sessions.set(sessionId, { ...session, createdAt: Date.now() });
|
|
4117
5768
|
return sessionId;
|
|
4118
5769
|
}
|
|
@@ -4149,7 +5800,7 @@ function pageHtml(message) {
|
|
|
4149
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>`;
|
|
4150
5801
|
}
|
|
4151
5802
|
function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
|
|
4152
|
-
return new Promise((
|
|
5803
|
+
return new Promise((resolve2, reject) => {
|
|
4153
5804
|
let settled = false;
|
|
4154
5805
|
const finish = (server2, fn) => {
|
|
4155
5806
|
if (settled) return;
|
|
@@ -4180,7 +5831,7 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal
|
|
|
4180
5831
|
}
|
|
4181
5832
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4182
5833
|
res.end(pageHtml("Login complete."));
|
|
4183
|
-
finish(server, () =>
|
|
5834
|
+
finish(server, () => resolve2(code));
|
|
4184
5835
|
});
|
|
4185
5836
|
const abort = () => finish(server, () => reject(new Error("login: cancelled")));
|
|
4186
5837
|
if (signal?.aborted) {
|
|
@@ -4275,32 +5926,43 @@ function createPoolKeysLoader(getProviderRow, autoDisabled) {
|
|
|
4275
5926
|
}
|
|
4276
5927
|
|
|
4277
5928
|
// src/commands/paths.ts
|
|
4278
|
-
var
|
|
5929
|
+
var import_node_path8 = require("path");
|
|
4279
5930
|
function defaultVouchersPath(configPath) {
|
|
4280
|
-
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");
|
|
4281
5935
|
}
|
|
4282
5936
|
function defaultPricingPath(configPath) {
|
|
4283
|
-
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");
|
|
4284
5944
|
}
|
|
4285
5945
|
function defaultUsageEventsPath(configPath) {
|
|
4286
|
-
return (0,
|
|
5946
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "usage-events.jsonl");
|
|
4287
5947
|
}
|
|
4288
5948
|
function defaultAuditDir(configPath) {
|
|
4289
|
-
return (0,
|
|
5949
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "audit");
|
|
4290
5950
|
}
|
|
4291
5951
|
function defaultBillingDir(configPath) {
|
|
4292
|
-
return (0,
|
|
5952
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)(configPath), "billing");
|
|
4293
5953
|
}
|
|
4294
5954
|
|
|
4295
5955
|
// src/ports/ConfigFileProviderConfigSource.ts
|
|
4296
|
-
var
|
|
5956
|
+
var import_core2 = require("@omnicross/core");
|
|
4297
5957
|
var EMPTY_CHAIN = {
|
|
4298
5958
|
providerTransformers: [],
|
|
4299
5959
|
modelTransformers: []
|
|
4300
5960
|
};
|
|
4301
5961
|
var FORMAT_TRANSFORMER = {
|
|
5962
|
+
openai: "openai",
|
|
4302
5963
|
anthropic: "anthropic",
|
|
4303
|
-
gemini: "gemini"
|
|
5964
|
+
gemini: "gemini",
|
|
5965
|
+
"openai-response": "openai-response"
|
|
4304
5966
|
};
|
|
4305
5967
|
var ConfigFileProviderConfigSource = class {
|
|
4306
5968
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -4316,8 +5978,8 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4316
5978
|
reloadHook;
|
|
4317
5979
|
constructor(config) {
|
|
4318
5980
|
for (const p of config.providers) this.providers.set(p.id, p);
|
|
4319
|
-
this.transformerService = new
|
|
4320
|
-
void (0,
|
|
5981
|
+
this.transformerService = new import_core2.TransformerService();
|
|
5982
|
+
void (0, import_core2.registerBuiltinTransformers)(this.transformerService);
|
|
4321
5983
|
}
|
|
4322
5984
|
// ── Reload hook (key-pool design D4) ───────────────────────────────────────
|
|
4323
5985
|
/**
|
|
@@ -4338,7 +6000,7 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4338
6000
|
}
|
|
4339
6001
|
/** Await the built-in transformer registration (tests await this before dispatch). */
|
|
4340
6002
|
async ready() {
|
|
4341
|
-
await (0,
|
|
6003
|
+
await (0, import_core2.registerBuiltinTransformers)(this.transformerService);
|
|
4342
6004
|
}
|
|
4343
6005
|
// ── Hot-reload seam (admin dashboard, RT3 design D6) ───────────────────────
|
|
4344
6006
|
/**
|
|
@@ -4369,7 +6031,7 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4369
6031
|
}
|
|
4370
6032
|
async getMainTransformer(providerId) {
|
|
4371
6033
|
const row = this.providers.get(providerId);
|
|
4372
|
-
if (!row
|
|
6034
|
+
if (!row) return null;
|
|
4373
6035
|
const name = FORMAT_TRANSFORMER[row.apiFormat];
|
|
4374
6036
|
const instances = this.transformerService.resolveTransformerReferences([name]);
|
|
4375
6037
|
return instances[0] ?? null;
|
|
@@ -4379,11 +6041,8 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4379
6041
|
if (!row) return EMPTY_CHAIN;
|
|
4380
6042
|
const customRefs = row.transformer?.use ?? [];
|
|
4381
6043
|
if (customRefs.length === 0) return EMPTY_CHAIN;
|
|
4382
|
-
const formatName = row.apiFormat === "openai" ? void 0 : FORMAT_TRANSFORMER[row.apiFormat];
|
|
4383
|
-
const effectiveRefs = formatName ? customRefs.filter((ref) => (typeof ref === "string" ? ref : ref[0]) !== formatName) : customRefs;
|
|
4384
|
-
if (effectiveRefs.length === 0) return EMPTY_CHAIN;
|
|
4385
6044
|
return {
|
|
4386
|
-
providerTransformers: this.transformerService.resolveTransformerReferences(
|
|
6045
|
+
providerTransformers: this.transformerService.resolveTransformerReferences(customRefs),
|
|
4387
6046
|
modelTransformers: []
|
|
4388
6047
|
};
|
|
4389
6048
|
}
|
|
@@ -4414,7 +6073,7 @@ function resolvePreferredApiKey(row) {
|
|
|
4414
6073
|
}
|
|
4415
6074
|
function toLLMProvider(row) {
|
|
4416
6075
|
const apiFormat = row.apiFormat === "gemini" ? "google" : row.apiFormat;
|
|
4417
|
-
const transformer =
|
|
6076
|
+
const transformer = { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
|
|
4418
6077
|
const allModels = row.models ?? [];
|
|
4419
6078
|
const models = row.modelConfigs ? allModels.filter((id) => row.modelConfigs.find((c) => c.id === id)?.enabled !== false) : allModels;
|
|
4420
6079
|
return {
|
|
@@ -4450,7 +6109,7 @@ function toLLMProvider(row) {
|
|
|
4450
6109
|
}
|
|
4451
6110
|
|
|
4452
6111
|
// src/ports/ConfigurableLogger.ts
|
|
4453
|
-
var
|
|
6112
|
+
var import_node_fs8 = require("fs");
|
|
4454
6113
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
4455
6114
|
var RESERVED_JSON_KEYS = /* @__PURE__ */ new Set(["ts", "level", "msg", "error"]);
|
|
4456
6115
|
var ConfigurableLogger = class {
|
|
@@ -4484,7 +6143,7 @@ var ConfigurableLogger = class {
|
|
|
4484
6143
|
const stream = this.fileStream;
|
|
4485
6144
|
this.fileStream = null;
|
|
4486
6145
|
if (!stream) return Promise.resolve();
|
|
4487
|
-
return new Promise((
|
|
6146
|
+
return new Promise((resolve2) => stream.end(() => resolve2()));
|
|
4488
6147
|
}
|
|
4489
6148
|
emit(level, message, error, meta) {
|
|
4490
6149
|
if (LEVEL_ORDER[level] > this.threshold) return;
|
|
@@ -4526,7 +6185,7 @@ var ConfigurableLogger = class {
|
|
|
4526
6185
|
if (this.fileDisabled || !this.filePath) return null;
|
|
4527
6186
|
if (this.fileStream) return this.fileStream;
|
|
4528
6187
|
try {
|
|
4529
|
-
const stream = (0,
|
|
6188
|
+
const stream = (0, import_node_fs8.createWriteStream)(this.filePath, { flags: "a" });
|
|
4530
6189
|
stream.on("error", () => {
|
|
4531
6190
|
this.fileDisabled = true;
|
|
4532
6191
|
this.fileStream = null;
|
|
@@ -4595,7 +6254,7 @@ function safeStringify(value) {
|
|
|
4595
6254
|
}
|
|
4596
6255
|
|
|
4597
6256
|
// src/ports/JsonApiServerSettingsStore.ts
|
|
4598
|
-
var
|
|
6257
|
+
var import_node_fs9 = require("fs");
|
|
4599
6258
|
var import_outbound_api3 = require("@omnicross/core/outbound-api");
|
|
4600
6259
|
var JsonApiServerSettingsStore = class {
|
|
4601
6260
|
/**
|
|
@@ -4622,7 +6281,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
4622
6281
|
if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return;
|
|
4623
6282
|
const file = this.readFile();
|
|
4624
6283
|
file.server = this.encryptSecrets(value);
|
|
4625
|
-
(0,
|
|
6284
|
+
(0, import_node_fs9.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
4626
6285
|
}
|
|
4627
6286
|
/** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
|
|
4628
6287
|
encryptSecrets(config) {
|
|
@@ -4645,7 +6304,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
4645
6304
|
/** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
|
|
4646
6305
|
readFile() {
|
|
4647
6306
|
try {
|
|
4648
|
-
const raw = (0,
|
|
6307
|
+
const raw = (0, import_node_fs9.readFileSync)(this.configPath, "utf8");
|
|
4649
6308
|
const parsed = JSON.parse(raw);
|
|
4650
6309
|
if (parsed && typeof parsed === "object") return parsed;
|
|
4651
6310
|
} catch {
|
|
@@ -4655,8 +6314,8 @@ var JsonApiServerSettingsStore = class {
|
|
|
4655
6314
|
};
|
|
4656
6315
|
|
|
4657
6316
|
// src/ports/JsonlUsageEventStore.ts
|
|
4658
|
-
var
|
|
4659
|
-
var
|
|
6317
|
+
var import_node_crypto11 = require("crypto");
|
|
6318
|
+
var import_node_fs10 = require("fs");
|
|
4660
6319
|
var JsonlUsageEventStore = class {
|
|
4661
6320
|
constructor(eventsPath, isPriced) {
|
|
4662
6321
|
this.eventsPath = eventsPath;
|
|
@@ -4668,10 +6327,10 @@ var JsonlUsageEventStore = class {
|
|
|
4668
6327
|
async insert(input) {
|
|
4669
6328
|
const row = {
|
|
4670
6329
|
...input,
|
|
4671
|
-
id: (0,
|
|
6330
|
+
id: (0, import_node_crypto11.randomUUID)(),
|
|
4672
6331
|
ts: input.ts ?? Date.now()
|
|
4673
6332
|
};
|
|
4674
|
-
(0,
|
|
6333
|
+
(0, import_node_fs10.appendFileSync)(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
|
|
4675
6334
|
return row.id;
|
|
4676
6335
|
}
|
|
4677
6336
|
async getTotals(range) {
|
|
@@ -4766,15 +6425,15 @@ var JsonlUsageEventStore = class {
|
|
|
4766
6425
|
* Used to lazily seed the outbound key-policy spend tracker (once per key). A
|
|
4767
6426
|
* key with no attributed events yields all zeros.
|
|
4768
6427
|
*/
|
|
4769
|
-
async getSpendByKey(
|
|
6428
|
+
async getSpendByKey(query2) {
|
|
4770
6429
|
let totalUsd = 0;
|
|
4771
6430
|
let dailyUsd = 0;
|
|
4772
6431
|
let weeklyUsd = 0;
|
|
4773
|
-
for (const row of this.readRows({ startTs: 0, endTs:
|
|
4774
|
-
if (row.apiKeyId !==
|
|
6432
|
+
for (const row of this.readRows({ startTs: 0, endTs: query2.endTs })) {
|
|
6433
|
+
if (row.apiKeyId !== query2.apiKeyId) continue;
|
|
4775
6434
|
totalUsd += row.costUsd;
|
|
4776
|
-
if (row.ts >=
|
|
4777
|
-
if (row.ts >=
|
|
6435
|
+
if (row.ts >= query2.dayStartTs) dailyUsd += row.costUsd;
|
|
6436
|
+
if (row.ts >= query2.weekStartTs) weeklyUsd += row.costUsd;
|
|
4778
6437
|
}
|
|
4779
6438
|
return { totalUsd, dailyUsd, weeklyUsd };
|
|
4780
6439
|
}
|
|
@@ -4859,10 +6518,10 @@ var JsonlUsageEventStore = class {
|
|
|
4859
6518
|
}
|
|
4860
6519
|
/** Parse every line, skipping malformed/torn lines defensively. */
|
|
4861
6520
|
readAllRows() {
|
|
4862
|
-
if (!(0,
|
|
6521
|
+
if (!(0, import_node_fs10.existsSync)(this.eventsPath)) return [];
|
|
4863
6522
|
let raw;
|
|
4864
6523
|
try {
|
|
4865
|
-
raw = (0,
|
|
6524
|
+
raw = (0, import_node_fs10.readFileSync)(this.eventsPath, "utf8");
|
|
4866
6525
|
} catch {
|
|
4867
6526
|
return [];
|
|
4868
6527
|
}
|
|
@@ -4946,7 +6605,7 @@ function isUsageEventRecord(parsed) {
|
|
|
4946
6605
|
}
|
|
4947
6606
|
|
|
4948
6607
|
// src/ports/JsonOutboundKeyDb.ts
|
|
4949
|
-
var
|
|
6608
|
+
var import_node_fs11 = require("fs");
|
|
4950
6609
|
var JsonOutboundKeyDb = class {
|
|
4951
6610
|
constructor(keysPath) {
|
|
4952
6611
|
this.keysPath = keysPath;
|
|
@@ -4972,7 +6631,10 @@ var JsonOutboundKeyDb = class {
|
|
|
4972
6631
|
enabled: true,
|
|
4973
6632
|
createdAt: input.createdAt ?? Date.now(),
|
|
4974
6633
|
lastUsedAt: null,
|
|
4975
|
-
revokedAt: null
|
|
6634
|
+
revokedAt: null,
|
|
6635
|
+
kind: input.kind,
|
|
6636
|
+
allowedEndpoints: input.allowedEndpoints,
|
|
6637
|
+
loopbackOnly: input.loopbackOnly
|
|
4976
6638
|
};
|
|
4977
6639
|
rows.push(row);
|
|
4978
6640
|
this.writeRows(rows);
|
|
@@ -5049,16 +6711,16 @@ var JsonOutboundKeyDb = class {
|
|
|
5049
6711
|
}
|
|
5050
6712
|
/** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5051
6713
|
readRows() {
|
|
5052
|
-
if (!(0,
|
|
6714
|
+
if (!(0, import_node_fs11.existsSync)(this.keysPath)) return [];
|
|
5053
6715
|
try {
|
|
5054
|
-
const parsed = JSON.parse((0,
|
|
6716
|
+
const parsed = JSON.parse((0, import_node_fs11.readFileSync)(this.keysPath, "utf8"));
|
|
5055
6717
|
return Array.isArray(parsed) ? parsed : [];
|
|
5056
6718
|
} catch {
|
|
5057
6719
|
return [];
|
|
5058
6720
|
}
|
|
5059
6721
|
}
|
|
5060
6722
|
writeRows(rows) {
|
|
5061
|
-
(0,
|
|
6723
|
+
(0, import_node_fs11.writeFileSync)(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
5062
6724
|
}
|
|
5063
6725
|
};
|
|
5064
6726
|
function applyPolicyField(row, field, value) {
|
|
@@ -5068,12 +6730,29 @@ function applyPolicyField(row, field, value) {
|
|
|
5068
6730
|
}
|
|
5069
6731
|
|
|
5070
6732
|
// src/ports/JsonPricingStore.ts
|
|
5071
|
-
var
|
|
6733
|
+
var import_node_fs12 = require("fs");
|
|
6734
|
+
var import_node_crypto12 = require("crypto");
|
|
5072
6735
|
var JsonPricingStore = class {
|
|
5073
6736
|
constructor(pricingPath) {
|
|
5074
6737
|
this.pricingPath = pricingPath;
|
|
5075
6738
|
}
|
|
5076
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
|
+
}
|
|
5077
6756
|
async getAll() {
|
|
5078
6757
|
return this.readRows();
|
|
5079
6758
|
}
|
|
@@ -5086,17 +6765,17 @@ var JsonPricingStore = class {
|
|
|
5086
6765
|
*/
|
|
5087
6766
|
async upsert(input, asUserEdit) {
|
|
5088
6767
|
const rows = this.readRows();
|
|
5089
|
-
const entry = this.applyUpsert(rows, input, asUserEdit);
|
|
6768
|
+
const entry = this.applyUpsert(rows, input, asUserEdit, "litellm");
|
|
5090
6769
|
this.writeRows(rows);
|
|
5091
6770
|
return entry;
|
|
5092
6771
|
}
|
|
5093
6772
|
/**
|
|
5094
6773
|
* Apply a batch fetched from a pricing source. Rows whose local copy is
|
|
5095
6774
|
* user-edited are NOT applied — they come back as `{ current, incoming }`
|
|
5096
|
-
* conflicts; everything else is upserted
|
|
5097
|
-
* for the whole batch.
|
|
6775
|
+
* conflicts; everything else is upserted with the supplied automatic source.
|
|
6776
|
+
* ONE file write for the whole batch.
|
|
5098
6777
|
*/
|
|
5099
|
-
async bulkApplyFromSource(entries) {
|
|
6778
|
+
async bulkApplyFromSource(entries, source = "litellm") {
|
|
5100
6779
|
const rows = this.readRows();
|
|
5101
6780
|
const applied = [];
|
|
5102
6781
|
const conflicts = [];
|
|
@@ -5112,7 +6791,8 @@ var JsonPricingStore = class {
|
|
|
5112
6791
|
rows,
|
|
5113
6792
|
incoming,
|
|
5114
6793
|
/* asUserEdit */
|
|
5115
|
-
false
|
|
6794
|
+
false,
|
|
6795
|
+
source
|
|
5116
6796
|
));
|
|
5117
6797
|
}
|
|
5118
6798
|
if (applied.length > 0) this.writeRows(rows);
|
|
@@ -5135,7 +6815,8 @@ var JsonPricingStore = class {
|
|
|
5135
6815
|
rows,
|
|
5136
6816
|
r.incoming,
|
|
5137
6817
|
/* asUserEdit */
|
|
5138
|
-
false
|
|
6818
|
+
false,
|
|
6819
|
+
"litellm"
|
|
5139
6820
|
);
|
|
5140
6821
|
overwrittenCount += 1;
|
|
5141
6822
|
}
|
|
@@ -5156,7 +6837,7 @@ var JsonPricingStore = class {
|
|
|
5156
6837
|
return true;
|
|
5157
6838
|
}
|
|
5158
6839
|
/** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
|
|
5159
|
-
applyUpsert(rows, input, asUserEdit) {
|
|
6840
|
+
applyUpsert(rows, input, asUserEdit, automaticSource) {
|
|
5160
6841
|
const now = Date.now();
|
|
5161
6842
|
const entry = {
|
|
5162
6843
|
providerId: input.providerId,
|
|
@@ -5165,7 +6846,7 @@ var JsonPricingStore = class {
|
|
|
5165
6846
|
outputPricePer1m: input.outputPricePer1m,
|
|
5166
6847
|
cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
|
|
5167
6848
|
cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
|
|
5168
|
-
source: asUserEdit ? "user" :
|
|
6849
|
+
source: asUserEdit ? "user" : automaticSource,
|
|
5169
6850
|
userEdited: asUserEdit,
|
|
5170
6851
|
editedAt: asUserEdit ? now : null,
|
|
5171
6852
|
updatedAt: now
|
|
@@ -5179,21 +6860,142 @@ var JsonPricingStore = class {
|
|
|
5179
6860
|
}
|
|
5180
6861
|
/** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5181
6862
|
readRows() {
|
|
5182
|
-
if (!(0,
|
|
6863
|
+
if (!(0, import_node_fs12.existsSync)(this.pricingPath)) return [];
|
|
5183
6864
|
try {
|
|
5184
|
-
const parsed = JSON.parse((0,
|
|
6865
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(this.pricingPath, "utf8"));
|
|
5185
6866
|
return Array.isArray(parsed) ? parsed : [];
|
|
5186
6867
|
} catch {
|
|
5187
6868
|
return [];
|
|
5188
6869
|
}
|
|
5189
6870
|
}
|
|
5190
6871
|
writeRows(rows) {
|
|
5191
|
-
|
|
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);
|
|
5192
6991
|
}
|
|
5193
6992
|
};
|
|
6993
|
+
function finiteOrNull(value) {
|
|
6994
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6995
|
+
}
|
|
5194
6996
|
|
|
5195
6997
|
// src/ports/JsonVoucherDb.ts
|
|
5196
|
-
var
|
|
6998
|
+
var import_node_fs14 = require("fs");
|
|
5197
6999
|
var JsonVoucherDb = class {
|
|
5198
7000
|
constructor(vouchersPath) {
|
|
5199
7001
|
this.vouchersPath = vouchersPath;
|
|
@@ -5271,55 +7073,32 @@ var JsonVoucherDb = class {
|
|
|
5271
7073
|
}
|
|
5272
7074
|
/** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5273
7075
|
readRows() {
|
|
5274
|
-
if (!(0,
|
|
7076
|
+
if (!(0, import_node_fs14.existsSync)(this.vouchersPath)) return [];
|
|
5275
7077
|
try {
|
|
5276
|
-
const parsed = JSON.parse((0,
|
|
7078
|
+
const parsed = JSON.parse((0, import_node_fs14.readFileSync)(this.vouchersPath, "utf8"));
|
|
5277
7079
|
return Array.isArray(parsed) ? parsed : [];
|
|
5278
7080
|
} catch {
|
|
5279
7081
|
return [];
|
|
5280
7082
|
}
|
|
5281
7083
|
}
|
|
5282
7084
|
writeRows(rows) {
|
|
5283
|
-
(0,
|
|
7085
|
+
(0, import_node_fs14.writeFileSync)(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
5284
7086
|
}
|
|
5285
7087
|
};
|
|
5286
7088
|
|
|
5287
7089
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5288
|
-
var
|
|
5289
|
-
var
|
|
5290
|
-
var
|
|
5291
|
-
var
|
|
5292
|
-
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");
|
|
5293
7096
|
var import_subscriptions3 = require("@omnicross/subscriptions");
|
|
5294
7097
|
|
|
5295
7098
|
// src/ports/account-sync.ts
|
|
5296
|
-
var IMPORT_EXPIRY_MARGIN_MS = 6e4;
|
|
5297
7099
|
function viewOf(tokens) {
|
|
5298
7100
|
return tokens;
|
|
5299
7101
|
}
|
|
5300
|
-
function decideExternalImport(captured, external, now = Date.now()) {
|
|
5301
|
-
if (!external?.accessToken) return "no-credential";
|
|
5302
|
-
const capturedRt = viewOf(captured).refreshToken;
|
|
5303
|
-
const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
|
|
5304
|
-
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
|
|
5305
|
-
return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
|
|
5306
|
-
}
|
|
5307
|
-
function buildImportedTokens(captured, external) {
|
|
5308
|
-
const imported = {
|
|
5309
|
-
...captured,
|
|
5310
|
-
accessToken: external.accessToken,
|
|
5311
|
-
status: "authorized",
|
|
5312
|
-
errorMessage: void 0,
|
|
5313
|
-
syncWarning: void 0,
|
|
5314
|
-
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5315
|
-
};
|
|
5316
|
-
if (external.refreshToken) imported.refreshToken = external.refreshToken;
|
|
5317
|
-
if (external.expiresAt) imported.expiresAt = external.expiresAt;
|
|
5318
|
-
else delete imported.expiresAt;
|
|
5319
|
-
if (external.idToken) imported.idToken = external.idToken;
|
|
5320
|
-
if (external.scopes) imported.scopes = external.scopes;
|
|
5321
|
-
return imported;
|
|
5322
|
-
}
|
|
5323
7102
|
function buildTokensFromExternal(provider, external) {
|
|
5324
7103
|
const base = {
|
|
5325
7104
|
authMethod: "oauth",
|
|
@@ -5340,14 +7119,6 @@ function buildTokensFromExternal(provider, external) {
|
|
|
5340
7119
|
if (external.idToken) tokens.idToken = external.idToken;
|
|
5341
7120
|
return tokens;
|
|
5342
7121
|
}
|
|
5343
|
-
function isExternalDivergent(stored, external) {
|
|
5344
|
-
if (!external?.accessToken || !external.refreshToken) return false;
|
|
5345
|
-
const view = viewOf(stored);
|
|
5346
|
-
if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
|
|
5347
|
-
const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
|
|
5348
|
-
const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
|
|
5349
|
-
return !Number.isFinite(storedExp) || externalExp > storedExp;
|
|
5350
|
-
}
|
|
5351
7122
|
function findDuplicateCredentialIds(accounts) {
|
|
5352
7123
|
const byCredential = /* @__PURE__ */ new Map();
|
|
5353
7124
|
for (const account of accounts) {
|
|
@@ -5366,11 +7137,11 @@ function findDuplicateCredentialIds(accounts) {
|
|
|
5366
7137
|
}
|
|
5367
7138
|
|
|
5368
7139
|
// src/ports/external-cli-credentials.ts
|
|
5369
|
-
var
|
|
5370
|
-
var
|
|
5371
|
-
var
|
|
5372
|
-
function externalStorePath(provider, home = (0,
|
|
5373
|
-
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");
|
|
5374
7145
|
}
|
|
5375
7146
|
function decodeJwtExpiryMs(token) {
|
|
5376
7147
|
try {
|
|
@@ -5417,12 +7188,12 @@ function parseCodexTokensEnvelope(raw) {
|
|
|
5417
7188
|
}
|
|
5418
7189
|
return parsed;
|
|
5419
7190
|
}
|
|
5420
|
-
function readExternalCliCredentials(provider, home = (0,
|
|
7191
|
+
function readExternalCliCredentials(provider, home = (0, import_node_os3.homedir)()) {
|
|
5421
7192
|
const path2 = externalStorePath(provider, home);
|
|
5422
|
-
if (!(0,
|
|
7193
|
+
if (!(0, import_node_fs15.existsSync)(path2)) return null;
|
|
5423
7194
|
let raw;
|
|
5424
7195
|
try {
|
|
5425
|
-
const parsed = JSON.parse((0,
|
|
7196
|
+
const parsed = JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
|
|
5426
7197
|
raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
5427
7198
|
} catch {
|
|
5428
7199
|
return null;
|
|
@@ -5430,84 +7201,6 @@ function readExternalCliCredentials(provider, home = (0, import_node_os2.homedir
|
|
|
5430
7201
|
return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
|
|
5431
7202
|
}
|
|
5432
7203
|
|
|
5433
|
-
// src/ports/external-cli-store.ts
|
|
5434
|
-
var import_node_fs12 = require("fs");
|
|
5435
|
-
var import_node_os3 = require("os");
|
|
5436
|
-
var import_node_path6 = require("path");
|
|
5437
|
-
function markerPath(provider, home) {
|
|
5438
|
-
return `${externalStorePath(provider, home)}.omnicross-managed`;
|
|
5439
|
-
}
|
|
5440
|
-
function backupPath(provider, home) {
|
|
5441
|
-
return `${externalStorePath(provider, home)}.omnicross-backup`;
|
|
5442
|
-
}
|
|
5443
|
-
function buildClaudeOAuthEnvelope(tokens) {
|
|
5444
|
-
if (!tokens.accessToken) return null;
|
|
5445
|
-
const envelope = { accessToken: tokens.accessToken };
|
|
5446
|
-
if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
|
|
5447
|
-
if (tokens.expiresAt) {
|
|
5448
|
-
const ms = Date.parse(tokens.expiresAt);
|
|
5449
|
-
if (Number.isFinite(ms)) envelope.expiresAt = ms;
|
|
5450
|
-
}
|
|
5451
|
-
if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
|
|
5452
|
-
return envelope;
|
|
5453
|
-
}
|
|
5454
|
-
function buildCodexTokensEnvelope(tokens) {
|
|
5455
|
-
if (!tokens.accessToken && !tokens.idToken) return null;
|
|
5456
|
-
const envelope = { access_token: tokens.accessToken ?? "" };
|
|
5457
|
-
if (tokens.idToken) envelope.id_token = tokens.idToken;
|
|
5458
|
-
if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
|
|
5459
|
-
return envelope;
|
|
5460
|
-
}
|
|
5461
|
-
function readExistingObject(path2) {
|
|
5462
|
-
if (!(0, import_node_fs12.existsSync)(path2)) return {};
|
|
5463
|
-
try {
|
|
5464
|
-
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
|
|
5465
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5466
|
-
} catch {
|
|
5467
|
-
return {};
|
|
5468
|
-
}
|
|
5469
|
-
}
|
|
5470
|
-
function writeAtomic(path2, content) {
|
|
5471
|
-
(0, import_node_fs12.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
|
|
5472
|
-
const temp = `${path2}.omnicross-tmp`;
|
|
5473
|
-
(0, import_node_fs12.writeFileSync)(temp, content, "utf8");
|
|
5474
|
-
(0, import_node_fs12.renameSync)(temp, path2);
|
|
5475
|
-
}
|
|
5476
|
-
function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
|
|
5477
|
-
return {
|
|
5478
|
-
readMarkerAccountId(provider) {
|
|
5479
|
-
const path2 = markerPath(provider, home);
|
|
5480
|
-
if (!(0, import_node_fs12.existsSync)(path2)) return void 0;
|
|
5481
|
-
try {
|
|
5482
|
-
const parsed = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
|
|
5483
|
-
return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
|
|
5484
|
-
} catch {
|
|
5485
|
-
return void 0;
|
|
5486
|
-
}
|
|
5487
|
-
},
|
|
5488
|
-
writeMarker(provider, accountId) {
|
|
5489
|
-
writeAtomic(
|
|
5490
|
-
markerPath(provider, home),
|
|
5491
|
-
JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
5492
|
-
);
|
|
5493
|
-
},
|
|
5494
|
-
writeBack(provider, accountId, tokens) {
|
|
5495
|
-
const owner = this.readMarkerAccountId(provider);
|
|
5496
|
-
if (owner !== accountId) return false;
|
|
5497
|
-
const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
|
|
5498
|
-
if (!envelope) return false;
|
|
5499
|
-
const storePath = externalStorePath(provider, home);
|
|
5500
|
-
if ((0, import_node_fs12.existsSync)(storePath) && !(0, import_node_fs12.existsSync)(backupPath(provider, home))) {
|
|
5501
|
-
(0, import_node_fs12.copyFileSync)(storePath, backupPath(provider, home));
|
|
5502
|
-
}
|
|
5503
|
-
const existing = readExistingObject(storePath);
|
|
5504
|
-
const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
|
|
5505
|
-
writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
|
|
5506
|
-
return true;
|
|
5507
|
-
}
|
|
5508
|
-
};
|
|
5509
|
-
}
|
|
5510
|
-
|
|
5511
7204
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5512
7205
|
var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
|
|
5513
7206
|
var JsonSubscriptionCredentialStore = class {
|
|
@@ -5520,32 +7213,30 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5520
7213
|
* proxy-aware {@link fetchUpstream} that threads the
|
|
5521
7214
|
* `{ providerId, accountId }` ctx (upstream-proxy M1) so a
|
|
5522
7215
|
* per-account/per-provider proxy is honored on refresh exactly
|
|
5523
|
-
* as on relay
|
|
7216
|
+
* as on relay refresh egresses from the SAME proxy IP as the
|
|
5524
7217
|
* account's traffic. NOT used by any read/write path.
|
|
5525
7218
|
*/
|
|
5526
|
-
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials
|
|
7219
|
+
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials) {
|
|
5527
7220
|
this.tokensPath = tokensPath;
|
|
5528
7221
|
this.box = box;
|
|
5529
7222
|
this.fetchImpl = fetchImpl;
|
|
5530
7223
|
this.externalCliReader = externalCliReader;
|
|
5531
|
-
this.externalCliStore = externalCliStore;
|
|
5532
7224
|
}
|
|
5533
7225
|
tokensPath;
|
|
5534
7226
|
box;
|
|
5535
7227
|
fetchImpl;
|
|
5536
7228
|
externalCliReader;
|
|
5537
|
-
externalCliStore;
|
|
5538
7229
|
/**
|
|
5539
7230
|
* The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
|
|
5540
7231
|
* TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
|
|
5541
7232
|
* through {@link fetchUpstream} with the account's `{ providerId, accountId }`
|
|
5542
|
-
* ctx so the per-account/provider proxy applies. `@internal`
|
|
7233
|
+
* ctx so the per-account/provider proxy applies. `@internal` also a test seam.
|
|
5543
7234
|
*/
|
|
5544
7235
|
buildRefreshFetch(providerId, accountId) {
|
|
5545
|
-
return this.fetchImpl ?? ((url, init) => (0,
|
|
7236
|
+
return this.fetchImpl ?? ((url, init) => (0, import_upstreamFetch4.fetchUpstream)(url, init, { providerId, accountId }));
|
|
5546
7237
|
}
|
|
5547
7238
|
/**
|
|
5548
|
-
* In-flight refresh coalescing
|
|
7239
|
+
* In-flight refresh coalescing. OAuth refresh tokens are
|
|
5549
7240
|
* SINGLE-USE: two concurrent refreshes of one account each spend the same
|
|
5550
7241
|
* token and the loser bricks a healthy account. Every refresh entry point
|
|
5551
7242
|
* (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
|
|
@@ -5560,13 +7251,13 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5560
7251
|
return run;
|
|
5561
7252
|
}
|
|
5562
7253
|
/** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
|
|
5563
|
-
* file is absent/corrupt). This is the hot read
|
|
7254
|
+
* file is absent/corrupt). This is the hot read the codex / gemini auth
|
|
5564
7255
|
* strategies pull `accessToken` / `expiresAt` / `status` from it. */
|
|
5565
7256
|
async getFullConfig() {
|
|
5566
7257
|
return this.readConfig();
|
|
5567
7258
|
}
|
|
5568
7259
|
/** Current Claude OAuth access token, or `null` when none is stored. No inline
|
|
5569
|
-
* refresh here
|
|
7260
|
+
* refresh here the lead-window / 401-retry refresh is driven by the
|
|
5570
7261
|
* subscription auth strategy, which calls `refreshClaudeToken` (now real). */
|
|
5571
7262
|
async getValidClaudeAccessToken() {
|
|
5572
7263
|
return this.readConfig().claude?.accessToken ?? null;
|
|
@@ -5591,13 +7282,14 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5591
7282
|
/**
|
|
5592
7283
|
* DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
|
|
5593
7284
|
* each provider's accounts to the secret-free `SubscriptionAccountSanitized`
|
|
5594
|
-
* shape (id/label/status/expiresAt/hasAccessToken/isActive)
|
|
7285
|
+
* shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
|
|
5595
7286
|
* Used by the admin accounts GET (secret-IN-never-OUT).
|
|
5596
7287
|
*/
|
|
5597
7288
|
async listSanitizedAccounts() {
|
|
5598
7289
|
const config = this.readConfig();
|
|
5599
|
-
const health2 = (0,
|
|
5600
|
-
const
|
|
7290
|
+
const health2 = (0, import_SubscriptionAccountHealth2.getSharedAccountHealth)();
|
|
7291
|
+
const allowanceScheduling = (0, import_AccountAllowanceScheduling3.getSharedAccountAllowanceScheduling)();
|
|
7292
|
+
const identityStore = (0, import_SubscriptionIdentityStore2.getSharedIdentityStore)();
|
|
5601
7293
|
const fingerprintOn = identityStore.isEnabled();
|
|
5602
7294
|
const now = Date.now();
|
|
5603
7295
|
const out = {};
|
|
@@ -5606,7 +7298,13 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5606
7298
|
if (sanitized.length === 0) continue;
|
|
5607
7299
|
for (const account of sanitized) {
|
|
5608
7300
|
const status = health2.getStatus(provider, account.id, now);
|
|
7301
|
+
const allowance = allowanceScheduling.preview(provider, account.id, account.priority ?? 50, now);
|
|
5609
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;
|
|
5610
7308
|
account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
|
|
5611
7309
|
if (fingerprintOn && provider === "claude") {
|
|
5612
7310
|
account.identityCaptured = identityStore.hasIdentity(provider, account.id);
|
|
@@ -5614,31 +7312,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5614
7312
|
account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
|
|
5615
7313
|
}
|
|
5616
7314
|
}
|
|
5617
|
-
out[provider] = this.
|
|
7315
|
+
out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
|
|
5618
7316
|
}
|
|
5619
7317
|
return out;
|
|
5620
7318
|
}
|
|
5621
7319
|
/**
|
|
5622
|
-
* List-time credential
|
|
5623
|
-
*
|
|
5624
|
-
* credential
|
|
5625
|
-
* rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
|
|
5626
|
-
* a failed refresh (`external-not-rotated`) takes precedence — it is the most
|
|
5627
|
-
* 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.
|
|
5628
7323
|
*/
|
|
5629
|
-
|
|
7324
|
+
attachDuplicateWarnings(config, provider, sanitized) {
|
|
5630
7325
|
const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
|
|
5631
|
-
let divergentId;
|
|
5632
|
-
if (provider === "claude" || provider === "codex") {
|
|
5633
|
-
const active = getActiveAccount(config, provider);
|
|
5634
|
-
if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
|
|
5635
|
-
divergentId = active.id;
|
|
5636
|
-
}
|
|
5637
|
-
}
|
|
5638
|
-
if (duplicates.size === 0 && !divergentId) return sanitized;
|
|
5639
7326
|
return sanitized.map((account) => {
|
|
5640
|
-
const computed =
|
|
5641
|
-
|
|
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 };
|
|
5642
7334
|
});
|
|
5643
7335
|
}
|
|
5644
7336
|
/** Read the external CLI store, never letting an fs/parse error escape. */
|
|
@@ -5651,11 +7343,11 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5651
7343
|
}
|
|
5652
7344
|
/**
|
|
5653
7345
|
* Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
|
|
5654
|
-
* the block has no refresh_token (setup-token / manual)
|
|
7346
|
+
* the block has no refresh_token (setup-token / manual) no upstream call, the
|
|
5655
7347
|
* block is untouched. Otherwise mint via the shared claude refresh flow and
|
|
5656
7348
|
* write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
|
|
5657
|
-
* On failure
|
|
5658
|
-
* errorMessage
|
|
7349
|
+
* On failure status:expired +
|
|
7350
|
+
* errorMessage `false`.
|
|
5659
7351
|
*/
|
|
5660
7352
|
async refreshClaudeToken() {
|
|
5661
7353
|
return this.coalesce("claude:active", async () => {
|
|
@@ -5680,19 +7372,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5680
7372
|
syncWarning: void 0
|
|
5681
7373
|
};
|
|
5682
7374
|
this.writeBackById("claude", capturedId, next);
|
|
5683
|
-
this.resyncExternal("claude", capturedId, next);
|
|
5684
7375
|
return true;
|
|
5685
7376
|
} catch (error) {
|
|
5686
|
-
if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
|
|
5687
|
-
const r = await import_subscriptions3.claudeOAuth.refreshAccessToken(rt, refreshFetch);
|
|
5688
|
-
return {
|
|
5689
|
-
accessToken: r.accessToken,
|
|
5690
|
-
refreshToken: r.refreshToken,
|
|
5691
|
-
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5692
|
-
};
|
|
5693
|
-
})) {
|
|
5694
|
-
return true;
|
|
5695
|
-
}
|
|
5696
7377
|
this.markExpiredById("claude", capturedId, claude, error);
|
|
5697
7378
|
return false;
|
|
5698
7379
|
}
|
|
@@ -5727,20 +7408,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5727
7408
|
syncWarning: void 0
|
|
5728
7409
|
};
|
|
5729
7410
|
this.writeBackById("codex", capturedId, next);
|
|
5730
|
-
this.resyncExternal("codex", capturedId, next);
|
|
5731
7411
|
return true;
|
|
5732
7412
|
} catch (error) {
|
|
5733
|
-
if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
|
|
5734
|
-
const r = await import_subscriptions3.codexOAuth.refreshAccessToken(rt, refreshFetch);
|
|
5735
|
-
return {
|
|
5736
|
-
accessToken: r.accessToken,
|
|
5737
|
-
refreshToken: r.refreshToken,
|
|
5738
|
-
idToken: r.idToken,
|
|
5739
|
-
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5740
|
-
};
|
|
5741
|
-
})) {
|
|
5742
|
-
return true;
|
|
5743
|
-
}
|
|
5744
7413
|
this.markExpiredById("codex", capturedId, codex, error);
|
|
5745
7414
|
return false;
|
|
5746
7415
|
}
|
|
@@ -5783,11 +7452,10 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5783
7452
|
});
|
|
5784
7453
|
}
|
|
5785
7454
|
/**
|
|
5786
|
-
* Refresh a SPECIFIC account by id (background scheduler sweep
|
|
5787
|
-
*
|
|
5788
|
-
*
|
|
5789
|
-
*
|
|
5790
|
-
* 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`.
|
|
5791
7459
|
*/
|
|
5792
7460
|
async refreshAccountById(provider, id) {
|
|
5793
7461
|
return this.coalesce(`${provider}:${id}`, async () => {
|
|
@@ -5801,7 +7469,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5801
7469
|
const next = {
|
|
5802
7470
|
...captured,
|
|
5803
7471
|
accessToken: refreshed.accessToken,
|
|
5804
|
-
// Gemini's refresh response omits a new refresh token
|
|
7472
|
+
// Gemini's refresh response omits a new refresh token keep the captured.
|
|
5805
7473
|
refreshToken: refreshed.refreshToken ?? captured.refreshToken,
|
|
5806
7474
|
expiresAt: refreshed.expiresAt,
|
|
5807
7475
|
status: "authorized",
|
|
@@ -5811,7 +7479,6 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5811
7479
|
};
|
|
5812
7480
|
if (refreshed.idToken) next.idToken = refreshed.idToken;
|
|
5813
7481
|
this.writeBackById(provider, id, next);
|
|
5814
|
-
if (provider !== "gemini") this.resyncExternal(provider, id, next);
|
|
5815
7482
|
return true;
|
|
5816
7483
|
} catch (error) {
|
|
5817
7484
|
this.markExpiredById(provider, id, captured, error);
|
|
@@ -5819,7 +7486,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5819
7486
|
}
|
|
5820
7487
|
});
|
|
5821
7488
|
}
|
|
5822
|
-
//
|
|
7489
|
+
// By-id account-pool surface (subscription-account-scheduling, design D6)
|
|
5823
7490
|
/**
|
|
5824
7491
|
* Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
|
|
5825
7492
|
* provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
|
|
@@ -5852,7 +7519,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5852
7519
|
/**
|
|
5853
7520
|
* Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
|
|
5854
7521
|
* `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
|
|
5855
|
-
*
|
|
7522
|
+
* `false` (no refresh affordance).
|
|
5856
7523
|
*/
|
|
5857
7524
|
async refreshAccountToken(providerId, accountId) {
|
|
5858
7525
|
if (providerId === "opencodego") return false;
|
|
@@ -5874,7 +7541,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5874
7541
|
* (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
|
|
5875
7542
|
* whitelisted fingerprint headers; the token mirror is untouched); a no-op for
|
|
5876
7543
|
* an unknown id. Called by the identity store's persistence port on a first-seen
|
|
5877
|
-
* 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
|
|
5878
7545
|
* store's port wrapper swallows a rejection so the relay hot path is unaffected.
|
|
5879
7546
|
*/
|
|
5880
7547
|
async setAccountIdentity(providerId, accountId, identity) {
|
|
@@ -5899,7 +7566,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5899
7566
|
* DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
|
|
5900
7567
|
* the port). Passing `undefined` clears the override. Write-only password: when
|
|
5901
7568
|
* the incoming structured proxy omits the password but the account already had
|
|
5902
|
-
* one, the current (decrypted) password is preserved
|
|
7569
|
+
* one, the current (decrypted) password is preserved editing host/port never
|
|
5903
7570
|
* wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
|
|
5904
7571
|
*/
|
|
5905
7572
|
async setAccountProxy(providerId, accountId, proxy) {
|
|
@@ -5934,75 +7601,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5934
7601
|
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5935
7602
|
};
|
|
5936
7603
|
}
|
|
5937
|
-
/**
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
* UI can tell "genuine revocation" apart from a plain refresh failure.
|
|
5945
|
-
*/
|
|
5946
|
-
async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
|
|
5947
|
-
const markerOwner = this.safeReadMarker(provider);
|
|
5948
|
-
if (markerOwner && markerOwner !== capturedId) return false;
|
|
5949
|
-
const external = this.safeReadExternal(provider);
|
|
5950
|
-
const decision = decideExternalImport(captured, external);
|
|
5951
|
-
if (decision === "not-rotated") {
|
|
5952
|
-
captured.syncWarning = "external-not-rotated";
|
|
5953
|
-
return false;
|
|
5954
|
-
}
|
|
5955
|
-
if (decision !== "import" || !external) return false;
|
|
5956
|
-
let imported = buildImportedTokens(
|
|
5957
|
-
captured,
|
|
5958
|
-
external
|
|
5959
|
-
);
|
|
5960
|
-
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
|
|
5961
|
-
if (!accessStillValid) {
|
|
5962
|
-
try {
|
|
5963
|
-
const refreshed = await refreshWithToken(external.refreshToken);
|
|
5964
|
-
imported = {
|
|
5965
|
-
...imported,
|
|
5966
|
-
accessToken: refreshed.accessToken,
|
|
5967
|
-
refreshToken: refreshed.refreshToken ?? imported.refreshToken,
|
|
5968
|
-
expiresAt: refreshed.expiresAt,
|
|
5969
|
-
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5970
|
-
};
|
|
5971
|
-
if (refreshed.idToken) imported.idToken = refreshed.idToken;
|
|
5972
|
-
} catch {
|
|
5973
|
-
return false;
|
|
5974
|
-
}
|
|
5975
|
-
}
|
|
5976
|
-
this.writeBackById(provider, capturedId, imported);
|
|
5977
|
-
this.resyncExternal(provider, capturedId, imported);
|
|
5978
|
-
return true;
|
|
5979
|
-
}
|
|
5980
|
-
/**
|
|
5981
|
-
* Marker-gated external write-back (external-cli-sync). After a successful
|
|
5982
|
-
* refresh of the account that OWNS the provider's native CLI store (imported
|
|
5983
|
-
* via `importExternalCliAccount`), push the rotated credential back into the
|
|
5984
|
-
* file — otherwise the daemon's refresh invalidates the single-use refresh
|
|
5985
|
-
* token and silently logs the bare CLI out. NON-FATAL: the internal store is
|
|
5986
|
-
* already persisted; a failed external write only leaves the file stale,
|
|
5987
|
-
* which the `external-divergent` warning surfaces.
|
|
5988
|
-
*/
|
|
5989
|
-
resyncExternal(provider, accountId, tokens) {
|
|
5990
|
-
try {
|
|
5991
|
-
this.externalCliStore.writeBack(provider, accountId, tokens);
|
|
5992
|
-
} catch {
|
|
5993
|
-
}
|
|
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;
|
|
5994
7611
|
}
|
|
5995
|
-
/**
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
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;
|
|
6002
7619
|
}
|
|
6003
7620
|
/**
|
|
6004
7621
|
* DAEMON-ONLY (admin import button): which providers have a usable external
|
|
6005
|
-
* CLI credential on THIS machine. Pure detection
|
|
7622
|
+
* CLI credential on THIS machine. Pure detection reads the native files,
|
|
6006
7623
|
* never mutates anything, never returns a token.
|
|
6007
7624
|
*/
|
|
6008
7625
|
async listExternalCliAvailability() {
|
|
@@ -6013,21 +7630,22 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6013
7630
|
}
|
|
6014
7631
|
/**
|
|
6015
7632
|
* DAEMON-ONLY (admin import button): import the external CLI's current login
|
|
6016
|
-
* as a NEW account (+ activate)
|
|
6017
|
-
*
|
|
6018
|
-
*
|
|
6019
|
-
*
|
|
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.
|
|
6020
7637
|
*/
|
|
6021
7638
|
async importExternalCliAccount(provider, label) {
|
|
6022
7639
|
const external = this.safeReadExternal(provider);
|
|
6023
7640
|
if (!external?.accessToken) return { ok: false, reason: "no-credential" };
|
|
6024
7641
|
const tokens = buildTokensFromExternal(provider, external);
|
|
6025
7642
|
const result = await this.appendProviderAccount(provider, tokens, label);
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
7643
|
+
return {
|
|
7644
|
+
ok: true,
|
|
7645
|
+
id: result.id,
|
|
7646
|
+
nativeCredentialMode: "read-only",
|
|
7647
|
+
refreshWritesNativeCredentials: false
|
|
7648
|
+
};
|
|
6031
7649
|
}
|
|
6032
7650
|
/**
|
|
6033
7651
|
* Materialize a lazily-synthesized account id to disk (design D3). On a legacy
|
|
@@ -6060,7 +7678,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6060
7678
|
this.writeBackById(providerId, capturedId, {
|
|
6061
7679
|
...block,
|
|
6062
7680
|
status: "expired",
|
|
6063
|
-
errorMessage
|
|
7681
|
+
errorMessage,
|
|
7682
|
+
syncWarning: "syncWarning" in block && block.syncWarning === "duplicate-token" ? "duplicate-token" : void 0
|
|
6064
7683
|
});
|
|
6065
7684
|
}
|
|
6066
7685
|
/**
|
|
@@ -6069,7 +7688,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6069
7688
|
* `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
|
|
6070
7689
|
* OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
|
|
6071
7690
|
* tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
|
|
6072
|
-
* 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
|
|
6073
7692
|
* sees this write.
|
|
6074
7693
|
*/
|
|
6075
7694
|
async writeProviderTokens(providerId, config) {
|
|
@@ -6079,7 +7698,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6079
7698
|
}
|
|
6080
7699
|
/**
|
|
6081
7700
|
* DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
|
|
6082
|
-
* (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
|
|
6083
7702
|
* `omnicross login <provider> --label` to add an account instead of overwriting.
|
|
6084
7703
|
*/
|
|
6085
7704
|
async appendProviderAccount(providerId, config, label) {
|
|
@@ -6113,7 +7732,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6113
7732
|
}
|
|
6114
7733
|
/**
|
|
6115
7734
|
* DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
|
|
6116
|
-
* rejects an unknown id. Label-only
|
|
7735
|
+
* rejects an unknown id. Label-only no token material is read or written
|
|
6117
7736
|
* (the secret-free invariant holds).
|
|
6118
7737
|
*/
|
|
6119
7738
|
async renameAccount(providerId, id, label) {
|
|
@@ -6136,12 +7755,12 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6136
7755
|
}
|
|
6137
7756
|
/** Write the merged config to disk as pretty JSON (mkdir parent if needed).
|
|
6138
7757
|
* Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
|
|
6139
|
-
*
|
|
6140
|
-
* write
|
|
7758
|
+
* `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
7759
|
+
* write incl. child 4's future refresh writes lands encrypted. */
|
|
6141
7760
|
persist(config) {
|
|
6142
|
-
(0,
|
|
7761
|
+
(0, import_node_fs16.mkdirSync)((0, import_node_path10.dirname)(this.tokensPath), { recursive: true });
|
|
6143
7762
|
const encrypted = encryptTokens(config, this.box);
|
|
6144
|
-
(0,
|
|
7763
|
+
(0, import_node_fs16.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
|
|
6145
7764
|
}
|
|
6146
7765
|
/**
|
|
6147
7766
|
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
@@ -6149,18 +7768,18 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6149
7768
|
* subscription bearer path is byte-identical).
|
|
6150
7769
|
*
|
|
6151
7770
|
* The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
|
|
6152
|
-
* file
|
|
7771
|
+
* file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
|
|
6153
7772
|
* wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
|
|
6154
|
-
* box's clear, secret-free error (secrets spec "
|
|
6155
|
-
* SHALL fail-fast, SHALL NOT
|
|
6156
|
-
* 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
|
|
6157
7776
|
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
6158
7777
|
*/
|
|
6159
7778
|
readConfig() {
|
|
6160
|
-
if (!(0,
|
|
7779
|
+
if (!(0, import_node_fs16.existsSync)(this.tokensPath)) return { updatedAt: "" };
|
|
6161
7780
|
let parsed;
|
|
6162
7781
|
try {
|
|
6163
|
-
const raw = JSON.parse((0,
|
|
7782
|
+
const raw = JSON.parse((0, import_node_fs16.readFileSync)(this.tokensPath, "utf8"));
|
|
6164
7783
|
parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
6165
7784
|
} catch {
|
|
6166
7785
|
parsed = null;
|
|
@@ -6172,7 +7791,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6172
7791
|
};
|
|
6173
7792
|
|
|
6174
7793
|
// src/AccountHealthProbeScheduler.ts
|
|
6175
|
-
var
|
|
7794
|
+
var import_upstreamFetch5 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
6176
7795
|
|
|
6177
7796
|
// src/probe/ProbeStrategy.ts
|
|
6178
7797
|
var PROVIDER_PROBE_PLANS = {
|
|
@@ -6215,7 +7834,7 @@ var AccountHealthProbeScheduler = class {
|
|
|
6215
7834
|
this.logger = logger;
|
|
6216
7835
|
this.config = config;
|
|
6217
7836
|
this.now = opts.now ?? Date.now;
|
|
6218
|
-
this.fetchImpl = opts.fetchImpl ??
|
|
7837
|
+
this.fetchImpl = opts.fetchImpl ?? import_upstreamFetch5.fetchUpstream;
|
|
6219
7838
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6220
7839
|
this.planFor = opts.planFor ?? probePlanFor;
|
|
6221
7840
|
}
|
|
@@ -6488,8 +8107,8 @@ var AccountHealthSweeper = class {
|
|
|
6488
8107
|
};
|
|
6489
8108
|
|
|
6490
8109
|
// src/audit/AuditPruneSweeper.ts
|
|
6491
|
-
var
|
|
6492
|
-
var
|
|
8110
|
+
var import_node_fs17 = require("fs");
|
|
8111
|
+
var import_node_path11 = require("path");
|
|
6493
8112
|
|
|
6494
8113
|
// src/audit/auditFiles.ts
|
|
6495
8114
|
var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
@@ -6515,8 +8134,8 @@ function auditFileDateMs(fileName) {
|
|
|
6515
8134
|
var DAY_MS = 24 * 60 * 6e4;
|
|
6516
8135
|
var SWEEP_INTERVAL_MS2 = 60 * 6e4;
|
|
6517
8136
|
var AuditPruneSweeper = class {
|
|
6518
|
-
constructor(
|
|
6519
|
-
this.auditDir =
|
|
8137
|
+
constructor(auditDir2, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
|
|
8138
|
+
this.auditDir = auditDir2;
|
|
6520
8139
|
this.logger = logger;
|
|
6521
8140
|
this.config = config;
|
|
6522
8141
|
this.intervalMs = intervalMs;
|
|
@@ -6563,16 +8182,16 @@ var AuditPruneSweeper = class {
|
|
|
6563
8182
|
if (!this.config.enabled || this.sweeping) return 0;
|
|
6564
8183
|
this.sweeping = true;
|
|
6565
8184
|
try {
|
|
6566
|
-
if (!(0,
|
|
8185
|
+
if (!(0, import_node_fs17.existsSync)(this.auditDir)) return 0;
|
|
6567
8186
|
const today = new Date(this.now());
|
|
6568
8187
|
const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
|
|
6569
8188
|
const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
|
|
6570
8189
|
let removed = 0;
|
|
6571
|
-
for (const file of (0,
|
|
8190
|
+
for (const file of (0, import_node_fs17.readdirSync)(this.auditDir)) {
|
|
6572
8191
|
const dateMs = auditFileDateMs(file);
|
|
6573
8192
|
if (dateMs === null || dateMs >= cutoff) continue;
|
|
6574
8193
|
try {
|
|
6575
|
-
(0,
|
|
8194
|
+
(0, import_node_fs17.unlinkSync)((0, import_node_path11.join)(this.auditDir, file));
|
|
6576
8195
|
removed += 1;
|
|
6577
8196
|
} catch (error) {
|
|
6578
8197
|
this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
|
|
@@ -6595,26 +8214,26 @@ var AuditPruneSweeper = class {
|
|
|
6595
8214
|
};
|
|
6596
8215
|
|
|
6597
8216
|
// src/audit/auditReader.ts
|
|
6598
|
-
var
|
|
6599
|
-
var
|
|
8217
|
+
var import_node_fs18 = require("fs");
|
|
8218
|
+
var import_node_path12 = require("path");
|
|
6600
8219
|
var DEFAULT_LIMIT = 200;
|
|
6601
8220
|
var MAX_LIMIT = 2e3;
|
|
6602
|
-
function readAuditRecords(
|
|
6603
|
-
if (!(0,
|
|
8221
|
+
function readAuditRecords(auditDir2, query2 = {}) {
|
|
8222
|
+
if (!(0, import_node_fs18.existsSync)(auditDir2)) return [];
|
|
6604
8223
|
let files;
|
|
6605
8224
|
try {
|
|
6606
|
-
files = (0,
|
|
8225
|
+
files = (0, import_node_fs18.readdirSync)(auditDir2).filter((f) => AUDIT_FILE_RE.test(f));
|
|
6607
8226
|
} catch {
|
|
6608
8227
|
return [];
|
|
6609
8228
|
}
|
|
6610
|
-
const from = typeof
|
|
6611
|
-
const to = typeof
|
|
6612
|
-
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)));
|
|
6613
8232
|
const matched = [];
|
|
6614
8233
|
for (const file of files.sort().reverse()) {
|
|
6615
8234
|
let raw;
|
|
6616
8235
|
try {
|
|
6617
|
-
raw = (0,
|
|
8236
|
+
raw = (0, import_node_fs18.readFileSync)((0, import_node_path12.join)(auditDir2, file), "utf8");
|
|
6618
8237
|
} catch {
|
|
6619
8238
|
continue;
|
|
6620
8239
|
}
|
|
@@ -6628,7 +8247,7 @@ function readAuditRecords(auditDir, query = {}) {
|
|
|
6628
8247
|
continue;
|
|
6629
8248
|
}
|
|
6630
8249
|
if (!isAuditRecord(rec)) continue;
|
|
6631
|
-
if (
|
|
8250
|
+
if (query2.keyId !== void 0 && rec.keyId !== query2.keyId) continue;
|
|
6632
8251
|
if (rec.ts < from || rec.ts > to) continue;
|
|
6633
8252
|
matched.push(rec);
|
|
6634
8253
|
}
|
|
@@ -6643,11 +8262,11 @@ function isAuditRecord(value) {
|
|
|
6643
8262
|
}
|
|
6644
8263
|
|
|
6645
8264
|
// src/audit/AuditWriter.ts
|
|
6646
|
-
var
|
|
6647
|
-
var
|
|
8265
|
+
var import_node_fs19 = require("fs");
|
|
8266
|
+
var import_node_path13 = require("path");
|
|
6648
8267
|
var AuditWriter = class {
|
|
6649
|
-
constructor(
|
|
6650
|
-
this.auditDir =
|
|
8268
|
+
constructor(auditDir2, logger, defer = (fn) => setTimeout(fn, 0)) {
|
|
8269
|
+
this.auditDir = auditDir2;
|
|
6651
8270
|
this.logger = logger;
|
|
6652
8271
|
this.defer = defer;
|
|
6653
8272
|
}
|
|
@@ -6677,19 +8296,19 @@ var AuditWriter = class {
|
|
|
6677
8296
|
*/
|
|
6678
8297
|
appendNow(record) {
|
|
6679
8298
|
if (!this.dirEnsured) {
|
|
6680
|
-
(0,
|
|
8299
|
+
(0, import_node_fs19.mkdirSync)(this.auditDir, { recursive: true });
|
|
6681
8300
|
this.dirEnsured = true;
|
|
6682
8301
|
}
|
|
6683
|
-
const file = (0,
|
|
6684
|
-
(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");
|
|
6685
8304
|
}
|
|
6686
8305
|
};
|
|
6687
8306
|
|
|
6688
8307
|
// src/billing/BillingPublisher.ts
|
|
6689
|
-
var
|
|
6690
|
-
var
|
|
6691
|
-
var
|
|
6692
|
-
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");
|
|
6693
8312
|
|
|
6694
8313
|
// src/billing/billingFiles.ts
|
|
6695
8314
|
var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
@@ -6712,7 +8331,7 @@ var BillingPublisher = class {
|
|
|
6712
8331
|
constructor(billingDir, logger, opts = {}) {
|
|
6713
8332
|
this.billingDir = billingDir;
|
|
6714
8333
|
this.logger = logger;
|
|
6715
|
-
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0,
|
|
8334
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0, import_upstreamFetch6.fetchUpstream)(url, init));
|
|
6716
8335
|
this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
|
|
6717
8336
|
this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
|
|
6718
8337
|
this.now = opts.now ?? Date.now;
|
|
@@ -6759,8 +8378,8 @@ var BillingPublisher = class {
|
|
|
6759
8378
|
*/
|
|
6760
8379
|
appendNow(event) {
|
|
6761
8380
|
this.ensureDir();
|
|
6762
|
-
const file = (0,
|
|
6763
|
-
(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");
|
|
6764
8383
|
}
|
|
6765
8384
|
/**
|
|
6766
8385
|
* One best-effort delivery attempt for an event ALREADY in the ledger. POSTs the
|
|
@@ -6777,7 +8396,7 @@ var BillingPublisher = class {
|
|
|
6777
8396
|
const headers = { "Content-Type": "application/json" };
|
|
6778
8397
|
const secret = this.config?.secret;
|
|
6779
8398
|
if (secret) {
|
|
6780
|
-
const hmac = (0,
|
|
8399
|
+
const hmac = (0, import_node_crypto13.createHmac)("sha256", secret).update(body).digest("hex");
|
|
6781
8400
|
headers["X-Omnicross-Billing-Signature"] = `sha256=${hmac}`;
|
|
6782
8401
|
}
|
|
6783
8402
|
const res = await this.fetchImpl(endpoint, {
|
|
@@ -6809,8 +8428,8 @@ var BillingPublisher = class {
|
|
|
6809
8428
|
markDelivered(event) {
|
|
6810
8429
|
try {
|
|
6811
8430
|
this.ensureDir();
|
|
6812
|
-
const file = (0,
|
|
6813
|
-
(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");
|
|
6814
8433
|
} catch (error) {
|
|
6815
8434
|
this.logger.warn("[BillingPublisher] failed to append delivery marker", {
|
|
6816
8435
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -6819,20 +8438,20 @@ var BillingPublisher = class {
|
|
|
6819
8438
|
}
|
|
6820
8439
|
ensureDir() {
|
|
6821
8440
|
if (this.dirEnsured) return;
|
|
6822
|
-
(0,
|
|
8441
|
+
(0, import_node_fs20.mkdirSync)(this.billingDir, { recursive: true });
|
|
6823
8442
|
this.dirEnsured = true;
|
|
6824
8443
|
}
|
|
6825
8444
|
};
|
|
6826
8445
|
|
|
6827
8446
|
// src/billing/billingReader.ts
|
|
6828
|
-
var
|
|
6829
|
-
var
|
|
8447
|
+
var import_node_fs21 = require("fs");
|
|
8448
|
+
var import_node_path15 = require("path");
|
|
6830
8449
|
function readBillingLedger(billingDir) {
|
|
6831
8450
|
const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
|
|
6832
|
-
if (!(0,
|
|
8451
|
+
if (!(0, import_node_fs21.existsSync)(billingDir)) return view;
|
|
6833
8452
|
let files;
|
|
6834
8453
|
try {
|
|
6835
|
-
files = (0,
|
|
8454
|
+
files = (0, import_node_fs21.readdirSync)(billingDir);
|
|
6836
8455
|
} catch {
|
|
6837
8456
|
return view;
|
|
6838
8457
|
}
|
|
@@ -6863,7 +8482,7 @@ function readBillingStatus(billingDir) {
|
|
|
6863
8482
|
function parseLines(dir, file) {
|
|
6864
8483
|
let raw;
|
|
6865
8484
|
try {
|
|
6866
|
-
raw = (0,
|
|
8485
|
+
raw = (0, import_node_fs21.readFileSync)((0, import_node_path15.join)(dir, file), "utf8");
|
|
6867
8486
|
} catch {
|
|
6868
8487
|
return [];
|
|
6869
8488
|
}
|
|
@@ -7018,8 +8637,9 @@ var TokenRefreshScheduler = class {
|
|
|
7018
8637
|
const expiresAt = Date.parse(t.expiresAt);
|
|
7019
8638
|
return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
|
|
7020
8639
|
}
|
|
7021
|
-
/** Refresh one account; failures are logged, never thrown
|
|
7022
|
-
*
|
|
8640
|
+
/** Refresh one managed account; failures are logged, never thrown. The
|
|
8641
|
+
* store marks only the targeted account `expired` on a failed refresh.
|
|
8642
|
+
*/
|
|
7023
8643
|
async refreshOne(provider, id, isActive) {
|
|
7024
8644
|
try {
|
|
7025
8645
|
const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
|
|
@@ -7049,8 +8669,8 @@ var TokenRefreshScheduler = class {
|
|
|
7049
8669
|
};
|
|
7050
8670
|
|
|
7051
8671
|
// src/webhook/WebhookDispatcher.ts
|
|
7052
|
-
var
|
|
7053
|
-
var
|
|
8672
|
+
var import_node_crypto14 = require("crypto");
|
|
8673
|
+
var import_upstreamFetch7 = require("@omnicross/core/pipeline/upstreamFetch");
|
|
7054
8674
|
var WEBHOOK_MAX_ATTEMPTS = 3;
|
|
7055
8675
|
var WEBHOOK_QUEUE_MAX = 1e3;
|
|
7056
8676
|
var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
|
|
@@ -7070,7 +8690,7 @@ var WebhookDispatcher = class {
|
|
|
7070
8690
|
sleep;
|
|
7071
8691
|
now;
|
|
7072
8692
|
constructor(opts = {}) {
|
|
7073
|
-
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0,
|
|
8693
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => (0, import_upstreamFetch7.fetchUpstream)(url, init));
|
|
7074
8694
|
this.logger = opts.logger;
|
|
7075
8695
|
this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
|
|
7076
8696
|
this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
|
|
@@ -7181,7 +8801,7 @@ function buildCustom(event, dest) {
|
|
|
7181
8801
|
const body = JSON.stringify(event);
|
|
7182
8802
|
const headers = {};
|
|
7183
8803
|
if (dest.secret) {
|
|
7184
|
-
const hmac = (0,
|
|
8804
|
+
const hmac = (0, import_node_crypto14.createHmac)("sha256", dest.secret).update(body).digest("hex");
|
|
7185
8805
|
headers["X-Omnicross-Signature"] = `sha256=${hmac}`;
|
|
7186
8806
|
}
|
|
7187
8807
|
return { body, headers };
|
|
@@ -7196,7 +8816,7 @@ function buildFeishu(event, dest, nowMs) {
|
|
|
7196
8816
|
const stringToSign = `${timestamp}
|
|
7197
8817
|
${dest.secret}`;
|
|
7198
8818
|
payload["timestamp"] = timestamp;
|
|
7199
|
-
payload["sign"] = (0,
|
|
8819
|
+
payload["sign"] = (0, import_node_crypto14.createHmac)("sha256", stringToSign).digest("base64");
|
|
7200
8820
|
}
|
|
7201
8821
|
return { body: JSON.stringify(payload), headers: {} };
|
|
7202
8822
|
}
|
|
@@ -7224,11 +8844,32 @@ function buildDaemon(config, paths) {
|
|
|
7224
8844
|
setSecretBox(secretBox3);
|
|
7225
8845
|
setSecretBox2(secretBox3);
|
|
7226
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
|
+
);
|
|
7227
8856
|
const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
|
|
7228
8857
|
const keyDb = new JsonOutboundKeyDb(paths.keysPath);
|
|
7229
8858
|
const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
|
|
7230
8859
|
const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
|
|
8860
|
+
const integrationStateStore = new IntegrationStateStore(
|
|
8861
|
+
defaultIntegrationsPath(paths.configPath),
|
|
8862
|
+
secretBox3
|
|
8863
|
+
);
|
|
7231
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
|
+
);
|
|
7232
8873
|
const subscriptionAccounts = new import_subscriptions4.SubscriptionAccountService(credentialStore);
|
|
7233
8874
|
(0, import_subscriptions4.setSubscriptionAccountService)(subscriptionAccounts);
|
|
7234
8875
|
const subscriptionRegistry = new import_subscriptions4.SubscriptionProviderRegistry(
|
|
@@ -7237,7 +8878,7 @@ function buildDaemon(config, paths) {
|
|
|
7237
8878
|
);
|
|
7238
8879
|
(0, import_subscriptions4.setSubscriptionProviderRegistry)(subscriptionRegistry);
|
|
7239
8880
|
setServerProxyConfig(decryptedConfig.server?.proxy);
|
|
7240
|
-
(0,
|
|
8881
|
+
(0, import_upstreamFetch8.setUpstreamProxyResolver)(
|
|
7241
8882
|
createUpstreamProxyResolver({
|
|
7242
8883
|
getAccountProxy: (providerId, accountId) => credentialStore.getAccountProxy(providerId, accountId)
|
|
7243
8884
|
})
|
|
@@ -7257,7 +8898,17 @@ function buildDaemon(config, paths) {
|
|
|
7257
8898
|
}
|
|
7258
8899
|
);
|
|
7259
8900
|
const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
|
|
7260
|
-
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
|
+
);
|
|
7261
8912
|
const usageEventStore = new JsonlUsageEventStore(
|
|
7262
8913
|
defaultUsageEventsPath(paths.configPath),
|
|
7263
8914
|
async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
|
|
@@ -7270,7 +8921,7 @@ function buildDaemon(config, paths) {
|
|
|
7270
8921
|
llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
|
|
7271
8922
|
const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
|
|
7272
8923
|
credentialStore,
|
|
7273
|
-
(0,
|
|
8924
|
+
(0, import_SubscriptionAccountHealth3.getSharedAccountHealth)(),
|
|
7274
8925
|
logger,
|
|
7275
8926
|
import_outbound_api4.DEFAULT_ACCOUNT_PROBE
|
|
7276
8927
|
);
|
|
@@ -7304,7 +8955,7 @@ function buildDaemon(config, paths) {
|
|
|
7304
8955
|
// lines through the injected logger (honors level/format/file sink).
|
|
7305
8956
|
logger
|
|
7306
8957
|
});
|
|
7307
|
-
const
|
|
8958
|
+
const auditDir2 = defaultAuditDir(paths.configPath);
|
|
7308
8959
|
const billingDir = defaultBillingDir(paths.configPath);
|
|
7309
8960
|
const adminServer = new AdminServer({
|
|
7310
8961
|
configPath: paths.configPath,
|
|
@@ -7318,6 +8969,9 @@ function buildDaemon(config, paths) {
|
|
|
7318
8969
|
settingsStore,
|
|
7319
8970
|
outboundApiServer,
|
|
7320
8971
|
subscriptionAccounts,
|
|
8972
|
+
accountAllowanceService,
|
|
8973
|
+
allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
|
|
8974
|
+
accountProbeService: accountHealthProbeScheduler,
|
|
7321
8975
|
// Least-authority token WRITER (design D4) — the concrete credential store
|
|
7322
8976
|
// exposes `writeProviderTokens` / `clearProvider` as daemon-only methods (NOT
|
|
7323
8977
|
// on the `SubscriptionCredentialStore` port). The admin API sees ONLY these two
|
|
@@ -7338,7 +8992,7 @@ function buildDaemon(config, paths) {
|
|
|
7338
8992
|
// inject a mock so no real token endpoint is hit.
|
|
7339
8993
|
// upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
|
|
7340
8994
|
// helper so interactive login honors a configured proxy (global/env layers).
|
|
7341
|
-
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => (0,
|
|
8995
|
+
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => (0, import_upstreamFetch8.fetchUpstream)(url, init)),
|
|
7342
8996
|
subscriptionAccountAppender: credentialStore,
|
|
7343
8997
|
// Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
|
|
7344
8998
|
// + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
|
|
@@ -7356,6 +9010,16 @@ function buildDaemon(config, paths) {
|
|
|
7356
9010
|
cliTerminalOpener: paths.cliTerminalOpener,
|
|
7357
9011
|
cliPathProbe: paths.cliPathProbe,
|
|
7358
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
|
+
},
|
|
7359
9023
|
// Usage/pricing admin surface (usage-pricing child): stats queries go
|
|
7360
9024
|
// through the recorder facade, pricing mutations through the engine, and
|
|
7361
9025
|
// the row DELETE through the concrete store (delete is store-local — the
|
|
@@ -7380,19 +9044,19 @@ function buildDaemon(config, paths) {
|
|
|
7380
9044
|
// date-rotated audit store. Bound to the store dir here so the AdminServer
|
|
7381
9045
|
// carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
|
|
7382
9046
|
// NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
|
|
7383
|
-
auditReader: (
|
|
9047
|
+
auditReader: (query2) => readAuditRecords(auditDir2, query2),
|
|
7384
9048
|
// billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
|
|
7385
9049
|
// secret-free total/delivered/pending counts of the durable ledger.
|
|
7386
9050
|
billingStatusReader: () => readBillingStatus(billingDir)
|
|
7387
9051
|
});
|
|
7388
9052
|
const webhookDispatcher = new WebhookDispatcher({
|
|
7389
9053
|
logger,
|
|
7390
|
-
fetchImpl: (url, init) => (0,
|
|
9054
|
+
fetchImpl: (url, init) => (0, import_upstreamFetch8.fetchUpstream)(url, init)
|
|
7391
9055
|
});
|
|
7392
|
-
setWebhookRuntime(webhookDispatcher, (0,
|
|
7393
|
-
const auditWriter = new AuditWriter(
|
|
7394
|
-
const auditPruneSweeper = new AuditPruneSweeper(
|
|
7395
|
-
setAuditRuntime(auditWriter, auditPruneSweeper);
|
|
9056
|
+
setWebhookRuntime(webhookDispatcher, (0, import_SubscriptionAccountHealth3.getSharedAccountHealth)());
|
|
9057
|
+
const auditWriter = new AuditWriter(auditDir2, logger);
|
|
9058
|
+
const auditPruneSweeper = new AuditPruneSweeper(auditDir2, logger, import_audit_types.DEFAULT_AUDIT_CONFIG);
|
|
9059
|
+
setAuditRuntime(auditWriter, auditPruneSweeper, auditDir2);
|
|
7396
9060
|
const billingPublisher = new BillingPublisher(billingDir, logger);
|
|
7397
9061
|
const billingRetrySweeper = new BillingRetrySweeper(
|
|
7398
9062
|
billingDir,
|
|
@@ -7404,7 +9068,7 @@ function buildDaemon(config, paths) {
|
|
|
7404
9068
|
const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
|
|
7405
9069
|
const accountHealthSweeper = new AccountHealthSweeper(
|
|
7406
9070
|
credentialStore,
|
|
7407
|
-
(0,
|
|
9071
|
+
(0, import_SubscriptionAccountHealth3.getSharedAccountHealth)(),
|
|
7408
9072
|
logger
|
|
7409
9073
|
);
|
|
7410
9074
|
return {
|
|
@@ -7419,8 +9083,11 @@ function buildDaemon(config, paths) {
|
|
|
7419
9083
|
credentialStore,
|
|
7420
9084
|
subscriptionRegistry,
|
|
7421
9085
|
subscriptionAccounts,
|
|
9086
|
+
accountAllowanceService,
|
|
9087
|
+
claudeAllowanceRefreshScheduler,
|
|
7422
9088
|
pricingStore,
|
|
7423
9089
|
pricingEngine,
|
|
9090
|
+
pricingRefreshScheduler,
|
|
7424
9091
|
usageRecorder,
|
|
7425
9092
|
adminServer,
|
|
7426
9093
|
tokenRefreshScheduler,
|
|
@@ -7439,7 +9106,7 @@ function resetDaemonSingletonsForTests() {
|
|
|
7439
9106
|
(0, import_subscriptionRegistryPort.setSubscriptionRegistryForOutbound)(null);
|
|
7440
9107
|
(0, import_subscriptions4.setSubscriptionProviderRegistry)(null);
|
|
7441
9108
|
(0, import_subscriptions4.setSubscriptionAccountService)(null);
|
|
7442
|
-
(0,
|
|
9109
|
+
(0, import_upstreamFetch8.setUpstreamProxyResolver)(null);
|
|
7443
9110
|
setServerProxyConfig(void 0);
|
|
7444
9111
|
(0, import_gemini_code_assist_resolver.setGeminiCodeAssistResolver)(null);
|
|
7445
9112
|
setSecretBox(null);
|
|
@@ -7447,12 +9114,14 @@ function resetDaemonSingletonsForTests() {
|
|
|
7447
9114
|
resetWebhookRuntimeForTests();
|
|
7448
9115
|
resetAuditRuntimeForTests();
|
|
7449
9116
|
resetBillingRuntimeForTests();
|
|
7450
|
-
(0,
|
|
9117
|
+
(0, import_SubscriptionIdentityStore3.__resetSharedIdentityStoreForTests)();
|
|
9118
|
+
(0, import_AccountAllowanceStore4.__resetSharedAccountAllowanceStoreForTests)();
|
|
9119
|
+
(0, import_AccountAllowanceScheduling4.__resetSharedAccountAllowanceSchedulingForTests)();
|
|
7451
9120
|
}
|
|
7452
9121
|
function isTokensStoreReadable(tokensPath) {
|
|
7453
9122
|
try {
|
|
7454
|
-
if (!(0,
|
|
7455
|
-
(0,
|
|
9123
|
+
if (!(0, import_node_fs22.existsSync)(tokensPath)) return true;
|
|
9124
|
+
(0, import_node_fs22.accessSync)(tokensPath, import_node_fs22.constants.R_OK);
|
|
7456
9125
|
return true;
|
|
7457
9126
|
} catch {
|
|
7458
9127
|
return false;
|
|
@@ -7498,6 +9167,9 @@ function inferApiFormat(provider) {
|
|
|
7498
9167
|
if (hay.includes("generativelanguage") || hay.includes("gemini") || hay.includes("google")) {
|
|
7499
9168
|
return { format: "gemini", ambiguous: false };
|
|
7500
9169
|
}
|
|
9170
|
+
if (hay.includes("/responses")) {
|
|
9171
|
+
return { format: "openai-response", ambiguous: false };
|
|
9172
|
+
}
|
|
7501
9173
|
if (hay.includes("openai") || hay.includes("/v1") || hay.includes("chat/completions")) {
|
|
7502
9174
|
return { format: "openai", ambiguous: false };
|
|
7503
9175
|
}
|