@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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/bootstrap.ts
2
- import { accessSync, constants as fsConstants, existsSync as existsSync14 } from "fs";
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
- getOutboundApiServer
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 getSharedAccountHealth2 } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
14
- import { fetchUpstream as fetchUpstream6, setUpstreamProxyResolver } from "@omnicross/core/pipeline/upstreamFetch";
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 query = {};
624
+ const query2 = {};
158
625
  const keyId = url.searchParams.get("keyId");
159
- if (keyId && keyId.trim()) query.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) query.from = from;
628
+ if (from !== void 0) query2.from = from;
162
629
  const to = intParam(url.searchParams.get("to"));
163
- if (to !== void 0) query.to = to;
630
+ if (to !== void 0) query2.to = to;
164
631
  const limit = intParam(url.searchParams.get("limit"));
165
- if (limit !== void 0) query.limit = limit;
166
- const records = reader ? reader(query) : [];
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((resolve) => {
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
- resolve(parsed && typeof parsed === "object" ? parsed : {});
712
+ resolve2(parsed && typeof parsed === "object" ? parsed : {});
246
713
  } catch {
247
- resolve({});
714
+ resolve2({});
248
715
  }
249
716
  });
250
- req.on("error", () => resolve({}));
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 { fetchUpstream } from "@omnicross/core/pipeline/upstreamFetch";
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 readFileSync2, writeFileSync as writeFileSync2 } from "fs";
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 = readFileSync(path2);
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
- mkdirSync(dirname(path2), { recursive: true });
374
- writeFileSync(path2, key, { mode: 384 });
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 (existsSync(keyFilePath)) {
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 = ["openai", "anthropic", "gemini"];
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
- transformer: validateTransformer(p["transformer"]),
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 = readFileSync2(path2, "utf8");
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
- writeFileSync2(path2, JSON.stringify(toWrite, null, 2) + "\n", "utf8");
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": null,
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 existsSync2 } from "fs";
1324
- import { delimiter, join as join2 } from "path";
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 = join2(seg, candidate);
1355
- if (existsSync2(full)) return full;
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((resolve) => {
2716
+ var defaultCommandRunner = (command) => new Promise((resolve2) => {
1443
2717
  exec(command, { timeout: 18e4 }, (err5, _stdout, stderr) => {
1444
- if (err5) resolve({ ok: false, error: stderr.trim() || err5.message });
1445
- else resolve({ ok: true });
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 = randomUUID();
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 isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
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 (!isPlainObject(audit)) {
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 isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
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 (!isPlainObject2(billing)) {
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((resolve, reject) => {
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 resolve({});
3074
+ if (!raw.trim()) return resolve2({});
1711
3075
  try {
1712
3076
  const parsed = JSON.parse(raw);
1713
- resolve(parsed && typeof parsed === "object" ? parsed : {});
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 isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
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 (!isPlainObject3(webhook)) {
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 (!isPlainObject3(raw)) {
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 join3 } from "path";
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 ? join3(auditDir, "upstream-trace.jsonl") : null);
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 randomUUID2 } from "crypto";
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 = randomUUID2();
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(query) {
2475
- const startTs = parseFiniteInt(query.get("startTs"));
2476
- const endTs = parseFiniteInt(query.get("endTs"));
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, query, deps) {
2490
- const range = parseRange(query);
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 = query.get("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(query, deps) {
2585
- const providerId = query.get("providerId")?.trim() ?? "";
2586
- const modelId = query.get("modelId")?.trim() ?? "";
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
- decisions.push({ providerId, modelId, action });
2645
- pendingIncoming.set(key, incoming);
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
- const resolution = await deps.pricingEngine.resolveConflicts(decisions, pendingIncoming);
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((resolve, reject) => {
4140
+ return new Promise((resolve2, reject) => {
2654
4141
  const chunks = [];
2655
4142
  req.on("data", (chunk) => chunks.push(chunk));
2656
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
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 writeJson2(res, status, body) {
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
- writeJson2(res, status, { error: { type: "admin_api_error", message } });
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
- writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, 200, { apiKey: row.apiKey ?? "" });
4375
+ return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
2884
4376
  }
2885
4377
  if (method === "GET") {
2886
- return writeJson2(res, 200, { providers: cfg.providers.map(toProviderView) });
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 writeJson2(res, 201, { provider: toProviderView(provider) });
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 writeJson2(res, 200, { provider: toProviderView(updated) });
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 writeJson2(res, 200, { ok: true });
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 writeJson2(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
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 writeJson2(res, 200, { models: [], unsupportedFormat: true });
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 fetchUpstream(url, { method: "GET", headers }, { providerId: "byo" });
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 writeJson2(res, 200, {
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 writeJson2(res, 200, { models });
4471
+ return writeJson3(res, 200, { models });
2980
4472
  } catch (err5) {
2981
4473
  const message = err5 instanceof Error ? err5.message : String(err5);
2982
- return writeJson2(res, 200, { models: [], error: `discovery failed: ${message}` });
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 writeJson2(res, 200, { ok: false, unsupportedFormat: true });
4485
+ return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
2994
4486
  }
2995
4487
  const resolvedKey = resolveEnvKey(row.apiKey);
2996
4488
  if (!resolvedKey) {
2997
- return writeJson2(res, 200, { ok: false, message: "no API key configured for this provider" });
4489
+ return writeJson3(res, 200, { ok: false, message: "no API key configured for this provider" });
2998
4490
  }
2999
- const url = row.baseUrl.replace(/\/+$/, "");
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 fetchUpstream(
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 writeJson2(res, 200, { ok: false, status: response.status, latencyMs, message });
4528
+ return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
3033
4529
  }
3034
- return writeJson2(res, 200, {
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 writeJson2(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
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 writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
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 writeJson2(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
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 writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
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 writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
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 writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
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") return null;
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 writeJson2(res, 200, { presets, excluded });
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 writeJson2(res, 200, { keys: rows.map(toKeyInfo) });
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 writeJson2(res, 200, { keys });
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 writeJson2(res, 201, {
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 writeJson2(res, ok ? 200 : 404, { ok });
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 writeJson2(res, ok ? 200 : 404, { ok, enabled });
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 writeJson2(res, ok ? 200 : 404, { ok, maxConcurrency: value });
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 writeJson2(res, ok ? 200 : 404, { ok });
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 isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
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 (!isPlainObject4(umq)) {
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 (!isPlainObject4(cq)) {
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 (!isPlainObject4(ah)) {
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 writeJson2(res, 200, { server });
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
- if (merged.enabled) {
3492
- const missing = validateServerModelConfig(merged);
3493
- if (missing.length > 0) {
3494
- if (deps.outboundApiServer.getStatus().running) {
3495
- await deps.outboundApiServer.stop();
3496
- }
3497
- return writeJson2(res, 200, {
3498
- server: merged,
3499
- error: { code: "incomplete-model-config", missing }
3500
- });
3501
- }
3502
- }
3503
- try {
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 writeJson2(res, 200, { accounts, providerAccounts, externalCli });
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, 200, status2 ? { account: status2 } : { ok: true });
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 writeJson2(res, 200, { ok: true, account: status2 ?? void 0 });
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 writeJson2(res, 200, { ok, account: status2 ?? void 0 });
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 writeJson2(res, 200, { ok: true });
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 writeJson2(res, 200, { ok: true });
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 writeJson2(res, 200, { ok: true });
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 writeJson2(res, 200, { ok: true });
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 writeJson2(res, 200, { ok: true });
5243
+ return writeJson3(res, 200, { ok: true });
3645
5244
  }
3646
- if (method === "DELETE" && rest.length >= 2) {
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
- return writeJson2(res, 200, { ok: true });
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
- return writeJson2(res, 200, { ok: true });
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 writeJson2(res, 200, status ? { account: status } : { ok: true });
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 writeJson2(res, result.status, result.body);
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 = serverConfig.endpoints.map((e) => {
3710
- if (isKindMappedEndpoint(e.endpoint)) {
3711
- return { endpoint: e.endpoint, kinds: e.modelMap ?? {}, useSubscription: e.useSubscription };
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 (e.endpoint === "chat") {
3714
- return { endpoint: e.endpoint, models: e.models ?? [], useSubscription: e.useSubscription };
5381
+ if (endpoint === "chat") {
5382
+ return {
5383
+ endpoint,
5384
+ models: [...new Set(routes.flatMap((route) => route.models ?? []))],
5385
+ useSubscription
5386
+ };
3715
5387
  }
3716
- return { endpoint: e.endpoint, model: e.defaultModel ?? "", useSubscription: e.useSubscription };
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 writeJson2(res, 200, { ...status, endpoints, queueStatus });
5396
+ return writeJson3(res, 200, { ...status, endpoints, queueStatus });
3721
5397
  }
3722
- return writeJson2(res, 200, { ...status, endpoints });
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, isRecord(payload) ? payload : {});
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 isRecord(v) {
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((resolve) => {
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
- resolve();
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
- resolve();
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 existsSync3, statSync } from "fs";
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 existsSync3(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
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 existsSync3(path.join(dist, "index.html")) ? dist : null;
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 (!existsSync3(target) || statSync(target).isDirectory()) {
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.5" : "0.0.0-dev";
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((resolve, reject) => {
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(resolve, reject);
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
- resolve(addr.port);
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((resolve) => {
4028
- server.close(() => resolve());
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((resolve, reject) => {
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, () => resolve(code));
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 dirname2, join as join4 } from "path";
5946
+ import { dirname as dirname5, join as join5 } from "path";
4271
5947
  function defaultVouchersPath(configPath) {
4272
- return join4(dirname2(configPath), "vouchers.json");
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 join4(dirname2(configPath), "pricing.json");
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 join4(dirname2(configPath), "usage-events.jsonl");
5963
+ return join5(dirname5(configPath), "usage-events.jsonl");
4279
5964
  }
4280
5965
  function defaultAuditDir(configPath) {
4281
- return join4(dirname2(configPath), "audit");
5966
+ return join5(dirname5(configPath), "audit");
4282
5967
  }
4283
5968
  function defaultBillingDir(configPath) {
4284
- return join4(dirname2(configPath), "billing");
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 || row.apiFormat === "openai") return null;
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(effectiveRefs),
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 = row.apiFormat === "openai" ? void 0 : { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
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((resolve) => stream.end(() => resolve()));
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 readFileSync3, writeFileSync as writeFileSync3 } from "fs";
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
- writeFileSync3(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
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 = readFileSync3(this.configPath, "utf8");
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 randomUUID3 } from "crypto";
4654
- import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
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: randomUUID3(),
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(query) {
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: query.endTs })) {
4769
- if (row.apiKeyId !== query.apiKeyId) continue;
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 >= query.dayStartTs) dailyUsd += row.costUsd;
4772
- if (row.ts >= query.weekStartTs) weeklyUsd += row.costUsd;
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 (!existsSync4(this.eventsPath)) return [];
6541
+ if (!existsSync7(this.eventsPath)) return [];
4858
6542
  let raw;
4859
6543
  try {
4860
- raw = readFileSync4(this.eventsPath, "utf8");
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 existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
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 (!existsSync5(this.keysPath)) return [];
6734
+ if (!existsSync8(this.keysPath)) return [];
5048
6735
  try {
5049
- const parsed = JSON.parse(readFileSync5(this.keysPath, "utf8"));
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
- writeFileSync4(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
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 existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
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 (source 'litellm'). ONE file write
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" : "litellm",
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 (!existsSync6(this.pricingPath)) return [];
6883
+ if (!existsSync9(this.pricingPath)) return [];
5178
6884
  try {
5179
- const parsed = JSON.parse(readFileSync6(this.pricingPath, "utf8"));
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
- writeFileSync5(this.pricingPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
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 existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
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 (!existsSync7(this.vouchersPath)) return [];
7096
+ if (!existsSync11(this.vouchersPath)) return [];
5270
7097
  try {
5271
- const parsed = JSON.parse(readFileSync7(this.vouchersPath, "utf8"));
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
- writeFileSync6(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
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 existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
5284
- import { dirname as dirname4 } from "path";
5285
- import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
5286
- import { fetchUpstream as fetchUpstream2 } from "@omnicross/core/pipeline/upstreamFetch";
5287
- import { getSharedIdentityStore } from "@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore";
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 existsSync8, readFileSync as readFileSync8 } from "fs";
5369
- import { homedir as homedir2 } from "os";
5370
- import { join as join5 } from "path";
5371
- function externalStorePath(provider, home = homedir2()) {
5372
- return provider === "claude" ? join5(home, ".claude", ".credentials.json") : join5(home, ".codex", "auth.json");
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 = homedir2()) {
7215
+ function readExternalCliCredentials(provider, home = homedir3()) {
5420
7216
  const path2 = externalStorePath(provider, home);
5421
- if (!existsSync8(path2)) return null;
7217
+ if (!existsSync12(path2)) return null;
5422
7218
  let raw;
5423
7219
  try {
5424
- const parsed = JSON.parse(readFileSync8(path2, "utf8"));
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 refresh egresses from the SAME proxy IP as the
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, externalCliStore = createExternalCliStore()) {
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` also a test seam.
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) => fetchUpstream2(url, init, { providerId, accountId }));
7260
+ return this.fetchImpl ?? ((url, init) => fetchUpstream3(url, init, { providerId, accountId }));
5545
7261
  }
5546
7262
  /**
5547
- * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
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 the codex / gemini auth
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 the lead-window / 401-retry refresh is driven by the
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) NEVER a token.
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 = getSharedAccountHealth();
5599
- const identityStore = getSharedIdentityStore();
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.attachSyncWarnings(config, provider, sanitized);
7339
+ out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
5617
7340
  }
5618
7341
  return out;
5619
7342
  }
5620
7343
  /**
5621
- * List-time credential-conflict warnings (external-cli-sync). Computed, not
5622
- * persisted: (a) `duplicate-token` when two accounts of one provider share a
5623
- * credential, (b) `external-divergent` when the external CLI native store has
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
- attachSyncWarnings(config, provider, sanitized) {
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 = account.id === divergentId ? "external-divergent" : duplicates.has(account.id) ? "duplicate-token" : void 0;
5640
- return { ...account, syncWarning: account.syncWarning ?? computed };
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) no upstream call, the
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 status:expired +
5657
- * errorMessage `false`.
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
- * external-cli-sync). Unlike the active-account refreshers it does NOT
5787
- * attempt the external-import fallback the external CLI file's lineage can
5788
- * only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
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 keep the captured.
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
- // ── By-id account-pool surface (subscription-account-scheduling, design D6) ──
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
- * `false` (no refresh affordance).
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 the
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 editing host/port never
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
- * External-import fallback for a FAILED active-account refresh
5938
- * (external-cli-sync). Reads the CLI native store; imports when the external
5939
- * lineage ROTATED (different refresh token) or its access token is still
5940
- * valid. When the imported access token is already expired it refreshes once
5941
- * with the rotated refresh token. A `not-rotated` outcome persists the
5942
- * `external-not-rotated` warning on the (about-to-be-expired) account so the
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
- /** Read the marker's owning account id, never letting an fs error escape. */
5995
- safeReadMarker(provider) {
5996
- try {
5997
- return this.externalCliStore.readMarkerAccountId(provider);
5998
- } catch {
5999
- return void 0;
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 reads the native files,
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), and take MANAGED ownership of the native
6016
- * store (marker) so subsequent refreshes write back keeping the bare CLI
6017
- * and the daemon on the same live credential instead of silently killing one
6018
- * side's single-use refresh token.
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
- try {
6026
- this.externalCliStore.writeMarker(provider, result.id);
6027
- } catch {
6028
- }
6029
- return { ok: true, id: result.id };
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 the next read
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 used by
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 no token material is read or written
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
- * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
6139
- * write incl. child 4's future refresh writes lands encrypted. */
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
- mkdirSync3(dirname4(this.tokensPath), { recursive: true });
7785
+ mkdirSync4(dirname6(this.tokensPath), { recursive: true });
6142
7786
  const encrypted = encryptTokens(config, this.box);
6143
- writeFileSync8(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
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 empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
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 "错误密钥 / 篡改的解密失败 UX":
6154
- * SHALL fail-fast, SHALL NOT 静默降级 — a swallowed decrypt would report "no
6155
- * tokens" and silently send the WRONG bearer upstream 401). Mirrors
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 (!existsSync10(this.tokensPath)) return { updatedAt: "" };
7803
+ if (!existsSync13(this.tokensPath)) return { updatedAt: "" };
6160
7804
  let parsed;
6161
7805
  try {
6162
- const raw = JSON.parse(readFileSync10(this.tokensPath, "utf8"));
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 fetchUpstream3 } from "@omnicross/core/pipeline/upstreamFetch";
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 ?? fetchUpstream3;
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 existsSync11, readdirSync, unlinkSync } from "fs";
6491
- import { join as join6 } from "path";
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 (!existsSync11(this.auditDir)) return 0;
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
- unlinkSync(join6(this.auditDir, file));
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 existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
6598
- import { join as join7 } from "path";
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, query = {}) {
6602
- if (!existsSync12(auditDir2)) return [];
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 query.from === "number" ? query.from : -Infinity;
6610
- const to = typeof query.to === "number" ? query.to : Infinity;
6611
- const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit ?? DEFAULT_LIMIT)));
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 = readFileSync11(join7(auditDir2, file), "utf8");
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 (query.keyId !== void 0 && rec.keyId !== query.keyId) continue;
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 mkdirSync4 } from "fs";
6646
- import { join as join8 } from "path";
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
- mkdirSync4(this.auditDir, { recursive: true });
8323
+ mkdirSync5(this.auditDir, { recursive: true });
6680
8324
  this.dirEnsured = true;
6681
8325
  }
6682
- const file = join8(this.auditDir, auditFileName(record.ts));
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 mkdirSync5 } from "fs";
8332
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync6 } from "fs";
6689
8333
  import { createHmac } from "crypto";
6690
- import { join as join9 } from "path";
6691
- import { fetchUpstream as fetchUpstream4 } from "@omnicross/core/pipeline/upstreamFetch";
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) => fetchUpstream4(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 = join9(this.billingDir, billingFileName(event.ts));
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 = join9(this.billingDir, deliveredFileName(event.ts));
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
- mkdirSync5(this.billingDir, { recursive: true });
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 existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync12 } from "fs";
6828
- import { join as join10 } from "path";
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 (!existsSync13(billingDir)) return view;
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 = readFileSync12(join10(dir, file), "utf8");
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 (the store has
7021
- * already flagged the account `expired`). */
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 fetchUpstream5 } from "@omnicross/core/pipeline/upstreamFetch";
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) => fetchUpstream5(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
- getSharedAccountHealth2(),
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) => fetchUpstream6(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: (query) => readAuditRecords(auditDir2, query),
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) => fetchUpstream6(url, init)
9078
+ fetchImpl: (url, init) => fetchUpstream7(url, init)
7390
9079
  });
7391
- setWebhookRuntime(webhookDispatcher, getSharedAccountHealth2());
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
- getSharedAccountHealth2(),
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 (!existsSync14(tokensPath)) return true;
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
  }