@omnicross/daemon 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2425 -660
- package/dist/cli.js +2416 -618
- package/dist/index.cjs +2286 -622
- package/dist/index.d.cts +452 -189
- package/dist/index.d.ts +452 -189
- package/dist/index.js +2273 -576
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/bootstrap.ts
|
|
2
|
-
import { accessSync, constants as fsConstants, existsSync as
|
|
2
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync17 } from "fs";
|
|
3
3
|
import { DEFAULT_AUDIT_CONFIG } from "@omnicross/contracts/audit-types";
|
|
4
4
|
import { DEFAULT_BILLING_CONFIG } from "@omnicross/contracts/billing-types";
|
|
5
5
|
import { getGeminiCodeAssistProjectResolver } from "@omnicross/core/auth/GeminiCodeAssistProjectResolver";
|
|
@@ -7,11 +7,22 @@ import { ApiKeyPoolService } from "@omnicross/core/completion/ApiKeyPoolService"
|
|
|
7
7
|
import {
|
|
8
8
|
__resetOutboundApiServerForTests,
|
|
9
9
|
DEFAULT_ACCOUNT_PROBE,
|
|
10
|
-
|
|
10
|
+
DEFAULT_OUTBOUND_PORT,
|
|
11
|
+
getOutboundApiServer,
|
|
12
|
+
normalizeServerConfig
|
|
11
13
|
} from "@omnicross/core/outbound-api";
|
|
12
14
|
import { setSubscriptionRegistryForOutbound } from "@omnicross/core/outbound-api/subscriptionRegistryPort";
|
|
13
|
-
import { getSharedAccountHealth as
|
|
14
|
-
import {
|
|
15
|
+
import { getSharedAccountHealth as getSharedAccountHealth3 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
|
|
16
|
+
import {
|
|
17
|
+
__resetSharedAccountAllowanceStoreForTests,
|
|
18
|
+
AccountAllowanceStore as AccountAllowanceStore3,
|
|
19
|
+
setSharedAccountAllowanceStore
|
|
20
|
+
} from "@omnicross/core/pipeline/AccountAllowanceStore";
|
|
21
|
+
import {
|
|
22
|
+
__resetSharedAccountAllowanceSchedulingForTests,
|
|
23
|
+
getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling4
|
|
24
|
+
} from "@omnicross/core/pipeline/AccountAllowanceScheduling";
|
|
25
|
+
import { fetchUpstream as fetchUpstream7, setUpstreamProxyResolver } from "@omnicross/core/pipeline/upstreamFetch";
|
|
15
26
|
import { __resetSharedIdentityStoreForTests } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
|
|
16
27
|
import { setGeminiCodeAssistResolver } from "@omnicross/core/ports/gemini-code-assist-resolver";
|
|
17
28
|
import {
|
|
@@ -132,6 +143,462 @@ function handleCodexOAuthStatus(sessionId, deps) {
|
|
|
132
143
|
return { status: 200, body: { state: s.status, ...s.error ? { message: s.error } : {} } };
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
// src/allowance/AccountAllowanceService.ts
|
|
147
|
+
import {
|
|
148
|
+
getSharedAccountAllowanceStore as getSharedAccountAllowanceStore2
|
|
149
|
+
} from "@omnicross/core/pipeline/AccountAllowanceStore";
|
|
150
|
+
import {
|
|
151
|
+
getSharedAccountAllowanceScheduling
|
|
152
|
+
} from "@omnicross/core/pipeline/AccountAllowanceScheduling";
|
|
153
|
+
|
|
154
|
+
// src/allowance/ClaudeAllowanceCollector.ts
|
|
155
|
+
import {
|
|
156
|
+
getSharedAccountAllowanceStore
|
|
157
|
+
} from "@omnicross/core/pipeline/AccountAllowanceStore";
|
|
158
|
+
import { fetchUpstream } from "@omnicross/core/pipeline/upstreamFetch";
|
|
159
|
+
import { applyFingerprint } from "@omnicross/core/provider-proxy/identity/fingerprintHeaders";
|
|
160
|
+
import {
|
|
161
|
+
getSharedIdentityStore
|
|
162
|
+
} from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
|
|
163
|
+
var CLAUDE_ALLOWANCE_CACHE_MS = 5 * 6e4;
|
|
164
|
+
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
165
|
+
function finitePercent(value) {
|
|
166
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
167
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
168
|
+
return Number.isFinite(number) && number >= 0 && number <= 100 ? number : null;
|
|
169
|
+
}
|
|
170
|
+
function isoInstant(value) {
|
|
171
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
172
|
+
const time = Date.parse(value);
|
|
173
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : void 0;
|
|
174
|
+
}
|
|
175
|
+
function secondsUntil(instant, now) {
|
|
176
|
+
if (!instant) return void 0;
|
|
177
|
+
return Math.max(0, Math.floor((Date.parse(instant) - now) / 1e3));
|
|
178
|
+
}
|
|
179
|
+
function windowFromPayload(id, payload, now) {
|
|
180
|
+
const usedPercent = finitePercent(payload?.utilization);
|
|
181
|
+
const resetsAt = isoInstant(payload?.resets_at);
|
|
182
|
+
const isSonnet = id === "seven-day-sonnet";
|
|
183
|
+
const isFiveHour = id === "five-hour";
|
|
184
|
+
return {
|
|
185
|
+
id,
|
|
186
|
+
label: isFiveHour ? "5 hours" : isSonnet ? "7 days \xB7 Sonnet" : "7 days",
|
|
187
|
+
scope: isSonnet ? "model-family" : "all",
|
|
188
|
+
modelFamily: isSonnet ? "sonnet" : void 0,
|
|
189
|
+
usedPercent,
|
|
190
|
+
windowMinutes: isFiveHour ? 5 * 60 : 7 * 24 * 60,
|
|
191
|
+
resetsAt,
|
|
192
|
+
remainingSeconds: secondsUntil(resetsAt, now),
|
|
193
|
+
state: usedPercent !== null || resetsAt ? "fresh" : "unavailable"
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function emptyClaudeWindows(state) {
|
|
197
|
+
return [
|
|
198
|
+
{
|
|
199
|
+
id: "five-hour",
|
|
200
|
+
label: "5 hours",
|
|
201
|
+
scope: "all",
|
|
202
|
+
usedPercent: null,
|
|
203
|
+
windowMinutes: 5 * 60,
|
|
204
|
+
state
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "seven-day",
|
|
208
|
+
label: "7 days",
|
|
209
|
+
scope: "all",
|
|
210
|
+
usedPercent: null,
|
|
211
|
+
windowMinutes: 7 * 24 * 60,
|
|
212
|
+
state
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "seven-day-sonnet",
|
|
216
|
+
label: "7 days \xB7 Sonnet",
|
|
217
|
+
scope: "model-family",
|
|
218
|
+
modelFamily: "sonnet",
|
|
219
|
+
usedPercent: null,
|
|
220
|
+
windowMinutes: 7 * 24 * 60,
|
|
221
|
+
state
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
function hasHeader(headers, name) {
|
|
226
|
+
const wanted = name.toLowerCase();
|
|
227
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === wanted);
|
|
228
|
+
}
|
|
229
|
+
var ClaudeAllowanceCollector = class {
|
|
230
|
+
constructor(credentials, store = getSharedAccountAllowanceStore(), fetchImpl = (url, init, accountId) => fetchUpstream(url, init, { providerId: "claude", accountId }), identityStore = getSharedIdentityStore(), now = Date.now) {
|
|
231
|
+
this.credentials = credentials;
|
|
232
|
+
this.store = store;
|
|
233
|
+
this.fetchImpl = fetchImpl;
|
|
234
|
+
this.identityStore = identityStore;
|
|
235
|
+
this.now = now;
|
|
236
|
+
}
|
|
237
|
+
credentials;
|
|
238
|
+
store;
|
|
239
|
+
fetchImpl;
|
|
240
|
+
identityStore;
|
|
241
|
+
now;
|
|
242
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
243
|
+
async collectMany(accounts, options = {}) {
|
|
244
|
+
const settled = await Promise.allSettled(accounts.map((account) => this.collect(account, options)));
|
|
245
|
+
return settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
246
|
+
}
|
|
247
|
+
collect(account, options = {}) {
|
|
248
|
+
const now = this.now();
|
|
249
|
+
const unsupported = account.tokens.isSetupToken || account.tokens.authMethod !== "oauth";
|
|
250
|
+
if (unsupported) {
|
|
251
|
+
const existing = this.store.get("claude", account.id, now);
|
|
252
|
+
if (existing?.windows.every((window) => window.state === "unsupported")) return Promise.resolve(existing);
|
|
253
|
+
const snapshot = this.unsupportedSnapshot(account.id, now);
|
|
254
|
+
this.store.set(snapshot);
|
|
255
|
+
return Promise.resolve(snapshot);
|
|
256
|
+
}
|
|
257
|
+
const cached = this.store.get("claude", account.id, now);
|
|
258
|
+
if (!options.force && cached && this.isCacheValid(cached, now, options.refreshAheadMs)) return Promise.resolve(cached);
|
|
259
|
+
const running = this.inFlight.get(account.id);
|
|
260
|
+
if (running) return running;
|
|
261
|
+
const promise = this.fetchAccount(account.id).catch(() => this.failureSnapshot(account.id, "claude_usage_request_failed", this.now())).finally(() => this.inFlight.delete(account.id));
|
|
262
|
+
this.inFlight.set(account.id, promise);
|
|
263
|
+
return promise;
|
|
264
|
+
}
|
|
265
|
+
isCacheValid(snapshot, now, refreshAheadMs) {
|
|
266
|
+
if (snapshot.source !== "oauth-usage-api") return false;
|
|
267
|
+
if (snapshot.windows.every((window) => window.state === "unsupported")) return true;
|
|
268
|
+
const expiresAt = snapshot.expiresAt ? Date.parse(snapshot.expiresAt) : 0;
|
|
269
|
+
const ahead = typeof refreshAheadMs === "number" && Number.isFinite(refreshAheadMs) ? Math.max(0, refreshAheadMs) : 0;
|
|
270
|
+
return Number.isFinite(expiresAt) && expiresAt > now + ahead;
|
|
271
|
+
}
|
|
272
|
+
async fetchAccount(accountId) {
|
|
273
|
+
let token = await this.credentials.getAccessTokenForAccount("claude", accountId);
|
|
274
|
+
if (!token) return this.failureSnapshot(accountId, "claude_usage_token_unavailable", this.now());
|
|
275
|
+
let response = await this.request(accountId, token);
|
|
276
|
+
if (response.status === 401) {
|
|
277
|
+
const refreshed = await this.credentials.refreshAccountToken("claude", accountId);
|
|
278
|
+
if (!refreshed) return this.failureSnapshot(accountId, "claude_usage_unauthorized", this.now());
|
|
279
|
+
token = await this.credentials.getAccessTokenForAccount("claude", accountId);
|
|
280
|
+
if (!token) return this.failureSnapshot(accountId, "claude_usage_token_unavailable", this.now());
|
|
281
|
+
response = await this.request(accountId, token);
|
|
282
|
+
}
|
|
283
|
+
if (response.status === 403) {
|
|
284
|
+
const snapshot2 = this.unsupportedSnapshot(accountId, this.now(), "claude_usage_unsupported");
|
|
285
|
+
this.store.set(snapshot2);
|
|
286
|
+
return snapshot2;
|
|
287
|
+
}
|
|
288
|
+
if (!response.ok) {
|
|
289
|
+
return this.failureSnapshot(accountId, "claude_usage_http_error", this.now());
|
|
290
|
+
}
|
|
291
|
+
let payload;
|
|
292
|
+
try {
|
|
293
|
+
payload = await response.json();
|
|
294
|
+
} catch {
|
|
295
|
+
return this.failureSnapshot(accountId, "claude_usage_invalid_response", this.now());
|
|
296
|
+
}
|
|
297
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
298
|
+
return this.failureSnapshot(accountId, "claude_usage_invalid_response", this.now());
|
|
299
|
+
}
|
|
300
|
+
const now = this.now();
|
|
301
|
+
const usage = payload;
|
|
302
|
+
const snapshot = {
|
|
303
|
+
providerId: "claude",
|
|
304
|
+
accountId,
|
|
305
|
+
source: "oauth-usage-api",
|
|
306
|
+
observedAt: new Date(now).toISOString(),
|
|
307
|
+
expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
|
|
308
|
+
windows: [
|
|
309
|
+
windowFromPayload("five-hour", usage.five_hour, now),
|
|
310
|
+
windowFromPayload("seven-day", usage.seven_day, now),
|
|
311
|
+
windowFromPayload("seven-day-sonnet", usage.seven_day_sonnet, now)
|
|
312
|
+
]
|
|
313
|
+
};
|
|
314
|
+
this.store.set(snapshot);
|
|
315
|
+
return snapshot;
|
|
316
|
+
}
|
|
317
|
+
request(accountId, token) {
|
|
318
|
+
const headers = {
|
|
319
|
+
Authorization: `Bearer ${token}`,
|
|
320
|
+
Accept: "application/json",
|
|
321
|
+
"Content-Type": "application/json",
|
|
322
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
323
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
324
|
+
};
|
|
325
|
+
applyFingerprint(this.identityStore, headers, "claude", accountId, void 0);
|
|
326
|
+
if (!hasHeader(headers, "user-agent")) {
|
|
327
|
+
headers["User-Agent"] = "claude-cli/2.0.53 (external, cli)";
|
|
328
|
+
}
|
|
329
|
+
return this.fetchImpl(CLAUDE_USAGE_URL, {
|
|
330
|
+
method: "GET",
|
|
331
|
+
headers,
|
|
332
|
+
signal: AbortSignal.timeout(15e3)
|
|
333
|
+
}, accountId);
|
|
334
|
+
}
|
|
335
|
+
failureSnapshot(accountId, code, now) {
|
|
336
|
+
const existing = this.store.get("claude", accountId, now);
|
|
337
|
+
const snapshot = existing ? {
|
|
338
|
+
...existing,
|
|
339
|
+
expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
|
|
340
|
+
windows: existing.windows.map((window) => ({
|
|
341
|
+
...window,
|
|
342
|
+
state: window.state === "unsupported" ? "unsupported" : window.usedPercent !== null || window.resetsAt ? "stale" : "unavailable"
|
|
343
|
+
})),
|
|
344
|
+
lastErrorCode: code
|
|
345
|
+
} : {
|
|
346
|
+
providerId: "claude",
|
|
347
|
+
accountId,
|
|
348
|
+
source: "oauth-usage-api",
|
|
349
|
+
observedAt: new Date(now).toISOString(),
|
|
350
|
+
expiresAt: new Date(now + CLAUDE_ALLOWANCE_CACHE_MS).toISOString(),
|
|
351
|
+
windows: emptyClaudeWindows("unavailable"),
|
|
352
|
+
lastErrorCode: code
|
|
353
|
+
};
|
|
354
|
+
this.store.set(snapshot);
|
|
355
|
+
return snapshot;
|
|
356
|
+
}
|
|
357
|
+
unsupportedSnapshot(accountId, now, code = "claude_usage_unsupported_auth") {
|
|
358
|
+
return {
|
|
359
|
+
providerId: "claude",
|
|
360
|
+
accountId,
|
|
361
|
+
source: "oauth-usage-api",
|
|
362
|
+
observedAt: new Date(now).toISOString(),
|
|
363
|
+
windows: emptyClaudeWindows("unsupported"),
|
|
364
|
+
lastErrorCode: code
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/allowance/AccountAllowanceService.ts
|
|
370
|
+
function codexUnavailable(accountId, now) {
|
|
371
|
+
return {
|
|
372
|
+
providerId: "codex",
|
|
373
|
+
accountId,
|
|
374
|
+
source: "response-headers",
|
|
375
|
+
observedAt: new Date(now).toISOString(),
|
|
376
|
+
windows: [
|
|
377
|
+
{ id: "primary", label: "Primary", scope: "all", usedPercent: null, state: "unavailable" },
|
|
378
|
+
{ id: "secondary", label: "Secondary", scope: "all", usedPercent: null, state: "unavailable" }
|
|
379
|
+
],
|
|
380
|
+
lastErrorCode: "codex_allowance_not_observed"
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
var AccountAllowanceService = class {
|
|
384
|
+
constructor(credentials, store = getSharedAccountAllowanceStore2(), collector, now = Date.now) {
|
|
385
|
+
this.credentials = credentials;
|
|
386
|
+
this.store = store;
|
|
387
|
+
this.now = now;
|
|
388
|
+
this.claudeCollector = collector ?? new ClaudeAllowanceCollector(credentials, store);
|
|
389
|
+
}
|
|
390
|
+
credentials;
|
|
391
|
+
store;
|
|
392
|
+
now;
|
|
393
|
+
claudeCollector;
|
|
394
|
+
/**
|
|
395
|
+
* Read all/filtered snapshots. Claude's five-minute cache is refreshed lazily;
|
|
396
|
+
* Codex remains passive and reports not-observed until a real model response.
|
|
397
|
+
*/
|
|
398
|
+
async list(filter = {}) {
|
|
399
|
+
const config = await this.credentials.getFullConfig();
|
|
400
|
+
this.store.pruneToKnownAccounts([
|
|
401
|
+
...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
|
|
402
|
+
...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
|
|
403
|
+
]);
|
|
404
|
+
const wantsClaude = !filter.providerId || filter.providerId === "claude";
|
|
405
|
+
const claudeAccounts = (config.claudeAccounts ?? []).filter(
|
|
406
|
+
(account) => !filter.accountId || account.id === filter.accountId
|
|
407
|
+
);
|
|
408
|
+
if (wantsClaude) await this.claudeCollector.collectMany(claudeAccounts);
|
|
409
|
+
const wantsCodex = !filter.providerId || filter.providerId === "codex";
|
|
410
|
+
const codexAccounts = (config.codexAccounts ?? []).filter(
|
|
411
|
+
(account) => !filter.accountId || account.id === filter.accountId
|
|
412
|
+
);
|
|
413
|
+
if (wantsCodex) {
|
|
414
|
+
for (const account of codexAccounts) {
|
|
415
|
+
if (!this.store.get("codex", account.id)) this.store.set(codexUnavailable(account.id, this.now()));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const known = /* @__PURE__ */ new Set();
|
|
419
|
+
if (wantsClaude) for (const account of claudeAccounts) known.add(`claude\0${account.id}`);
|
|
420
|
+
if (wantsCodex) for (const account of codexAccounts) known.add(`codex\0${account.id}`);
|
|
421
|
+
return this.store.list(filter).filter((snapshot) => known.has(`${snapshot.providerId}\0${snapshot.accountId}`));
|
|
422
|
+
}
|
|
423
|
+
/** Force-refresh Claude usage for one account or every stored Claude account. */
|
|
424
|
+
async refreshClaude(accountId) {
|
|
425
|
+
const config = await this.credentials.getFullConfig();
|
|
426
|
+
this.store.pruneToKnownAccounts([
|
|
427
|
+
...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
|
|
428
|
+
...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
|
|
429
|
+
]);
|
|
430
|
+
const accounts = (config.claudeAccounts ?? []).filter(
|
|
431
|
+
(account) => !accountId || account.id === accountId
|
|
432
|
+
);
|
|
433
|
+
return this.claudeCollector.collectMany(accounts, { force: true });
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Keep Claude snapshots warm for allowance-aware routing. This deliberately
|
|
437
|
+
* excludes Codex (whose quota is learned from real response headers) and
|
|
438
|
+
* preserves the collector's cache + per-account in-flight coalescing.
|
|
439
|
+
*/
|
|
440
|
+
async maintainClaudeCache(refreshAheadMs) {
|
|
441
|
+
const config = await this.credentials.getFullConfig();
|
|
442
|
+
this.store.pruneToKnownAccounts([
|
|
443
|
+
...(config.claudeAccounts ?? []).map((account) => ({ providerId: "claude", accountId: account.id })),
|
|
444
|
+
...(config.codexAccounts ?? []).map((account) => ({ providerId: "codex", accountId: account.id }))
|
|
445
|
+
]);
|
|
446
|
+
await this.claudeCollector.collectMany(config.claudeAccounts ?? [], { refreshAheadMs });
|
|
447
|
+
}
|
|
448
|
+
/** Remove a cache row as soon as an account is deleted by the admin path. */
|
|
449
|
+
removeAccountSnapshot(providerId, accountId) {
|
|
450
|
+
this.store.delete(providerId, accountId);
|
|
451
|
+
}
|
|
452
|
+
/** Remove all allowance rows for a provider block that was deleted. */
|
|
453
|
+
removeProviderSnapshots(providerId) {
|
|
454
|
+
for (const snapshot of this.store.list({ providerId })) {
|
|
455
|
+
this.store.delete(snapshot.providerId, snapshot.accountId);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/** Secret-free policy diagnostics for the settings/accounts UI. */
|
|
459
|
+
getSchedulingStatus() {
|
|
460
|
+
const scheduling = getSharedAccountAllowanceScheduling();
|
|
461
|
+
return { config: scheduling.getConfig(), history: scheduling.getHistory() };
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// src/allowance/ClaudeAllowanceRefreshScheduler.ts
|
|
466
|
+
var CLAUDE_ALLOWANCE_CHECK_INTERVAL_MS = 6e4;
|
|
467
|
+
var CLAUDE_ALLOWANCE_REFRESH_AHEAD_MS = 9e4;
|
|
468
|
+
var ClaudeAllowanceRefreshScheduler = class {
|
|
469
|
+
constructor(service, logger, intervalMs = CLAUDE_ALLOWANCE_CHECK_INTERVAL_MS, refreshAheadMs = CLAUDE_ALLOWANCE_REFRESH_AHEAD_MS) {
|
|
470
|
+
this.service = service;
|
|
471
|
+
this.logger = logger;
|
|
472
|
+
this.intervalMs = intervalMs;
|
|
473
|
+
this.refreshAheadMs = refreshAheadMs;
|
|
474
|
+
}
|
|
475
|
+
service;
|
|
476
|
+
logger;
|
|
477
|
+
intervalMs;
|
|
478
|
+
refreshAheadMs;
|
|
479
|
+
timer = null;
|
|
480
|
+
started = false;
|
|
481
|
+
enabled = false;
|
|
482
|
+
sweeping = false;
|
|
483
|
+
/**
|
|
484
|
+
* Apply live server policy. Once started, enable/disable changes arm or disarm
|
|
485
|
+
* immediately; the initial enabled sweep is fire-and-forget.
|
|
486
|
+
*/
|
|
487
|
+
configure(config) {
|
|
488
|
+
const nextEnabled = config?.enabled === true;
|
|
489
|
+
if (this.enabled === nextEnabled) return;
|
|
490
|
+
this.enabled = nextEnabled;
|
|
491
|
+
if (!this.started) return;
|
|
492
|
+
if (nextEnabled) {
|
|
493
|
+
this.arm();
|
|
494
|
+
void this.sweep();
|
|
495
|
+
} else {
|
|
496
|
+
this.disarm();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/** Start the lifecycle. Disabled policy remains completely inert. */
|
|
500
|
+
start() {
|
|
501
|
+
if (this.started) return;
|
|
502
|
+
this.started = true;
|
|
503
|
+
if (!this.enabled) return;
|
|
504
|
+
this.arm();
|
|
505
|
+
void this.sweep();
|
|
506
|
+
}
|
|
507
|
+
/** Stop all future checks. Idempotent and safe during an in-flight refresh. */
|
|
508
|
+
dispose() {
|
|
509
|
+
this.started = false;
|
|
510
|
+
this.disarm();
|
|
511
|
+
}
|
|
512
|
+
/** One non-overlapping cache-maintenance pass. Exposed for focused tests. */
|
|
513
|
+
async sweep() {
|
|
514
|
+
if (!this.enabled || this.sweeping) return;
|
|
515
|
+
this.sweeping = true;
|
|
516
|
+
try {
|
|
517
|
+
await this.service.maintainClaudeCache(this.refreshAheadMs);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
this.logger.warn("Claude allowance background refresh failed", {
|
|
520
|
+
error: error instanceof Error ? error.message : String(error)
|
|
521
|
+
});
|
|
522
|
+
} finally {
|
|
523
|
+
this.sweeping = false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
arm() {
|
|
527
|
+
if (this.timer) return;
|
|
528
|
+
this.timer = setInterval(() => void this.sweep(), this.intervalMs);
|
|
529
|
+
this.timer.unref?.();
|
|
530
|
+
}
|
|
531
|
+
disarm() {
|
|
532
|
+
if (this.timer) clearInterval(this.timer);
|
|
533
|
+
this.timer = null;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/allowance/JsonAccountAllowancePersistence.ts
|
|
538
|
+
import { randomUUID } from "crypto";
|
|
539
|
+
import {
|
|
540
|
+
existsSync,
|
|
541
|
+
mkdirSync,
|
|
542
|
+
readFileSync,
|
|
543
|
+
renameSync,
|
|
544
|
+
rmSync,
|
|
545
|
+
statSync,
|
|
546
|
+
writeFileSync
|
|
547
|
+
} from "fs";
|
|
548
|
+
import { dirname } from "path";
|
|
549
|
+
import { normalizeAccountAllowanceSnapshot } from "@omnicross/core/pipeline/AccountAllowanceStore";
|
|
550
|
+
var ACCOUNT_ALLOWANCE_CACHE_VERSION = 1;
|
|
551
|
+
var MAX_PERSISTED_ALLOWANCE_SNAPSHOTS = 256;
|
|
552
|
+
var MAX_ALLOWANCE_CACHE_BYTES = 1e6;
|
|
553
|
+
var JsonAccountAllowancePersistence = class {
|
|
554
|
+
constructor(cachePath) {
|
|
555
|
+
this.cachePath = cachePath;
|
|
556
|
+
}
|
|
557
|
+
cachePath;
|
|
558
|
+
/** Read only the `snapshots` payload; all row validation remains defensive. */
|
|
559
|
+
load() {
|
|
560
|
+
if (!existsSync(this.cachePath)) return [];
|
|
561
|
+
try {
|
|
562
|
+
if (statSync(this.cachePath).size > MAX_ALLOWANCE_CACHE_BYTES) return [];
|
|
563
|
+
const raw = readFileSync(this.cachePath, "utf8");
|
|
564
|
+
if (!raw.trim()) return [];
|
|
565
|
+
const parsed = JSON.parse(raw);
|
|
566
|
+
if (Array.isArray(parsed)) return parsed;
|
|
567
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
|
|
568
|
+
const file = parsed;
|
|
569
|
+
return file.version === ACCOUNT_ALLOWANCE_CACHE_VERSION && Array.isArray(file.snapshots) ? file.snapshots : [];
|
|
570
|
+
} catch {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/** Replace the file atomically; the target remains intact if replacement fails. */
|
|
575
|
+
save(snapshots) {
|
|
576
|
+
const rows = [];
|
|
577
|
+
for (const snapshot of snapshots) {
|
|
578
|
+
const normalized = normalizeAccountAllowanceSnapshot(snapshot);
|
|
579
|
+
if (!normalized) continue;
|
|
580
|
+
rows.push(normalized);
|
|
581
|
+
if (rows.length >= MAX_PERSISTED_ALLOWANCE_SNAPSHOTS) break;
|
|
582
|
+
}
|
|
583
|
+
const file = {
|
|
584
|
+
version: ACCOUNT_ALLOWANCE_CACHE_VERSION,
|
|
585
|
+
snapshots: rows
|
|
586
|
+
};
|
|
587
|
+
const serialized = JSON.stringify(file, null, 2) + "\n";
|
|
588
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_ALLOWANCE_CACHE_BYTES) {
|
|
589
|
+
throw new Error("account allowance cache exceeds its size limit");
|
|
590
|
+
}
|
|
591
|
+
mkdirSync(dirname(this.cachePath), { recursive: true });
|
|
592
|
+
const temporaryPath = `${this.cachePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
593
|
+
try {
|
|
594
|
+
writeFileSync(temporaryPath, serialized, { encoding: "utf8", flag: "wx" });
|
|
595
|
+
renameSync(temporaryPath, this.cachePath);
|
|
596
|
+
} finally {
|
|
597
|
+
rmSync(temporaryPath, { force: true });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
135
602
|
// src/admin/AdminServer.ts
|
|
136
603
|
import { timingSafeEqual } from "crypto";
|
|
137
604
|
import http2 from "http";
|
|
@@ -154,16 +621,16 @@ function intParam(value) {
|
|
|
154
621
|
}
|
|
155
622
|
function handleAuditQuery(req, res, reader) {
|
|
156
623
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
157
|
-
const
|
|
624
|
+
const query2 = {};
|
|
158
625
|
const keyId = url.searchParams.get("keyId");
|
|
159
|
-
if (keyId && keyId.trim())
|
|
626
|
+
if (keyId && keyId.trim()) query2.keyId = keyId.trim();
|
|
160
627
|
const from = intParam(url.searchParams.get("from"));
|
|
161
|
-
if (from !== void 0)
|
|
628
|
+
if (from !== void 0) query2.from = from;
|
|
162
629
|
const to = intParam(url.searchParams.get("to"));
|
|
163
|
-
if (to !== void 0)
|
|
630
|
+
if (to !== void 0) query2.to = to;
|
|
164
631
|
const limit = intParam(url.searchParams.get("limit"));
|
|
165
|
-
if (limit !== void 0)
|
|
166
|
-
const records = reader ? reader(
|
|
632
|
+
if (limit !== void 0) query2.limit = limit;
|
|
633
|
+
const records = reader ? reader(query2) : [];
|
|
167
634
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
168
635
|
res.end(JSON.stringify({ records }));
|
|
169
636
|
}
|
|
@@ -235,19 +702,19 @@ function resetWebhookRuntimeForTests() {
|
|
|
235
702
|
|
|
236
703
|
// src/admin/webhookTestApi.ts
|
|
237
704
|
function readJsonBody(req) {
|
|
238
|
-
return new Promise((
|
|
705
|
+
return new Promise((resolve2) => {
|
|
239
706
|
const chunks = [];
|
|
240
707
|
req.on("data", (c) => chunks.push(c));
|
|
241
708
|
req.on("end", () => {
|
|
242
709
|
try {
|
|
243
710
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
244
711
|
const parsed = raw ? JSON.parse(raw) : {};
|
|
245
|
-
|
|
712
|
+
resolve2(parsed && typeof parsed === "object" ? parsed : {});
|
|
246
713
|
} catch {
|
|
247
|
-
|
|
714
|
+
resolve2({});
|
|
248
715
|
}
|
|
249
716
|
});
|
|
250
|
-
req.on("error", () =>
|
|
717
|
+
req.on("error", () => resolve2({}));
|
|
251
718
|
});
|
|
252
719
|
}
|
|
253
720
|
async function handleWebhookTest(req, res) {
|
|
@@ -267,17 +734,19 @@ async function handleWebhookTest(req, res) {
|
|
|
267
734
|
import http from "http";
|
|
268
735
|
import {
|
|
269
736
|
createNamedKey,
|
|
737
|
+
gatewayBindingToEndpointConfig,
|
|
270
738
|
isKindMappedEndpoint,
|
|
271
739
|
loadServerConfig as loadServerConfig2,
|
|
272
740
|
mergeServerConfig,
|
|
273
741
|
normalizeProxyConfig,
|
|
274
|
-
saveServerConfig
|
|
275
|
-
validateServerModelConfig
|
|
742
|
+
saveServerConfig
|
|
276
743
|
} from "@omnicross/core/outbound-api";
|
|
277
|
-
import {
|
|
744
|
+
import { getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling2 } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
|
|
745
|
+
import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
|
|
746
|
+
import { fetchUpstream as fetchUpstream2 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
278
747
|
|
|
279
748
|
// src/config.ts
|
|
280
|
-
import { readFileSync as
|
|
749
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
281
750
|
|
|
282
751
|
// src/secrets/envelope.ts
|
|
283
752
|
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
@@ -336,9 +805,9 @@ function decryptValue(envelope, key) {
|
|
|
336
805
|
|
|
337
806
|
// src/secrets/masterKey.ts
|
|
338
807
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
339
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
808
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
340
809
|
import { homedir } from "os";
|
|
341
|
-
import { dirname, join } from "path";
|
|
810
|
+
import { dirname as dirname2, join } from "path";
|
|
342
811
|
var MASTER_KEY_ENV = "OMNICROSS_MASTER_KEY";
|
|
343
812
|
var KEY_BYTES2 = 32;
|
|
344
813
|
function defaultMasterKeyPath() {
|
|
@@ -358,7 +827,7 @@ function decodeEnvKey(raw) {
|
|
|
358
827
|
return buf;
|
|
359
828
|
}
|
|
360
829
|
function readKeyFile(path2) {
|
|
361
|
-
const raw =
|
|
830
|
+
const raw = readFileSync2(path2);
|
|
362
831
|
if (raw.length === KEY_BYTES2) return raw;
|
|
363
832
|
const text = raw.toString("utf8").trim();
|
|
364
833
|
if (/^[0-9a-fA-F]{64}$/.test(text)) return Buffer.from(text, "hex");
|
|
@@ -370,8 +839,8 @@ function readKeyFile(path2) {
|
|
|
370
839
|
}
|
|
371
840
|
function generateKeyFile(path2) {
|
|
372
841
|
const key = randomBytes2(KEY_BYTES2);
|
|
373
|
-
|
|
374
|
-
|
|
842
|
+
mkdirSync2(dirname2(path2), { recursive: true });
|
|
843
|
+
writeFileSync2(path2, key, { mode: 384 });
|
|
375
844
|
try {
|
|
376
845
|
chmodSync(path2, 384);
|
|
377
846
|
} catch {
|
|
@@ -384,7 +853,7 @@ function resolveMasterKey(options = {}) {
|
|
|
384
853
|
return decodeEnvKey(envRaw);
|
|
385
854
|
}
|
|
386
855
|
const keyFilePath = options.keyFilePath ?? defaultMasterKeyPath();
|
|
387
|
-
if (
|
|
856
|
+
if (existsSync2(keyFilePath)) {
|
|
388
857
|
return readKeyFile(keyFilePath);
|
|
389
858
|
}
|
|
390
859
|
return generateKeyFile(keyFilePath);
|
|
@@ -663,7 +1132,19 @@ function validateLogging(raw) {
|
|
|
663
1132
|
if (typeof l["file"] === "string" && l["file"].length > 0) out.file = l["file"];
|
|
664
1133
|
return out.level !== void 0 || out.format !== void 0 || out.file !== void 0 ? out : void 0;
|
|
665
1134
|
}
|
|
666
|
-
var VALID_FORMATS = [
|
|
1135
|
+
var VALID_FORMATS = [
|
|
1136
|
+
"openai",
|
|
1137
|
+
"anthropic",
|
|
1138
|
+
"gemini",
|
|
1139
|
+
"openai-response"
|
|
1140
|
+
];
|
|
1141
|
+
var FORMAT_AXIS_TRANSFORMERS = [
|
|
1142
|
+
"openai",
|
|
1143
|
+
"anthropic",
|
|
1144
|
+
"gemini",
|
|
1145
|
+
"openai-response",
|
|
1146
|
+
"gemini-code-assist"
|
|
1147
|
+
];
|
|
667
1148
|
function validateApiKeys(raw) {
|
|
668
1149
|
if (!Array.isArray(raw)) return void 0;
|
|
669
1150
|
const out = [];
|
|
@@ -768,6 +1249,33 @@ function validateApiModes(raw) {
|
|
|
768
1249
|
}
|
|
769
1250
|
return out.length > 0 ? out : void 0;
|
|
770
1251
|
}
|
|
1252
|
+
function transformerEntryName(entry) {
|
|
1253
|
+
return typeof entry === "string" ? entry : entry[0];
|
|
1254
|
+
}
|
|
1255
|
+
function migrateFormatAxis(apiFormat, transformer) {
|
|
1256
|
+
const use = transformer?.use;
|
|
1257
|
+
if (!use || use.length === 0) return { apiFormat, transformer };
|
|
1258
|
+
const hasFormatEntry = use.some((e) => FORMAT_AXIS_TRANSFORMERS.includes(transformerEntryName(e)));
|
|
1259
|
+
if (!hasFormatEntry) return { apiFormat, transformer };
|
|
1260
|
+
let migratedFormat = apiFormat;
|
|
1261
|
+
if (apiFormat === "openai") {
|
|
1262
|
+
const promoted = use.map(transformerEntryName).find((n) => VALID_FORMATS.includes(n));
|
|
1263
|
+
if (promoted) migratedFormat = promoted;
|
|
1264
|
+
}
|
|
1265
|
+
const rest = use.filter((e) => !FORMAT_AXIS_TRANSFORMERS.includes(transformerEntryName(e)));
|
|
1266
|
+
const next = {};
|
|
1267
|
+
let kept = false;
|
|
1268
|
+
if (rest.length > 0) {
|
|
1269
|
+
next.use = rest;
|
|
1270
|
+
kept = true;
|
|
1271
|
+
}
|
|
1272
|
+
for (const key of Object.keys(transformer)) {
|
|
1273
|
+
if (key === "use") continue;
|
|
1274
|
+
next[key] = transformer[key];
|
|
1275
|
+
kept = true;
|
|
1276
|
+
}
|
|
1277
|
+
return { apiFormat: migratedFormat, transformer: kept ? next : void 0 };
|
|
1278
|
+
}
|
|
771
1279
|
function validateProvider(raw, index) {
|
|
772
1280
|
if (!raw || typeof raw !== "object") {
|
|
773
1281
|
throw new Error(`config: providers[${index}] is not an object`);
|
|
@@ -798,10 +1306,14 @@ function validateProvider(raw, index) {
|
|
|
798
1306
|
const apiVersion = typeof p["apiVersion"] === "string" && p["apiVersion"].length > 0 ? p["apiVersion"] : void 0;
|
|
799
1307
|
const maxConcurrency = typeof p["maxConcurrency"] === "number" && Number.isFinite(p["maxConcurrency"]) ? p["maxConcurrency"] : void 0;
|
|
800
1308
|
const modelsEndpoint = typeof p["modelsEndpoint"] === "string" && p["modelsEndpoint"].length > 0 ? p["modelsEndpoint"] : void 0;
|
|
1309
|
+
const { apiFormat: migratedFormat, transformer: migratedTransformer } = migrateFormatAxis(
|
|
1310
|
+
apiFormat,
|
|
1311
|
+
validateTransformer(p["transformer"])
|
|
1312
|
+
);
|
|
801
1313
|
return {
|
|
802
1314
|
id,
|
|
803
1315
|
name,
|
|
804
|
-
apiFormat,
|
|
1316
|
+
apiFormat: migratedFormat,
|
|
805
1317
|
baseUrl,
|
|
806
1318
|
apiKey,
|
|
807
1319
|
models: Array.isArray(models) ? models.filter((m) => typeof m === "string") : void 0,
|
|
@@ -815,7 +1327,9 @@ function validateProvider(raw, index) {
|
|
|
815
1327
|
modelsEndpoint,
|
|
816
1328
|
// Provider transformer config (app-parity child 5): load-guard, collapse-to-
|
|
817
1329
|
// undefined; non-secret; ENFORCED via resolveTransformerChain (parity-2 child 2).
|
|
818
|
-
|
|
1330
|
+
// Format-axis entries are stripped by `migrateFormatAxis` — `use[]` is the
|
|
1331
|
+
// MODIFIER axis only.
|
|
1332
|
+
transformer: migratedTransformer,
|
|
819
1333
|
// Coding-plan endpoint (app-parity-2 child 3): load-guard, collapse-to-undefined.
|
|
820
1334
|
// SECRET-bearing (apiKey encrypted at rest); enforced by core's resolveProviderEndpoint.
|
|
821
1335
|
codingPlan: validateCodingPlan(p["codingPlan"]),
|
|
@@ -847,7 +1361,7 @@ function setSecretBox(box) {
|
|
|
847
1361
|
function loadConfig(path2) {
|
|
848
1362
|
let raw;
|
|
849
1363
|
try {
|
|
850
|
-
raw =
|
|
1364
|
+
raw = readFileSync3(path2, "utf8");
|
|
851
1365
|
} catch {
|
|
852
1366
|
throw new Error(`config: cannot read file at '${path2}'`);
|
|
853
1367
|
}
|
|
@@ -862,7 +1376,7 @@ function loadConfig(path2) {
|
|
|
862
1376
|
}
|
|
863
1377
|
function saveConfig(path2, cfg) {
|
|
864
1378
|
const toWrite = secretBox ? encryptConfigSecrets(cfg, secretBox) : cfg;
|
|
865
|
-
|
|
1379
|
+
writeFileSync3(path2, JSON.stringify(toWrite, null, 2) + "\n", "utf8");
|
|
866
1380
|
}
|
|
867
1381
|
|
|
868
1382
|
// src/pool/resolveEnvKey.ts
|
|
@@ -874,6 +1388,706 @@ function resolveEnvKey(rawKey) {
|
|
|
874
1388
|
return rawKey;
|
|
875
1389
|
}
|
|
876
1390
|
|
|
1391
|
+
// src/integrations/IntegrationManager.ts
|
|
1392
|
+
import { createHash } from "crypto";
|
|
1393
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
1394
|
+
import { homedir as homedir2 } from "os";
|
|
1395
|
+
import { dirname as dirname4, join as join2, resolve } from "path";
|
|
1396
|
+
import { createIntegrationKey } from "@omnicross/core";
|
|
1397
|
+
|
|
1398
|
+
// src/integrations/IntegrationStateStore.ts
|
|
1399
|
+
import {
|
|
1400
|
+
chmodSync as chmodSync2,
|
|
1401
|
+
existsSync as existsSync3,
|
|
1402
|
+
mkdirSync as mkdirSync3,
|
|
1403
|
+
readFileSync as readFileSync4,
|
|
1404
|
+
renameSync as renameSync2,
|
|
1405
|
+
unlinkSync,
|
|
1406
|
+
writeFileSync as writeFileSync4
|
|
1407
|
+
} from "fs";
|
|
1408
|
+
import { dirname as dirname3 } from "path";
|
|
1409
|
+
var EMPTY_STATE = { version: 1, clients: {} };
|
|
1410
|
+
var IntegrationStateStore = class {
|
|
1411
|
+
constructor(path2, box) {
|
|
1412
|
+
this.path = path2;
|
|
1413
|
+
this.box = box;
|
|
1414
|
+
}
|
|
1415
|
+
path;
|
|
1416
|
+
box;
|
|
1417
|
+
load() {
|
|
1418
|
+
if (!existsSync3(this.path)) return { ...EMPTY_STATE, clients: {} };
|
|
1419
|
+
let raw;
|
|
1420
|
+
try {
|
|
1421
|
+
raw = JSON.parse(readFileSync4(this.path, "utf8"));
|
|
1422
|
+
} catch {
|
|
1423
|
+
throw new Error(`integration state '${this.path}' is not valid JSON`);
|
|
1424
|
+
}
|
|
1425
|
+
if (!isState(raw)) {
|
|
1426
|
+
throw new Error(`integration state '${this.path}' has an unsupported shape`);
|
|
1427
|
+
}
|
|
1428
|
+
return {
|
|
1429
|
+
version: 1,
|
|
1430
|
+
gatewayKey: raw.gatewayKey ? { ...raw.gatewayKey, secret: this.box.decryptMaybe(raw.gatewayKey.secret) } : void 0,
|
|
1431
|
+
clients: decryptClients(raw.clients, this.box)
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
save(state) {
|
|
1435
|
+
const encrypted = {
|
|
1436
|
+
version: 1,
|
|
1437
|
+
gatewayKey: state.gatewayKey ? { ...state.gatewayKey, secret: this.box.encrypt(state.gatewayKey.secret) } : void 0,
|
|
1438
|
+
clients: encryptClients(state.clients, this.box)
|
|
1439
|
+
};
|
|
1440
|
+
atomicWrite(this.path, JSON.stringify(encrypted, null, 2) + "\n");
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
function transformClients(clients, transform) {
|
|
1444
|
+
const out = {};
|
|
1445
|
+
for (const client of ["codex", "claude"]) {
|
|
1446
|
+
const row = clients[client];
|
|
1447
|
+
if (row) {
|
|
1448
|
+
out[client] = {
|
|
1449
|
+
...row,
|
|
1450
|
+
originalContent: transform(row.originalContent),
|
|
1451
|
+
credentialFile: row.credentialFile ? { ...row.credentialFile, originalContent: transform(row.credentialFile.originalContent) } : void 0
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return out;
|
|
1456
|
+
}
|
|
1457
|
+
function decryptClients(clients, box) {
|
|
1458
|
+
return transformClients(clients, (value) => box.decryptMaybe(value));
|
|
1459
|
+
}
|
|
1460
|
+
function encryptClients(clients, box) {
|
|
1461
|
+
return transformClients(clients, (value) => box.encrypt(value));
|
|
1462
|
+
}
|
|
1463
|
+
function isState(value) {
|
|
1464
|
+
if (!value || typeof value !== "object") return false;
|
|
1465
|
+
const row = value;
|
|
1466
|
+
if (row.version !== 1 || !row.clients || typeof row.clients !== "object") return false;
|
|
1467
|
+
if (row.gatewayKey !== void 0) {
|
|
1468
|
+
const key = row.gatewayKey;
|
|
1469
|
+
if (!key || typeof key !== "object" || typeof key.id !== "string" || typeof key.secret !== "string" || typeof key.createdAt !== "number") return false;
|
|
1470
|
+
}
|
|
1471
|
+
for (const client of ["codex", "claude"]) {
|
|
1472
|
+
const candidate = row.clients[client];
|
|
1473
|
+
if (candidate === void 0) continue;
|
|
1474
|
+
if (!isInstallRecord(candidate, client)) return false;
|
|
1475
|
+
}
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
function isInstallRecord(value, client) {
|
|
1479
|
+
if (!value || typeof value !== "object") return false;
|
|
1480
|
+
const row = value;
|
|
1481
|
+
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));
|
|
1482
|
+
}
|
|
1483
|
+
function isManagedFileRecord(value) {
|
|
1484
|
+
if (!value || typeof value !== "object") return false;
|
|
1485
|
+
const row = value;
|
|
1486
|
+
return typeof row.path === "string" && typeof row.originalExisted === "boolean" && typeof row.originalContent === "string" && typeof row.originalHash === "string" && typeof row.installedHash === "string";
|
|
1487
|
+
}
|
|
1488
|
+
function atomicWrite(path2, content) {
|
|
1489
|
+
mkdirSync3(dirname3(path2), { recursive: true });
|
|
1490
|
+
const temp = `${path2}.tmp-${process.pid}-${Date.now()}`;
|
|
1491
|
+
writeFileSync4(temp, content, { encoding: "utf8", mode: 384 });
|
|
1492
|
+
try {
|
|
1493
|
+
renameSync2(temp, path2);
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
try {
|
|
1496
|
+
unlinkSync(temp);
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
throw error;
|
|
1500
|
+
} finally {
|
|
1501
|
+
if (existsSync3(path2)) {
|
|
1502
|
+
try {
|
|
1503
|
+
chmodSync2(path2, 384);
|
|
1504
|
+
} catch {
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/integrations/configAdapters.ts
|
|
1511
|
+
var CODEX_BEGIN = "# >>> omnicross managed provider >>>";
|
|
1512
|
+
var CODEX_END = "# <<< omnicross managed provider <<<";
|
|
1513
|
+
var CODEX_PROVIDER = "omnicross";
|
|
1514
|
+
var CLAUDE_API_KEY_SENTINEL = "omnicross-gateway";
|
|
1515
|
+
function renderCodexConfig(input) {
|
|
1516
|
+
if (input.existing.includes(CODEX_BEGIN) || input.existing.includes(CODEX_END)) {
|
|
1517
|
+
throw new Error("Codex config contains an unmanaged/orphaned Omnicross marker");
|
|
1518
|
+
}
|
|
1519
|
+
if (/^\s*\[\s*model_providers\s*\.\s*["']?omnicross["']?\s*]/m.test(input.existing)) {
|
|
1520
|
+
throw new Error("Codex config already defines model_providers.omnicross");
|
|
1521
|
+
}
|
|
1522
|
+
const eol = input.existing.includes("\r\n") ? "\r\n" : "\n";
|
|
1523
|
+
const lines = input.existing.replace(/\r\n/g, "\n").split("\n");
|
|
1524
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
|
|
1525
|
+
const rootEnd = firstTable < 0 ? lines.length : firstTable;
|
|
1526
|
+
const assignments = {
|
|
1527
|
+
model_provider: [],
|
|
1528
|
+
preferred_auth_method: []
|
|
1529
|
+
};
|
|
1530
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
1531
|
+
if (/^\s*#/.test(lines[index])) continue;
|
|
1532
|
+
for (const key of Object.keys(assignments)) {
|
|
1533
|
+
if (new RegExp(`^\\s*${key}\\s*=`).test(lines[index])) assignments[key].push(index);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (assignments.model_provider.length > 1) {
|
|
1537
|
+
throw new Error("Codex config has duplicate top-level model_provider keys");
|
|
1538
|
+
}
|
|
1539
|
+
if (assignments.preferred_auth_method.length > 1) {
|
|
1540
|
+
throw new Error("Codex config has duplicate top-level preferred_auth_method keys");
|
|
1541
|
+
}
|
|
1542
|
+
const managedRoot = {
|
|
1543
|
+
model_provider: `model_provider = "${CODEX_PROVIDER}" # managed by Omnicross`,
|
|
1544
|
+
preferred_auth_method: 'preferred_auth_method = "apikey" # managed by Omnicross'
|
|
1545
|
+
};
|
|
1546
|
+
const missing = [];
|
|
1547
|
+
for (const key of Object.keys(assignments)) {
|
|
1548
|
+
const [index] = assignments[key];
|
|
1549
|
+
if (index === void 0) missing.push(managedRoot[key]);
|
|
1550
|
+
else lines[index] = managedRoot[key];
|
|
1551
|
+
}
|
|
1552
|
+
if (missing.length > 0) lines.splice(rootEnd, 0, ...missing, "");
|
|
1553
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1554
|
+
const base = lines.length > 0 ? `${lines.join("\n")}
|
|
1555
|
+
|
|
1556
|
+
` : "";
|
|
1557
|
+
const root = trimTrailingSlash(input.gatewayBaseUrl);
|
|
1558
|
+
const block = [
|
|
1559
|
+
CODEX_BEGIN,
|
|
1560
|
+
`[model_providers.${CODEX_PROVIDER}]`,
|
|
1561
|
+
'name = "Omnicross Local Gateway"',
|
|
1562
|
+
`base_url = ${tomlString(`${root}/v1`)}`,
|
|
1563
|
+
'wire_api = "responses"',
|
|
1564
|
+
"requires_openai_auth = true",
|
|
1565
|
+
"supports_websockets = false",
|
|
1566
|
+
CODEX_END,
|
|
1567
|
+
""
|
|
1568
|
+
].join("\n");
|
|
1569
|
+
return (base + block).replace(/\n/g, eol);
|
|
1570
|
+
}
|
|
1571
|
+
function renderCodexAuth(secret) {
|
|
1572
|
+
return JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: secret }, null, 2) + "\n";
|
|
1573
|
+
}
|
|
1574
|
+
function renderClaudeSettings(existing, gatewayBaseUrl, secret) {
|
|
1575
|
+
let parsed = {};
|
|
1576
|
+
if (existing.trim()) {
|
|
1577
|
+
try {
|
|
1578
|
+
parsed = JSON.parse(existing);
|
|
1579
|
+
} catch {
|
|
1580
|
+
throw new Error("Claude settings file is not valid JSON");
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (!isPlainObject(parsed)) throw new Error("Claude settings root must be a JSON object");
|
|
1584
|
+
const settings = { ...parsed };
|
|
1585
|
+
const oldEnv = settings.env;
|
|
1586
|
+
if (oldEnv !== void 0 && !isPlainObject(oldEnv)) {
|
|
1587
|
+
throw new Error("Claude settings env field must be a JSON object");
|
|
1588
|
+
}
|
|
1589
|
+
settings.env = {
|
|
1590
|
+
...oldEnv,
|
|
1591
|
+
ANTHROPIC_BASE_URL: trimTrailingSlash(gatewayBaseUrl),
|
|
1592
|
+
ANTHROPIC_AUTH_TOKEN: secret,
|
|
1593
|
+
ANTHROPIC_API_KEY: CLAUDE_API_KEY_SENTINEL
|
|
1594
|
+
};
|
|
1595
|
+
return JSON.stringify(settings, null, 2) + "\n";
|
|
1596
|
+
}
|
|
1597
|
+
function restoreCodexBase(current, original) {
|
|
1598
|
+
const hasBegin = current.includes(CODEX_BEGIN);
|
|
1599
|
+
const hasEnd = current.includes(CODEX_END);
|
|
1600
|
+
if (hasBegin !== hasEnd) throw new Error("Codex config has an incomplete Omnicross managed block");
|
|
1601
|
+
const eol = current.includes("\r\n") ? "\r\n" : "\n";
|
|
1602
|
+
let normalized = current.replace(/\r\n/g, "\n");
|
|
1603
|
+
if (hasBegin) {
|
|
1604
|
+
const start = normalized.indexOf(CODEX_BEGIN);
|
|
1605
|
+
const endMarker = normalized.indexOf(CODEX_END, start);
|
|
1606
|
+
if (endMarker < 0) throw new Error("Codex config has an incomplete Omnicross managed block");
|
|
1607
|
+
const end = normalized.indexOf("\n", endMarker);
|
|
1608
|
+
normalized = normalized.slice(0, start) + (end < 0 ? "" : normalized.slice(end + 1));
|
|
1609
|
+
}
|
|
1610
|
+
const lines = normalized.split("\n");
|
|
1611
|
+
for (const key of ["model_provider", "preferred_auth_method"]) {
|
|
1612
|
+
const originalAssignment = rootAssignment(original, key);
|
|
1613
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
|
|
1614
|
+
const rootEnd = firstTable < 0 ? lines.length : firstTable;
|
|
1615
|
+
const managedIndex = lines.slice(0, rootEnd).findIndex(
|
|
1616
|
+
(line) => new RegExp(`^\\s*${key}\\s*=.*#\\s*managed by Omnicross\\s*$`).test(line)
|
|
1617
|
+
);
|
|
1618
|
+
if (managedIndex >= 0) {
|
|
1619
|
+
if (originalAssignment) lines[managedIndex] = originalAssignment;
|
|
1620
|
+
else lines.splice(managedIndex, 1);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return lines.join("\n").replace(/\n/g, eol);
|
|
1624
|
+
}
|
|
1625
|
+
function restoreClaudeBase(current, original, gatewayBaseUrl, secret) {
|
|
1626
|
+
const currentRoot = parseSettings(current);
|
|
1627
|
+
const originalRoot = parseSettings(original);
|
|
1628
|
+
const env = isPlainObject(currentRoot.env) ? { ...currentRoot.env } : {};
|
|
1629
|
+
const originalEnv = isPlainObject(originalRoot.env) ? originalRoot.env : {};
|
|
1630
|
+
const expected = {
|
|
1631
|
+
ANTHROPIC_BASE_URL: trimTrailingSlash(gatewayBaseUrl),
|
|
1632
|
+
ANTHROPIC_AUTH_TOKEN: secret,
|
|
1633
|
+
ANTHROPIC_API_KEY: CLAUDE_API_KEY_SENTINEL
|
|
1634
|
+
};
|
|
1635
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1636
|
+
if (env[key] !== value) continue;
|
|
1637
|
+
if (Object.prototype.hasOwnProperty.call(originalEnv, key)) env[key] = originalEnv[key];
|
|
1638
|
+
else delete env[key];
|
|
1639
|
+
}
|
|
1640
|
+
const next = { ...currentRoot };
|
|
1641
|
+
if (Object.keys(env).length > 0 || Object.prototype.hasOwnProperty.call(originalRoot, "env")) next.env = env;
|
|
1642
|
+
else delete next.env;
|
|
1643
|
+
return JSON.stringify(next, null, 2) + "\n";
|
|
1644
|
+
}
|
|
1645
|
+
function tomlString(value) {
|
|
1646
|
+
return JSON.stringify(value);
|
|
1647
|
+
}
|
|
1648
|
+
function trimTrailingSlash(value) {
|
|
1649
|
+
return value.replace(/\/+$/, "");
|
|
1650
|
+
}
|
|
1651
|
+
function isPlainObject(value) {
|
|
1652
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1653
|
+
}
|
|
1654
|
+
function parseSettings(value) {
|
|
1655
|
+
if (!value.trim()) return {};
|
|
1656
|
+
let parsed;
|
|
1657
|
+
try {
|
|
1658
|
+
parsed = JSON.parse(value);
|
|
1659
|
+
} catch {
|
|
1660
|
+
throw new Error("Claude settings file is not valid JSON");
|
|
1661
|
+
}
|
|
1662
|
+
if (!isPlainObject(parsed)) throw new Error("Claude settings root must be a JSON object");
|
|
1663
|
+
return parsed;
|
|
1664
|
+
}
|
|
1665
|
+
function rootAssignment(content, key) {
|
|
1666
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
1667
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line) && !/^\s*#/.test(line));
|
|
1668
|
+
const root = lines.slice(0, firstTable < 0 ? lines.length : firstTable);
|
|
1669
|
+
return root.find((line) => new RegExp(`^\\s*${key}\\s*=`).test(line) && !/^\s*#/.test(line));
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// src/integrations/IntegrationManager.ts
|
|
1673
|
+
var IntegrationConflictError = class extends Error {
|
|
1674
|
+
constructor(message) {
|
|
1675
|
+
super(message);
|
|
1676
|
+
this.name = "IntegrationConflictError";
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
var IntegrationManager = class {
|
|
1680
|
+
constructor(options) {
|
|
1681
|
+
this.options = options;
|
|
1682
|
+
assertLoopbackGatewayUrl(options.gatewayBaseUrl);
|
|
1683
|
+
this.homeDir = options.homeDir ?? homedir2();
|
|
1684
|
+
}
|
|
1685
|
+
options;
|
|
1686
|
+
homeDir;
|
|
1687
|
+
async listStatus() {
|
|
1688
|
+
const state = this.options.stateStore.load();
|
|
1689
|
+
const keyUsable = await this.isKeyUsable(state);
|
|
1690
|
+
return ["codex", "claude"].map((client) => this.statusFor(client, state, keyUsable));
|
|
1691
|
+
}
|
|
1692
|
+
async plan(client, configPath = this.defaultConfigPath(client)) {
|
|
1693
|
+
const state = this.options.stateStore.load();
|
|
1694
|
+
const record = state.clients[client];
|
|
1695
|
+
const target = record?.configPath ?? resolve(configPath);
|
|
1696
|
+
const status = this.statusFor(client, state, await this.isKeyUsable(state));
|
|
1697
|
+
const changes = client === "codex" ? [
|
|
1698
|
+
"model_provider",
|
|
1699
|
+
"preferred_auth_method",
|
|
1700
|
+
"model_providers.omnicross",
|
|
1701
|
+
"auth.json.auth_mode",
|
|
1702
|
+
"auth.json.OPENAI_API_KEY"
|
|
1703
|
+
] : ["env.ANTHROPIC_BASE_URL", "env.ANTHROPIC_AUTH_TOKEN", "env.ANTHROPIC_API_KEY"];
|
|
1704
|
+
if (!record) return { client, configPath: target, action: "install", canApply: true, changes, warnings: [] };
|
|
1705
|
+
if (status.status === "enabled") {
|
|
1706
|
+
return { client, configPath: target, action: "none", canApply: true, changes: [], warnings: [] };
|
|
1707
|
+
}
|
|
1708
|
+
return {
|
|
1709
|
+
client,
|
|
1710
|
+
configPath: target,
|
|
1711
|
+
action: "repair",
|
|
1712
|
+
canApply: true,
|
|
1713
|
+
changes,
|
|
1714
|
+
warnings: ["Configuration changed after installation; repair preserves unrelated current settings."]
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
async install(client, configPath = this.defaultConfigPath(client)) {
|
|
1718
|
+
const target = resolve(configPath);
|
|
1719
|
+
const state = this.options.stateStore.load();
|
|
1720
|
+
const existingRecord = state.clients[client];
|
|
1721
|
+
if (existingRecord) {
|
|
1722
|
+
const status = this.statusFor(client, state, await this.isKeyUsable(state));
|
|
1723
|
+
if (status.status === "enabled") return status;
|
|
1724
|
+
throw new IntegrationConflictError(
|
|
1725
|
+
`${client} integration configuration has drifted; restore or remove it before reinstalling`
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
const key = await this.ensureGatewayKey(state);
|
|
1729
|
+
const original = readOptional(target);
|
|
1730
|
+
const originalContent = original ?? "";
|
|
1731
|
+
const installed = this.renderInstalled(client, originalContent, key.secret);
|
|
1732
|
+
const credentialPath = client === "codex" ? this.codexAuthPathForConfig(target) : void 0;
|
|
1733
|
+
const originalCredential = credentialPath ? readOptional(credentialPath) : null;
|
|
1734
|
+
const installedCredential = credentialPath ? renderCodexAuth(key.secret) : void 0;
|
|
1735
|
+
const record = {
|
|
1736
|
+
client,
|
|
1737
|
+
configPath: target,
|
|
1738
|
+
originalExisted: original !== null,
|
|
1739
|
+
originalContent,
|
|
1740
|
+
originalHash: sha256(originalContent),
|
|
1741
|
+
installedHash: sha256(installed),
|
|
1742
|
+
installedAt: Date.now(),
|
|
1743
|
+
gatewayBaseUrl: this.options.gatewayBaseUrl,
|
|
1744
|
+
credentialFile: credentialPath && installedCredential ? managedFileRecord(credentialPath, originalCredential, installedCredential) : void 0
|
|
1745
|
+
};
|
|
1746
|
+
const prior = state.clients[client];
|
|
1747
|
+
state.clients[client] = record;
|
|
1748
|
+
this.options.stateStore.save(state);
|
|
1749
|
+
try {
|
|
1750
|
+
applyFileChangesWithRollback([
|
|
1751
|
+
{ path: target, content: installed },
|
|
1752
|
+
...credentialPath && installedCredential ? [{ path: credentialPath, content: installedCredential }] : []
|
|
1753
|
+
]);
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
if (prior) state.clients[client] = prior;
|
|
1756
|
+
else delete state.clients[client];
|
|
1757
|
+
this.options.stateStore.save(state);
|
|
1758
|
+
throw error;
|
|
1759
|
+
}
|
|
1760
|
+
return this.statusFor(client, state, true);
|
|
1761
|
+
}
|
|
1762
|
+
async repair(client) {
|
|
1763
|
+
const state = this.options.stateStore.load();
|
|
1764
|
+
const record = state.clients[client];
|
|
1765
|
+
if (!record) return this.install(client);
|
|
1766
|
+
const previouslyInstalledSecret = state.gatewayKey?.secret;
|
|
1767
|
+
const currentFile = readOptional(record.configPath);
|
|
1768
|
+
if (client === "claude" && currentFile !== null && !previouslyInstalledSecret) {
|
|
1769
|
+
throw new IntegrationConflictError(
|
|
1770
|
+
"Claude integration key state is missing; refusing to repair an ambiguous settings file"
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const key = await this.ensureGatewayKey(state);
|
|
1774
|
+
const current = currentFile ?? record.originalContent;
|
|
1775
|
+
const base = client === "codex" ? restoreCodexBase(current, record.originalContent) : restoreClaudeBase(
|
|
1776
|
+
current,
|
|
1777
|
+
record.originalContent,
|
|
1778
|
+
record.gatewayBaseUrl,
|
|
1779
|
+
previouslyInstalledSecret ?? key.secret
|
|
1780
|
+
);
|
|
1781
|
+
const installed = this.renderInstalled(client, base, key.secret);
|
|
1782
|
+
const credentialPath = client === "codex" ? record.credentialFile?.path ?? this.codexAuthPathForConfig(record.configPath) : void 0;
|
|
1783
|
+
const currentCredential = credentialPath ? readOptional(credentialPath) : null;
|
|
1784
|
+
const originalCredential = record.credentialFile ? originalSnapshotForRepair(record.credentialFile, currentCredential) : currentCredential;
|
|
1785
|
+
const installedCredential = credentialPath ? renderCodexAuth(key.secret) : void 0;
|
|
1786
|
+
const prior = {
|
|
1787
|
+
...record,
|
|
1788
|
+
credentialFile: record.credentialFile ? { ...record.credentialFile } : void 0
|
|
1789
|
+
};
|
|
1790
|
+
Object.assign(record, {
|
|
1791
|
+
originalExisted: currentFile !== null || record.originalExisted,
|
|
1792
|
+
originalContent: base,
|
|
1793
|
+
originalHash: sha256(base),
|
|
1794
|
+
installedHash: sha256(installed),
|
|
1795
|
+
installedAt: Date.now(),
|
|
1796
|
+
gatewayBaseUrl: this.options.gatewayBaseUrl,
|
|
1797
|
+
credentialFile: credentialPath && installedCredential ? managedFileRecord(credentialPath, originalCredential, installedCredential) : void 0
|
|
1798
|
+
});
|
|
1799
|
+
this.options.stateStore.save(state);
|
|
1800
|
+
try {
|
|
1801
|
+
applyFileChangesWithRollback([
|
|
1802
|
+
{ path: record.configPath, content: installed },
|
|
1803
|
+
...credentialPath && installedCredential ? [{ path: credentialPath, content: installedCredential }] : []
|
|
1804
|
+
]);
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
state.clients[client] = prior;
|
|
1807
|
+
this.options.stateStore.save(state);
|
|
1808
|
+
throw error;
|
|
1809
|
+
}
|
|
1810
|
+
return this.statusFor(client, state, true);
|
|
1811
|
+
}
|
|
1812
|
+
async remove(client) {
|
|
1813
|
+
const state = this.options.stateStore.load();
|
|
1814
|
+
const record = state.clients[client];
|
|
1815
|
+
if (!record) return this.statusFor(client, state, await this.isKeyUsable(state));
|
|
1816
|
+
const files = [primaryManagedFile(record), ...record.credentialFile ? [record.credentialFile] : []];
|
|
1817
|
+
const currentFiles = files.map((file) => ({ file, current: readOptional(file.path) }));
|
|
1818
|
+
const dispositions = currentFiles.map(({ file, current }) => managedFileDisposition(file, current));
|
|
1819
|
+
if (dispositions.some((disposition) => disposition !== "installed" && disposition !== "restored")) {
|
|
1820
|
+
throw new IntegrationConflictError(
|
|
1821
|
+
`${client} configuration changed after Omnicross installed it; refusing to overwrite user edits`
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
const changes = currentFiles.flatMap(({ file }, index) => dispositions[index] === "installed" ? [{ path: file.path, content: file.originalExisted ? file.originalContent : null }] : []);
|
|
1825
|
+
applyFileChangesWithRollback(changes);
|
|
1826
|
+
delete state.clients[client];
|
|
1827
|
+
this.options.stateStore.save(state);
|
|
1828
|
+
return this.statusFor(client, state, await this.isKeyUsable(state));
|
|
1829
|
+
}
|
|
1830
|
+
async rotateGatewayKey() {
|
|
1831
|
+
const state = this.options.stateStore.load();
|
|
1832
|
+
const previousGatewayKey = state.gatewayKey;
|
|
1833
|
+
const oldKeyId = state.gatewayKey?.id;
|
|
1834
|
+
const claude = state.clients.claude;
|
|
1835
|
+
const codex = state.clients.codex;
|
|
1836
|
+
let nextClaude;
|
|
1837
|
+
let nextCodexAuth;
|
|
1838
|
+
if (claude) {
|
|
1839
|
+
const current = readOptional(claude.configPath);
|
|
1840
|
+
if (current === null || sha256(current) !== claude.installedHash) {
|
|
1841
|
+
throw new IntegrationConflictError("Claude configuration drift must be resolved before key rotation");
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (codex) {
|
|
1845
|
+
const current = readOptional(codex.configPath);
|
|
1846
|
+
if (current === null || sha256(current) !== codex.installedHash) {
|
|
1847
|
+
throw new IntegrationConflictError("Codex configuration drift must be resolved before key rotation");
|
|
1848
|
+
}
|
|
1849
|
+
if (!codex.credentialFile) {
|
|
1850
|
+
throw new IntegrationConflictError("Codex integration must be repaired before key rotation");
|
|
1851
|
+
}
|
|
1852
|
+
const currentAuth = readOptional(codex.credentialFile.path);
|
|
1853
|
+
if (currentAuth === null || sha256(currentAuth) !== codex.credentialFile.installedHash) {
|
|
1854
|
+
throw new IntegrationConflictError("Codex credential drift must be resolved before key rotation");
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
const created = await createIntegrationKey(this.options.keyDb, "Omnicross native CLI integration");
|
|
1858
|
+
const nextGatewayKey = {
|
|
1859
|
+
id: created.id,
|
|
1860
|
+
secret: created.plaintextOnce,
|
|
1861
|
+
createdAt: created.createdAt
|
|
1862
|
+
};
|
|
1863
|
+
state.gatewayKey = nextGatewayKey;
|
|
1864
|
+
const previousClaudeHash = claude?.installedHash;
|
|
1865
|
+
const previousCodexAuthHash = codex?.credentialFile?.installedHash;
|
|
1866
|
+
if (claude) {
|
|
1867
|
+
const current = readOptional(claude.configPath) ?? "{}";
|
|
1868
|
+
nextClaude = renderClaudeSettings(current, this.options.gatewayBaseUrl, created.plaintextOnce);
|
|
1869
|
+
claude.installedHash = sha256(nextClaude);
|
|
1870
|
+
}
|
|
1871
|
+
if (codex?.credentialFile) {
|
|
1872
|
+
nextCodexAuth = renderCodexAuth(created.plaintextOnce);
|
|
1873
|
+
codex.credentialFile.installedHash = sha256(nextCodexAuth);
|
|
1874
|
+
}
|
|
1875
|
+
try {
|
|
1876
|
+
this.options.stateStore.save(state);
|
|
1877
|
+
applyFileChangesWithRollback([
|
|
1878
|
+
...codex?.credentialFile && nextCodexAuth !== void 0 ? [{ path: codex.credentialFile.path, content: nextCodexAuth }] : [],
|
|
1879
|
+
...claude && nextClaude !== void 0 ? [{ path: claude.configPath, content: nextClaude }] : []
|
|
1880
|
+
]);
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
state.gatewayKey = previousGatewayKey;
|
|
1883
|
+
if (claude && previousClaudeHash !== void 0) claude.installedHash = previousClaudeHash;
|
|
1884
|
+
if (codex?.credentialFile && previousCodexAuthHash !== void 0) {
|
|
1885
|
+
codex.credentialFile.installedHash = previousCodexAuthHash;
|
|
1886
|
+
}
|
|
1887
|
+
try {
|
|
1888
|
+
this.options.stateStore.save(state);
|
|
1889
|
+
} finally {
|
|
1890
|
+
await this.options.keyDb.outboundApiKeysRevoke(created.id);
|
|
1891
|
+
}
|
|
1892
|
+
throw error;
|
|
1893
|
+
}
|
|
1894
|
+
if (oldKeyId && oldKeyId !== created.id) await this.options.keyDb.outboundApiKeysRevoke(oldKeyId);
|
|
1895
|
+
return { keyId: created.id };
|
|
1896
|
+
}
|
|
1897
|
+
async getGatewayToken() {
|
|
1898
|
+
const state = this.options.stateStore.load();
|
|
1899
|
+
if (!state.gatewayKey || !await this.isKeyUsable(state)) {
|
|
1900
|
+
throw new Error("Omnicross integration key is missing or revoked; reinstall the CLI integration");
|
|
1901
|
+
}
|
|
1902
|
+
return state.gatewayKey.secret;
|
|
1903
|
+
}
|
|
1904
|
+
async ensureGatewayKey(state) {
|
|
1905
|
+
if (state.gatewayKey && await this.isKeyUsable(state)) return state.gatewayKey;
|
|
1906
|
+
const created = await createIntegrationKey(this.options.keyDb, "Omnicross native CLI integration");
|
|
1907
|
+
const previousGatewayKey = state.gatewayKey;
|
|
1908
|
+
const nextGatewayKey = {
|
|
1909
|
+
id: created.id,
|
|
1910
|
+
secret: created.plaintextOnce,
|
|
1911
|
+
createdAt: created.createdAt
|
|
1912
|
+
};
|
|
1913
|
+
state.gatewayKey = nextGatewayKey;
|
|
1914
|
+
try {
|
|
1915
|
+
this.options.stateStore.save(state);
|
|
1916
|
+
return nextGatewayKey;
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
state.gatewayKey = previousGatewayKey;
|
|
1919
|
+
try {
|
|
1920
|
+
await this.options.keyDb.outboundApiKeysRevoke(created.id);
|
|
1921
|
+
} catch {
|
|
1922
|
+
}
|
|
1923
|
+
throw error;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
async isKeyUsable(state) {
|
|
1927
|
+
if (!state.gatewayKey) return false;
|
|
1928
|
+
const rows = await this.options.keyDb.outboundApiKeysList();
|
|
1929
|
+
return rows.some((row) => row.id === state.gatewayKey?.id && row.enabled && row.revokedAt === null && row.kind === "integration");
|
|
1930
|
+
}
|
|
1931
|
+
statusFor(client, state, keyUsable) {
|
|
1932
|
+
const record = state.clients[client];
|
|
1933
|
+
if (!record) return { client, status: "not-installed", configPath: this.defaultConfigPath(client) };
|
|
1934
|
+
const current = readOptional(record.configPath);
|
|
1935
|
+
if (current === null) {
|
|
1936
|
+
return {
|
|
1937
|
+
client,
|
|
1938
|
+
status: "configuration-missing",
|
|
1939
|
+
configPath: record.configPath,
|
|
1940
|
+
installedAt: record.installedAt,
|
|
1941
|
+
gatewayBaseUrl: record.gatewayBaseUrl
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
if (sha256(current) !== record.installedHash) {
|
|
1945
|
+
return {
|
|
1946
|
+
client,
|
|
1947
|
+
status: "configuration-drift",
|
|
1948
|
+
configPath: record.configPath,
|
|
1949
|
+
installedAt: record.installedAt,
|
|
1950
|
+
gatewayBaseUrl: record.gatewayBaseUrl
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
if (client === "codex") {
|
|
1954
|
+
if (!record.credentialFile) {
|
|
1955
|
+
return {
|
|
1956
|
+
client,
|
|
1957
|
+
status: "configuration-drift",
|
|
1958
|
+
configPath: record.configPath,
|
|
1959
|
+
installedAt: record.installedAt,
|
|
1960
|
+
gatewayBaseUrl: record.gatewayBaseUrl,
|
|
1961
|
+
message: "Codex integration uses a legacy authentication layout and must be repaired."
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
const credential = readOptional(record.credentialFile.path);
|
|
1965
|
+
if (credential === null) {
|
|
1966
|
+
return {
|
|
1967
|
+
client,
|
|
1968
|
+
status: "configuration-missing",
|
|
1969
|
+
configPath: record.configPath,
|
|
1970
|
+
installedAt: record.installedAt,
|
|
1971
|
+
gatewayBaseUrl: record.gatewayBaseUrl,
|
|
1972
|
+
message: "Codex auth.json is missing."
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (sha256(credential) !== record.credentialFile.installedHash) {
|
|
1976
|
+
return {
|
|
1977
|
+
client,
|
|
1978
|
+
status: "configuration-drift",
|
|
1979
|
+
configPath: record.configPath,
|
|
1980
|
+
installedAt: record.installedAt,
|
|
1981
|
+
gatewayBaseUrl: record.gatewayBaseUrl,
|
|
1982
|
+
message: "Codex auth.json changed after installation."
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
client,
|
|
1988
|
+
status: keyUsable ? "enabled" : "key-missing",
|
|
1989
|
+
configPath: record.configPath,
|
|
1990
|
+
installedAt: record.installedAt,
|
|
1991
|
+
gatewayBaseUrl: record.gatewayBaseUrl
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
defaultConfigPath(client) {
|
|
1995
|
+
return client === "codex" ? join2(this.homeDir, ".codex", "config.toml") : join2(this.homeDir, ".claude", "settings.json");
|
|
1996
|
+
}
|
|
1997
|
+
codexAuthPathForConfig(configPath) {
|
|
1998
|
+
return join2(dirname4(configPath), "auth.json");
|
|
1999
|
+
}
|
|
2000
|
+
renderInstalled(client, base, secret) {
|
|
2001
|
+
if (client === "claude") {
|
|
2002
|
+
return renderClaudeSettings(base, this.options.gatewayBaseUrl, secret);
|
|
2003
|
+
}
|
|
2004
|
+
return renderCodexConfig({
|
|
2005
|
+
existing: base,
|
|
2006
|
+
gatewayBaseUrl: this.options.gatewayBaseUrl
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
function readOptional(path2) {
|
|
2011
|
+
return existsSync4(path2) ? readFileSync5(path2, "utf8") : null;
|
|
2012
|
+
}
|
|
2013
|
+
function sha256(value) {
|
|
2014
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
2015
|
+
}
|
|
2016
|
+
function managedFileRecord(path2, original, installed) {
|
|
2017
|
+
const originalContent = original ?? "";
|
|
2018
|
+
return {
|
|
2019
|
+
path: path2,
|
|
2020
|
+
originalExisted: original !== null,
|
|
2021
|
+
originalContent,
|
|
2022
|
+
originalHash: sha256(originalContent),
|
|
2023
|
+
installedHash: sha256(installed)
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
function primaryManagedFile(record) {
|
|
2027
|
+
return {
|
|
2028
|
+
path: record.configPath,
|
|
2029
|
+
originalExisted: record.originalExisted,
|
|
2030
|
+
originalContent: record.originalContent,
|
|
2031
|
+
originalHash: record.originalHash,
|
|
2032
|
+
installedHash: record.installedHash
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
function managedFileDisposition(record, current) {
|
|
2036
|
+
if (current !== null && sha256(current) === record.installedHash) return "installed";
|
|
2037
|
+
const matchesOriginalExistence = record.originalExisted ? current !== null : current === null;
|
|
2038
|
+
if (matchesOriginalExistence && sha256(current ?? "") === record.originalHash) return "restored";
|
|
2039
|
+
return current === null ? "missing" : "drift";
|
|
2040
|
+
}
|
|
2041
|
+
function originalSnapshotForRepair(record, current) {
|
|
2042
|
+
const disposition = managedFileDisposition(record, current);
|
|
2043
|
+
if (disposition === "installed" || disposition === "restored") {
|
|
2044
|
+
return record.originalExisted ? record.originalContent : null;
|
|
2045
|
+
}
|
|
2046
|
+
return current;
|
|
2047
|
+
}
|
|
2048
|
+
function applyFileChangesWithRollback(changes) {
|
|
2049
|
+
if (changes.length === 0) return;
|
|
2050
|
+
const snapshots = changes.map((change) => ({ path: change.path, content: readOptional(change.path) }));
|
|
2051
|
+
try {
|
|
2052
|
+
for (const change of changes) writeOptional(change.path, change.content);
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
const rollbackFailures = [];
|
|
2055
|
+
for (const snapshot of [...snapshots].reverse()) {
|
|
2056
|
+
try {
|
|
2057
|
+
writeOptional(snapshot.path, snapshot.content);
|
|
2058
|
+
} catch {
|
|
2059
|
+
rollbackFailures.push(snapshot.path);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (rollbackFailures.length > 0) {
|
|
2063
|
+
throw new IntegrationConflictError(
|
|
2064
|
+
`CLI integration update failed and rollback could not restore: ${rollbackFailures.join(", ")}`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
throw error;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
function writeOptional(path2, content) {
|
|
2071
|
+
if (content !== null) {
|
|
2072
|
+
atomicWrite(path2, content);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
if (existsSync4(path2)) unlinkSync2(path2);
|
|
2076
|
+
}
|
|
2077
|
+
function assertLoopbackGatewayUrl(value) {
|
|
2078
|
+
let url;
|
|
2079
|
+
try {
|
|
2080
|
+
url = new URL(value);
|
|
2081
|
+
} catch {
|
|
2082
|
+
throw new Error("gatewayBaseUrl must be a valid loopback URL");
|
|
2083
|
+
}
|
|
2084
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
2085
|
+
const literalLoopback = host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
2086
|
+
if (url.protocol !== "http:" || !literalLoopback || url.username || url.password || url.search || url.hash) {
|
|
2087
|
+
throw new Error("native CLI integrations require an unauthenticated literal HTTP loopback gateway URL");
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
877
2091
|
// src/preset-catalog.ts
|
|
878
2092
|
import * as presetsModule from "@omnicross/contracts/provider-presets";
|
|
879
2093
|
function normalizeCatalogModule(m) {
|
|
@@ -894,14 +2108,13 @@ function getCatalog() {
|
|
|
894
2108
|
|
|
895
2109
|
// src/preset-map.ts
|
|
896
2110
|
var EXCLUSION_REASONS = {
|
|
897
|
-
"openai-response": "daemon rows have no openai-response format; the Responses API needs a transformer chain that a BYO daemon provider row cannot express.",
|
|
898
2111
|
"azure-openai": "Azure needs an apiVersion + a deployment-name-as-model URL template + an empty baseUrl; a daemon provider row cannot express that shape."
|
|
899
2112
|
};
|
|
900
2113
|
var FORMAT_MAP = {
|
|
901
2114
|
openai: "openai",
|
|
902
2115
|
anthropic: "anthropic",
|
|
903
2116
|
google: "gemini",
|
|
904
|
-
"openai-response":
|
|
2117
|
+
"openai-response": "openai-response",
|
|
905
2118
|
"azure-openai": null
|
|
906
2119
|
};
|
|
907
2120
|
function resolveFormat(raw) {
|
|
@@ -1108,6 +2321,67 @@ var VALID_PROVIDER_IDS = [
|
|
|
1108
2321
|
function asSubscriptionProviderId(id) {
|
|
1109
2322
|
return VALID_PROVIDER_IDS.includes(id) ? id : null;
|
|
1110
2323
|
}
|
|
2324
|
+
var ACCOUNT_PATCH_KEYS = /* @__PURE__ */ new Set(["label", "enabled", "priority", "group", "tags"]);
|
|
2325
|
+
function validateAccountMetadataPatch(body) {
|
|
2326
|
+
const keys = Object.keys(body);
|
|
2327
|
+
if (keys.length === 0 || keys.some((key) => !ACCOUNT_PATCH_KEYS.has(key))) return null;
|
|
2328
|
+
const patch = {};
|
|
2329
|
+
if ("label" in body) {
|
|
2330
|
+
if (typeof body["label"] !== "string" || body["label"].trim().length > 120) return null;
|
|
2331
|
+
patch.label = body["label"].trim();
|
|
2332
|
+
}
|
|
2333
|
+
if ("enabled" in body) {
|
|
2334
|
+
if (typeof body["enabled"] !== "boolean") return null;
|
|
2335
|
+
patch.enabled = body["enabled"];
|
|
2336
|
+
}
|
|
2337
|
+
if ("priority" in body) {
|
|
2338
|
+
const priority = body["priority"];
|
|
2339
|
+
if (typeof priority !== "number" || !Number.isFinite(priority) || priority < -1e4 || priority > 1e4) return null;
|
|
2340
|
+
patch.priority = priority;
|
|
2341
|
+
}
|
|
2342
|
+
if ("group" in body) {
|
|
2343
|
+
const group = body["group"];
|
|
2344
|
+
if (group !== null && typeof group !== "string") return null;
|
|
2345
|
+
const normalized = typeof group === "string" ? group.trim() : null;
|
|
2346
|
+
if (normalized !== null && normalized.length > 80) return null;
|
|
2347
|
+
patch.group = normalized || null;
|
|
2348
|
+
}
|
|
2349
|
+
if ("tags" in body) {
|
|
2350
|
+
const tags = body["tags"];
|
|
2351
|
+
if (!Array.isArray(tags) || tags.length > 20) return null;
|
|
2352
|
+
const normalized = tags.map((tag) => typeof tag === "string" ? tag.trim() : "");
|
|
2353
|
+
if (normalized.some((tag) => !tag || tag.length > 40)) return null;
|
|
2354
|
+
patch.tags = [...new Set(normalized)];
|
|
2355
|
+
}
|
|
2356
|
+
return patch;
|
|
2357
|
+
}
|
|
2358
|
+
function validateAccountBatchBody(body) {
|
|
2359
|
+
const action = body["action"];
|
|
2360
|
+
const rawAccounts = body["accounts"];
|
|
2361
|
+
if (!Array.isArray(rawAccounts) || rawAccounts.length < 1 || rawAccounts.length > 100) return null;
|
|
2362
|
+
if (action !== "enable" && action !== "disable" && action !== "set-group" && action !== "delete") return null;
|
|
2363
|
+
const refs = [];
|
|
2364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2365
|
+
for (const raw of rawAccounts) {
|
|
2366
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2367
|
+
const row = raw;
|
|
2368
|
+
const providerId = typeof row["providerId"] === "string" ? asSubscriptionProviderId(row["providerId"]) : null;
|
|
2369
|
+
const accountId = typeof row["accountId"] === "string" ? row["accountId"].trim() : "";
|
|
2370
|
+
if (!providerId || !accountId || accountId.length > 200) return null;
|
|
2371
|
+
const key = `${providerId}\0${accountId}`;
|
|
2372
|
+
if (seen.has(key)) return null;
|
|
2373
|
+
seen.add(key);
|
|
2374
|
+
refs.push({ providerId, accountId });
|
|
2375
|
+
}
|
|
2376
|
+
if (action === "set-group") {
|
|
2377
|
+
const group = body["group"];
|
|
2378
|
+
if (group !== null && typeof group !== "string") return null;
|
|
2379
|
+
const normalized = typeof group === "string" ? group.trim() : null;
|
|
2380
|
+
if (normalized !== null && normalized.length > 80) return null;
|
|
2381
|
+
return { refs, mutation: { action, group: normalized || null } };
|
|
2382
|
+
}
|
|
2383
|
+
return { refs, mutation: { action } };
|
|
2384
|
+
}
|
|
1111
2385
|
var CLAUDE_AUTH_METHODS = /* @__PURE__ */ new Set(["oauth", "setup_token", "manual"]);
|
|
1112
2386
|
var OAUTH_AUTH_METHODS = /* @__PURE__ */ new Set(["oauth", "manual"]);
|
|
1113
2387
|
var TOKEN_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -1319,9 +2593,9 @@ async function exchangeGemini(code, codeVerifier, exchangeFetch) {
|
|
|
1319
2593
|
|
|
1320
2594
|
// src/admin/cliLaunch.ts
|
|
1321
2595
|
import { exec, spawn } from "child_process";
|
|
1322
|
-
import { randomUUID } from "crypto";
|
|
1323
|
-
import { existsSync as
|
|
1324
|
-
import { delimiter, join as
|
|
2596
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2597
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2598
|
+
import { delimiter, join as join3 } from "path";
|
|
1325
2599
|
import {
|
|
1326
2600
|
buildChatCliLaunchConfig,
|
|
1327
2601
|
buildClaudeCliLaunchConfig,
|
|
@@ -1351,8 +2625,8 @@ function isLaunchCliId(id) {
|
|
|
1351
2625
|
function probeDefault(candidate) {
|
|
1352
2626
|
const segments = (process.env["PATH"] ?? "").split(delimiter).filter(Boolean);
|
|
1353
2627
|
for (const seg of segments) {
|
|
1354
|
-
const full =
|
|
1355
|
-
if (
|
|
2628
|
+
const full = join3(seg, candidate);
|
|
2629
|
+
if (existsSync5(full)) return full;
|
|
1356
2630
|
}
|
|
1357
2631
|
return null;
|
|
1358
2632
|
}
|
|
@@ -1439,10 +2713,10 @@ var sessions = /* @__PURE__ */ new Map();
|
|
|
1439
2713
|
function errBody(message) {
|
|
1440
2714
|
return { error: { type: "admin_api_error", message } };
|
|
1441
2715
|
}
|
|
1442
|
-
var defaultCommandRunner = (command) => new Promise((
|
|
2716
|
+
var defaultCommandRunner = (command) => new Promise((resolve2) => {
|
|
1443
2717
|
exec(command, { timeout: 18e4 }, (err5, _stdout, stderr) => {
|
|
1444
|
-
if (err5)
|
|
1445
|
-
else
|
|
2718
|
+
if (err5) resolve2({ ok: false, error: stderr.trim() || err5.message });
|
|
2719
|
+
else resolve2({ ok: true });
|
|
1446
2720
|
});
|
|
1447
2721
|
});
|
|
1448
2722
|
async function handleCliInstall(cli, runner = defaultCommandRunner) {
|
|
@@ -1504,7 +2778,7 @@ async function handleCliLaunch(cli, body, ctx) {
|
|
|
1504
2778
|
launch.onSessionEnd();
|
|
1505
2779
|
return { status: 500, body: errBody(err5 instanceof Error ? err5.message : "failed to open terminal") };
|
|
1506
2780
|
}
|
|
1507
|
-
const id =
|
|
2781
|
+
const id = randomUUID2();
|
|
1508
2782
|
sessions.set(id, {
|
|
1509
2783
|
id,
|
|
1510
2784
|
cli,
|
|
@@ -1517,12 +2791,12 @@ async function handleCliLaunch(cli, body, ctx) {
|
|
|
1517
2791
|
}
|
|
1518
2792
|
|
|
1519
2793
|
// src/admin/auditConfigBody.ts
|
|
1520
|
-
var
|
|
2794
|
+
var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1521
2795
|
function validateAuditSegment(patch) {
|
|
1522
2796
|
const errors = [];
|
|
1523
2797
|
const audit = patch.audit;
|
|
1524
2798
|
if (audit === void 0) return errors;
|
|
1525
|
-
if (!
|
|
2799
|
+
if (!isPlainObject2(audit)) {
|
|
1526
2800
|
errors.push("audit must be an object");
|
|
1527
2801
|
return errors;
|
|
1528
2802
|
}
|
|
@@ -1544,12 +2818,12 @@ function validateAuditSegment(patch) {
|
|
|
1544
2818
|
|
|
1545
2819
|
// src/admin/billingConfigBody.ts
|
|
1546
2820
|
var BILLING_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
1547
|
-
var
|
|
2821
|
+
var isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1548
2822
|
function validateBillingSegment(patch) {
|
|
1549
2823
|
const errors = [];
|
|
1550
2824
|
const billing = patch.billing;
|
|
1551
2825
|
if (billing === void 0) return errors;
|
|
1552
|
-
if (!
|
|
2826
|
+
if (!isPlainObject3(billing)) {
|
|
1553
2827
|
errors.push("billing must be an object");
|
|
1554
2828
|
return errors;
|
|
1555
2829
|
}
|
|
@@ -1685,6 +2959,96 @@ function parseKeyPolicyBody(body) {
|
|
|
1685
2959
|
return { ok: true, policy };
|
|
1686
2960
|
}
|
|
1687
2961
|
|
|
2962
|
+
// src/admin/gatewayBindingBody.ts
|
|
2963
|
+
var ENDPOINTS = /* @__PURE__ */ new Set(["chat", "responses", "messages", "gemini"]);
|
|
2964
|
+
var TARGET_KINDS = /* @__PURE__ */ new Set(["account", "account-group", "account-pool", "provider"]);
|
|
2965
|
+
var FALLBACKS = /* @__PURE__ */ new Set(["next", "fail", "global"]);
|
|
2966
|
+
function isRecord(value) {
|
|
2967
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2968
|
+
}
|
|
2969
|
+
function nonBlank(value) {
|
|
2970
|
+
return typeof value === "string" && value.trim() !== "";
|
|
2971
|
+
}
|
|
2972
|
+
function validateStringArray(value, path2, errors) {
|
|
2973
|
+
if (!Array.isArray(value) || value.some((entry) => !nonBlank(entry))) {
|
|
2974
|
+
errors.push(`${path2} must be an array of non-empty strings`);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
function validateGatewayBindingsSegment(patch) {
|
|
2978
|
+
if (!Object.prototype.hasOwnProperty.call(patch, "bindings")) return [];
|
|
2979
|
+
const raw = patch.bindings;
|
|
2980
|
+
if (!Array.isArray(raw)) return ["bindings must be an array"];
|
|
2981
|
+
if (raw.length > 1e3) return ["bindings cannot contain more than 1000 entries"];
|
|
2982
|
+
const errors = [];
|
|
2983
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2984
|
+
raw.forEach((entry, index) => {
|
|
2985
|
+
const path2 = `bindings[${index}]`;
|
|
2986
|
+
if (!isRecord(entry)) {
|
|
2987
|
+
errors.push(`${path2} must be an object`);
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
if (!nonBlank(entry.id)) errors.push(`${path2}.id is required`);
|
|
2991
|
+
else if (ids.has(entry.id.trim())) errors.push(`${path2}.id must be unique`);
|
|
2992
|
+
else ids.add(entry.id.trim());
|
|
2993
|
+
if (!nonBlank(entry.name)) errors.push(`${path2}.name is required`);
|
|
2994
|
+
if (typeof entry.enabled !== "boolean") errors.push(`${path2}.enabled must be boolean`);
|
|
2995
|
+
if (!ENDPOINTS.has(String(entry.endpoint))) errors.push(`${path2}.endpoint is invalid`);
|
|
2996
|
+
if (!FALLBACKS.has(String(entry.fallback))) {
|
|
2997
|
+
errors.push(`${path2}.fallback must be next or fail`);
|
|
2998
|
+
}
|
|
2999
|
+
if (entry.priority !== void 0 && (typeof entry.priority !== "number" || !Number.isInteger(entry.priority) || entry.priority < 0 || entry.priority > 1e4)) {
|
|
3000
|
+
errors.push(`${path2}.priority must be an integer from 0 to 10000`);
|
|
3001
|
+
}
|
|
3002
|
+
if (entry.apiKeyIds !== void 0) validateStringArray(entry.apiKeyIds, `${path2}.apiKeyIds`, errors);
|
|
3003
|
+
if (entry.keyScope !== void 0 && entry.keyScope !== "all" && entry.keyScope !== "selected") {
|
|
3004
|
+
errors.push(`${path2}.keyScope must be all or selected`);
|
|
3005
|
+
}
|
|
3006
|
+
if (entry.modelMode !== void 0 && entry.modelMode !== "passthrough" && entry.modelMode !== "mapped") {
|
|
3007
|
+
errors.push(`${path2}.modelMode must be passthrough or mapped`);
|
|
3008
|
+
}
|
|
3009
|
+
if (entry.modelMappings !== void 0) {
|
|
3010
|
+
if (!Array.isArray(entry.modelMappings)) {
|
|
3011
|
+
errors.push(`${path2}.modelMappings must be an array`);
|
|
3012
|
+
} else if (entry.modelMappings.length > 100) {
|
|
3013
|
+
errors.push(`${path2}.modelMappings cannot contain more than 100 entries`);
|
|
3014
|
+
} else if (entry.modelMappings.some(
|
|
3015
|
+
(mapping) => !isRecord(mapping) || !nonBlank(mapping.source) || !nonBlank(mapping.target)
|
|
3016
|
+
)) {
|
|
3017
|
+
errors.push(`${path2}.modelMappings must contain non-empty source and target strings`);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
if (!isRecord(entry.target) || !TARGET_KINDS.has(String(entry.target.kind))) {
|
|
3021
|
+
errors.push(`${path2}.target is invalid`);
|
|
3022
|
+
} else {
|
|
3023
|
+
if (!nonBlank(entry.target.providerId)) errors.push(`${path2}.target.providerId is required`);
|
|
3024
|
+
if (entry.target.kind === "account" && !nonBlank(entry.target.accountId)) {
|
|
3025
|
+
errors.push(`${path2}.target.accountId is required`);
|
|
3026
|
+
}
|
|
3027
|
+
if (entry.target.kind === "account-group" && !nonBlank(entry.target.group)) {
|
|
3028
|
+
errors.push(`${path2}.target.group is required`);
|
|
3029
|
+
}
|
|
3030
|
+
if (entry.target.kind === "provider" && entry.target.keyId !== void 0 && !nonBlank(entry.target.keyId)) {
|
|
3031
|
+
errors.push(`${path2}.target.keyId must be a non-empty string`);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
if (entry.modelMap !== void 0) {
|
|
3035
|
+
if (!isRecord(entry.modelMap) || Object.values(entry.modelMap).some((value) => typeof value !== "string")) {
|
|
3036
|
+
errors.push(`${path2}.modelMap must contain string values`);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
if (entry.models !== void 0) validateStringArray(entry.models, `${path2}.models`, errors);
|
|
3040
|
+
if (entry.backgroundModelIds !== void 0) {
|
|
3041
|
+
validateStringArray(entry.backgroundModelIds, `${path2}.backgroundModelIds`, errors);
|
|
3042
|
+
}
|
|
3043
|
+
for (const field of ["defaultModel", "backgroundModel"]) {
|
|
3044
|
+
if (entry[field] !== void 0 && typeof entry[field] !== "string") {
|
|
3045
|
+
errors.push(`${path2}.${field} must be a string`);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
return errors;
|
|
3050
|
+
}
|
|
3051
|
+
|
|
1688
3052
|
// src/admin/voucherAdmin.ts
|
|
1689
3053
|
import {
|
|
1690
3054
|
generateVoucherCode,
|
|
@@ -1702,15 +3066,15 @@ function writeErr(res, status, message) {
|
|
|
1702
3066
|
writeJson(res, status, { error: { type: "voucher_error", message } });
|
|
1703
3067
|
}
|
|
1704
3068
|
function readJsonBody2(req) {
|
|
1705
|
-
return new Promise((
|
|
3069
|
+
return new Promise((resolve2, reject) => {
|
|
1706
3070
|
const chunks = [];
|
|
1707
3071
|
req.on("data", (c) => chunks.push(c));
|
|
1708
3072
|
req.on("end", () => {
|
|
1709
3073
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1710
|
-
if (!raw.trim()) return
|
|
3074
|
+
if (!raw.trim()) return resolve2({});
|
|
1711
3075
|
try {
|
|
1712
3076
|
const parsed = JSON.parse(raw);
|
|
1713
|
-
|
|
3077
|
+
resolve2(parsed && typeof parsed === "object" ? parsed : {});
|
|
1714
3078
|
} catch {
|
|
1715
3079
|
reject(new Error("invalid-json"));
|
|
1716
3080
|
}
|
|
@@ -1803,12 +3167,12 @@ import {
|
|
|
1803
3167
|
WEBHOOK_EVENT_KINDS
|
|
1804
3168
|
} from "@omnicross/contracts/webhook-types";
|
|
1805
3169
|
var WEBHOOK_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
1806
|
-
var
|
|
3170
|
+
var isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1807
3171
|
function validateWebhookSegment(patch) {
|
|
1808
3172
|
const errors = [];
|
|
1809
3173
|
const webhook = patch.webhook;
|
|
1810
3174
|
if (webhook === void 0) return errors;
|
|
1811
|
-
if (!
|
|
3175
|
+
if (!isPlainObject4(webhook)) {
|
|
1812
3176
|
errors.push("webhook must be an object");
|
|
1813
3177
|
return errors;
|
|
1814
3178
|
}
|
|
@@ -1822,7 +3186,7 @@ function validateWebhookSegment(patch) {
|
|
|
1822
3186
|
}
|
|
1823
3187
|
const seenIds = /* @__PURE__ */ new Set();
|
|
1824
3188
|
for (const [i, raw] of (Array.isArray(destinations) ? destinations : []).entries()) {
|
|
1825
|
-
if (!
|
|
3189
|
+
if (!isPlainObject4(raw)) {
|
|
1826
3190
|
errors.push(`webhook.destinations[${i}] must be an object`);
|
|
1827
3191
|
continue;
|
|
1828
3192
|
}
|
|
@@ -1888,7 +3252,7 @@ function preserveWebhookSecrets(incoming, current) {
|
|
|
1888
3252
|
}
|
|
1889
3253
|
|
|
1890
3254
|
// src/audit/auditRuntime.ts
|
|
1891
|
-
import { join as
|
|
3255
|
+
import { join as join4 } from "path";
|
|
1892
3256
|
import { setAuditCaptureConfig, setAuditSink } from "@omnicross/core/pipeline/auditSink";
|
|
1893
3257
|
import { setUpstreamTracePath } from "@omnicross/core/pipeline/upstreamTrace";
|
|
1894
3258
|
var writer = null;
|
|
@@ -1909,7 +3273,7 @@ function applyAuditConfig(config) {
|
|
|
1909
3273
|
sweeper.configure(config);
|
|
1910
3274
|
sweeper.start();
|
|
1911
3275
|
}
|
|
1912
|
-
setUpstreamTracePath(config.captureBodies ?
|
|
3276
|
+
setUpstreamTracePath(config.captureBodies ? join4(auditDir, "upstream-trace.jsonl") : null);
|
|
1913
3277
|
} else {
|
|
1914
3278
|
setAuditCaptureConfig(null);
|
|
1915
3279
|
setAuditSink(null);
|
|
@@ -1967,7 +3331,7 @@ function resetBillingRuntimeForTests() {
|
|
|
1967
3331
|
}
|
|
1968
3332
|
|
|
1969
3333
|
// src/ports/account-multi.ts
|
|
1970
|
-
import { randomUUID as
|
|
3334
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1971
3335
|
var PROVIDER_KEYS = {
|
|
1972
3336
|
claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
|
|
1973
3337
|
codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
|
|
@@ -2033,7 +3397,7 @@ function migrateLazily(config) {
|
|
|
2033
3397
|
}
|
|
2034
3398
|
function addAccount(config, p, tokens, label) {
|
|
2035
3399
|
const accounts = [...getAccounts(config, p)];
|
|
2036
|
-
const id =
|
|
3400
|
+
const id = randomUUID3();
|
|
2037
3401
|
accounts.push({
|
|
2038
3402
|
id,
|
|
2039
3403
|
label: label ?? `Account ${accounts.length + 1}`,
|
|
@@ -2109,9 +3473,13 @@ function sanitizeAccounts(config, p) {
|
|
|
2109
3473
|
const activeId = getActiveId(config, p);
|
|
2110
3474
|
return accounts.map((a) => {
|
|
2111
3475
|
const t = a.tokens;
|
|
3476
|
+
const enabled = a.enabled !== false;
|
|
2112
3477
|
return {
|
|
2113
3478
|
id: a.id,
|
|
2114
3479
|
label: a.label,
|
|
3480
|
+
enabled,
|
|
3481
|
+
group: a.group?.trim() || p,
|
|
3482
|
+
tags: a.tags ?? [],
|
|
2115
3483
|
status: t.status ?? "unconfigured",
|
|
2116
3484
|
authMethod: t.authMethod,
|
|
2117
3485
|
subscriptionLevel: t.subscriptionLevel,
|
|
@@ -2120,6 +3488,8 @@ function sanitizeAccounts(config, p) {
|
|
|
2120
3488
|
isSetupToken: t.isSetupToken,
|
|
2121
3489
|
hasAccessToken: !!(t.accessToken || t.apiKey),
|
|
2122
3490
|
isActive: a.id === activeId,
|
|
3491
|
+
schedulable: enabled,
|
|
3492
|
+
errorMessage: sanitizeDiagnosticMessage(t.errorMessage),
|
|
2123
3493
|
// Scheduling metadata (subscription-account-scheduling): editable priority
|
|
2124
3494
|
// (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
|
|
2125
3495
|
priority: a.priority,
|
|
@@ -2134,6 +3504,54 @@ function sanitizeAccounts(config, p) {
|
|
|
2134
3504
|
};
|
|
2135
3505
|
});
|
|
2136
3506
|
}
|
|
3507
|
+
function sanitizeDiagnosticMessage(value) {
|
|
3508
|
+
if (!value) return void 0;
|
|
3509
|
+
const lower = value.toLowerCase();
|
|
3510
|
+
if (lower.includes("timeout") || lower.includes("timed out")) return "Credential operation timed out.";
|
|
3511
|
+
if (lower.includes("network") || lower.includes("fetch")) return "Credential network request failed.";
|
|
3512
|
+
if (lower.includes("401") || lower.includes("unauthorized") || lower.includes("revoked")) {
|
|
3513
|
+
return "Credential authorization was rejected.";
|
|
3514
|
+
}
|
|
3515
|
+
return "Credential operation failed.";
|
|
3516
|
+
}
|
|
3517
|
+
function patchAccountMetadata(config, p, id, patch) {
|
|
3518
|
+
const accounts = getAccounts(config, p);
|
|
3519
|
+
if (!accounts.some((account) => account.id === id)) return { ok: false };
|
|
3520
|
+
setAccounts(config, p, accounts.map((account) => {
|
|
3521
|
+
if (account.id !== id) return account;
|
|
3522
|
+
const next = { ...account };
|
|
3523
|
+
if (patch.label !== void 0) next.label = patch.label;
|
|
3524
|
+
if (patch.enabled !== void 0) next.enabled = patch.enabled;
|
|
3525
|
+
if (patch.priority !== void 0) next.priority = patch.priority;
|
|
3526
|
+
if (patch.group !== void 0) {
|
|
3527
|
+
if (patch.group === null || patch.group === "") delete next.group;
|
|
3528
|
+
else next.group = patch.group;
|
|
3529
|
+
}
|
|
3530
|
+
if (patch.tags !== void 0) next.tags = patch.tags;
|
|
3531
|
+
return next;
|
|
3532
|
+
}));
|
|
3533
|
+
return { ok: true };
|
|
3534
|
+
}
|
|
3535
|
+
function batchManageAccounts(config, refs, mutation) {
|
|
3536
|
+
for (const ref of refs) {
|
|
3537
|
+
if (!getAccounts(config, ref.providerId).some((account) => account.id === ref.accountId)) {
|
|
3538
|
+
return { ok: false, missing: ref };
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
for (const ref of refs) {
|
|
3542
|
+
if (mutation.action === "delete") {
|
|
3543
|
+
removeAccount(config, ref.providerId, ref.accountId);
|
|
3544
|
+
} else {
|
|
3545
|
+
patchAccountMetadata(
|
|
3546
|
+
config,
|
|
3547
|
+
ref.providerId,
|
|
3548
|
+
ref.accountId,
|
|
3549
|
+
mutation.action === "set-group" ? { group: mutation.group } : { enabled: mutation.action === "enable" }
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
return { ok: true, affected: refs.length };
|
|
3554
|
+
}
|
|
2137
3555
|
function renameAccount(config, p, id, label) {
|
|
2138
3556
|
const accounts = getAccounts(config, p);
|
|
2139
3557
|
if (!accounts.some((a) => a.id === id)) return { ok: false };
|
|
@@ -2471,9 +3889,9 @@ function parseFiniteInt(raw) {
|
|
|
2471
3889
|
const n = Number(raw);
|
|
2472
3890
|
return Number.isFinite(n) && Number.isInteger(n) ? n : null;
|
|
2473
3891
|
}
|
|
2474
|
-
function parseRange(
|
|
2475
|
-
const startTs = parseFiniteInt(
|
|
2476
|
-
const endTs = parseFiniteInt(
|
|
3892
|
+
function parseRange(query2) {
|
|
3893
|
+
const startTs = parseFiniteInt(query2.get("startTs"));
|
|
3894
|
+
const endTs = parseFiniteInt(query2.get("endTs"));
|
|
2477
3895
|
if (startTs === null || endTs === null) {
|
|
2478
3896
|
return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
|
|
2479
3897
|
}
|
|
@@ -2486,8 +3904,8 @@ var BUCKET_SPAN_MS = {
|
|
|
2486
3904
|
month: 28 * 864e5
|
|
2487
3905
|
};
|
|
2488
3906
|
var MAX_TIMESERIES_BUCKETS = 2e3;
|
|
2489
|
-
async function handleUsageGet(view,
|
|
2490
|
-
const range = parseRange(
|
|
3907
|
+
async function handleUsageGet(view, query2, deps) {
|
|
3908
|
+
const range = parseRange(query2);
|
|
2491
3909
|
if (!isRange(range)) return range;
|
|
2492
3910
|
switch (view) {
|
|
2493
3911
|
case "totals":
|
|
@@ -2495,7 +3913,7 @@ async function handleUsageGet(view, query, deps) {
|
|
|
2495
3913
|
case "by-model":
|
|
2496
3914
|
return { status: 200, body: await deps.usageRecorder.getByModel(range) };
|
|
2497
3915
|
case "timeseries": {
|
|
2498
|
-
const bucket =
|
|
3916
|
+
const bucket = query2.get("bucket");
|
|
2499
3917
|
if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
|
|
2500
3918
|
return err4(400, "bucket must be one of 'hour', 'day', 'month'");
|
|
2501
3919
|
}
|
|
@@ -2581,9 +3999,9 @@ async function handlePricingUpsert(body, deps) {
|
|
|
2581
3999
|
const entry = await deps.pricingEngine.upsertManual(input);
|
|
2582
4000
|
return { status: 200, body: { entry } };
|
|
2583
4001
|
}
|
|
2584
|
-
async function handlePricingDelete(
|
|
2585
|
-
const providerId =
|
|
2586
|
-
const modelId =
|
|
4002
|
+
async function handlePricingDelete(query2, deps) {
|
|
4003
|
+
const providerId = query2.get("providerId")?.trim() ?? "";
|
|
4004
|
+
const modelId = query2.get("modelId")?.trim() ?? "";
|
|
2587
4005
|
if (!providerId || !modelId) {
|
|
2588
4006
|
return err4(400, "delete requires providerId and modelId query params");
|
|
2589
4007
|
}
|
|
@@ -2600,7 +4018,8 @@ async function handlePricingFetchLatest(deps) {
|
|
|
2600
4018
|
appliedCount: result.applied.length,
|
|
2601
4019
|
conflicts: result.conflicts,
|
|
2602
4020
|
fetchedAt: result.fetchedAt,
|
|
2603
|
-
sourceUrl: result.sourceUrl
|
|
4021
|
+
sourceUrl: result.sourceUrl,
|
|
4022
|
+
sources: result.sources
|
|
2604
4023
|
}
|
|
2605
4024
|
};
|
|
2606
4025
|
} catch (e) {
|
|
@@ -2636,24 +4055,92 @@ async function handlePricingResolveConflicts(body, deps) {
|
|
|
2636
4055
|
if (incoming.providerId !== providerId || incoming.modelId !== modelId) {
|
|
2637
4056
|
return err4(400, "resolution providerId/modelId must match the echoed incoming entry");
|
|
2638
4057
|
}
|
|
2639
|
-
const key = `${providerId}::${modelId}`;
|
|
2640
|
-
if (action === "overwrite" && !userEditedKeys.has(key)) {
|
|
2641
|
-
staleCount += 1;
|
|
2642
|
-
continue;
|
|
4058
|
+
const key = `${providerId}::${modelId}`;
|
|
4059
|
+
if (action === "overwrite" && !userEditedKeys.has(key)) {
|
|
4060
|
+
staleCount += 1;
|
|
4061
|
+
continue;
|
|
4062
|
+
}
|
|
4063
|
+
decisions.push({ providerId, modelId, action });
|
|
4064
|
+
pendingIncoming.set(key, incoming);
|
|
4065
|
+
}
|
|
4066
|
+
const resolution = await deps.pricingEngine.resolveConflicts(decisions, pendingIncoming);
|
|
4067
|
+
return { status: 200, body: { ...resolution, staleCount } };
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
// src/admin/accountAllowanceApi.ts
|
|
4071
|
+
function writeJson2(res, status, body) {
|
|
4072
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
4073
|
+
res.end(JSON.stringify(body));
|
|
4074
|
+
}
|
|
4075
|
+
function writeError(res, status, message) {
|
|
4076
|
+
writeJson2(res, status, { error: { type: "account_allowance_error", message } });
|
|
4077
|
+
}
|
|
4078
|
+
function readJson(req) {
|
|
4079
|
+
return new Promise((resolve2, reject) => {
|
|
4080
|
+
const chunks = [];
|
|
4081
|
+
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
4082
|
+
req.on("end", () => {
|
|
4083
|
+
try {
|
|
4084
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
4085
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
4086
|
+
resolve2(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
|
|
4087
|
+
} catch (error) {
|
|
4088
|
+
reject(error);
|
|
4089
|
+
}
|
|
4090
|
+
});
|
|
4091
|
+
req.on("error", reject);
|
|
4092
|
+
});
|
|
4093
|
+
}
|
|
4094
|
+
function query(req) {
|
|
4095
|
+
const raw = req.url ?? "";
|
|
4096
|
+
const index = raw.indexOf("?");
|
|
4097
|
+
return new URLSearchParams(index >= 0 ? raw.slice(index + 1) : "");
|
|
4098
|
+
}
|
|
4099
|
+
function allowanceProvider(value) {
|
|
4100
|
+
if (!value) return void 0;
|
|
4101
|
+
return value === "claude" || value === "codex" ? value : null;
|
|
4102
|
+
}
|
|
4103
|
+
async function handleAccountAllowanceApi(req, res, method, rest, service) {
|
|
4104
|
+
if (!service) return writeError(res, 501, "account allowance service is not available");
|
|
4105
|
+
if (method === "GET" && rest.length === 1 && rest[0] === "scheduling") {
|
|
4106
|
+
if (!service.getSchedulingStatus) {
|
|
4107
|
+
return writeError(res, 501, "allowance scheduling diagnostics are not available");
|
|
4108
|
+
}
|
|
4109
|
+
return writeJson2(res, 200, { scheduling: service.getSchedulingStatus() });
|
|
4110
|
+
}
|
|
4111
|
+
if (method === "GET") {
|
|
4112
|
+
const params = query(req);
|
|
4113
|
+
const pathProvider = rest.length >= 2 ? rest[0] : null;
|
|
4114
|
+
const providerId = allowanceProvider(pathProvider ?? params.get("providerId") ?? params.get("provider"));
|
|
4115
|
+
if (providerId === null) return writeError(res, 400, "providerId must be claude or codex");
|
|
4116
|
+
const accountId = rest.length >= 2 ? rest[1] : params.get("accountId") ?? void 0;
|
|
4117
|
+
const allowances = await service.list({ providerId, accountId });
|
|
4118
|
+
return writeJson2(res, 200, { allowances });
|
|
4119
|
+
}
|
|
4120
|
+
if (method === "POST" && rest[0] === "refresh") {
|
|
4121
|
+
const body = await readJson(req);
|
|
4122
|
+
const requestedProvider = allowanceProvider(
|
|
4123
|
+
typeof body["providerId"] === "string" ? body["providerId"] : "claude"
|
|
4124
|
+
);
|
|
4125
|
+
if (requestedProvider !== "claude") {
|
|
4126
|
+
return writeError(res, 400, "only Claude allowances support explicit refresh");
|
|
2643
4127
|
}
|
|
2644
|
-
|
|
2645
|
-
|
|
4128
|
+
const accountId = typeof body["accountId"] === "string" && body["accountId"].trim() ? body["accountId"].trim() : void 0;
|
|
4129
|
+
const allowances = await service.refreshClaude(accountId);
|
|
4130
|
+
if (accountId && allowances.length === 0) {
|
|
4131
|
+
return writeError(res, 404, `Claude account '${accountId}' not found`);
|
|
4132
|
+
}
|
|
4133
|
+
return writeJson2(res, 200, { allowances });
|
|
2646
4134
|
}
|
|
2647
|
-
|
|
2648
|
-
return { status: 200, body: { ...resolution, staleCount } };
|
|
4135
|
+
return writeError(res, 405, `method ${method} not allowed on account allowances`);
|
|
2649
4136
|
}
|
|
2650
4137
|
|
|
2651
4138
|
// src/admin/adminApi.ts
|
|
2652
4139
|
function readBody(req) {
|
|
2653
|
-
return new Promise((
|
|
4140
|
+
return new Promise((resolve2, reject) => {
|
|
2654
4141
|
const chunks = [];
|
|
2655
4142
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
2656
|
-
req.on("end", () =>
|
|
4143
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
2657
4144
|
req.on("error", reject);
|
|
2658
4145
|
});
|
|
2659
4146
|
}
|
|
@@ -2667,12 +4154,12 @@ async function readJsonBody3(req) {
|
|
|
2667
4154
|
return {};
|
|
2668
4155
|
}
|
|
2669
4156
|
}
|
|
2670
|
-
function
|
|
4157
|
+
function writeJson3(res, status, body) {
|
|
2671
4158
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
2672
4159
|
res.end(JSON.stringify(body));
|
|
2673
4160
|
}
|
|
2674
4161
|
function writeJsonError(res, status, message) {
|
|
2675
|
-
|
|
4162
|
+
writeJson3(res, status, { error: { type: "admin_api_error", message } });
|
|
2676
4163
|
}
|
|
2677
4164
|
function maskProviderApiKey(apiKey) {
|
|
2678
4165
|
if (!apiKey) return "";
|
|
@@ -2689,6 +4176,9 @@ function toKeyInfo(row) {
|
|
|
2689
4176
|
createdAt: row.createdAt,
|
|
2690
4177
|
lastUsedAt: row.lastUsedAt,
|
|
2691
4178
|
revoked: row.revokedAt !== null,
|
|
4179
|
+
kind: row.kind,
|
|
4180
|
+
allowedEndpoints: row.allowedEndpoints,
|
|
4181
|
+
loopbackOnly: row.loopbackOnly,
|
|
2692
4182
|
maxConcurrency: row.maxConcurrency,
|
|
2693
4183
|
// Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
|
|
2694
4184
|
// the UI reads them to render + pre-fill the policy editor.
|
|
@@ -2774,6 +4264,8 @@ async function handleAdminApi(req, res, path2, deps) {
|
|
|
2774
4264
|
return await handleAccounts(req, res, method, rest, deps);
|
|
2775
4265
|
case "cli":
|
|
2776
4266
|
return await handleCli(req, res, method, rest, deps);
|
|
4267
|
+
case "integrations":
|
|
4268
|
+
return await handleIntegrations(req, res, method, rest, deps);
|
|
2777
4269
|
case "status":
|
|
2778
4270
|
return await handleStatus(res, method, deps);
|
|
2779
4271
|
case "playground":
|
|
@@ -2801,7 +4293,7 @@ function requestQuery(req) {
|
|
|
2801
4293
|
return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
|
|
2802
4294
|
}
|
|
2803
4295
|
function writeResult(res, result) {
|
|
2804
|
-
|
|
4296
|
+
writeJson3(res, result.status, result.body);
|
|
2805
4297
|
}
|
|
2806
4298
|
async function handleUsage(req, res, method, rest, deps) {
|
|
2807
4299
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
|
|
@@ -2810,7 +4302,7 @@ async function handleUsage(req, res, method, rest, deps) {
|
|
|
2810
4302
|
async function handleDashboardRoute(res, method, deps) {
|
|
2811
4303
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
|
|
2812
4304
|
const result = await handleDashboard(deps);
|
|
2813
|
-
return
|
|
4305
|
+
return writeJson3(res, result.status, result.body);
|
|
2814
4306
|
}
|
|
2815
4307
|
async function handlePricing(req, res, method, rest, deps) {
|
|
2816
4308
|
if (rest.length === 0) {
|
|
@@ -2843,13 +4335,13 @@ async function handleMigrationExport(req, res, method, deps) {
|
|
|
2843
4335
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
|
|
2844
4336
|
const body = await readJsonBody3(req);
|
|
2845
4337
|
const result = await handleExport(body, migrationDeps(deps));
|
|
2846
|
-
return
|
|
4338
|
+
return writeJson3(res, result.status, result.body);
|
|
2847
4339
|
}
|
|
2848
4340
|
async function handleMigrationImport(req, res, method, deps) {
|
|
2849
4341
|
if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
|
|
2850
4342
|
const body = await readJsonBody3(req);
|
|
2851
4343
|
const result = await handleImport(body, migrationDeps(deps));
|
|
2852
|
-
return
|
|
4344
|
+
return writeJson3(res, result.status, result.body);
|
|
2853
4345
|
}
|
|
2854
4346
|
async function handleProviders(req, res, method, rest, deps) {
|
|
2855
4347
|
const cfg = loadConfig(deps.configPath);
|
|
@@ -2880,10 +4372,10 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2880
4372
|
if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
|
|
2881
4373
|
const row = cfg.providers.find((p) => p.id === rest[0]);
|
|
2882
4374
|
if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
|
|
2883
|
-
return
|
|
4375
|
+
return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
|
|
2884
4376
|
}
|
|
2885
4377
|
if (method === "GET") {
|
|
2886
|
-
return
|
|
4378
|
+
return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
|
|
2887
4379
|
}
|
|
2888
4380
|
if (method === "POST") {
|
|
2889
4381
|
const body = await readJsonBody3(req);
|
|
@@ -2894,7 +4386,7 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2894
4386
|
}
|
|
2895
4387
|
cfg.providers.push(provider);
|
|
2896
4388
|
persistProviders(cfg, deps);
|
|
2897
|
-
return
|
|
4389
|
+
return writeJson3(res, 201, { provider: toProviderView(provider) });
|
|
2898
4390
|
}
|
|
2899
4391
|
const id = rest[0];
|
|
2900
4392
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -2907,12 +4399,12 @@ async function handleProviders(req, res, method, rest, deps) {
|
|
|
2907
4399
|
if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
|
|
2908
4400
|
cfg.providers[idx] = updated;
|
|
2909
4401
|
persistProviders(cfg, deps);
|
|
2910
|
-
return
|
|
4402
|
+
return writeJson3(res, 200, { provider: toProviderView(updated) });
|
|
2911
4403
|
}
|
|
2912
4404
|
if (method === "DELETE") {
|
|
2913
4405
|
cfg.providers.splice(idx, 1);
|
|
2914
4406
|
persistProviders(cfg, deps);
|
|
2915
|
-
return
|
|
4407
|
+
return writeJson3(res, 200, { ok: true });
|
|
2916
4408
|
}
|
|
2917
4409
|
return writeJsonError(res, 405, `method ${method} not allowed on providers`);
|
|
2918
4410
|
}
|
|
@@ -2945,14 +4437,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
|
|
|
2945
4437
|
}
|
|
2946
4438
|
cfg.providers = reordered;
|
|
2947
4439
|
persistProviders(cfg, deps);
|
|
2948
|
-
return
|
|
4440
|
+
return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
|
|
2949
4441
|
}
|
|
2950
4442
|
async function handleDiscoverModels(res, id, cfg) {
|
|
2951
4443
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
2952
4444
|
const row = cfg.providers.find((p) => p.id === id);
|
|
2953
4445
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
2954
|
-
if (row.apiFormat !== "openai") {
|
|
2955
|
-
return
|
|
4446
|
+
if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
|
|
4447
|
+
return writeJson3(res, 200, { models: [], unsupportedFormat: true });
|
|
2956
4448
|
}
|
|
2957
4449
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
2958
4450
|
const base = row.baseUrl.replace(/\/+$/, "");
|
|
@@ -2960,7 +4452,7 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
2960
4452
|
try {
|
|
2961
4453
|
const headers = { Accept: "application/json" };
|
|
2962
4454
|
if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
2963
|
-
const response = await
|
|
4455
|
+
const response = await fetchUpstream2(url, { method: "GET", headers }, { providerId: "byo" });
|
|
2964
4456
|
if (!response.ok) {
|
|
2965
4457
|
const text = await response.text().catch(() => "");
|
|
2966
4458
|
let message = text.slice(0, 300);
|
|
@@ -2969,17 +4461,17 @@ async function handleDiscoverModels(res, id, cfg) {
|
|
|
2969
4461
|
message = parsed?.error?.message || parsed?.message || message;
|
|
2970
4462
|
} catch {
|
|
2971
4463
|
}
|
|
2972
|
-
return
|
|
4464
|
+
return writeJson3(res, 200, {
|
|
2973
4465
|
models: [],
|
|
2974
4466
|
error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
|
|
2975
4467
|
});
|
|
2976
4468
|
}
|
|
2977
4469
|
const data = await response.json();
|
|
2978
4470
|
const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
|
|
2979
|
-
return
|
|
4471
|
+
return writeJson3(res, 200, { models });
|
|
2980
4472
|
} catch (err5) {
|
|
2981
4473
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
2982
|
-
return
|
|
4474
|
+
return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
|
|
2983
4475
|
}
|
|
2984
4476
|
}
|
|
2985
4477
|
async function handleTestModel(req, res, id, cfg) {
|
|
@@ -2990,13 +4482,13 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
2990
4482
|
const model = typeof body["model"] === "string" ? body["model"].trim() : "";
|
|
2991
4483
|
if (!model) return writeJsonError(res, 400, "test requires a { model } string");
|
|
2992
4484
|
if (row.apiFormat === "gemini") {
|
|
2993
|
-
return
|
|
4485
|
+
return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
|
|
2994
4486
|
}
|
|
2995
4487
|
const resolvedKey = resolveEnvKey(row.apiKey);
|
|
2996
4488
|
if (!resolvedKey) {
|
|
2997
|
-
return
|
|
4489
|
+
return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
|
|
2998
4490
|
}
|
|
2999
|
-
|
|
4491
|
+
let url = row.baseUrl.replace(/\/+$/, "");
|
|
3000
4492
|
const prompt = "Reply with the single word: OK.";
|
|
3001
4493
|
const headers = { "Content-Type": "application/json" };
|
|
3002
4494
|
let payload;
|
|
@@ -3004,6 +4496,10 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3004
4496
|
headers["x-api-key"] = resolvedKey;
|
|
3005
4497
|
headers["anthropic-version"] = "2023-06-01";
|
|
3006
4498
|
payload = { model, max_tokens: 16, messages: [{ role: "user", content: prompt }] };
|
|
4499
|
+
} else if (row.apiFormat === "openai-response") {
|
|
4500
|
+
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
4501
|
+
if (!/\/responses$/.test(url)) url = `${url}/v1/responses`;
|
|
4502
|
+
payload = { model, max_output_tokens: 16, stream: false, input: prompt };
|
|
3007
4503
|
} else {
|
|
3008
4504
|
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
3009
4505
|
payload = {
|
|
@@ -3015,7 +4511,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3015
4511
|
}
|
|
3016
4512
|
const startedAt = Date.now();
|
|
3017
4513
|
try {
|
|
3018
|
-
const response = await
|
|
4514
|
+
const response = await fetchUpstream2(
|
|
3019
4515
|
url,
|
|
3020
4516
|
{ method: "POST", headers, body: JSON.stringify(payload) },
|
|
3021
4517
|
{ providerId: "byo" }
|
|
@@ -3029,9 +4525,9 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3029
4525
|
message = parsed?.error?.message || parsed?.message || message;
|
|
3030
4526
|
} catch {
|
|
3031
4527
|
}
|
|
3032
|
-
return
|
|
4528
|
+
return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
|
|
3033
4529
|
}
|
|
3034
|
-
return
|
|
4530
|
+
return writeJson3(res, 200, {
|
|
3035
4531
|
ok: true,
|
|
3036
4532
|
status: response.status,
|
|
3037
4533
|
latencyMs,
|
|
@@ -3039,7 +4535,7 @@ async function handleTestModel(req, res, id, cfg) {
|
|
|
3039
4535
|
});
|
|
3040
4536
|
} catch (err5) {
|
|
3041
4537
|
const message = err5 instanceof Error ? err5.message : String(err5);
|
|
3042
|
-
return
|
|
4538
|
+
return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
|
|
3043
4539
|
}
|
|
3044
4540
|
}
|
|
3045
4541
|
function extractSampleText(text, apiFormat) {
|
|
@@ -3080,7 +4576,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
|
|
|
3080
4576
|
const row = cfg.providers.find((p) => p.id === id);
|
|
3081
4577
|
if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
|
|
3082
4578
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3083
|
-
return
|
|
4579
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3084
4580
|
}
|
|
3085
4581
|
function parsePoolKeyInput(body, existing) {
|
|
3086
4582
|
const out = {};
|
|
@@ -3111,7 +4607,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
|
|
|
3111
4607
|
row.apiKeys = [...row.apiKeys ?? [], entry];
|
|
3112
4608
|
persistProviders(cfg, deps);
|
|
3113
4609
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3114
|
-
return
|
|
4610
|
+
return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3115
4611
|
}
|
|
3116
4612
|
async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
3117
4613
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3131,7 +4627,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
3131
4627
|
row.apiKeys[keyIdx] = entry;
|
|
3132
4628
|
persistProviders(cfg, deps);
|
|
3133
4629
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3134
|
-
return
|
|
4630
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3135
4631
|
}
|
|
3136
4632
|
async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
3137
4633
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3145,7 +4641,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
|
|
|
3145
4641
|
if (row.apiKeys.length === 0) row.apiKeys = void 0;
|
|
3146
4642
|
persistProviders(cfg, deps);
|
|
3147
4643
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3148
|
-
return
|
|
4644
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3149
4645
|
}
|
|
3150
4646
|
async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
3151
4647
|
if (!id) return writeJsonError(res, 400, "provider id required in path");
|
|
@@ -3159,7 +4655,7 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
|
|
|
3159
4655
|
row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
|
|
3160
4656
|
persistProviders(cfg, deps);
|
|
3161
4657
|
const cooldown = await deps.apiKeyPool.getKeyHealth(id);
|
|
3162
|
-
return
|
|
4658
|
+
return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
|
|
3163
4659
|
}
|
|
3164
4660
|
function parseApiKeysInput(raw, existing) {
|
|
3165
4661
|
if (!Array.isArray(raw)) return existing;
|
|
@@ -3277,7 +4773,9 @@ function parseProviderInput(body, existing) {
|
|
|
3277
4773
|
const baseUrl = body["baseUrl"];
|
|
3278
4774
|
if (!id) return null;
|
|
3279
4775
|
const name = typeof body["name"] === "string" && body["name"].length > 0 ? body["name"] : body["name"] === null ? void 0 : existing?.name;
|
|
3280
|
-
if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini")
|
|
4776
|
+
if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini" && apiFormat !== "openai-response") {
|
|
4777
|
+
return null;
|
|
4778
|
+
}
|
|
3281
4779
|
if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
|
|
3282
4780
|
const rawKey = body["apiKey"];
|
|
3283
4781
|
let apiKey = typeof rawKey === "string" && rawKey.length > 0 ? rawKey : existing?.apiKey ?? "";
|
|
@@ -3299,10 +4797,11 @@ function parseProviderInput(body, existing) {
|
|
|
3299
4797
|
apiKey = mode.apiKey;
|
|
3300
4798
|
}
|
|
3301
4799
|
}
|
|
4800
|
+
const migrated = migrateFormatAxis(apiFormat, transformer);
|
|
3302
4801
|
return {
|
|
3303
4802
|
id,
|
|
3304
4803
|
name,
|
|
3305
|
-
apiFormat,
|
|
4804
|
+
apiFormat: migrated.apiFormat,
|
|
3306
4805
|
baseUrl: baseUrl.trim(),
|
|
3307
4806
|
apiKey,
|
|
3308
4807
|
models,
|
|
@@ -3313,7 +4812,7 @@ function parseProviderInput(body, existing) {
|
|
|
3313
4812
|
apiVersion,
|
|
3314
4813
|
maxConcurrency,
|
|
3315
4814
|
modelsEndpoint,
|
|
3316
|
-
transformer,
|
|
4815
|
+
transformer: migrated.transformer,
|
|
3317
4816
|
codingPlan,
|
|
3318
4817
|
apiModes,
|
|
3319
4818
|
selectedApiModeId
|
|
@@ -3330,13 +4829,13 @@ function handlePresets(res, method) {
|
|
|
3330
4829
|
baseUrl: p.baseUrl,
|
|
3331
4830
|
models: p.models
|
|
3332
4831
|
}));
|
|
3333
|
-
return
|
|
4832
|
+
return writeJson3(res, 200, { presets, excluded });
|
|
3334
4833
|
}
|
|
3335
4834
|
async function handleKeys(req, res, method, rest, deps) {
|
|
3336
4835
|
if (method === "GET" && rest.length === 0) {
|
|
3337
4836
|
const rows = await deps.keyDb.outboundApiKeysList();
|
|
3338
4837
|
const reader = deps.keySpendReader;
|
|
3339
|
-
if (!reader) return
|
|
4838
|
+
if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
|
|
3340
4839
|
const now = Date.now();
|
|
3341
4840
|
const keys = await Promise.all(
|
|
3342
4841
|
rows.map(async (row) => {
|
|
@@ -3348,13 +4847,13 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3348
4847
|
return info;
|
|
3349
4848
|
})
|
|
3350
4849
|
);
|
|
3351
|
-
return
|
|
4850
|
+
return writeJson3(res, 200, { keys });
|
|
3352
4851
|
}
|
|
3353
4852
|
if (method === "POST" && rest.length === 0) {
|
|
3354
4853
|
const body = await readJsonBody3(req);
|
|
3355
4854
|
const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
|
|
3356
4855
|
const created = await createNamedKey(deps.keyDb, name);
|
|
3357
|
-
return
|
|
4856
|
+
return writeJson3(res, 201, {
|
|
3358
4857
|
id: created.id,
|
|
3359
4858
|
name: created.name,
|
|
3360
4859
|
keyPrefix: created.keyPrefix,
|
|
@@ -3366,13 +4865,13 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3366
4865
|
const action = rest[1];
|
|
3367
4866
|
if (method === "POST" && id && action === "revoke") {
|
|
3368
4867
|
const ok = await deps.keyDb.outboundApiKeysRevoke(id);
|
|
3369
|
-
return
|
|
4868
|
+
return writeJson3(res, ok ? 200 : 404, { ok });
|
|
3370
4869
|
}
|
|
3371
4870
|
if (method === "POST" && id && action === "enabled") {
|
|
3372
4871
|
const body = await readJsonBody3(req);
|
|
3373
4872
|
const enabled = body["enabled"] === true;
|
|
3374
4873
|
const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
|
|
3375
|
-
return
|
|
4874
|
+
return writeJson3(res, ok ? 200 : 404, { ok, enabled });
|
|
3376
4875
|
}
|
|
3377
4876
|
if (method === "POST" && id && action === "max-concurrency") {
|
|
3378
4877
|
const body = await readJsonBody3(req);
|
|
@@ -3390,14 +4889,14 @@ async function handleKeys(req, res, method, rest, deps) {
|
|
|
3390
4889
|
);
|
|
3391
4890
|
}
|
|
3392
4891
|
const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
|
|
3393
|
-
return
|
|
4892
|
+
return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
|
|
3394
4893
|
}
|
|
3395
4894
|
if (method === "POST" && id && action === "policy") {
|
|
3396
4895
|
const body = await readJsonBody3(req);
|
|
3397
4896
|
const parsed = parseKeyPolicyBody(body);
|
|
3398
4897
|
if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
|
|
3399
4898
|
const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
|
|
3400
|
-
return
|
|
4899
|
+
return writeJson3(res, ok ? 200 : 404, { ok });
|
|
3401
4900
|
}
|
|
3402
4901
|
return writeJsonError(res, 405, `method ${method} not allowed on keys`);
|
|
3403
4902
|
}
|
|
@@ -3408,10 +4907,10 @@ function validateQueueSegments(patch) {
|
|
|
3408
4907
|
errors.push(`${label} must be a number ${min}..${max}`);
|
|
3409
4908
|
}
|
|
3410
4909
|
};
|
|
3411
|
-
const
|
|
4910
|
+
const isPlainObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3412
4911
|
const umq = patch.userMessageQueue;
|
|
3413
4912
|
if (umq !== void 0) {
|
|
3414
|
-
if (!
|
|
4913
|
+
if (!isPlainObject5(umq)) {
|
|
3415
4914
|
errors.push("userMessageQueue must be an object");
|
|
3416
4915
|
} else {
|
|
3417
4916
|
if (typeof umq.enabled !== "boolean") {
|
|
@@ -3423,7 +4922,7 @@ function validateQueueSegments(patch) {
|
|
|
3423
4922
|
}
|
|
3424
4923
|
const cq = patch.concurrencyQueue;
|
|
3425
4924
|
if (cq !== void 0) {
|
|
3426
|
-
if (!
|
|
4925
|
+
if (!isPlainObject5(cq)) {
|
|
3427
4926
|
errors.push("concurrencyQueue must be an object");
|
|
3428
4927
|
} else {
|
|
3429
4928
|
checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
|
|
@@ -3433,7 +4932,7 @@ function validateQueueSegments(patch) {
|
|
|
3433
4932
|
}
|
|
3434
4933
|
const ah = patch.accountHealth;
|
|
3435
4934
|
if (ah !== void 0) {
|
|
3436
|
-
if (!
|
|
4935
|
+
if (!isPlainObject5(ah)) {
|
|
3437
4936
|
errors.push("accountHealth must be an object");
|
|
3438
4937
|
} else {
|
|
3439
4938
|
if (typeof ah.overloadCooldownEnabled !== "boolean") {
|
|
@@ -3444,6 +4943,31 @@ function validateQueueSegments(patch) {
|
|
|
3444
4943
|
}
|
|
3445
4944
|
return errors;
|
|
3446
4945
|
}
|
|
4946
|
+
function validateAllowanceSchedulingSegment(patch) {
|
|
4947
|
+
const value = patch.allowanceScheduling;
|
|
4948
|
+
if (value === void 0) return [];
|
|
4949
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4950
|
+
return ["allowanceScheduling must be an object"];
|
|
4951
|
+
}
|
|
4952
|
+
const allowance = value;
|
|
4953
|
+
const errors = [];
|
|
4954
|
+
const checkNumber = (field, min, max) => {
|
|
4955
|
+
const candidate = allowance[field];
|
|
4956
|
+
if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate < min || candidate > max) {
|
|
4957
|
+
errors.push(`allowanceScheduling.${field} must be a number ${min}..${max}`);
|
|
4958
|
+
}
|
|
4959
|
+
};
|
|
4960
|
+
if (typeof allowance.enabled !== "boolean") {
|
|
4961
|
+
errors.push("allowanceScheduling.enabled must be a boolean");
|
|
4962
|
+
}
|
|
4963
|
+
checkNumber("demoteAtPercent", 0, 100);
|
|
4964
|
+
checkNumber("pauseAtPercent", 0, 100);
|
|
4965
|
+
checkNumber("priorityPenalty", 1, 1e3);
|
|
4966
|
+
if (typeof allowance.demoteAtPercent === "number" && typeof allowance.pauseAtPercent === "number" && allowance.pauseAtPercent < allowance.demoteAtPercent) {
|
|
4967
|
+
errors.push("allowanceScheduling.pauseAtPercent must be >= demoteAtPercent");
|
|
4968
|
+
}
|
|
4969
|
+
return errors;
|
|
4970
|
+
}
|
|
3447
4971
|
async function handleServer(req, res, method, deps) {
|
|
3448
4972
|
if (method === "GET") {
|
|
3449
4973
|
const config = await loadServerConfig2(deps.settingsStore);
|
|
@@ -3451,7 +4975,7 @@ async function handleServer(req, res, method, deps) {
|
|
|
3451
4975
|
if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
|
|
3452
4976
|
if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
|
|
3453
4977
|
if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
|
|
3454
|
-
return
|
|
4978
|
+
return writeJson3(res, 200, { server });
|
|
3455
4979
|
}
|
|
3456
4980
|
if (method === "PUT") {
|
|
3457
4981
|
const patch = await readJsonBody3(req);
|
|
@@ -3459,6 +4983,18 @@ async function handleServer(req, res, method, deps) {
|
|
|
3459
4983
|
if (queueErrors.length > 0) {
|
|
3460
4984
|
return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
|
|
3461
4985
|
}
|
|
4986
|
+
const allowanceErrors = validateAllowanceSchedulingSegment(patch);
|
|
4987
|
+
if (allowanceErrors.length > 0) {
|
|
4988
|
+
return writeJsonError(
|
|
4989
|
+
res,
|
|
4990
|
+
400,
|
|
4991
|
+
`invalid allowance scheduling config: ${allowanceErrors.join("; ")}`
|
|
4992
|
+
);
|
|
4993
|
+
}
|
|
4994
|
+
const bindingErrors = validateGatewayBindingsSegment(patch);
|
|
4995
|
+
if (bindingErrors.length > 0) {
|
|
4996
|
+
return writeJsonError(res, 400, `invalid gateway bindings: ${bindingErrors.join("; ")}`);
|
|
4997
|
+
}
|
|
3462
4998
|
const webhookErrors = validateWebhookSegment(patch);
|
|
3463
4999
|
if (webhookErrors.length > 0) {
|
|
3464
5000
|
return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
|
|
@@ -3485,66 +5021,113 @@ async function handleServer(req, res, method, deps) {
|
|
|
3485
5021
|
const merged = mergeServerConfig(current, effectivePatch);
|
|
3486
5022
|
await saveServerConfig(deps.settingsStore, merged);
|
|
3487
5023
|
setServerProxyConfig(merged.proxy);
|
|
5024
|
+
getSharedAccountAllowanceScheduling2().configure(merged.allowanceScheduling);
|
|
5025
|
+
deps.allowanceRefreshScheduler?.configure(merged.allowanceScheduling);
|
|
3488
5026
|
applyWebhookConfig(merged.webhook);
|
|
3489
5027
|
applyAuditConfig(merged.audit);
|
|
3490
5028
|
applyBillingConfig(merged.billing);
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
}
|
|
3503
|
-
|
|
3504
|
-
await deps.outboundApiServer.applyConfig({
|
|
3505
|
-
enabled: merged.enabled,
|
|
3506
|
-
networkBinding: merged.networkBinding,
|
|
3507
|
-
endpoints: merged.endpoints,
|
|
3508
|
-
port: merged.port,
|
|
3509
|
-
userMessageQueue: merged.userMessageQueue,
|
|
3510
|
-
concurrencyQueue: merged.concurrencyQueue,
|
|
3511
|
-
// voucher-redemption #9: hot-apply the voucher flag so enabling the product
|
|
3512
|
-
// takes effect without a restart.
|
|
3513
|
-
voucher: merged.voucher
|
|
3514
|
-
});
|
|
3515
|
-
} catch (err5) {
|
|
3516
|
-
const missing = incompleteConfigMissing(err5);
|
|
3517
|
-
if (missing) {
|
|
3518
|
-
return writeJson2(res, 200, {
|
|
3519
|
-
server: merged,
|
|
3520
|
-
error: { code: "incomplete-model-config", missing }
|
|
3521
|
-
});
|
|
3522
|
-
}
|
|
3523
|
-
throw err5;
|
|
3524
|
-
}
|
|
3525
|
-
return writeJson2(res, 200, { server: merged });
|
|
5029
|
+
await deps.outboundApiServer.applyConfig({
|
|
5030
|
+
enabled: merged.enabled,
|
|
5031
|
+
networkBinding: merged.networkBinding,
|
|
5032
|
+
endpoints: merged.endpoints,
|
|
5033
|
+
bindings: merged.bindings,
|
|
5034
|
+
port: merged.port,
|
|
5035
|
+
userMessageQueue: merged.userMessageQueue,
|
|
5036
|
+
concurrencyQueue: merged.concurrencyQueue,
|
|
5037
|
+
// voucher-redemption #9: hot-apply the voucher flag so enabling the product
|
|
5038
|
+
// takes effect without a restart.
|
|
5039
|
+
voucher: merged.voucher
|
|
5040
|
+
});
|
|
5041
|
+
return writeJson3(res, 200, { server: merged });
|
|
3526
5042
|
}
|
|
3527
5043
|
return writeJsonError(res, 405, `method ${method} not allowed on server`);
|
|
3528
5044
|
}
|
|
3529
|
-
function incompleteConfigMissing(err5) {
|
|
3530
|
-
if (typeof err5 !== "object" || err5 === null) return null;
|
|
3531
|
-
const missing = err5.missing;
|
|
3532
|
-
return Array.isArray(missing) ? missing : null;
|
|
3533
|
-
}
|
|
3534
5045
|
async function handleAccounts(req, res, method, rest, deps) {
|
|
5046
|
+
if (rest[0] === "allowances") {
|
|
5047
|
+
return handleAccountAllowanceApi(
|
|
5048
|
+
req,
|
|
5049
|
+
res,
|
|
5050
|
+
method,
|
|
5051
|
+
rest.slice(1),
|
|
5052
|
+
deps.accountAllowanceService
|
|
5053
|
+
);
|
|
5054
|
+
}
|
|
3535
5055
|
if (method === "GET" && rest.length === 0) {
|
|
3536
5056
|
const accounts = await deps.subscriptionAccounts.listAll();
|
|
3537
5057
|
const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
3538
5058
|
const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
|
|
3539
|
-
return
|
|
5059
|
+
return writeJson3(res, 200, { accounts, providerAccounts, externalCli });
|
|
5060
|
+
}
|
|
5061
|
+
if (method === "POST" && rest[0] === "batch" && rest.length === 1) {
|
|
5062
|
+
const body = await readJsonBody3(req);
|
|
5063
|
+
const parsed = validateAccountBatchBody(body);
|
|
5064
|
+
if (!parsed) return writeJsonError(res, 400, "invalid account batch request");
|
|
5065
|
+
const result = await deps.subscriptionTokenWriter.batchManageAccounts(parsed.refs, parsed.mutation);
|
|
5066
|
+
if (!result.ok) {
|
|
5067
|
+
return writeJsonError(
|
|
5068
|
+
res,
|
|
5069
|
+
404,
|
|
5070
|
+
`account '${result.missing.accountId}' not found for provider '${result.missing.providerId}'`
|
|
5071
|
+
);
|
|
5072
|
+
}
|
|
5073
|
+
if (parsed.mutation.action === "delete") {
|
|
5074
|
+
for (const ref of parsed.refs) {
|
|
5075
|
+
deps.accountAllowanceService?.removeAccountSnapshot?.(ref.providerId, ref.accountId);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
return writeJson3(res, 200, { ok: true, affected: result.affected });
|
|
3540
5079
|
}
|
|
3541
5080
|
if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
|
|
3542
5081
|
const result = handleCodexOAuthStatus(rest[2], deps);
|
|
3543
|
-
return
|
|
5082
|
+
return writeJson3(res, result.status, result.body);
|
|
3544
5083
|
}
|
|
3545
5084
|
if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
|
|
3546
5085
|
const result = handleCodexOAuthCancel(rest[2], deps);
|
|
3547
|
-
return
|
|
5086
|
+
return writeJson3(res, result.status, result.body);
|
|
5087
|
+
}
|
|
5088
|
+
if (method === "GET" && rest.length === 3 && rest[2] === "diagnostics") {
|
|
5089
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5090
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5091
|
+
const accountId = rest[1];
|
|
5092
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5093
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5094
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5095
|
+
}
|
|
5096
|
+
const health2 = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
|
|
5097
|
+
const allowance = deps.accountAllowanceService?.getSchedulingStatus?.()?.history.filter((entry) => entry.providerId === providerId && entry.accountId === accountId).map((entry) => ({
|
|
5098
|
+
kind: "allowance-policy",
|
|
5099
|
+
at: Date.parse(entry.decidedAt),
|
|
5100
|
+
providerId: entry.providerId,
|
|
5101
|
+
accountId: entry.accountId,
|
|
5102
|
+
action: entry.action,
|
|
5103
|
+
reason: entry.reason,
|
|
5104
|
+
usedPercent: entry.usedPercent,
|
|
5105
|
+
resumeAt: entry.resumeAt
|
|
5106
|
+
})) ?? [];
|
|
5107
|
+
const diagnostics = [...health2, ...allowance].sort((left, right) => right.at - left.at).slice(0, 200);
|
|
5108
|
+
return writeJson3(res, 200, { diagnostics });
|
|
5109
|
+
}
|
|
5110
|
+
if (method === "GET" && rest.length === 3 && rest[2] === "events") {
|
|
5111
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5112
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5113
|
+
const accountId = rest[1];
|
|
5114
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5115
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5116
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5117
|
+
}
|
|
5118
|
+
const snapshot = deps.accountProbeService?.getAllHistory().find((entry) => entry.providerId === providerId && entry.accountId === accountId);
|
|
5119
|
+
const diagnostics = getSharedAccountHealth().getDiagnostics({ providerId, accountId });
|
|
5120
|
+
return writeJson3(res, 200, { events: snapshot?.records ?? [], diagnostics });
|
|
5121
|
+
}
|
|
5122
|
+
if (method === "PATCH" && rest.length === 2) {
|
|
5123
|
+
const providerId = asSubscriptionProviderId(rest[0]);
|
|
5124
|
+
if (!providerId) return writeJsonError(res, 400, `unknown subscription provider '${rest[0] ?? ""}'`);
|
|
5125
|
+
const body = await readJsonBody3(req);
|
|
5126
|
+
const patch = validateAccountMetadataPatch(body);
|
|
5127
|
+
if (!patch) return writeJsonError(res, 400, "invalid account metadata patch");
|
|
5128
|
+
const result = await deps.subscriptionTokenWriter.patchAccountMetadata(providerId, rest[1], patch);
|
|
5129
|
+
if (!result.ok) return writeJsonError(res, 404, `account '${rest[1]}' not found`);
|
|
5130
|
+
return writeJson3(res, 200, { ok: true });
|
|
3548
5131
|
}
|
|
3549
5132
|
if (method === "PUT" || method === "POST" || method === "DELETE") {
|
|
3550
5133
|
const providerId = asSubscriptionProviderId(rest[0]);
|
|
@@ -3553,12 +5136,12 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3553
5136
|
}
|
|
3554
5137
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
|
|
3555
5138
|
const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
|
|
3556
|
-
return
|
|
5139
|
+
return writeJson3(res, result.status, result.body);
|
|
3557
5140
|
}
|
|
3558
5141
|
if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
|
|
3559
5142
|
const body2 = await readJsonBody3(req);
|
|
3560
5143
|
const result = await handleOAuthComplete(providerId, body2, deps);
|
|
3561
|
-
return
|
|
5144
|
+
return writeJson3(res, result.status, result.body);
|
|
3562
5145
|
}
|
|
3563
5146
|
if (method === "POST" && rest[1] === "accounts") {
|
|
3564
5147
|
const body2 = await readJsonBody3(req);
|
|
@@ -3569,7 +5152,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3569
5152
|
const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
|
|
3570
5153
|
await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
|
|
3571
5154
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3572
|
-
return
|
|
5155
|
+
return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
|
|
3573
5156
|
}
|
|
3574
5157
|
if (method === "POST" && rest[1] === "import-external") {
|
|
3575
5158
|
if (providerId !== "claude" && providerId !== "codex") {
|
|
@@ -3582,7 +5165,13 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3582
5165
|
return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
|
|
3583
5166
|
}
|
|
3584
5167
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3585
|
-
return
|
|
5168
|
+
return writeJson3(res, 200, {
|
|
5169
|
+
ok: true,
|
|
5170
|
+
account: status2 ?? void 0,
|
|
5171
|
+
nativeCredentialMode: result.nativeCredentialMode,
|
|
5172
|
+
refreshWritesNativeCredentials: result.refreshWritesNativeCredentials,
|
|
5173
|
+
message: "Imported a read-only copy. Omnicross does not manage the native CLI credential file and future refreshes do not write it."
|
|
5174
|
+
});
|
|
3586
5175
|
}
|
|
3587
5176
|
if (method === "POST" && rest[1] === "refresh") {
|
|
3588
5177
|
if (providerId === "opencodego") {
|
|
@@ -3591,7 +5180,17 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3591
5180
|
const writer2 = deps.subscriptionTokenWriter;
|
|
3592
5181
|
const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
|
|
3593
5182
|
const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3594
|
-
return
|
|
5183
|
+
return writeJson3(res, 200, { ok, account: status2 ?? void 0 });
|
|
5184
|
+
}
|
|
5185
|
+
if (method === "POST" && rest.length === 3 && rest[2] === "test") {
|
|
5186
|
+
const accountId = rest[1];
|
|
5187
|
+
if (!deps.accountProbeService) return writeJsonError(res, 501, "account probe service unavailable");
|
|
5188
|
+
const listed = await deps.subscriptionTokenWriter.listSanitizedAccounts();
|
|
5189
|
+
if (!(listed[providerId] ?? []).some((account) => account.id === accountId)) {
|
|
5190
|
+
return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
5191
|
+
}
|
|
5192
|
+
const result = await deps.accountProbeService.probeAccount(providerId, accountId);
|
|
5193
|
+
return writeJson3(res, 200, { ok: result.ok, marked: result.marked });
|
|
3595
5194
|
}
|
|
3596
5195
|
if (method === "POST" && rest[2] === "label") {
|
|
3597
5196
|
const accountId = rest[1];
|
|
@@ -3599,7 +5198,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3599
5198
|
const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
|
|
3600
5199
|
const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
|
|
3601
5200
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3602
|
-
return
|
|
5201
|
+
return writeJson3(res, 200, { ok: true });
|
|
3603
5202
|
}
|
|
3604
5203
|
if (method === "POST" && rest[2] === "priority") {
|
|
3605
5204
|
const accountId = rest[1];
|
|
@@ -3611,7 +5210,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3611
5210
|
}
|
|
3612
5211
|
const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
|
|
3613
5212
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3614
|
-
return
|
|
5213
|
+
return writeJson3(res, 200, { ok: true });
|
|
3615
5214
|
}
|
|
3616
5215
|
if (method === "POST" && rest[2] === "proxy") {
|
|
3617
5216
|
const accountId = rest[1];
|
|
@@ -3624,7 +5223,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3624
5223
|
}
|
|
3625
5224
|
const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
|
|
3626
5225
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3627
|
-
return
|
|
5226
|
+
return writeJson3(res, 200, { ok: true });
|
|
3628
5227
|
}
|
|
3629
5228
|
if (method === "POST" && rest[2] === "supported-models") {
|
|
3630
5229
|
const accountId = rest[1];
|
|
@@ -3633,7 +5232,7 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3633
5232
|
if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
|
|
3634
5233
|
const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
|
|
3635
5234
|
if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3636
|
-
return
|
|
5235
|
+
return writeJson3(res, 200, { ok: true });
|
|
3637
5236
|
}
|
|
3638
5237
|
if (method === "PUT" && rest[1] === "active") {
|
|
3639
5238
|
const body2 = await readJsonBody3(req);
|
|
@@ -3641,17 +5240,22 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3641
5240
|
if (!id) return writeJsonError(res, 400, "active switch requires { id }");
|
|
3642
5241
|
const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
|
|
3643
5242
|
if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
|
|
3644
|
-
return
|
|
5243
|
+
return writeJson3(res, 200, { ok: true });
|
|
3645
5244
|
}
|
|
3646
|
-
if (method === "DELETE" && rest.length
|
|
5245
|
+
if (method === "DELETE" && rest.length === 2) {
|
|
3647
5246
|
const accountId = rest[1];
|
|
3648
5247
|
const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
|
|
3649
5248
|
if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
|
|
3650
|
-
|
|
5249
|
+
deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
|
|
5250
|
+
return writeJson3(res, 200, { ok: true });
|
|
3651
5251
|
}
|
|
3652
|
-
if (method === "DELETE") {
|
|
5252
|
+
if (method === "DELETE" && rest.length === 1) {
|
|
3653
5253
|
await deps.subscriptionTokenWriter.clearProvider(providerId);
|
|
3654
|
-
|
|
5254
|
+
deps.accountAllowanceService?.removeProviderSnapshots?.(providerId);
|
|
5255
|
+
return writeJson3(res, 200, { ok: true });
|
|
5256
|
+
}
|
|
5257
|
+
if (method === "DELETE") {
|
|
5258
|
+
return writeJsonError(res, 405, "method DELETE not allowed on this accounts path");
|
|
3655
5259
|
}
|
|
3656
5260
|
const body = await readJsonBody3(req);
|
|
3657
5261
|
const config = validateTokenBody(providerId, body);
|
|
@@ -3660,22 +5264,22 @@ async function handleAccounts(req, res, method, rest, deps) {
|
|
|
3660
5264
|
}
|
|
3661
5265
|
await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
|
|
3662
5266
|
const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
|
|
3663
|
-
return
|
|
5267
|
+
return writeJson3(res, 200, status ? { account: status } : { ok: true });
|
|
3664
5268
|
}
|
|
3665
5269
|
return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
|
|
3666
5270
|
}
|
|
3667
5271
|
async function handleCli(req, res, method, rest, deps) {
|
|
3668
5272
|
if (method === "GET" && rest.length === 0) {
|
|
3669
5273
|
const result = handleCliList(process.platform, deps.cliPathProbe);
|
|
3670
|
-
return
|
|
5274
|
+
return writeJson3(res, result.status, result.body);
|
|
3671
5275
|
}
|
|
3672
5276
|
if (method === "GET" && rest[0] === "sessions") {
|
|
3673
5277
|
const result = handleCliSessions();
|
|
3674
|
-
return
|
|
5278
|
+
return writeJson3(res, result.status, result.body);
|
|
3675
5279
|
}
|
|
3676
5280
|
if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
|
|
3677
5281
|
const result = handleCliStop(rest[1]);
|
|
3678
|
-
return
|
|
5282
|
+
return writeJson3(res, result.status, result.body);
|
|
3679
5283
|
}
|
|
3680
5284
|
if (method === "POST" && rest[1] === "install") {
|
|
3681
5285
|
const cli = rest[0];
|
|
@@ -3683,7 +5287,7 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
3683
5287
|
return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
|
|
3684
5288
|
}
|
|
3685
5289
|
const result = await handleCliInstall(cli, deps.cliCommandRunner);
|
|
3686
|
-
return
|
|
5290
|
+
return writeJson3(res, result.status, result.body);
|
|
3687
5291
|
}
|
|
3688
5292
|
if (method === "POST" && rest[1] === "launch") {
|
|
3689
5293
|
const cli = rest[0];
|
|
@@ -3698,28 +5302,100 @@ async function handleCli(req, res, method, rest, deps) {
|
|
|
3698
5302
|
opener: deps.cliTerminalOpener,
|
|
3699
5303
|
probe: deps.cliPathProbe
|
|
3700
5304
|
});
|
|
3701
|
-
return
|
|
5305
|
+
return writeJson3(res, result.status, result.body);
|
|
3702
5306
|
}
|
|
3703
5307
|
return writeJsonError(res, 405, `method ${method} not allowed on cli`);
|
|
3704
5308
|
}
|
|
5309
|
+
async function handleIntegrations(req, res, method, rest, deps) {
|
|
5310
|
+
const factory = deps.integrationManagerFactory;
|
|
5311
|
+
if (!factory) return writeJsonError(res, 501, "native CLI integration is not available");
|
|
5312
|
+
const manager = factory();
|
|
5313
|
+
try {
|
|
5314
|
+
if (method === "GET" && rest.length === 0) {
|
|
5315
|
+
return writeJson3(res, 200, {
|
|
5316
|
+
integrations: await manager.listStatus(),
|
|
5317
|
+
gateway: deps.outboundApiServer.getStatus()
|
|
5318
|
+
});
|
|
5319
|
+
}
|
|
5320
|
+
if (method === "POST" && rest.length === 1 && rest[0] === "rotate") {
|
|
5321
|
+
await manager.rotateGatewayKey();
|
|
5322
|
+
return writeJson3(res, 200, { ok: true, integrations: await manager.listStatus() });
|
|
5323
|
+
}
|
|
5324
|
+
const client = rest[0];
|
|
5325
|
+
if (!isIntegrationClient(client)) {
|
|
5326
|
+
return writeJsonError(res, 400, `unknown integration client '${client ?? ""}'`);
|
|
5327
|
+
}
|
|
5328
|
+
if (method === "POST" && rest[1] === "plan") {
|
|
5329
|
+
const body = await readJsonBody3(req);
|
|
5330
|
+
const configPath = body.configPath;
|
|
5331
|
+
if (configPath !== void 0 && typeof configPath !== "string") {
|
|
5332
|
+
return writeJsonError(res, 400, "configPath must be a string");
|
|
5333
|
+
}
|
|
5334
|
+
const plan = await manager.plan(client, configPath);
|
|
5335
|
+
return writeJson3(res, 200, { plan });
|
|
5336
|
+
}
|
|
5337
|
+
if (method === "POST" && (rest[1] === "install" || rest[1] === "apply")) {
|
|
5338
|
+
const body = await readJsonBody3(req);
|
|
5339
|
+
const configPath = body.configPath;
|
|
5340
|
+
if (configPath !== void 0 && typeof configPath !== "string") {
|
|
5341
|
+
return writeJsonError(res, 400, "configPath must be a string");
|
|
5342
|
+
}
|
|
5343
|
+
const status = await manager.install(client, configPath);
|
|
5344
|
+
return writeJson3(res, 200, { integration: status });
|
|
5345
|
+
}
|
|
5346
|
+
if (method === "POST" && rest[1] === "repair") {
|
|
5347
|
+
const status = await manager.repair(client);
|
|
5348
|
+
return writeJson3(res, 200, { integration: status });
|
|
5349
|
+
}
|
|
5350
|
+
if (method === "DELETE" && rest.length === 1 || method === "POST" && rest[1] === "remove") {
|
|
5351
|
+
const status = await manager.remove(client);
|
|
5352
|
+
return writeJson3(res, 200, { integration: status });
|
|
5353
|
+
}
|
|
5354
|
+
return writeJsonError(res, 405, `method ${method} not allowed on integrations`);
|
|
5355
|
+
} catch (error) {
|
|
5356
|
+
if (error instanceof IntegrationConflictError) {
|
|
5357
|
+
return writeJsonError(res, 409, error.message);
|
|
5358
|
+
}
|
|
5359
|
+
throw error;
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
function isIntegrationClient(value) {
|
|
5363
|
+
return value === "codex" || value === "claude";
|
|
5364
|
+
}
|
|
3705
5365
|
async function handleStatus(res, method, deps) {
|
|
3706
5366
|
if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
|
|
3707
5367
|
const status = deps.outboundApiServer.getStatus();
|
|
3708
5368
|
const serverConfig = await loadServerConfig2(deps.settingsStore);
|
|
3709
|
-
const endpoints =
|
|
3710
|
-
|
|
3711
|
-
|
|
5369
|
+
const endpoints = ["chat", "responses", "messages", "gemini"].map((endpoint) => {
|
|
5370
|
+
const routes = (serverConfig.bindings ?? []).filter((binding) => binding.enabled && binding.endpoint === endpoint).map((binding) => gatewayBindingToEndpointConfig(binding));
|
|
5371
|
+
const useSubscription = routes.some((route) => route.useSubscription);
|
|
5372
|
+
if (isKindMappedEndpoint(endpoint)) {
|
|
5373
|
+
const kinds = {};
|
|
5374
|
+
for (const route of routes) {
|
|
5375
|
+
for (const [kind, ref] of Object.entries(route.modelMap ?? {})) {
|
|
5376
|
+
if (ref?.trim() && !kinds[kind]) kinds[kind] = ref;
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
return { endpoint, kinds, useSubscription };
|
|
3712
5380
|
}
|
|
3713
|
-
if (
|
|
3714
|
-
return {
|
|
5381
|
+
if (endpoint === "chat") {
|
|
5382
|
+
return {
|
|
5383
|
+
endpoint,
|
|
5384
|
+
models: [...new Set(routes.flatMap((route) => route.models ?? []))],
|
|
5385
|
+
useSubscription
|
|
5386
|
+
};
|
|
3715
5387
|
}
|
|
3716
|
-
return {
|
|
5388
|
+
return {
|
|
5389
|
+
endpoint,
|
|
5390
|
+
model: routes.find((route) => route.defaultModel?.trim())?.defaultModel ?? "",
|
|
5391
|
+
useSubscription
|
|
5392
|
+
};
|
|
3717
5393
|
});
|
|
3718
5394
|
if (status.running) {
|
|
3719
5395
|
const queueStatus = deps.outboundApiServer.getQueueStatus();
|
|
3720
|
-
return
|
|
5396
|
+
return writeJson3(res, 200, { ...status, endpoints, queueStatus });
|
|
3721
5397
|
}
|
|
3722
|
-
return
|
|
5398
|
+
return writeJson3(res, 200, { ...status, endpoints });
|
|
3723
5399
|
}
|
|
3724
5400
|
function resolvePlaygroundPath(endpoint, body) {
|
|
3725
5401
|
switch (endpoint) {
|
|
@@ -3745,16 +5421,16 @@ async function handlePlayground(req, res, method, deps) {
|
|
|
3745
5421
|
const payload = body["body"];
|
|
3746
5422
|
const status = deps.outboundApiServer.getStatus();
|
|
3747
5423
|
if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
|
|
3748
|
-
const path2 = resolvePlaygroundPath(endpoint,
|
|
5424
|
+
const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
|
|
3749
5425
|
if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
|
|
3750
5426
|
const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
3751
5427
|
await proxyToOutbound(res, status.port, path2, key, upstreamBody);
|
|
3752
5428
|
}
|
|
3753
|
-
function
|
|
5429
|
+
function isRecord2(v) {
|
|
3754
5430
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3755
5431
|
}
|
|
3756
5432
|
function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
3757
|
-
return new Promise((
|
|
5433
|
+
return new Promise((resolve2) => {
|
|
3758
5434
|
const upstream = http.request(
|
|
3759
5435
|
{
|
|
3760
5436
|
host: "127.0.0.1",
|
|
@@ -3775,14 +5451,14 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
|
3775
5451
|
proxRes.on("data", (chunk) => res.write(chunk));
|
|
3776
5452
|
proxRes.on("end", () => {
|
|
3777
5453
|
res.end();
|
|
3778
|
-
|
|
5454
|
+
resolve2();
|
|
3779
5455
|
});
|
|
3780
5456
|
}
|
|
3781
5457
|
);
|
|
3782
5458
|
upstream.on("error", (err5) => {
|
|
3783
5459
|
if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
|
|
3784
5460
|
else res.end();
|
|
3785
|
-
|
|
5461
|
+
resolve2();
|
|
3786
5462
|
});
|
|
3787
5463
|
upstream.write(body);
|
|
3788
5464
|
upstream.end();
|
|
@@ -3790,7 +5466,7 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
|
|
|
3790
5466
|
}
|
|
3791
5467
|
|
|
3792
5468
|
// src/admin/uiStatic.ts
|
|
3793
|
-
import { existsSync as
|
|
5469
|
+
import { existsSync as existsSync6, statSync as statSync2 } from "fs";
|
|
3794
5470
|
import { readFile } from "fs/promises";
|
|
3795
5471
|
import { createRequire } from "module";
|
|
3796
5472
|
import path from "path";
|
|
@@ -3813,13 +5489,13 @@ var CONTENT_TYPES = {
|
|
|
3813
5489
|
function resolveUiDist() {
|
|
3814
5490
|
const fromEnv = process.env["OMNICROSS_UI_DIST"];
|
|
3815
5491
|
if (fromEnv) {
|
|
3816
|
-
return
|
|
5492
|
+
return existsSync6(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
|
|
3817
5493
|
}
|
|
3818
5494
|
try {
|
|
3819
5495
|
const req = createRequire(typeof __filename !== "undefined" ? __filename : import.meta.url);
|
|
3820
5496
|
const pkgJson = req.resolve("@omnicross/ui/package.json");
|
|
3821
5497
|
const dist = path.join(path.dirname(pkgJson), "dist");
|
|
3822
|
-
return
|
|
5498
|
+
return existsSync6(path.join(dist, "index.html")) ? dist : null;
|
|
3823
5499
|
} catch {
|
|
3824
5500
|
return null;
|
|
3825
5501
|
}
|
|
@@ -3868,7 +5544,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
3868
5544
|
return true;
|
|
3869
5545
|
}
|
|
3870
5546
|
let target = filePath;
|
|
3871
|
-
if (!
|
|
5547
|
+
if (!existsSync6(target) || statSync2(target).isDirectory()) {
|
|
3872
5548
|
if (path.extname(rel) === "") {
|
|
3873
5549
|
target = path.join(uiDist, "index.html");
|
|
3874
5550
|
} else {
|
|
@@ -3885,7 +5561,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
|
|
|
3885
5561
|
}
|
|
3886
5562
|
|
|
3887
5563
|
// src/admin/version.ts
|
|
3888
|
-
var DAEMON_VERSION = true ? "0.1.
|
|
5564
|
+
var DAEMON_VERSION = true ? "0.1.6" : "0.0.0-dev";
|
|
3889
5565
|
|
|
3890
5566
|
// src/admin/AdminServer.ts
|
|
3891
5567
|
var LOOPBACK_ADDR = "127.0.0.1";
|
|
@@ -3924,14 +5600,14 @@ var AdminServer = class {
|
|
|
3924
5600
|
}
|
|
3925
5601
|
/** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
|
|
3926
5602
|
listen(bindAddr, port) {
|
|
3927
|
-
return new Promise((
|
|
5603
|
+
return new Promise((resolve2, reject) => {
|
|
3928
5604
|
const server = http2.createServer((req, res) => {
|
|
3929
5605
|
this.onRequest(req, res);
|
|
3930
5606
|
});
|
|
3931
5607
|
const onError = (err5) => {
|
|
3932
5608
|
if (err5.code === "EADDRINUSE" && port !== 0) {
|
|
3933
5609
|
server.removeListener("error", onError);
|
|
3934
|
-
this.listen(bindAddr, 0).then(
|
|
5610
|
+
this.listen(bindAddr, 0).then(resolve2, reject);
|
|
3935
5611
|
return;
|
|
3936
5612
|
}
|
|
3937
5613
|
reject(err5);
|
|
@@ -3943,7 +5619,7 @@ var AdminServer = class {
|
|
|
3943
5619
|
server.removeListener("error", onError);
|
|
3944
5620
|
server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
|
|
3945
5621
|
this.server = server;
|
|
3946
|
-
|
|
5622
|
+
resolve2(addr.port);
|
|
3947
5623
|
} else {
|
|
3948
5624
|
reject(new Error("Failed to get admin server address"));
|
|
3949
5625
|
}
|
|
@@ -4024,8 +5700,8 @@ var AdminServer = class {
|
|
|
4024
5700
|
if (!server) return;
|
|
4025
5701
|
this.server = null;
|
|
4026
5702
|
this.boundPort = 0;
|
|
4027
|
-
return new Promise((
|
|
4028
|
-
server.close(() =>
|
|
5703
|
+
return new Promise((resolve2) => {
|
|
5704
|
+
server.close(() => resolve2());
|
|
4029
5705
|
});
|
|
4030
5706
|
}
|
|
4031
5707
|
/** A live status snapshot. */
|
|
@@ -4141,7 +5817,7 @@ function pageHtml(message) {
|
|
|
4141
5817
|
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>`;
|
|
4142
5818
|
}
|
|
4143
5819
|
function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
|
|
4144
|
-
return new Promise((
|
|
5820
|
+
return new Promise((resolve2, reject) => {
|
|
4145
5821
|
let settled = false;
|
|
4146
5822
|
const finish = (server2, fn) => {
|
|
4147
5823
|
if (settled) return;
|
|
@@ -4172,7 +5848,7 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal
|
|
|
4172
5848
|
}
|
|
4173
5849
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4174
5850
|
res.end(pageHtml("Login complete."));
|
|
4175
|
-
finish(server, () =>
|
|
5851
|
+
finish(server, () => resolve2(code));
|
|
4176
5852
|
});
|
|
4177
5853
|
const abort = () => finish(server, () => reject(new Error("login: cancelled")));
|
|
4178
5854
|
if (signal?.aborted) {
|
|
@@ -4267,21 +5943,30 @@ function createPoolKeysLoader(getProviderRow, autoDisabled) {
|
|
|
4267
5943
|
}
|
|
4268
5944
|
|
|
4269
5945
|
// src/commands/paths.ts
|
|
4270
|
-
import { dirname as
|
|
5946
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
4271
5947
|
function defaultVouchersPath(configPath) {
|
|
4272
|
-
return
|
|
5948
|
+
return join5(dirname5(configPath), "vouchers.json");
|
|
5949
|
+
}
|
|
5950
|
+
function defaultIntegrationsPath(configPath) {
|
|
5951
|
+
return join5(dirname5(configPath), "integrations.json");
|
|
4273
5952
|
}
|
|
4274
5953
|
function defaultPricingPath(configPath) {
|
|
4275
|
-
return
|
|
5954
|
+
return join5(dirname5(configPath), "pricing.json");
|
|
5955
|
+
}
|
|
5956
|
+
function defaultPricingRefreshStatePath(configPath) {
|
|
5957
|
+
return join5(dirname5(configPath), "pricing-refresh.json");
|
|
5958
|
+
}
|
|
5959
|
+
function defaultAccountAllowancePath(configPath) {
|
|
5960
|
+
return join5(dirname5(configPath), "allowance-cache.json");
|
|
4276
5961
|
}
|
|
4277
5962
|
function defaultUsageEventsPath(configPath) {
|
|
4278
|
-
return
|
|
5963
|
+
return join5(dirname5(configPath), "usage-events.jsonl");
|
|
4279
5964
|
}
|
|
4280
5965
|
function defaultAuditDir(configPath) {
|
|
4281
|
-
return
|
|
5966
|
+
return join5(dirname5(configPath), "audit");
|
|
4282
5967
|
}
|
|
4283
5968
|
function defaultBillingDir(configPath) {
|
|
4284
|
-
return
|
|
5969
|
+
return join5(dirname5(configPath), "billing");
|
|
4285
5970
|
}
|
|
4286
5971
|
|
|
4287
5972
|
// src/ports/ConfigFileProviderConfigSource.ts
|
|
@@ -4294,8 +5979,10 @@ var EMPTY_CHAIN = {
|
|
|
4294
5979
|
modelTransformers: []
|
|
4295
5980
|
};
|
|
4296
5981
|
var FORMAT_TRANSFORMER = {
|
|
5982
|
+
openai: "openai",
|
|
4297
5983
|
anthropic: "anthropic",
|
|
4298
|
-
gemini: "gemini"
|
|
5984
|
+
gemini: "gemini",
|
|
5985
|
+
"openai-response": "openai-response"
|
|
4299
5986
|
};
|
|
4300
5987
|
var ConfigFileProviderConfigSource = class {
|
|
4301
5988
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -4364,7 +6051,7 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4364
6051
|
}
|
|
4365
6052
|
async getMainTransformer(providerId) {
|
|
4366
6053
|
const row = this.providers.get(providerId);
|
|
4367
|
-
if (!row
|
|
6054
|
+
if (!row) return null;
|
|
4368
6055
|
const name = FORMAT_TRANSFORMER[row.apiFormat];
|
|
4369
6056
|
const instances = this.transformerService.resolveTransformerReferences([name]);
|
|
4370
6057
|
return instances[0] ?? null;
|
|
@@ -4374,11 +6061,8 @@ var ConfigFileProviderConfigSource = class {
|
|
|
4374
6061
|
if (!row) return EMPTY_CHAIN;
|
|
4375
6062
|
const customRefs = row.transformer?.use ?? [];
|
|
4376
6063
|
if (customRefs.length === 0) return EMPTY_CHAIN;
|
|
4377
|
-
const formatName = row.apiFormat === "openai" ? void 0 : FORMAT_TRANSFORMER[row.apiFormat];
|
|
4378
|
-
const effectiveRefs = formatName ? customRefs.filter((ref) => (typeof ref === "string" ? ref : ref[0]) !== formatName) : customRefs;
|
|
4379
|
-
if (effectiveRefs.length === 0) return EMPTY_CHAIN;
|
|
4380
6064
|
return {
|
|
4381
|
-
providerTransformers: this.transformerService.resolveTransformerReferences(
|
|
6065
|
+
providerTransformers: this.transformerService.resolveTransformerReferences(customRefs),
|
|
4382
6066
|
modelTransformers: []
|
|
4383
6067
|
};
|
|
4384
6068
|
}
|
|
@@ -4409,7 +6093,7 @@ function resolvePreferredApiKey(row) {
|
|
|
4409
6093
|
}
|
|
4410
6094
|
function toLLMProvider(row) {
|
|
4411
6095
|
const apiFormat = row.apiFormat === "gemini" ? "google" : row.apiFormat;
|
|
4412
|
-
const transformer =
|
|
6096
|
+
const transformer = { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
|
|
4413
6097
|
const allModels = row.models ?? [];
|
|
4414
6098
|
const models = row.modelConfigs ? allModels.filter((id) => row.modelConfigs.find((c) => c.id === id)?.enabled !== false) : allModels;
|
|
4415
6099
|
return {
|
|
@@ -4479,7 +6163,7 @@ var ConfigurableLogger = class {
|
|
|
4479
6163
|
const stream = this.fileStream;
|
|
4480
6164
|
this.fileStream = null;
|
|
4481
6165
|
if (!stream) return Promise.resolve();
|
|
4482
|
-
return new Promise((
|
|
6166
|
+
return new Promise((resolve2) => stream.end(() => resolve2()));
|
|
4483
6167
|
}
|
|
4484
6168
|
emit(level, message, error, meta) {
|
|
4485
6169
|
if (LEVEL_ORDER[level] > this.threshold) return;
|
|
@@ -4590,7 +6274,7 @@ function safeStringify(value) {
|
|
|
4590
6274
|
}
|
|
4591
6275
|
|
|
4592
6276
|
// src/ports/JsonApiServerSettingsStore.ts
|
|
4593
|
-
import { readFileSync as
|
|
6277
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
4594
6278
|
import { OUTBOUND_API_SERVER_CONFIG_KEY } from "@omnicross/core/outbound-api";
|
|
4595
6279
|
var JsonApiServerSettingsStore = class {
|
|
4596
6280
|
/**
|
|
@@ -4617,7 +6301,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
4617
6301
|
if (key !== OUTBOUND_API_SERVER_CONFIG_KEY) return;
|
|
4618
6302
|
const file = this.readFile();
|
|
4619
6303
|
file.server = this.encryptSecrets(value);
|
|
4620
|
-
|
|
6304
|
+
writeFileSync5(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
4621
6305
|
}
|
|
4622
6306
|
/** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
|
|
4623
6307
|
encryptSecrets(config) {
|
|
@@ -4640,7 +6324,7 @@ var JsonApiServerSettingsStore = class {
|
|
|
4640
6324
|
/** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
|
|
4641
6325
|
readFile() {
|
|
4642
6326
|
try {
|
|
4643
|
-
const raw =
|
|
6327
|
+
const raw = readFileSync6(this.configPath, "utf8");
|
|
4644
6328
|
const parsed = JSON.parse(raw);
|
|
4645
6329
|
if (parsed && typeof parsed === "object") return parsed;
|
|
4646
6330
|
} catch {
|
|
@@ -4650,8 +6334,8 @@ var JsonApiServerSettingsStore = class {
|
|
|
4650
6334
|
};
|
|
4651
6335
|
|
|
4652
6336
|
// src/ports/JsonlUsageEventStore.ts
|
|
4653
|
-
import { randomUUID as
|
|
4654
|
-
import { appendFileSync, existsSync as
|
|
6337
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
6338
|
+
import { appendFileSync, existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
4655
6339
|
var JsonlUsageEventStore = class {
|
|
4656
6340
|
constructor(eventsPath, isPriced) {
|
|
4657
6341
|
this.eventsPath = eventsPath;
|
|
@@ -4663,7 +6347,7 @@ var JsonlUsageEventStore = class {
|
|
|
4663
6347
|
async insert(input) {
|
|
4664
6348
|
const row = {
|
|
4665
6349
|
...input,
|
|
4666
|
-
id:
|
|
6350
|
+
id: randomUUID4(),
|
|
4667
6351
|
ts: input.ts ?? Date.now()
|
|
4668
6352
|
};
|
|
4669
6353
|
appendFileSync(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
|
|
@@ -4761,15 +6445,15 @@ var JsonlUsageEventStore = class {
|
|
|
4761
6445
|
* Used to lazily seed the outbound key-policy spend tracker (once per key). A
|
|
4762
6446
|
* key with no attributed events yields all zeros.
|
|
4763
6447
|
*/
|
|
4764
|
-
async getSpendByKey(
|
|
6448
|
+
async getSpendByKey(query2) {
|
|
4765
6449
|
let totalUsd = 0;
|
|
4766
6450
|
let dailyUsd = 0;
|
|
4767
6451
|
let weeklyUsd = 0;
|
|
4768
|
-
for (const row of this.readRows({ startTs: 0, endTs:
|
|
4769
|
-
if (row.apiKeyId !==
|
|
6452
|
+
for (const row of this.readRows({ startTs: 0, endTs: query2.endTs })) {
|
|
6453
|
+
if (row.apiKeyId !== query2.apiKeyId) continue;
|
|
4770
6454
|
totalUsd += row.costUsd;
|
|
4771
|
-
if (row.ts >=
|
|
4772
|
-
if (row.ts >=
|
|
6455
|
+
if (row.ts >= query2.dayStartTs) dailyUsd += row.costUsd;
|
|
6456
|
+
if (row.ts >= query2.weekStartTs) weeklyUsd += row.costUsd;
|
|
4773
6457
|
}
|
|
4774
6458
|
return { totalUsd, dailyUsd, weeklyUsd };
|
|
4775
6459
|
}
|
|
@@ -4854,10 +6538,10 @@ var JsonlUsageEventStore = class {
|
|
|
4854
6538
|
}
|
|
4855
6539
|
/** Parse every line, skipping malformed/torn lines defensively. */
|
|
4856
6540
|
readAllRows() {
|
|
4857
|
-
if (!
|
|
6541
|
+
if (!existsSync7(this.eventsPath)) return [];
|
|
4858
6542
|
let raw;
|
|
4859
6543
|
try {
|
|
4860
|
-
raw =
|
|
6544
|
+
raw = readFileSync7(this.eventsPath, "utf8");
|
|
4861
6545
|
} catch {
|
|
4862
6546
|
return [];
|
|
4863
6547
|
}
|
|
@@ -4941,7 +6625,7 @@ function isUsageEventRecord(parsed) {
|
|
|
4941
6625
|
}
|
|
4942
6626
|
|
|
4943
6627
|
// src/ports/JsonOutboundKeyDb.ts
|
|
4944
|
-
import { existsSync as
|
|
6628
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
4945
6629
|
var JsonOutboundKeyDb = class {
|
|
4946
6630
|
constructor(keysPath) {
|
|
4947
6631
|
this.keysPath = keysPath;
|
|
@@ -4967,7 +6651,10 @@ var JsonOutboundKeyDb = class {
|
|
|
4967
6651
|
enabled: true,
|
|
4968
6652
|
createdAt: input.createdAt ?? Date.now(),
|
|
4969
6653
|
lastUsedAt: null,
|
|
4970
|
-
revokedAt: null
|
|
6654
|
+
revokedAt: null,
|
|
6655
|
+
kind: input.kind,
|
|
6656
|
+
allowedEndpoints: input.allowedEndpoints,
|
|
6657
|
+
loopbackOnly: input.loopbackOnly
|
|
4971
6658
|
};
|
|
4972
6659
|
rows.push(row);
|
|
4973
6660
|
this.writeRows(rows);
|
|
@@ -5044,16 +6731,16 @@ var JsonOutboundKeyDb = class {
|
|
|
5044
6731
|
}
|
|
5045
6732
|
/** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5046
6733
|
readRows() {
|
|
5047
|
-
if (!
|
|
6734
|
+
if (!existsSync8(this.keysPath)) return [];
|
|
5048
6735
|
try {
|
|
5049
|
-
const parsed = JSON.parse(
|
|
6736
|
+
const parsed = JSON.parse(readFileSync8(this.keysPath, "utf8"));
|
|
5050
6737
|
return Array.isArray(parsed) ? parsed : [];
|
|
5051
6738
|
} catch {
|
|
5052
6739
|
return [];
|
|
5053
6740
|
}
|
|
5054
6741
|
}
|
|
5055
6742
|
writeRows(rows) {
|
|
5056
|
-
|
|
6743
|
+
writeFileSync6(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
5057
6744
|
}
|
|
5058
6745
|
};
|
|
5059
6746
|
function applyPolicyField(row, field, value) {
|
|
@@ -5063,12 +6750,29 @@ function applyPolicyField(row, field, value) {
|
|
|
5063
6750
|
}
|
|
5064
6751
|
|
|
5065
6752
|
// src/ports/JsonPricingStore.ts
|
|
5066
|
-
import { existsSync as
|
|
6753
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
6754
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
5067
6755
|
var JsonPricingStore = class {
|
|
5068
6756
|
constructor(pricingPath) {
|
|
5069
6757
|
this.pricingPath = pricingPath;
|
|
5070
6758
|
}
|
|
5071
6759
|
pricingPath;
|
|
6760
|
+
/**
|
|
6761
|
+
* Return whether the durable snapshot can actually serve at least one price.
|
|
6762
|
+
*
|
|
6763
|
+
* This intentionally checks the file itself instead of relying on refresh
|
|
6764
|
+
* metadata: a recent `lastSuccessAt` must not hide a deleted, truncated, or
|
|
6765
|
+
* otherwise unusable pricing table after a crash or manual file edit.
|
|
6766
|
+
*/
|
|
6767
|
+
hasUsableSnapshot() {
|
|
6768
|
+
if (!existsSync9(this.pricingPath)) return false;
|
|
6769
|
+
try {
|
|
6770
|
+
const parsed = JSON.parse(readFileSync9(this.pricingPath, "utf8"));
|
|
6771
|
+
return Array.isArray(parsed) && parsed.some(isUsablePricingRow);
|
|
6772
|
+
} catch {
|
|
6773
|
+
return false;
|
|
6774
|
+
}
|
|
6775
|
+
}
|
|
5072
6776
|
async getAll() {
|
|
5073
6777
|
return this.readRows();
|
|
5074
6778
|
}
|
|
@@ -5081,17 +6785,17 @@ var JsonPricingStore = class {
|
|
|
5081
6785
|
*/
|
|
5082
6786
|
async upsert(input, asUserEdit) {
|
|
5083
6787
|
const rows = this.readRows();
|
|
5084
|
-
const entry = this.applyUpsert(rows, input, asUserEdit);
|
|
6788
|
+
const entry = this.applyUpsert(rows, input, asUserEdit, "litellm");
|
|
5085
6789
|
this.writeRows(rows);
|
|
5086
6790
|
return entry;
|
|
5087
6791
|
}
|
|
5088
6792
|
/**
|
|
5089
6793
|
* Apply a batch fetched from a pricing source. Rows whose local copy is
|
|
5090
6794
|
* user-edited are NOT applied — they come back as `{ current, incoming }`
|
|
5091
|
-
* conflicts; everything else is upserted
|
|
5092
|
-
* for the whole batch.
|
|
6795
|
+
* conflicts; everything else is upserted with the supplied automatic source.
|
|
6796
|
+
* ONE file write for the whole batch.
|
|
5093
6797
|
*/
|
|
5094
|
-
async bulkApplyFromSource(entries) {
|
|
6798
|
+
async bulkApplyFromSource(entries, source = "litellm") {
|
|
5095
6799
|
const rows = this.readRows();
|
|
5096
6800
|
const applied = [];
|
|
5097
6801
|
const conflicts = [];
|
|
@@ -5107,7 +6811,8 @@ var JsonPricingStore = class {
|
|
|
5107
6811
|
rows,
|
|
5108
6812
|
incoming,
|
|
5109
6813
|
/* asUserEdit */
|
|
5110
|
-
false
|
|
6814
|
+
false,
|
|
6815
|
+
source
|
|
5111
6816
|
));
|
|
5112
6817
|
}
|
|
5113
6818
|
if (applied.length > 0) this.writeRows(rows);
|
|
@@ -5130,7 +6835,8 @@ var JsonPricingStore = class {
|
|
|
5130
6835
|
rows,
|
|
5131
6836
|
r.incoming,
|
|
5132
6837
|
/* asUserEdit */
|
|
5133
|
-
false
|
|
6838
|
+
false,
|
|
6839
|
+
"litellm"
|
|
5134
6840
|
);
|
|
5135
6841
|
overwrittenCount += 1;
|
|
5136
6842
|
}
|
|
@@ -5151,7 +6857,7 @@ var JsonPricingStore = class {
|
|
|
5151
6857
|
return true;
|
|
5152
6858
|
}
|
|
5153
6859
|
/** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
|
|
5154
|
-
applyUpsert(rows, input, asUserEdit) {
|
|
6860
|
+
applyUpsert(rows, input, asUserEdit, automaticSource) {
|
|
5155
6861
|
const now = Date.now();
|
|
5156
6862
|
const entry = {
|
|
5157
6863
|
providerId: input.providerId,
|
|
@@ -5160,7 +6866,7 @@ var JsonPricingStore = class {
|
|
|
5160
6866
|
outputPricePer1m: input.outputPricePer1m,
|
|
5161
6867
|
cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
|
|
5162
6868
|
cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
|
|
5163
|
-
source: asUserEdit ? "user" :
|
|
6869
|
+
source: asUserEdit ? "user" : automaticSource,
|
|
5164
6870
|
userEdited: asUserEdit,
|
|
5165
6871
|
editedAt: asUserEdit ? now : null,
|
|
5166
6872
|
updatedAt: now
|
|
@@ -5174,21 +6880,142 @@ var JsonPricingStore = class {
|
|
|
5174
6880
|
}
|
|
5175
6881
|
/** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5176
6882
|
readRows() {
|
|
5177
|
-
if (!
|
|
6883
|
+
if (!existsSync9(this.pricingPath)) return [];
|
|
5178
6884
|
try {
|
|
5179
|
-
const parsed = JSON.parse(
|
|
6885
|
+
const parsed = JSON.parse(readFileSync9(this.pricingPath, "utf8"));
|
|
5180
6886
|
return Array.isArray(parsed) ? parsed : [];
|
|
5181
6887
|
} catch {
|
|
5182
6888
|
return [];
|
|
5183
6889
|
}
|
|
5184
6890
|
}
|
|
5185
6891
|
writeRows(rows) {
|
|
5186
|
-
|
|
6892
|
+
const temporaryPath = `${this.pricingPath}.${process.pid}.${randomUUID5()}.tmp`;
|
|
6893
|
+
try {
|
|
6894
|
+
writeFileSync7(temporaryPath, JSON.stringify(rows, null, 2) + "\n", {
|
|
6895
|
+
encoding: "utf8",
|
|
6896
|
+
flag: "wx"
|
|
6897
|
+
});
|
|
6898
|
+
this.replaceFile(temporaryPath);
|
|
6899
|
+
} finally {
|
|
6900
|
+
rmSync2(temporaryPath, { force: true });
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
/** Isolated for deterministic failure testing; never removes the target. */
|
|
6904
|
+
replaceFile(temporaryPath) {
|
|
6905
|
+
renameSync3(temporaryPath, this.pricingPath);
|
|
6906
|
+
}
|
|
6907
|
+
};
|
|
6908
|
+
function isUsablePricingRow(value) {
|
|
6909
|
+
if (!value || typeof value !== "object") return false;
|
|
6910
|
+
const row = value;
|
|
6911
|
+
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);
|
|
6912
|
+
}
|
|
6913
|
+
|
|
6914
|
+
// src/pricing/PricingRefreshScheduler.ts
|
|
6915
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
6916
|
+
var EMPTY_STATE2 = {
|
|
6917
|
+
lastAttemptAt: null,
|
|
6918
|
+
lastSuccessAt: null,
|
|
6919
|
+
lastError: null,
|
|
6920
|
+
sources: []
|
|
6921
|
+
};
|
|
6922
|
+
var PricingRefreshScheduler = class {
|
|
6923
|
+
constructor(engine, catalog2, statePath, logger, options = {}) {
|
|
6924
|
+
this.engine = engine;
|
|
6925
|
+
this.catalog = catalog2;
|
|
6926
|
+
this.statePath = statePath;
|
|
6927
|
+
this.logger = logger;
|
|
6928
|
+
this.staleAfterMs = options.staleAfterMs ?? 24 * 60 * 60 * 1e3;
|
|
6929
|
+
this.intervalMs = options.intervalMs ?? 60 * 60 * 1e3;
|
|
6930
|
+
this.now = options.now ?? Date.now;
|
|
6931
|
+
}
|
|
6932
|
+
engine;
|
|
6933
|
+
catalog;
|
|
6934
|
+
statePath;
|
|
6935
|
+
logger;
|
|
6936
|
+
staleAfterMs;
|
|
6937
|
+
intervalMs;
|
|
6938
|
+
now;
|
|
6939
|
+
timer = null;
|
|
6940
|
+
inFlight = null;
|
|
6941
|
+
/** Fire one stale check immediately and arm an unref'ed periodic check. */
|
|
6942
|
+
start() {
|
|
6943
|
+
if (this.timer) return;
|
|
6944
|
+
void this.refreshIfStale();
|
|
6945
|
+
this.timer = setInterval(() => void this.refreshIfStale(), this.intervalMs);
|
|
6946
|
+
this.timer.unref?.();
|
|
6947
|
+
}
|
|
6948
|
+
dispose() {
|
|
6949
|
+
if (this.timer) clearInterval(this.timer);
|
|
6950
|
+
this.timer = null;
|
|
6951
|
+
}
|
|
6952
|
+
getState() {
|
|
6953
|
+
if (!existsSync10(this.statePath)) return { ...EMPTY_STATE2, sources: [] };
|
|
6954
|
+
try {
|
|
6955
|
+
const value = JSON.parse(readFileSync10(this.statePath, "utf8"));
|
|
6956
|
+
return {
|
|
6957
|
+
lastAttemptAt: finiteOrNull(value.lastAttemptAt),
|
|
6958
|
+
lastSuccessAt: finiteOrNull(value.lastSuccessAt),
|
|
6959
|
+
lastError: typeof value.lastError === "string" ? value.lastError : null,
|
|
6960
|
+
sources: Array.isArray(value.sources) ? value.sources : []
|
|
6961
|
+
};
|
|
6962
|
+
} catch {
|
|
6963
|
+
return { ...EMPTY_STATE2, sources: [] };
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
/** Public for admin/manual tests; concurrent checks share one promise. */
|
|
6967
|
+
refreshIfStale(force = false) {
|
|
6968
|
+
if (this.inFlight) return this.inFlight;
|
|
6969
|
+
const state = this.getState();
|
|
6970
|
+
if (!force && this.catalog.hasUsableSnapshot() && state.lastSuccessAt !== null && this.now() - state.lastSuccessAt < this.staleAfterMs) {
|
|
6971
|
+
return Promise.resolve();
|
|
6972
|
+
}
|
|
6973
|
+
const task = this.runRefresh(state);
|
|
6974
|
+
this.inFlight = task;
|
|
6975
|
+
return task.finally(() => {
|
|
6976
|
+
if (this.inFlight === task) this.inFlight = null;
|
|
6977
|
+
});
|
|
6978
|
+
}
|
|
6979
|
+
async runRefresh(previous) {
|
|
6980
|
+
const lastAttemptAt = this.now();
|
|
6981
|
+
try {
|
|
6982
|
+
const result = await this.engine.fetchLatestFromSource();
|
|
6983
|
+
const failed = result.sources.filter((source) => source.status === "failed");
|
|
6984
|
+
const complete = failed.length === 0;
|
|
6985
|
+
this.writeState({
|
|
6986
|
+
lastAttemptAt,
|
|
6987
|
+
// A partial refresh keeps useful rows, but remains stale so the failed
|
|
6988
|
+
// source is retried on the next hourly check instead of 24 hours later.
|
|
6989
|
+
lastSuccessAt: complete ? this.now() : previous.lastSuccessAt,
|
|
6990
|
+
lastError: complete ? null : failed.map((source) => `${source.source}: ${source.error ?? "failed"}`).join("; "),
|
|
6991
|
+
sources: result.sources
|
|
6992
|
+
});
|
|
6993
|
+
} catch (error) {
|
|
6994
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6995
|
+
this.writeState({
|
|
6996
|
+
lastAttemptAt,
|
|
6997
|
+
lastSuccessAt: previous.lastSuccessAt,
|
|
6998
|
+
lastError: message,
|
|
6999
|
+
sources: previous.sources
|
|
7000
|
+
});
|
|
7001
|
+
this.logger.warn("[PricingRefreshScheduler] background refresh failed; cached prices retained", {
|
|
7002
|
+
error: message
|
|
7003
|
+
});
|
|
7004
|
+
}
|
|
7005
|
+
}
|
|
7006
|
+
writeState(state) {
|
|
7007
|
+
const temporaryPath = `${this.statePath}.tmp`;
|
|
7008
|
+
writeFileSync8(temporaryPath, `${JSON.stringify(state, null, 2)}
|
|
7009
|
+
`, "utf8");
|
|
7010
|
+
renameSync4(temporaryPath, this.statePath);
|
|
5187
7011
|
}
|
|
5188
7012
|
};
|
|
7013
|
+
function finiteOrNull(value) {
|
|
7014
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
7015
|
+
}
|
|
5189
7016
|
|
|
5190
7017
|
// src/ports/JsonVoucherDb.ts
|
|
5191
|
-
import { existsSync as
|
|
7018
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
5192
7019
|
var JsonVoucherDb = class {
|
|
5193
7020
|
constructor(vouchersPath) {
|
|
5194
7021
|
this.vouchersPath = vouchersPath;
|
|
@@ -5266,25 +7093,26 @@ var JsonVoucherDb = class {
|
|
|
5266
7093
|
}
|
|
5267
7094
|
/** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
|
|
5268
7095
|
readRows() {
|
|
5269
|
-
if (!
|
|
7096
|
+
if (!existsSync11(this.vouchersPath)) return [];
|
|
5270
7097
|
try {
|
|
5271
|
-
const parsed = JSON.parse(
|
|
7098
|
+
const parsed = JSON.parse(readFileSync11(this.vouchersPath, "utf8"));
|
|
5272
7099
|
return Array.isArray(parsed) ? parsed : [];
|
|
5273
7100
|
} catch {
|
|
5274
7101
|
return [];
|
|
5275
7102
|
}
|
|
5276
7103
|
}
|
|
5277
7104
|
writeRows(rows) {
|
|
5278
|
-
|
|
7105
|
+
writeFileSync9(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
|
|
5279
7106
|
}
|
|
5280
7107
|
};
|
|
5281
7108
|
|
|
5282
7109
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5283
|
-
import { existsSync as
|
|
5284
|
-
import { dirname as
|
|
5285
|
-
import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
|
|
5286
|
-
import {
|
|
5287
|
-
import {
|
|
7110
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
|
|
7111
|
+
import { dirname as dirname6 } from "path";
|
|
7112
|
+
import { getSharedAccountHealth as getSharedAccountHealth2 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
|
|
7113
|
+
import { getSharedAccountAllowanceScheduling as getSharedAccountAllowanceScheduling3 } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
|
|
7114
|
+
import { fetchUpstream as fetchUpstream3 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
7115
|
+
import { getSharedIdentityStore as getSharedIdentityStore2 } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
|
|
5288
7116
|
import {
|
|
5289
7117
|
claudeOAuth as claudeOAuth2,
|
|
5290
7118
|
codexOAuth as codexOAuth2,
|
|
@@ -5292,33 +7120,9 @@ import {
|
|
|
5292
7120
|
} from "@omnicross/subscriptions";
|
|
5293
7121
|
|
|
5294
7122
|
// src/ports/account-sync.ts
|
|
5295
|
-
var IMPORT_EXPIRY_MARGIN_MS = 6e4;
|
|
5296
7123
|
function viewOf(tokens) {
|
|
5297
7124
|
return tokens;
|
|
5298
7125
|
}
|
|
5299
|
-
function decideExternalImport(captured, external, now = Date.now()) {
|
|
5300
|
-
if (!external?.accessToken) return "no-credential";
|
|
5301
|
-
const capturedRt = viewOf(captured).refreshToken;
|
|
5302
|
-
const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
|
|
5303
|
-
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
|
|
5304
|
-
return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
|
|
5305
|
-
}
|
|
5306
|
-
function buildImportedTokens(captured, external) {
|
|
5307
|
-
const imported = {
|
|
5308
|
-
...captured,
|
|
5309
|
-
accessToken: external.accessToken,
|
|
5310
|
-
status: "authorized",
|
|
5311
|
-
errorMessage: void 0,
|
|
5312
|
-
syncWarning: void 0,
|
|
5313
|
-
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5314
|
-
};
|
|
5315
|
-
if (external.refreshToken) imported.refreshToken = external.refreshToken;
|
|
5316
|
-
if (external.expiresAt) imported.expiresAt = external.expiresAt;
|
|
5317
|
-
else delete imported.expiresAt;
|
|
5318
|
-
if (external.idToken) imported.idToken = external.idToken;
|
|
5319
|
-
if (external.scopes) imported.scopes = external.scopes;
|
|
5320
|
-
return imported;
|
|
5321
|
-
}
|
|
5322
7126
|
function buildTokensFromExternal(provider, external) {
|
|
5323
7127
|
const base = {
|
|
5324
7128
|
authMethod: "oauth",
|
|
@@ -5339,14 +7143,6 @@ function buildTokensFromExternal(provider, external) {
|
|
|
5339
7143
|
if (external.idToken) tokens.idToken = external.idToken;
|
|
5340
7144
|
return tokens;
|
|
5341
7145
|
}
|
|
5342
|
-
function isExternalDivergent(stored, external) {
|
|
5343
|
-
if (!external?.accessToken || !external.refreshToken) return false;
|
|
5344
|
-
const view = viewOf(stored);
|
|
5345
|
-
if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
|
|
5346
|
-
const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
|
|
5347
|
-
const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
|
|
5348
|
-
return !Number.isFinite(storedExp) || externalExp > storedExp;
|
|
5349
|
-
}
|
|
5350
7146
|
function findDuplicateCredentialIds(accounts) {
|
|
5351
7147
|
const byCredential = /* @__PURE__ */ new Map();
|
|
5352
7148
|
for (const account of accounts) {
|
|
@@ -5365,11 +7161,11 @@ function findDuplicateCredentialIds(accounts) {
|
|
|
5365
7161
|
}
|
|
5366
7162
|
|
|
5367
7163
|
// src/ports/external-cli-credentials.ts
|
|
5368
|
-
import { existsSync as
|
|
5369
|
-
import { homedir as
|
|
5370
|
-
import { join as
|
|
5371
|
-
function externalStorePath(provider, home =
|
|
5372
|
-
return provider === "claude" ?
|
|
7164
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
|
|
7165
|
+
import { homedir as homedir3 } from "os";
|
|
7166
|
+
import { join as join6 } from "path";
|
|
7167
|
+
function externalStorePath(provider, home = homedir3()) {
|
|
7168
|
+
return provider === "claude" ? join6(home, ".claude", ".credentials.json") : join6(home, ".codex", "auth.json");
|
|
5373
7169
|
}
|
|
5374
7170
|
function decodeJwtExpiryMs(token) {
|
|
5375
7171
|
try {
|
|
@@ -5416,12 +7212,12 @@ function parseCodexTokensEnvelope(raw) {
|
|
|
5416
7212
|
}
|
|
5417
7213
|
return parsed;
|
|
5418
7214
|
}
|
|
5419
|
-
function readExternalCliCredentials(provider, home =
|
|
7215
|
+
function readExternalCliCredentials(provider, home = homedir3()) {
|
|
5420
7216
|
const path2 = externalStorePath(provider, home);
|
|
5421
|
-
if (!
|
|
7217
|
+
if (!existsSync12(path2)) return null;
|
|
5422
7218
|
let raw;
|
|
5423
7219
|
try {
|
|
5424
|
-
const parsed = JSON.parse(
|
|
7220
|
+
const parsed = JSON.parse(readFileSync12(path2, "utf8"));
|
|
5425
7221
|
raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
5426
7222
|
} catch {
|
|
5427
7223
|
return null;
|
|
@@ -5429,84 +7225,6 @@ function readExternalCliCredentials(provider, home = homedir2()) {
|
|
|
5429
7225
|
return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
|
|
5430
7226
|
}
|
|
5431
7227
|
|
|
5432
|
-
// src/ports/external-cli-store.ts
|
|
5433
|
-
import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync7 } from "fs";
|
|
5434
|
-
import { homedir as homedir3 } from "os";
|
|
5435
|
-
import { dirname as dirname3 } from "path";
|
|
5436
|
-
function markerPath(provider, home) {
|
|
5437
|
-
return `${externalStorePath(provider, home)}.omnicross-managed`;
|
|
5438
|
-
}
|
|
5439
|
-
function backupPath(provider, home) {
|
|
5440
|
-
return `${externalStorePath(provider, home)}.omnicross-backup`;
|
|
5441
|
-
}
|
|
5442
|
-
function buildClaudeOAuthEnvelope(tokens) {
|
|
5443
|
-
if (!tokens.accessToken) return null;
|
|
5444
|
-
const envelope = { accessToken: tokens.accessToken };
|
|
5445
|
-
if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
|
|
5446
|
-
if (tokens.expiresAt) {
|
|
5447
|
-
const ms = Date.parse(tokens.expiresAt);
|
|
5448
|
-
if (Number.isFinite(ms)) envelope.expiresAt = ms;
|
|
5449
|
-
}
|
|
5450
|
-
if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
|
|
5451
|
-
return envelope;
|
|
5452
|
-
}
|
|
5453
|
-
function buildCodexTokensEnvelope(tokens) {
|
|
5454
|
-
if (!tokens.accessToken && !tokens.idToken) return null;
|
|
5455
|
-
const envelope = { access_token: tokens.accessToken ?? "" };
|
|
5456
|
-
if (tokens.idToken) envelope.id_token = tokens.idToken;
|
|
5457
|
-
if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
|
|
5458
|
-
return envelope;
|
|
5459
|
-
}
|
|
5460
|
-
function readExistingObject(path2) {
|
|
5461
|
-
if (!existsSync9(path2)) return {};
|
|
5462
|
-
try {
|
|
5463
|
-
const parsed = JSON.parse(readFileSync9(path2, "utf8"));
|
|
5464
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5465
|
-
} catch {
|
|
5466
|
-
return {};
|
|
5467
|
-
}
|
|
5468
|
-
}
|
|
5469
|
-
function writeAtomic(path2, content) {
|
|
5470
|
-
mkdirSync2(dirname3(path2), { recursive: true });
|
|
5471
|
-
const temp = `${path2}.omnicross-tmp`;
|
|
5472
|
-
writeFileSync7(temp, content, "utf8");
|
|
5473
|
-
renameSync(temp, path2);
|
|
5474
|
-
}
|
|
5475
|
-
function createExternalCliStore(home = homedir3()) {
|
|
5476
|
-
return {
|
|
5477
|
-
readMarkerAccountId(provider) {
|
|
5478
|
-
const path2 = markerPath(provider, home);
|
|
5479
|
-
if (!existsSync9(path2)) return void 0;
|
|
5480
|
-
try {
|
|
5481
|
-
const parsed = JSON.parse(readFileSync9(path2, "utf8"));
|
|
5482
|
-
return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
|
|
5483
|
-
} catch {
|
|
5484
|
-
return void 0;
|
|
5485
|
-
}
|
|
5486
|
-
},
|
|
5487
|
-
writeMarker(provider, accountId) {
|
|
5488
|
-
writeAtomic(
|
|
5489
|
-
markerPath(provider, home),
|
|
5490
|
-
JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
5491
|
-
);
|
|
5492
|
-
},
|
|
5493
|
-
writeBack(provider, accountId, tokens) {
|
|
5494
|
-
const owner = this.readMarkerAccountId(provider);
|
|
5495
|
-
if (owner !== accountId) return false;
|
|
5496
|
-
const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
|
|
5497
|
-
if (!envelope) return false;
|
|
5498
|
-
const storePath = externalStorePath(provider, home);
|
|
5499
|
-
if (existsSync9(storePath) && !existsSync9(backupPath(provider, home))) {
|
|
5500
|
-
copyFileSync(storePath, backupPath(provider, home));
|
|
5501
|
-
}
|
|
5502
|
-
const existing = readExistingObject(storePath);
|
|
5503
|
-
const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
|
|
5504
|
-
writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
|
|
5505
|
-
return true;
|
|
5506
|
-
}
|
|
5507
|
-
};
|
|
5508
|
-
}
|
|
5509
|
-
|
|
5510
7228
|
// src/ports/JsonSubscriptionCredentialStore.ts
|
|
5511
7229
|
var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
|
|
5512
7230
|
var JsonSubscriptionCredentialStore = class {
|
|
@@ -5519,32 +7237,30 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5519
7237
|
* proxy-aware {@link fetchUpstream} that threads the
|
|
5520
7238
|
* `{ providerId, accountId }` ctx (upstream-proxy M1) so a
|
|
5521
7239
|
* per-account/per-provider proxy is honored on refresh exactly
|
|
5522
|
-
* as on relay
|
|
7240
|
+
* as on relay refresh egresses from the SAME proxy IP as the
|
|
5523
7241
|
* account's traffic. NOT used by any read/write path.
|
|
5524
7242
|
*/
|
|
5525
|
-
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials
|
|
7243
|
+
constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials) {
|
|
5526
7244
|
this.tokensPath = tokensPath;
|
|
5527
7245
|
this.box = box;
|
|
5528
7246
|
this.fetchImpl = fetchImpl;
|
|
5529
7247
|
this.externalCliReader = externalCliReader;
|
|
5530
|
-
this.externalCliStore = externalCliStore;
|
|
5531
7248
|
}
|
|
5532
7249
|
tokensPath;
|
|
5533
7250
|
box;
|
|
5534
7251
|
fetchImpl;
|
|
5535
7252
|
externalCliReader;
|
|
5536
|
-
externalCliStore;
|
|
5537
7253
|
/**
|
|
5538
7254
|
* The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
|
|
5539
7255
|
* TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
|
|
5540
7256
|
* through {@link fetchUpstream} with the account's `{ providerId, accountId }`
|
|
5541
|
-
* ctx so the per-account/provider proxy applies. `@internal`
|
|
7257
|
+
* ctx so the per-account/provider proxy applies. `@internal` also a test seam.
|
|
5542
7258
|
*/
|
|
5543
7259
|
buildRefreshFetch(providerId, accountId) {
|
|
5544
|
-
return this.fetchImpl ?? ((url, init) =>
|
|
7260
|
+
return this.fetchImpl ?? ((url, init) => fetchUpstream3(url, init, { providerId, accountId }));
|
|
5545
7261
|
}
|
|
5546
7262
|
/**
|
|
5547
|
-
* In-flight refresh coalescing
|
|
7263
|
+
* In-flight refresh coalescing. OAuth refresh tokens are
|
|
5548
7264
|
* SINGLE-USE: two concurrent refreshes of one account each spend the same
|
|
5549
7265
|
* token and the loser bricks a healthy account. Every refresh entry point
|
|
5550
7266
|
* (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
|
|
@@ -5559,13 +7275,13 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5559
7275
|
return run;
|
|
5560
7276
|
}
|
|
5561
7277
|
/** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
|
|
5562
|
-
* file is absent/corrupt). This is the hot read
|
|
7278
|
+
* file is absent/corrupt). This is the hot read the codex / gemini auth
|
|
5563
7279
|
* strategies pull `accessToken` / `expiresAt` / `status` from it. */
|
|
5564
7280
|
async getFullConfig() {
|
|
5565
7281
|
return this.readConfig();
|
|
5566
7282
|
}
|
|
5567
7283
|
/** Current Claude OAuth access token, or `null` when none is stored. No inline
|
|
5568
|
-
* refresh here
|
|
7284
|
+
* refresh here the lead-window / 401-retry refresh is driven by the
|
|
5569
7285
|
* subscription auth strategy, which calls `refreshClaudeToken` (now real). */
|
|
5570
7286
|
async getValidClaudeAccessToken() {
|
|
5571
7287
|
return this.readConfig().claude?.accessToken ?? null;
|
|
@@ -5590,13 +7306,14 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5590
7306
|
/**
|
|
5591
7307
|
* DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
|
|
5592
7308
|
* each provider's accounts to the secret-free `SubscriptionAccountSanitized`
|
|
5593
|
-
* shape (id/label/status/expiresAt/hasAccessToken/isActive)
|
|
7309
|
+
* shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
|
|
5594
7310
|
* Used by the admin accounts GET (secret-IN-never-OUT).
|
|
5595
7311
|
*/
|
|
5596
7312
|
async listSanitizedAccounts() {
|
|
5597
7313
|
const config = this.readConfig();
|
|
5598
|
-
const health2 =
|
|
5599
|
-
const
|
|
7314
|
+
const health2 = getSharedAccountHealth2();
|
|
7315
|
+
const allowanceScheduling = getSharedAccountAllowanceScheduling3();
|
|
7316
|
+
const identityStore = getSharedIdentityStore2();
|
|
5600
7317
|
const fingerprintOn = identityStore.isEnabled();
|
|
5601
7318
|
const now = Date.now();
|
|
5602
7319
|
const out = {};
|
|
@@ -5605,7 +7322,13 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5605
7322
|
if (sanitized.length === 0) continue;
|
|
5606
7323
|
for (const account of sanitized) {
|
|
5607
7324
|
const status = health2.getStatus(provider, account.id, now);
|
|
7325
|
+
const allowance = allowanceScheduling.preview(provider, account.id, account.priority ?? 50, now);
|
|
5608
7326
|
account.health = status.state;
|
|
7327
|
+
account.schedulable = account.enabled && status.state === "healthy" && allowance.schedulable;
|
|
7328
|
+
account.allowanceAction = allowance.action;
|
|
7329
|
+
account.allowanceEffectivePriority = allowance.effectivePriority;
|
|
7330
|
+
account.allowanceUsedPercent = allowance.usedPercent;
|
|
7331
|
+
account.allowanceResumeAt = allowance.resumeAt;
|
|
5609
7332
|
account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
|
|
5610
7333
|
if (fingerprintOn && provider === "claude") {
|
|
5611
7334
|
account.identityCaptured = identityStore.hasIdentity(provider, account.id);
|
|
@@ -5613,31 +7336,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5613
7336
|
account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
|
|
5614
7337
|
}
|
|
5615
7338
|
}
|
|
5616
|
-
out[provider] = this.
|
|
7339
|
+
out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
|
|
5617
7340
|
}
|
|
5618
7341
|
return out;
|
|
5619
7342
|
}
|
|
5620
7343
|
/**
|
|
5621
|
-
* List-time credential
|
|
5622
|
-
*
|
|
5623
|
-
* credential
|
|
5624
|
-
* rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
|
|
5625
|
-
* a failed refresh (`external-not-rotated`) takes precedence — it is the most
|
|
5626
|
-
* actionable state.
|
|
7344
|
+
* List-time managed-credential conflict warnings. Computed, not persisted:
|
|
7345
|
+
* `duplicate-token` is projected when two accounts of one provider share a
|
|
7346
|
+
* credential. This deliberately does not inspect either native CLI file.
|
|
5627
7347
|
*/
|
|
5628
|
-
|
|
7348
|
+
attachDuplicateWarnings(config, provider, sanitized) {
|
|
5629
7349
|
const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
|
|
5630
|
-
let divergentId;
|
|
5631
|
-
if (provider === "claude" || provider === "codex") {
|
|
5632
|
-
const active = getActiveAccount(config, provider);
|
|
5633
|
-
if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
|
|
5634
|
-
divergentId = active.id;
|
|
5635
|
-
}
|
|
5636
|
-
}
|
|
5637
|
-
if (duplicates.size === 0 && !divergentId) return sanitized;
|
|
5638
7350
|
return sanitized.map((account) => {
|
|
5639
|
-
const computed =
|
|
5640
|
-
|
|
7351
|
+
const computed = duplicates.has(account.id) ? "duplicate-token" : void 0;
|
|
7352
|
+
const persisted = account.syncWarning === "duplicate-token" ? account.syncWarning : void 0;
|
|
7353
|
+
if (!persisted && !computed) {
|
|
7354
|
+
const { syncWarning: _obsoleteWarning, ...withoutWarning } = account;
|
|
7355
|
+
return withoutWarning;
|
|
7356
|
+
}
|
|
7357
|
+
return { ...account, syncWarning: persisted ?? computed };
|
|
5641
7358
|
});
|
|
5642
7359
|
}
|
|
5643
7360
|
/** Read the external CLI store, never letting an fs/parse error escape. */
|
|
@@ -5650,11 +7367,11 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5650
7367
|
}
|
|
5651
7368
|
/**
|
|
5652
7369
|
* Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
|
|
5653
|
-
* the block has no refresh_token (setup-token / manual)
|
|
7370
|
+
* the block has no refresh_token (setup-token / manual) no upstream call, the
|
|
5654
7371
|
* block is untouched. Otherwise mint via the shared claude refresh flow and
|
|
5655
7372
|
* write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
|
|
5656
|
-
* On failure
|
|
5657
|
-
* errorMessage
|
|
7373
|
+
* On failure status:expired +
|
|
7374
|
+
* errorMessage `false`.
|
|
5658
7375
|
*/
|
|
5659
7376
|
async refreshClaudeToken() {
|
|
5660
7377
|
return this.coalesce("claude:active", async () => {
|
|
@@ -5679,19 +7396,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5679
7396
|
syncWarning: void 0
|
|
5680
7397
|
};
|
|
5681
7398
|
this.writeBackById("claude", capturedId, next);
|
|
5682
|
-
this.resyncExternal("claude", capturedId, next);
|
|
5683
7399
|
return true;
|
|
5684
7400
|
} catch (error) {
|
|
5685
|
-
if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
|
|
5686
|
-
const r = await claudeOAuth2.refreshAccessToken(rt, refreshFetch);
|
|
5687
|
-
return {
|
|
5688
|
-
accessToken: r.accessToken,
|
|
5689
|
-
refreshToken: r.refreshToken,
|
|
5690
|
-
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5691
|
-
};
|
|
5692
|
-
})) {
|
|
5693
|
-
return true;
|
|
5694
|
-
}
|
|
5695
7401
|
this.markExpiredById("claude", capturedId, claude, error);
|
|
5696
7402
|
return false;
|
|
5697
7403
|
}
|
|
@@ -5726,20 +7432,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5726
7432
|
syncWarning: void 0
|
|
5727
7433
|
};
|
|
5728
7434
|
this.writeBackById("codex", capturedId, next);
|
|
5729
|
-
this.resyncExternal("codex", capturedId, next);
|
|
5730
7435
|
return true;
|
|
5731
7436
|
} catch (error) {
|
|
5732
|
-
if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
|
|
5733
|
-
const r = await codexOAuth2.refreshAccessToken(rt, refreshFetch);
|
|
5734
|
-
return {
|
|
5735
|
-
accessToken: r.accessToken,
|
|
5736
|
-
refreshToken: r.refreshToken,
|
|
5737
|
-
idToken: r.idToken,
|
|
5738
|
-
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5739
|
-
};
|
|
5740
|
-
})) {
|
|
5741
|
-
return true;
|
|
5742
|
-
}
|
|
5743
7437
|
this.markExpiredById("codex", capturedId, codex, error);
|
|
5744
7438
|
return false;
|
|
5745
7439
|
}
|
|
@@ -5782,11 +7476,10 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5782
7476
|
});
|
|
5783
7477
|
}
|
|
5784
7478
|
/**
|
|
5785
|
-
* Refresh a SPECIFIC account by id (background scheduler sweep
|
|
5786
|
-
*
|
|
5787
|
-
*
|
|
5788
|
-
*
|
|
5789
|
-
* failure flags ONLY that account `expired`.
|
|
7479
|
+
* Refresh a SPECIFIC managed account by id (background scheduler sweep and
|
|
7480
|
+
* account-pool resolution). It uses only that account's stored refresh
|
|
7481
|
+
* token. Coalesced per `provider:id`; on failure flags ONLY that account
|
|
7482
|
+
* `expired`.
|
|
5790
7483
|
*/
|
|
5791
7484
|
async refreshAccountById(provider, id) {
|
|
5792
7485
|
return this.coalesce(`${provider}:${id}`, async () => {
|
|
@@ -5800,7 +7493,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5800
7493
|
const next = {
|
|
5801
7494
|
...captured,
|
|
5802
7495
|
accessToken: refreshed.accessToken,
|
|
5803
|
-
// Gemini's refresh response omits a new refresh token
|
|
7496
|
+
// Gemini's refresh response omits a new refresh token keep the captured.
|
|
5804
7497
|
refreshToken: refreshed.refreshToken ?? captured.refreshToken,
|
|
5805
7498
|
expiresAt: refreshed.expiresAt,
|
|
5806
7499
|
status: "authorized",
|
|
@@ -5810,7 +7503,6 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5810
7503
|
};
|
|
5811
7504
|
if (refreshed.idToken) next.idToken = refreshed.idToken;
|
|
5812
7505
|
this.writeBackById(provider, id, next);
|
|
5813
|
-
if (provider !== "gemini") this.resyncExternal(provider, id, next);
|
|
5814
7506
|
return true;
|
|
5815
7507
|
} catch (error) {
|
|
5816
7508
|
this.markExpiredById(provider, id, captured, error);
|
|
@@ -5818,7 +7510,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5818
7510
|
}
|
|
5819
7511
|
});
|
|
5820
7512
|
}
|
|
5821
|
-
//
|
|
7513
|
+
// By-id account-pool surface (subscription-account-scheduling, design D6)
|
|
5822
7514
|
/**
|
|
5823
7515
|
* Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
|
|
5824
7516
|
* provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
|
|
@@ -5851,7 +7543,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5851
7543
|
/**
|
|
5852
7544
|
* Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
|
|
5853
7545
|
* `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
|
|
5854
|
-
*
|
|
7546
|
+
* `false` (no refresh affordance).
|
|
5855
7547
|
*/
|
|
5856
7548
|
async refreshAccountToken(providerId, accountId) {
|
|
5857
7549
|
if (providerId === "opencodego") return false;
|
|
@@ -5873,7 +7565,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5873
7565
|
* (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
|
|
5874
7566
|
* whitelisted fingerprint headers; the token mirror is untouched); a no-op for
|
|
5875
7567
|
* an unknown id. Called by the identity store's persistence port on a first-seen
|
|
5876
|
-
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller
|
|
7568
|
+
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
|
|
5877
7569
|
* store's port wrapper swallows a rejection so the relay hot path is unaffected.
|
|
5878
7570
|
*/
|
|
5879
7571
|
async setAccountIdentity(providerId, accountId, identity) {
|
|
@@ -5898,7 +7590,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5898
7590
|
* DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
|
|
5899
7591
|
* the port). Passing `undefined` clears the override. Write-only password: when
|
|
5900
7592
|
* the incoming structured proxy omits the password but the account already had
|
|
5901
|
-
* one, the current (decrypted) password is preserved
|
|
7593
|
+
* one, the current (decrypted) password is preserved editing host/port never
|
|
5902
7594
|
* wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
|
|
5903
7595
|
*/
|
|
5904
7596
|
async setAccountProxy(providerId, accountId, proxy) {
|
|
@@ -5933,75 +7625,25 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
5933
7625
|
expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
|
|
5934
7626
|
};
|
|
5935
7627
|
}
|
|
5936
|
-
/**
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
* UI can tell "genuine revocation" apart from a plain refresh failure.
|
|
5944
|
-
*/
|
|
5945
|
-
async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
|
|
5946
|
-
const markerOwner = this.safeReadMarker(provider);
|
|
5947
|
-
if (markerOwner && markerOwner !== capturedId) return false;
|
|
5948
|
-
const external = this.safeReadExternal(provider);
|
|
5949
|
-
const decision = decideExternalImport(captured, external);
|
|
5950
|
-
if (decision === "not-rotated") {
|
|
5951
|
-
captured.syncWarning = "external-not-rotated";
|
|
5952
|
-
return false;
|
|
5953
|
-
}
|
|
5954
|
-
if (decision !== "import" || !external) return false;
|
|
5955
|
-
let imported = buildImportedTokens(
|
|
5956
|
-
captured,
|
|
5957
|
-
external
|
|
5958
|
-
);
|
|
5959
|
-
const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
|
|
5960
|
-
if (!accessStillValid) {
|
|
5961
|
-
try {
|
|
5962
|
-
const refreshed = await refreshWithToken(external.refreshToken);
|
|
5963
|
-
imported = {
|
|
5964
|
-
...imported,
|
|
5965
|
-
accessToken: refreshed.accessToken,
|
|
5966
|
-
refreshToken: refreshed.refreshToken ?? imported.refreshToken,
|
|
5967
|
-
expiresAt: refreshed.expiresAt,
|
|
5968
|
-
lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5969
|
-
};
|
|
5970
|
-
if (refreshed.idToken) imported.idToken = refreshed.idToken;
|
|
5971
|
-
} catch {
|
|
5972
|
-
return false;
|
|
5973
|
-
}
|
|
5974
|
-
}
|
|
5975
|
-
this.writeBackById(provider, capturedId, imported);
|
|
5976
|
-
this.resyncExternal(provider, capturedId, imported);
|
|
5977
|
-
return true;
|
|
5978
|
-
}
|
|
5979
|
-
/**
|
|
5980
|
-
* Marker-gated external write-back (external-cli-sync). After a successful
|
|
5981
|
-
* refresh of the account that OWNS the provider's native CLI store (imported
|
|
5982
|
-
* via `importExternalCliAccount`), push the rotated credential back into the
|
|
5983
|
-
* file — otherwise the daemon's refresh invalidates the single-use refresh
|
|
5984
|
-
* token and silently logs the bare CLI out. NON-FATAL: the internal store is
|
|
5985
|
-
* already persisted; a failed external write only leaves the file stale,
|
|
5986
|
-
* which the `external-divergent` warning surfaces.
|
|
5987
|
-
*/
|
|
5988
|
-
resyncExternal(provider, accountId, tokens) {
|
|
5989
|
-
try {
|
|
5990
|
-
this.externalCliStore.writeBack(provider, accountId, tokens);
|
|
5991
|
-
} catch {
|
|
5992
|
-
}
|
|
7628
|
+
/** Atomically patch one account's non-secret management metadata. */
|
|
7629
|
+
async patchAccountMetadata(providerId, accountId, patch) {
|
|
7630
|
+
const config = this.readConfig();
|
|
7631
|
+
const result = patchAccountMetadata(config, providerId, accountId, patch);
|
|
7632
|
+
if (!result.ok) return result;
|
|
7633
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7634
|
+
return result;
|
|
5993
7635
|
}
|
|
5994
|
-
/**
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
7636
|
+
/** Validate every target, then persist one all-or-nothing batch mutation. */
|
|
7637
|
+
async batchManageAccounts(refs, mutation) {
|
|
7638
|
+
const config = this.readConfig();
|
|
7639
|
+
const result = batchManageAccounts(config, refs, mutation);
|
|
7640
|
+
if (!result.ok) return result;
|
|
7641
|
+
this.persist({ ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7642
|
+
return result;
|
|
6001
7643
|
}
|
|
6002
7644
|
/**
|
|
6003
7645
|
* DAEMON-ONLY (admin import button): which providers have a usable external
|
|
6004
|
-
* CLI credential on THIS machine. Pure detection
|
|
7646
|
+
* CLI credential on THIS machine. Pure detection reads the native files,
|
|
6005
7647
|
* never mutates anything, never returns a token.
|
|
6006
7648
|
*/
|
|
6007
7649
|
async listExternalCliAvailability() {
|
|
@@ -6012,21 +7654,22 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6012
7654
|
}
|
|
6013
7655
|
/**
|
|
6014
7656
|
* DAEMON-ONLY (admin import button): import the external CLI's current login
|
|
6015
|
-
* as a NEW account (+ activate)
|
|
6016
|
-
*
|
|
6017
|
-
*
|
|
6018
|
-
*
|
|
7657
|
+
* as a NEW account (+ activate). This is a COPY-ONLY import: Omnicross never
|
|
7658
|
+
* claims, writes, moves, restores, or deletes the native CLI credential file
|
|
7659
|
+
* or any legacy `.omnicross-managed` marker/backup beside it. Subsequent
|
|
7660
|
+
* refreshes persist only Omnicross's encrypted token store.
|
|
6019
7661
|
*/
|
|
6020
7662
|
async importExternalCliAccount(provider, label) {
|
|
6021
7663
|
const external = this.safeReadExternal(provider);
|
|
6022
7664
|
if (!external?.accessToken) return { ok: false, reason: "no-credential" };
|
|
6023
7665
|
const tokens = buildTokensFromExternal(provider, external);
|
|
6024
7666
|
const result = await this.appendProviderAccount(provider, tokens, label);
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
7667
|
+
return {
|
|
7668
|
+
ok: true,
|
|
7669
|
+
id: result.id,
|
|
7670
|
+
nativeCredentialMode: "read-only",
|
|
7671
|
+
refreshWritesNativeCredentials: false
|
|
7672
|
+
};
|
|
6030
7673
|
}
|
|
6031
7674
|
/**
|
|
6032
7675
|
* Materialize a lazily-synthesized account id to disk (design D3). On a legacy
|
|
@@ -6059,7 +7702,8 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6059
7702
|
this.writeBackById(providerId, capturedId, {
|
|
6060
7703
|
...block,
|
|
6061
7704
|
status: "expired",
|
|
6062
|
-
errorMessage
|
|
7705
|
+
errorMessage,
|
|
7706
|
+
syncWarning: "syncWarning" in block && block.syncWarning === "duplicate-token" ? "duplicate-token" : void 0
|
|
6063
7707
|
});
|
|
6064
7708
|
}
|
|
6065
7709
|
/**
|
|
@@ -6068,7 +7712,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6068
7712
|
* `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
|
|
6069
7713
|
* OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
|
|
6070
7714
|
* tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
|
|
6071
|
-
* so a first-ever write still produces a valid config. No cache
|
|
7715
|
+
* so a first-ever write still produces a valid config. No cache the next read
|
|
6072
7716
|
* sees this write.
|
|
6073
7717
|
*/
|
|
6074
7718
|
async writeProviderTokens(providerId, config) {
|
|
@@ -6078,7 +7722,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6078
7722
|
}
|
|
6079
7723
|
/**
|
|
6080
7724
|
* DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
|
|
6081
|
-
* (optional label) and set it active, then re-derive the mirror
|
|
7725
|
+
* (optional label) and set it active, then re-derive the mirror used by
|
|
6082
7726
|
* `omnicross login <provider> --label` to add an account instead of overwriting.
|
|
6083
7727
|
*/
|
|
6084
7728
|
async appendProviderAccount(providerId, config, label) {
|
|
@@ -6112,7 +7756,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6112
7756
|
}
|
|
6113
7757
|
/**
|
|
6114
7758
|
* DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
|
|
6115
|
-
* rejects an unknown id. Label-only
|
|
7759
|
+
* rejects an unknown id. Label-only no token material is read or written
|
|
6116
7760
|
* (the secret-free invariant holds).
|
|
6117
7761
|
*/
|
|
6118
7762
|
async renameAccount(providerId, id, label) {
|
|
@@ -6135,12 +7779,12 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6135
7779
|
}
|
|
6136
7780
|
/** Write the merged config to disk as pretty JSON (mkdir parent if needed).
|
|
6137
7781
|
* Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
|
|
6138
|
-
*
|
|
6139
|
-
* write
|
|
7782
|
+
* `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
7783
|
+
* write incl. child 4's future refresh writes lands encrypted. */
|
|
6140
7784
|
persist(config) {
|
|
6141
|
-
|
|
7785
|
+
mkdirSync4(dirname6(this.tokensPath), { recursive: true });
|
|
6142
7786
|
const encrypted = encryptTokens(config, this.box);
|
|
6143
|
-
|
|
7787
|
+
writeFileSync10(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
|
|
6144
7788
|
}
|
|
6145
7789
|
/**
|
|
6146
7790
|
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
@@ -6148,18 +7792,18 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6148
7792
|
* subscription bearer path is byte-identical).
|
|
6149
7793
|
*
|
|
6150
7794
|
* The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
|
|
6151
|
-
* file
|
|
7795
|
+
* file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
|
|
6152
7796
|
* wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
|
|
6153
|
-
* box's clear, secret-free error (secrets spec "
|
|
6154
|
-
* SHALL fail-fast, SHALL NOT
|
|
6155
|
-
* tokens" and silently send the WRONG bearer upstream
|
|
7797
|
+
* box's clear, secret-free error (secrets spec "/ UX":
|
|
7798
|
+
* SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
|
|
7799
|
+
* tokens" and silently send the WRONG bearer upstream 401). Mirrors
|
|
6156
7800
|
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
6157
7801
|
*/
|
|
6158
7802
|
readConfig() {
|
|
6159
|
-
if (!
|
|
7803
|
+
if (!existsSync13(this.tokensPath)) return { updatedAt: "" };
|
|
6160
7804
|
let parsed;
|
|
6161
7805
|
try {
|
|
6162
|
-
const raw = JSON.parse(
|
|
7806
|
+
const raw = JSON.parse(readFileSync13(this.tokensPath, "utf8"));
|
|
6163
7807
|
parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
6164
7808
|
} catch {
|
|
6165
7809
|
parsed = null;
|
|
@@ -6171,7 +7815,7 @@ var JsonSubscriptionCredentialStore = class {
|
|
|
6171
7815
|
};
|
|
6172
7816
|
|
|
6173
7817
|
// src/AccountHealthProbeScheduler.ts
|
|
6174
|
-
import { fetchUpstream as
|
|
7818
|
+
import { fetchUpstream as fetchUpstream4 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
6175
7819
|
|
|
6176
7820
|
// src/probe/ProbeStrategy.ts
|
|
6177
7821
|
var PROVIDER_PROBE_PLANS = {
|
|
@@ -6214,7 +7858,7 @@ var AccountHealthProbeScheduler = class {
|
|
|
6214
7858
|
this.logger = logger;
|
|
6215
7859
|
this.config = config;
|
|
6216
7860
|
this.now = opts.now ?? Date.now;
|
|
6217
|
-
this.fetchImpl = opts.fetchImpl ??
|
|
7861
|
+
this.fetchImpl = opts.fetchImpl ?? fetchUpstream4;
|
|
6218
7862
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6219
7863
|
this.planFor = opts.planFor ?? probePlanFor;
|
|
6220
7864
|
}
|
|
@@ -6487,8 +8131,8 @@ var AccountHealthSweeper = class {
|
|
|
6487
8131
|
};
|
|
6488
8132
|
|
|
6489
8133
|
// src/audit/AuditPruneSweeper.ts
|
|
6490
|
-
import { existsSync as
|
|
6491
|
-
import { join as
|
|
8134
|
+
import { existsSync as existsSync14, readdirSync, unlinkSync as unlinkSync3 } from "fs";
|
|
8135
|
+
import { join as join7 } from "path";
|
|
6492
8136
|
|
|
6493
8137
|
// src/audit/auditFiles.ts
|
|
6494
8138
|
var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
@@ -6562,7 +8206,7 @@ var AuditPruneSweeper = class {
|
|
|
6562
8206
|
if (!this.config.enabled || this.sweeping) return 0;
|
|
6563
8207
|
this.sweeping = true;
|
|
6564
8208
|
try {
|
|
6565
|
-
if (!
|
|
8209
|
+
if (!existsSync14(this.auditDir)) return 0;
|
|
6566
8210
|
const today = new Date(this.now());
|
|
6567
8211
|
const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
|
|
6568
8212
|
const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
|
|
@@ -6571,7 +8215,7 @@ var AuditPruneSweeper = class {
|
|
|
6571
8215
|
const dateMs = auditFileDateMs(file);
|
|
6572
8216
|
if (dateMs === null || dateMs >= cutoff) continue;
|
|
6573
8217
|
try {
|
|
6574
|
-
|
|
8218
|
+
unlinkSync3(join7(this.auditDir, file));
|
|
6575
8219
|
removed += 1;
|
|
6576
8220
|
} catch (error) {
|
|
6577
8221
|
this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
|
|
@@ -6594,26 +8238,26 @@ var AuditPruneSweeper = class {
|
|
|
6594
8238
|
};
|
|
6595
8239
|
|
|
6596
8240
|
// src/audit/auditReader.ts
|
|
6597
|
-
import { existsSync as
|
|
6598
|
-
import { join as
|
|
8241
|
+
import { existsSync as existsSync15, readdirSync as readdirSync2, readFileSync as readFileSync14 } from "fs";
|
|
8242
|
+
import { join as join8 } from "path";
|
|
6599
8243
|
var DEFAULT_LIMIT = 200;
|
|
6600
8244
|
var MAX_LIMIT = 2e3;
|
|
6601
|
-
function readAuditRecords(auditDir2,
|
|
6602
|
-
if (!
|
|
8245
|
+
function readAuditRecords(auditDir2, query2 = {}) {
|
|
8246
|
+
if (!existsSync15(auditDir2)) return [];
|
|
6603
8247
|
let files;
|
|
6604
8248
|
try {
|
|
6605
8249
|
files = readdirSync2(auditDir2).filter((f) => AUDIT_FILE_RE.test(f));
|
|
6606
8250
|
} catch {
|
|
6607
8251
|
return [];
|
|
6608
8252
|
}
|
|
6609
|
-
const from = typeof
|
|
6610
|
-
const to = typeof
|
|
6611
|
-
const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(
|
|
8253
|
+
const from = typeof query2.from === "number" ? query2.from : -Infinity;
|
|
8254
|
+
const to = typeof query2.to === "number" ? query2.to : Infinity;
|
|
8255
|
+
const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query2.limit ?? DEFAULT_LIMIT)));
|
|
6612
8256
|
const matched = [];
|
|
6613
8257
|
for (const file of files.sort().reverse()) {
|
|
6614
8258
|
let raw;
|
|
6615
8259
|
try {
|
|
6616
|
-
raw =
|
|
8260
|
+
raw = readFileSync14(join8(auditDir2, file), "utf8");
|
|
6617
8261
|
} catch {
|
|
6618
8262
|
continue;
|
|
6619
8263
|
}
|
|
@@ -6627,7 +8271,7 @@ function readAuditRecords(auditDir2, query = {}) {
|
|
|
6627
8271
|
continue;
|
|
6628
8272
|
}
|
|
6629
8273
|
if (!isAuditRecord(rec)) continue;
|
|
6630
|
-
if (
|
|
8274
|
+
if (query2.keyId !== void 0 && rec.keyId !== query2.keyId) continue;
|
|
6631
8275
|
if (rec.ts < from || rec.ts > to) continue;
|
|
6632
8276
|
matched.push(rec);
|
|
6633
8277
|
}
|
|
@@ -6642,8 +8286,8 @@ function isAuditRecord(value) {
|
|
|
6642
8286
|
}
|
|
6643
8287
|
|
|
6644
8288
|
// src/audit/AuditWriter.ts
|
|
6645
|
-
import { appendFileSync as appendFileSync2, mkdirSync as
|
|
6646
|
-
import { join as
|
|
8289
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
8290
|
+
import { join as join9 } from "path";
|
|
6647
8291
|
var AuditWriter = class {
|
|
6648
8292
|
constructor(auditDir2, logger, defer = (fn) => setTimeout(fn, 0)) {
|
|
6649
8293
|
this.auditDir = auditDir2;
|
|
@@ -6676,19 +8320,19 @@ var AuditWriter = class {
|
|
|
6676
8320
|
*/
|
|
6677
8321
|
appendNow(record) {
|
|
6678
8322
|
if (!this.dirEnsured) {
|
|
6679
|
-
|
|
8323
|
+
mkdirSync5(this.auditDir, { recursive: true });
|
|
6680
8324
|
this.dirEnsured = true;
|
|
6681
8325
|
}
|
|
6682
|
-
const file =
|
|
8326
|
+
const file = join9(this.auditDir, auditFileName(record.ts));
|
|
6683
8327
|
appendFileSync2(file, JSON.stringify(record) + "\n", "utf8");
|
|
6684
8328
|
}
|
|
6685
8329
|
};
|
|
6686
8330
|
|
|
6687
8331
|
// src/billing/BillingPublisher.ts
|
|
6688
|
-
import { appendFileSync as appendFileSync3, mkdirSync as
|
|
8332
|
+
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync6 } from "fs";
|
|
6689
8333
|
import { createHmac } from "crypto";
|
|
6690
|
-
import { join as
|
|
6691
|
-
import { fetchUpstream as
|
|
8334
|
+
import { join as join10 } from "path";
|
|
8335
|
+
import { fetchUpstream as fetchUpstream5 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
6692
8336
|
|
|
6693
8337
|
// src/billing/billingFiles.ts
|
|
6694
8338
|
var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
@@ -6711,7 +8355,7 @@ var BillingPublisher = class {
|
|
|
6711
8355
|
constructor(billingDir, logger, opts = {}) {
|
|
6712
8356
|
this.billingDir = billingDir;
|
|
6713
8357
|
this.logger = logger;
|
|
6714
|
-
this.fetchImpl = opts.fetchImpl ?? ((url, init) =>
|
|
8358
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
|
|
6715
8359
|
this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
|
|
6716
8360
|
this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
|
|
6717
8361
|
this.now = opts.now ?? Date.now;
|
|
@@ -6758,7 +8402,7 @@ var BillingPublisher = class {
|
|
|
6758
8402
|
*/
|
|
6759
8403
|
appendNow(event) {
|
|
6760
8404
|
this.ensureDir();
|
|
6761
|
-
const file =
|
|
8405
|
+
const file = join10(this.billingDir, billingFileName(event.ts));
|
|
6762
8406
|
appendFileSync3(file, JSON.stringify(event) + "\n", "utf8");
|
|
6763
8407
|
}
|
|
6764
8408
|
/**
|
|
@@ -6808,7 +8452,7 @@ var BillingPublisher = class {
|
|
|
6808
8452
|
markDelivered(event) {
|
|
6809
8453
|
try {
|
|
6810
8454
|
this.ensureDir();
|
|
6811
|
-
const file =
|
|
8455
|
+
const file = join10(this.billingDir, deliveredFileName(event.ts));
|
|
6812
8456
|
appendFileSync3(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
|
|
6813
8457
|
} catch (error) {
|
|
6814
8458
|
this.logger.warn("[BillingPublisher] failed to append delivery marker", {
|
|
@@ -6818,17 +8462,17 @@ var BillingPublisher = class {
|
|
|
6818
8462
|
}
|
|
6819
8463
|
ensureDir() {
|
|
6820
8464
|
if (this.dirEnsured) return;
|
|
6821
|
-
|
|
8465
|
+
mkdirSync6(this.billingDir, { recursive: true });
|
|
6822
8466
|
this.dirEnsured = true;
|
|
6823
8467
|
}
|
|
6824
8468
|
};
|
|
6825
8469
|
|
|
6826
8470
|
// src/billing/billingReader.ts
|
|
6827
|
-
import { existsSync as
|
|
6828
|
-
import { join as
|
|
8471
|
+
import { existsSync as existsSync16, readdirSync as readdirSync3, readFileSync as readFileSync15 } from "fs";
|
|
8472
|
+
import { join as join11 } from "path";
|
|
6829
8473
|
function readBillingLedger(billingDir) {
|
|
6830
8474
|
const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
|
|
6831
|
-
if (!
|
|
8475
|
+
if (!existsSync16(billingDir)) return view;
|
|
6832
8476
|
let files;
|
|
6833
8477
|
try {
|
|
6834
8478
|
files = readdirSync3(billingDir);
|
|
@@ -6862,7 +8506,7 @@ function readBillingStatus(billingDir) {
|
|
|
6862
8506
|
function parseLines(dir, file) {
|
|
6863
8507
|
let raw;
|
|
6864
8508
|
try {
|
|
6865
|
-
raw =
|
|
8509
|
+
raw = readFileSync15(join11(dir, file), "utf8");
|
|
6866
8510
|
} catch {
|
|
6867
8511
|
return [];
|
|
6868
8512
|
}
|
|
@@ -7017,8 +8661,9 @@ var TokenRefreshScheduler = class {
|
|
|
7017
8661
|
const expiresAt = Date.parse(t.expiresAt);
|
|
7018
8662
|
return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
|
|
7019
8663
|
}
|
|
7020
|
-
/** Refresh one account; failures are logged, never thrown
|
|
7021
|
-
*
|
|
8664
|
+
/** Refresh one managed account; failures are logged, never thrown. The
|
|
8665
|
+
* store marks only the targeted account `expired` on a failed refresh.
|
|
8666
|
+
*/
|
|
7022
8667
|
async refreshOne(provider, id, isActive) {
|
|
7023
8668
|
try {
|
|
7024
8669
|
const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
|
|
@@ -7049,7 +8694,7 @@ var TokenRefreshScheduler = class {
|
|
|
7049
8694
|
|
|
7050
8695
|
// src/webhook/WebhookDispatcher.ts
|
|
7051
8696
|
import { createHmac as createHmac2 } from "crypto";
|
|
7052
|
-
import { fetchUpstream as
|
|
8697
|
+
import { fetchUpstream as fetchUpstream6 } from "@omnicross/core/pipeline/upstreamFetch";
|
|
7053
8698
|
var WEBHOOK_MAX_ATTEMPTS = 3;
|
|
7054
8699
|
var WEBHOOK_QUEUE_MAX = 1e3;
|
|
7055
8700
|
var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
|
|
@@ -7069,7 +8714,7 @@ var WebhookDispatcher = class {
|
|
|
7069
8714
|
sleep;
|
|
7070
8715
|
now;
|
|
7071
8716
|
constructor(opts = {}) {
|
|
7072
|
-
this.fetchImpl = opts.fetchImpl ?? ((url, init) =>
|
|
8717
|
+
this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream6(url, init));
|
|
7073
8718
|
this.logger = opts.logger;
|
|
7074
8719
|
this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
|
|
7075
8720
|
this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
|
|
@@ -7223,11 +8868,32 @@ function buildDaemon(config, paths) {
|
|
|
7223
8868
|
setSecretBox(secretBox3);
|
|
7224
8869
|
setSecretBox2(secretBox3);
|
|
7225
8870
|
const decryptedConfig = decryptConfigSecrets(config, secretBox3);
|
|
8871
|
+
const accountAllowanceStore = new AccountAllowanceStore3(
|
|
8872
|
+
Date.now,
|
|
8873
|
+
void 0,
|
|
8874
|
+
new JsonAccountAllowancePersistence(defaultAccountAllowancePath(paths.configPath))
|
|
8875
|
+
);
|
|
8876
|
+
setSharedAccountAllowanceStore(accountAllowanceStore);
|
|
8877
|
+
getSharedAccountAllowanceScheduling4().configure(
|
|
8878
|
+
normalizeServerConfig(decryptedConfig.server).allowanceScheduling
|
|
8879
|
+
);
|
|
7226
8880
|
const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
|
|
7227
8881
|
const keyDb = new JsonOutboundKeyDb(paths.keysPath);
|
|
7228
8882
|
const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
|
|
7229
8883
|
const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
|
|
8884
|
+
const integrationStateStore = new IntegrationStateStore(
|
|
8885
|
+
defaultIntegrationsPath(paths.configPath),
|
|
8886
|
+
secretBox3
|
|
8887
|
+
);
|
|
7230
8888
|
const credentialStore = new JsonSubscriptionCredentialStore(paths.tokensPath, secretBox3);
|
|
8889
|
+
const accountAllowanceService = new AccountAllowanceService(credentialStore, accountAllowanceStore);
|
|
8890
|
+
const claudeAllowanceRefreshScheduler = new ClaudeAllowanceRefreshScheduler(
|
|
8891
|
+
accountAllowanceService,
|
|
8892
|
+
logger
|
|
8893
|
+
);
|
|
8894
|
+
claudeAllowanceRefreshScheduler.configure(
|
|
8895
|
+
normalizeServerConfig(decryptedConfig.server).allowanceScheduling
|
|
8896
|
+
);
|
|
7231
8897
|
const subscriptionAccounts = new SubscriptionAccountService(credentialStore);
|
|
7232
8898
|
setSubscriptionAccountService(subscriptionAccounts);
|
|
7233
8899
|
const subscriptionRegistry = new SubscriptionProviderRegistry(
|
|
@@ -7256,7 +8922,17 @@ function buildDaemon(config, paths) {
|
|
|
7256
8922
|
}
|
|
7257
8923
|
);
|
|
7258
8924
|
const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
|
|
7259
|
-
const pricingEngine = new PricingEngine(pricingStore, logger
|
|
8925
|
+
const pricingEngine = new PricingEngine(pricingStore, logger, {
|
|
8926
|
+
// Catalog egress follows the same global/env proxy policy as every other
|
|
8927
|
+
// daemon upstream call; no provider/account override applies here.
|
|
8928
|
+
fetchImpl: ((input, init) => fetchUpstream7(String(input), init ?? {}))
|
|
8929
|
+
});
|
|
8930
|
+
const pricingRefreshScheduler = new PricingRefreshScheduler(
|
|
8931
|
+
pricingEngine,
|
|
8932
|
+
pricingStore,
|
|
8933
|
+
defaultPricingRefreshStatePath(paths.configPath),
|
|
8934
|
+
logger
|
|
8935
|
+
);
|
|
7260
8936
|
const usageEventStore = new JsonlUsageEventStore(
|
|
7261
8937
|
defaultUsageEventsPath(paths.configPath),
|
|
7262
8938
|
async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
|
|
@@ -7269,7 +8945,7 @@ function buildDaemon(config, paths) {
|
|
|
7269
8945
|
llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
|
|
7270
8946
|
const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
|
|
7271
8947
|
credentialStore,
|
|
7272
|
-
|
|
8948
|
+
getSharedAccountHealth3(),
|
|
7273
8949
|
logger,
|
|
7274
8950
|
DEFAULT_ACCOUNT_PROBE
|
|
7275
8951
|
);
|
|
@@ -7317,6 +8993,9 @@ function buildDaemon(config, paths) {
|
|
|
7317
8993
|
settingsStore,
|
|
7318
8994
|
outboundApiServer,
|
|
7319
8995
|
subscriptionAccounts,
|
|
8996
|
+
accountAllowanceService,
|
|
8997
|
+
allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
|
|
8998
|
+
accountProbeService: accountHealthProbeScheduler,
|
|
7320
8999
|
// Least-authority token WRITER (design D4) — the concrete credential store
|
|
7321
9000
|
// exposes `writeProviderTokens` / `clearProvider` as daemon-only methods (NOT
|
|
7322
9001
|
// on the `SubscriptionCredentialStore` port). The admin API sees ONLY these two
|
|
@@ -7337,7 +9016,7 @@ function buildDaemon(config, paths) {
|
|
|
7337
9016
|
// inject a mock so no real token endpoint is hit.
|
|
7338
9017
|
// upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
|
|
7339
9018
|
// helper so interactive login honors a configured proxy (global/env layers).
|
|
7340
|
-
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) =>
|
|
9019
|
+
oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream7(url, init)),
|
|
7341
9020
|
subscriptionAccountAppender: credentialStore,
|
|
7342
9021
|
// Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
|
|
7343
9022
|
// + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
|
|
@@ -7355,6 +9034,16 @@ function buildDaemon(config, paths) {
|
|
|
7355
9034
|
cliTerminalOpener: paths.cliTerminalOpener,
|
|
7356
9035
|
cliPathProbe: paths.cliPathProbe,
|
|
7357
9036
|
cliCommandRunner: paths.cliCommandRunner,
|
|
9037
|
+
integrationManagerFactory: () => {
|
|
9038
|
+
const live = outboundApiServer.getStatus();
|
|
9039
|
+
const port = live.port || decryptedConfig.server?.port || DEFAULT_OUTBOUND_PORT;
|
|
9040
|
+
return new IntegrationManager({
|
|
9041
|
+
configPath: paths.configPath,
|
|
9042
|
+
gatewayBaseUrl: live.loopbackUrl ?? `http://127.0.0.1:${port}`,
|
|
9043
|
+
keyDb,
|
|
9044
|
+
stateStore: integrationStateStore
|
|
9045
|
+
});
|
|
9046
|
+
},
|
|
7358
9047
|
// Usage/pricing admin surface (usage-pricing child): stats queries go
|
|
7359
9048
|
// through the recorder facade, pricing mutations through the engine, and
|
|
7360
9049
|
// the row DELETE through the concrete store (delete is store-local — the
|
|
@@ -7379,16 +9068,16 @@ function buildDaemon(config, paths) {
|
|
|
7379
9068
|
// date-rotated audit store. Bound to the store dir here so the AdminServer
|
|
7380
9069
|
// carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
|
|
7381
9070
|
// NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
|
|
7382
|
-
auditReader: (
|
|
9071
|
+
auditReader: (query2) => readAuditRecords(auditDir2, query2),
|
|
7383
9072
|
// billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
|
|
7384
9073
|
// secret-free total/delivered/pending counts of the durable ledger.
|
|
7385
9074
|
billingStatusReader: () => readBillingStatus(billingDir)
|
|
7386
9075
|
});
|
|
7387
9076
|
const webhookDispatcher = new WebhookDispatcher({
|
|
7388
9077
|
logger,
|
|
7389
|
-
fetchImpl: (url, init) =>
|
|
9078
|
+
fetchImpl: (url, init) => fetchUpstream7(url, init)
|
|
7390
9079
|
});
|
|
7391
|
-
setWebhookRuntime(webhookDispatcher,
|
|
9080
|
+
setWebhookRuntime(webhookDispatcher, getSharedAccountHealth3());
|
|
7392
9081
|
const auditWriter = new AuditWriter(auditDir2, logger);
|
|
7393
9082
|
const auditPruneSweeper = new AuditPruneSweeper(auditDir2, logger, DEFAULT_AUDIT_CONFIG);
|
|
7394
9083
|
setAuditRuntime(auditWriter, auditPruneSweeper, auditDir2);
|
|
@@ -7403,7 +9092,7 @@ function buildDaemon(config, paths) {
|
|
|
7403
9092
|
const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
|
|
7404
9093
|
const accountHealthSweeper = new AccountHealthSweeper(
|
|
7405
9094
|
credentialStore,
|
|
7406
|
-
|
|
9095
|
+
getSharedAccountHealth3(),
|
|
7407
9096
|
logger
|
|
7408
9097
|
);
|
|
7409
9098
|
return {
|
|
@@ -7418,8 +9107,11 @@ function buildDaemon(config, paths) {
|
|
|
7418
9107
|
credentialStore,
|
|
7419
9108
|
subscriptionRegistry,
|
|
7420
9109
|
subscriptionAccounts,
|
|
9110
|
+
accountAllowanceService,
|
|
9111
|
+
claudeAllowanceRefreshScheduler,
|
|
7421
9112
|
pricingStore,
|
|
7422
9113
|
pricingEngine,
|
|
9114
|
+
pricingRefreshScheduler,
|
|
7423
9115
|
usageRecorder,
|
|
7424
9116
|
adminServer,
|
|
7425
9117
|
tokenRefreshScheduler,
|
|
@@ -7447,10 +9139,12 @@ function resetDaemonSingletonsForTests() {
|
|
|
7447
9139
|
resetAuditRuntimeForTests();
|
|
7448
9140
|
resetBillingRuntimeForTests();
|
|
7449
9141
|
__resetSharedIdentityStoreForTests();
|
|
9142
|
+
__resetSharedAccountAllowanceStoreForTests();
|
|
9143
|
+
__resetSharedAccountAllowanceSchedulingForTests();
|
|
7450
9144
|
}
|
|
7451
9145
|
function isTokensStoreReadable(tokensPath) {
|
|
7452
9146
|
try {
|
|
7453
|
-
if (!
|
|
9147
|
+
if (!existsSync17(tokensPath)) return true;
|
|
7454
9148
|
accessSync(tokensPath, fsConstants.R_OK);
|
|
7455
9149
|
return true;
|
|
7456
9150
|
} catch {
|
|
@@ -7497,6 +9191,9 @@ function inferApiFormat(provider) {
|
|
|
7497
9191
|
if (hay.includes("generativelanguage") || hay.includes("gemini") || hay.includes("google")) {
|
|
7498
9192
|
return { format: "gemini", ambiguous: false };
|
|
7499
9193
|
}
|
|
9194
|
+
if (hay.includes("/responses")) {
|
|
9195
|
+
return { format: "openai-response", ambiguous: false };
|
|
9196
|
+
}
|
|
7500
9197
|
if (hay.includes("openai") || hay.includes("/v1") || hay.includes("chat/completions")) {
|
|
7501
9198
|
return { format: "openai", ambiguous: false };
|
|
7502
9199
|
}
|