@omnicross/daemon 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,12 +3252,16 @@ function preserveWebhookSecrets(incoming, current) {
1888
3252
  }
1889
3253
 
1890
3254
  // src/audit/auditRuntime.ts
3255
+ import { join as join4 } from "path";
1891
3256
  import { setAuditCaptureConfig, setAuditSink } from "@omnicross/core/pipeline/auditSink";
3257
+ import { setUpstreamTracePath } from "@omnicross/core/pipeline/upstreamTrace";
1892
3258
  var writer = null;
1893
3259
  var sweeper = null;
1894
- function setAuditRuntime(w, s) {
3260
+ var auditDir = "";
3261
+ function setAuditRuntime(w, s, dir) {
1895
3262
  writer = w;
1896
3263
  sweeper = s;
3264
+ auditDir = dir;
1897
3265
  }
1898
3266
  function applyAuditConfig(config) {
1899
3267
  const enabled = config?.enabled === true && writer !== null;
@@ -1905,9 +3273,11 @@ function applyAuditConfig(config) {
1905
3273
  sweeper.configure(config);
1906
3274
  sweeper.start();
1907
3275
  }
3276
+ setUpstreamTracePath(config.captureBodies ? join4(auditDir, "upstream-trace.jsonl") : null);
1908
3277
  } else {
1909
3278
  setAuditCaptureConfig(null);
1910
3279
  setAuditSink(null);
3280
+ setUpstreamTracePath(null);
1911
3281
  if (sweeper) {
1912
3282
  if (config) sweeper.configure(config);
1913
3283
  sweeper.dispose();
@@ -1917,9 +3287,11 @@ function applyAuditConfig(config) {
1917
3287
  function resetAuditRuntimeForTests() {
1918
3288
  setAuditCaptureConfig(null);
1919
3289
  setAuditSink(null);
3290
+ setUpstreamTracePath(null);
1920
3291
  if (sweeper) sweeper.dispose();
1921
3292
  writer = null;
1922
3293
  sweeper = null;
3294
+ auditDir = "";
1923
3295
  }
1924
3296
 
1925
3297
  // src/billing/billingRuntime.ts
@@ -1959,7 +3331,7 @@ function resetBillingRuntimeForTests() {
1959
3331
  }
1960
3332
 
1961
3333
  // src/ports/account-multi.ts
1962
- import { randomUUID as randomUUID2 } from "crypto";
3334
+ import { randomUUID as randomUUID3 } from "crypto";
1963
3335
  var PROVIDER_KEYS = {
1964
3336
  claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
1965
3337
  codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
@@ -2025,7 +3397,7 @@ function migrateLazily(config) {
2025
3397
  }
2026
3398
  function addAccount(config, p, tokens, label) {
2027
3399
  const accounts = [...getAccounts(config, p)];
2028
- const id = randomUUID2();
3400
+ const id = randomUUID3();
2029
3401
  accounts.push({
2030
3402
  id,
2031
3403
  label: label ?? `Account ${accounts.length + 1}`,
@@ -2101,9 +3473,13 @@ function sanitizeAccounts(config, p) {
2101
3473
  const activeId = getActiveId(config, p);
2102
3474
  return accounts.map((a) => {
2103
3475
  const t = a.tokens;
3476
+ const enabled = a.enabled !== false;
2104
3477
  return {
2105
3478
  id: a.id,
2106
3479
  label: a.label,
3480
+ enabled,
3481
+ group: a.group?.trim() || p,
3482
+ tags: a.tags ?? [],
2107
3483
  status: t.status ?? "unconfigured",
2108
3484
  authMethod: t.authMethod,
2109
3485
  subscriptionLevel: t.subscriptionLevel,
@@ -2112,6 +3488,8 @@ function sanitizeAccounts(config, p) {
2112
3488
  isSetupToken: t.isSetupToken,
2113
3489
  hasAccessToken: !!(t.accessToken || t.apiKey),
2114
3490
  isActive: a.id === activeId,
3491
+ schedulable: enabled,
3492
+ errorMessage: sanitizeDiagnosticMessage(t.errorMessage),
2115
3493
  // Scheduling metadata (subscription-account-scheduling): editable priority
2116
3494
  // (default 50 shown when unset) + display-only lastUsedAt. Secret-free.
2117
3495
  priority: a.priority,
@@ -2126,6 +3504,54 @@ function sanitizeAccounts(config, p) {
2126
3504
  };
2127
3505
  });
2128
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
+ }
2129
3555
  function renameAccount(config, p, id, label) {
2130
3556
  const accounts = getAccounts(config, p);
2131
3557
  if (!accounts.some((a) => a.id === id)) return { ok: false };
@@ -2463,9 +3889,9 @@ function parseFiniteInt(raw) {
2463
3889
  const n = Number(raw);
2464
3890
  return Number.isFinite(n) && Number.isInteger(n) ? n : null;
2465
3891
  }
2466
- function parseRange(query) {
2467
- const startTs = parseFiniteInt(query.get("startTs"));
2468
- const endTs = parseFiniteInt(query.get("endTs"));
3892
+ function parseRange(query2) {
3893
+ const startTs = parseFiniteInt(query2.get("startTs"));
3894
+ const endTs = parseFiniteInt(query2.get("endTs"));
2469
3895
  if (startTs === null || endTs === null) {
2470
3896
  return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
2471
3897
  }
@@ -2478,8 +3904,8 @@ var BUCKET_SPAN_MS = {
2478
3904
  month: 28 * 864e5
2479
3905
  };
2480
3906
  var MAX_TIMESERIES_BUCKETS = 2e3;
2481
- async function handleUsageGet(view, query, deps) {
2482
- const range = parseRange(query);
3907
+ async function handleUsageGet(view, query2, deps) {
3908
+ const range = parseRange(query2);
2483
3909
  if (!isRange(range)) return range;
2484
3910
  switch (view) {
2485
3911
  case "totals":
@@ -2487,7 +3913,7 @@ async function handleUsageGet(view, query, deps) {
2487
3913
  case "by-model":
2488
3914
  return { status: 200, body: await deps.usageRecorder.getByModel(range) };
2489
3915
  case "timeseries": {
2490
- const bucket = query.get("bucket");
3916
+ const bucket = query2.get("bucket");
2491
3917
  if (bucket !== "hour" && bucket !== "day" && bucket !== "month") {
2492
3918
  return err4(400, "bucket must be one of 'hour', 'day', 'month'");
2493
3919
  }
@@ -2573,9 +3999,9 @@ async function handlePricingUpsert(body, deps) {
2573
3999
  const entry = await deps.pricingEngine.upsertManual(input);
2574
4000
  return { status: 200, body: { entry } };
2575
4001
  }
2576
- async function handlePricingDelete(query, deps) {
2577
- const providerId = query.get("providerId")?.trim() ?? "";
2578
- 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() ?? "";
2579
4005
  if (!providerId || !modelId) {
2580
4006
  return err4(400, "delete requires providerId and modelId query params");
2581
4007
  }
@@ -2592,7 +4018,8 @@ async function handlePricingFetchLatest(deps) {
2592
4018
  appliedCount: result.applied.length,
2593
4019
  conflicts: result.conflicts,
2594
4020
  fetchedAt: result.fetchedAt,
2595
- sourceUrl: result.sourceUrl
4021
+ sourceUrl: result.sourceUrl,
4022
+ sources: result.sources
2596
4023
  }
2597
4024
  };
2598
4025
  } catch (e) {
@@ -2628,24 +4055,92 @@ async function handlePricingResolveConflicts(body, deps) {
2628
4055
  if (incoming.providerId !== providerId || incoming.modelId !== modelId) {
2629
4056
  return err4(400, "resolution providerId/modelId must match the echoed incoming entry");
2630
4057
  }
2631
- const key = `${providerId}::${modelId}`;
2632
- if (action === "overwrite" && !userEditedKeys.has(key)) {
2633
- staleCount += 1;
2634
- 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");
2635
4127
  }
2636
- decisions.push({ providerId, modelId, action });
2637
- 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 });
2638
4134
  }
2639
- const resolution = await deps.pricingEngine.resolveConflicts(decisions, pendingIncoming);
2640
- return { status: 200, body: { ...resolution, staleCount } };
4135
+ return writeError(res, 405, `method ${method} not allowed on account allowances`);
2641
4136
  }
2642
4137
 
2643
4138
  // src/admin/adminApi.ts
2644
4139
  function readBody(req) {
2645
- return new Promise((resolve, reject) => {
4140
+ return new Promise((resolve2, reject) => {
2646
4141
  const chunks = [];
2647
4142
  req.on("data", (chunk) => chunks.push(chunk));
2648
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
4143
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
2649
4144
  req.on("error", reject);
2650
4145
  });
2651
4146
  }
@@ -2659,12 +4154,12 @@ async function readJsonBody3(req) {
2659
4154
  return {};
2660
4155
  }
2661
4156
  }
2662
- function writeJson2(res, status, body) {
4157
+ function writeJson3(res, status, body) {
2663
4158
  res.writeHead(status, { "Content-Type": "application/json" });
2664
4159
  res.end(JSON.stringify(body));
2665
4160
  }
2666
4161
  function writeJsonError(res, status, message) {
2667
- writeJson2(res, status, { error: { type: "admin_api_error", message } });
4162
+ writeJson3(res, status, { error: { type: "admin_api_error", message } });
2668
4163
  }
2669
4164
  function maskProviderApiKey(apiKey) {
2670
4165
  if (!apiKey) return "";
@@ -2681,6 +4176,9 @@ function toKeyInfo(row) {
2681
4176
  createdAt: row.createdAt,
2682
4177
  lastUsedAt: row.lastUsedAt,
2683
4178
  revoked: row.revokedAt !== null,
4179
+ kind: row.kind,
4180
+ allowedEndpoints: row.allowedEndpoints,
4181
+ loopbackOnly: row.loopbackOnly,
2684
4182
  maxConcurrency: row.maxConcurrency,
2685
4183
  // Key-policy envelope (outbound-key-policy) — all secret-free scalar fields;
2686
4184
  // the UI reads them to render + pre-fill the policy editor.
@@ -2766,6 +4264,8 @@ async function handleAdminApi(req, res, path2, deps) {
2766
4264
  return await handleAccounts(req, res, method, rest, deps);
2767
4265
  case "cli":
2768
4266
  return await handleCli(req, res, method, rest, deps);
4267
+ case "integrations":
4268
+ return await handleIntegrations(req, res, method, rest, deps);
2769
4269
  case "status":
2770
4270
  return await handleStatus(res, method, deps);
2771
4271
  case "playground":
@@ -2793,7 +4293,7 @@ function requestQuery(req) {
2793
4293
  return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
2794
4294
  }
2795
4295
  function writeResult(res, result) {
2796
- writeJson2(res, result.status, result.body);
4296
+ writeJson3(res, result.status, result.body);
2797
4297
  }
2798
4298
  async function handleUsage(req, res, method, rest, deps) {
2799
4299
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
@@ -2802,7 +4302,7 @@ async function handleUsage(req, res, method, rest, deps) {
2802
4302
  async function handleDashboardRoute(res, method, deps) {
2803
4303
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on dashboard`);
2804
4304
  const result = await handleDashboard(deps);
2805
- return writeJson2(res, result.status, result.body);
4305
+ return writeJson3(res, result.status, result.body);
2806
4306
  }
2807
4307
  async function handlePricing(req, res, method, rest, deps) {
2808
4308
  if (rest.length === 0) {
@@ -2835,13 +4335,13 @@ async function handleMigrationExport(req, res, method, deps) {
2835
4335
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on export`);
2836
4336
  const body = await readJsonBody3(req);
2837
4337
  const result = await handleExport(body, migrationDeps(deps));
2838
- return writeJson2(res, result.status, result.body);
4338
+ return writeJson3(res, result.status, result.body);
2839
4339
  }
2840
4340
  async function handleMigrationImport(req, res, method, deps) {
2841
4341
  if (method !== "POST") return writeJsonError(res, 405, `method ${method} not allowed on import`);
2842
4342
  const body = await readJsonBody3(req);
2843
4343
  const result = await handleImport(body, migrationDeps(deps));
2844
- return writeJson2(res, result.status, result.body);
4344
+ return writeJson3(res, result.status, result.body);
2845
4345
  }
2846
4346
  async function handleProviders(req, res, method, rest, deps) {
2847
4347
  const cfg = loadConfig(deps.configPath);
@@ -2872,10 +4372,10 @@ async function handleProviders(req, res, method, rest, deps) {
2872
4372
  if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
2873
4373
  const row = cfg.providers.find((p) => p.id === rest[0]);
2874
4374
  if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
2875
- return writeJson2(res, 200, { apiKey: row.apiKey ?? "" });
4375
+ return writeJson3(res, 200, { apiKey: row.apiKey ?? "" });
2876
4376
  }
2877
4377
  if (method === "GET") {
2878
- return writeJson2(res, 200, { providers: cfg.providers.map(toProviderView) });
4378
+ return writeJson3(res, 200, { providers: cfg.providers.map(toProviderView) });
2879
4379
  }
2880
4380
  if (method === "POST") {
2881
4381
  const body = await readJsonBody3(req);
@@ -2886,7 +4386,7 @@ async function handleProviders(req, res, method, rest, deps) {
2886
4386
  }
2887
4387
  cfg.providers.push(provider);
2888
4388
  persistProviders(cfg, deps);
2889
- return writeJson2(res, 201, { provider: toProviderView(provider) });
4389
+ return writeJson3(res, 201, { provider: toProviderView(provider) });
2890
4390
  }
2891
4391
  const id = rest[0];
2892
4392
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -2899,12 +4399,12 @@ async function handleProviders(req, res, method, rest, deps) {
2899
4399
  if (!updated) return writeJsonError(res, 400, "invalid provider (apiFormat, baseUrl required)");
2900
4400
  cfg.providers[idx] = updated;
2901
4401
  persistProviders(cfg, deps);
2902
- return writeJson2(res, 200, { provider: toProviderView(updated) });
4402
+ return writeJson3(res, 200, { provider: toProviderView(updated) });
2903
4403
  }
2904
4404
  if (method === "DELETE") {
2905
4405
  cfg.providers.splice(idx, 1);
2906
4406
  persistProviders(cfg, deps);
2907
- return writeJson2(res, 200, { ok: true });
4407
+ return writeJson3(res, 200, { ok: true });
2908
4408
  }
2909
4409
  return writeJsonError(res, 405, `method ${method} not allowed on providers`);
2910
4410
  }
@@ -2937,14 +4437,14 @@ async function handleProviderReorder(req, res, cfg, deps) {
2937
4437
  }
2938
4438
  cfg.providers = reordered;
2939
4439
  persistProviders(cfg, deps);
2940
- return writeJson2(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
4440
+ return writeJson3(res, 200, { ok: true, providers: cfg.providers.map(toProviderView) });
2941
4441
  }
2942
4442
  async function handleDiscoverModels(res, id, cfg) {
2943
4443
  if (!id) return writeJsonError(res, 400, "provider id required in path");
2944
4444
  const row = cfg.providers.find((p) => p.id === id);
2945
4445
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
2946
- if (row.apiFormat !== "openai") {
2947
- return writeJson2(res, 200, { models: [], unsupportedFormat: true });
4446
+ if (row.apiFormat !== "openai" && row.apiFormat !== "openai-response") {
4447
+ return writeJson3(res, 200, { models: [], unsupportedFormat: true });
2948
4448
  }
2949
4449
  const resolvedKey = resolveEnvKey(row.apiKey);
2950
4450
  const base = row.baseUrl.replace(/\/+$/, "");
@@ -2952,7 +4452,7 @@ async function handleDiscoverModels(res, id, cfg) {
2952
4452
  try {
2953
4453
  const headers = { Accept: "application/json" };
2954
4454
  if (resolvedKey) headers["Authorization"] = `Bearer ${resolvedKey}`;
2955
- const response = await fetchUpstream(url, { method: "GET", headers }, { providerId: "byo" });
4455
+ const response = await fetchUpstream2(url, { method: "GET", headers }, { providerId: "byo" });
2956
4456
  if (!response.ok) {
2957
4457
  const text = await response.text().catch(() => "");
2958
4458
  let message = text.slice(0, 300);
@@ -2961,17 +4461,17 @@ async function handleDiscoverModels(res, id, cfg) {
2961
4461
  message = parsed?.error?.message || parsed?.message || message;
2962
4462
  } catch {
2963
4463
  }
2964
- return writeJson2(res, 200, {
4464
+ return writeJson3(res, 200, {
2965
4465
  models: [],
2966
4466
  error: `discovery failed (${response.status})${message ? `: ${message}` : ""}`
2967
4467
  });
2968
4468
  }
2969
4469
  const data = await response.json();
2970
4470
  const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
2971
- return writeJson2(res, 200, { models });
4471
+ return writeJson3(res, 200, { models });
2972
4472
  } catch (err5) {
2973
4473
  const message = err5 instanceof Error ? err5.message : String(err5);
2974
- return writeJson2(res, 200, { models: [], error: `discovery failed: ${message}` });
4474
+ return writeJson3(res, 200, { models: [], error: `discovery failed: ${message}` });
2975
4475
  }
2976
4476
  }
2977
4477
  async function handleTestModel(req, res, id, cfg) {
@@ -2982,13 +4482,13 @@ async function handleTestModel(req, res, id, cfg) {
2982
4482
  const model = typeof body["model"] === "string" ? body["model"].trim() : "";
2983
4483
  if (!model) return writeJsonError(res, 400, "test requires a { model } string");
2984
4484
  if (row.apiFormat === "gemini") {
2985
- return writeJson2(res, 200, { ok: false, unsupportedFormat: true });
4485
+ return writeJson3(res, 200, { ok: false, unsupportedFormat: true });
2986
4486
  }
2987
4487
  const resolvedKey = resolveEnvKey(row.apiKey);
2988
4488
  if (!resolvedKey) {
2989
- 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" });
2990
4490
  }
2991
- const url = row.baseUrl.replace(/\/+$/, "");
4491
+ let url = row.baseUrl.replace(/\/+$/, "");
2992
4492
  const prompt = "Reply with the single word: OK.";
2993
4493
  const headers = { "Content-Type": "application/json" };
2994
4494
  let payload;
@@ -2996,6 +4496,10 @@ async function handleTestModel(req, res, id, cfg) {
2996
4496
  headers["x-api-key"] = resolvedKey;
2997
4497
  headers["anthropic-version"] = "2023-06-01";
2998
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 };
2999
4503
  } else {
3000
4504
  headers["Authorization"] = `Bearer ${resolvedKey}`;
3001
4505
  payload = {
@@ -3007,7 +4511,7 @@ async function handleTestModel(req, res, id, cfg) {
3007
4511
  }
3008
4512
  const startedAt = Date.now();
3009
4513
  try {
3010
- const response = await fetchUpstream(
4514
+ const response = await fetchUpstream2(
3011
4515
  url,
3012
4516
  { method: "POST", headers, body: JSON.stringify(payload) },
3013
4517
  { providerId: "byo" }
@@ -3021,9 +4525,9 @@ async function handleTestModel(req, res, id, cfg) {
3021
4525
  message = parsed?.error?.message || parsed?.message || message;
3022
4526
  } catch {
3023
4527
  }
3024
- return writeJson2(res, 200, { ok: false, status: response.status, latencyMs, message });
4528
+ return writeJson3(res, 200, { ok: false, status: response.status, latencyMs, message });
3025
4529
  }
3026
- return writeJson2(res, 200, {
4530
+ return writeJson3(res, 200, {
3027
4531
  ok: true,
3028
4532
  status: response.status,
3029
4533
  latencyMs,
@@ -3031,7 +4535,7 @@ async function handleTestModel(req, res, id, cfg) {
3031
4535
  });
3032
4536
  } catch (err5) {
3033
4537
  const message = err5 instanceof Error ? err5.message : String(err5);
3034
- return writeJson2(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
4538
+ return writeJson3(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
3035
4539
  }
3036
4540
  }
3037
4541
  function extractSampleText(text, apiFormat) {
@@ -3072,7 +4576,7 @@ async function handleProviderKeys(res, id, cfg, deps) {
3072
4576
  const row = cfg.providers.find((p) => p.id === id);
3073
4577
  if (!row) return writeJsonError(res, 404, `provider '${id}' not found`);
3074
4578
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3075
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
4579
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3076
4580
  }
3077
4581
  function parsePoolKeyInput(body, existing) {
3078
4582
  const out = {};
@@ -3103,7 +4607,7 @@ async function handleAddProviderKey(req, res, id, cfg, deps) {
3103
4607
  row.apiKeys = [...row.apiKeys ?? [], entry];
3104
4608
  persistProviders(cfg, deps);
3105
4609
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3106
- return writeJson2(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
4610
+ return writeJson3(res, 201, { keys: toPoolKeyView(row, cooldown, deps) });
3107
4611
  }
3108
4612
  async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
3109
4613
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3123,7 +4627,7 @@ async function handleUpdateProviderKey(req, res, id, keyId, cfg, deps) {
3123
4627
  row.apiKeys[keyIdx] = entry;
3124
4628
  persistProviders(cfg, deps);
3125
4629
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3126
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
4630
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3127
4631
  }
3128
4632
  async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
3129
4633
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3137,7 +4641,7 @@ async function handleDeleteProviderKey(res, id, keyId, cfg, deps) {
3137
4641
  if (row.apiKeys.length === 0) row.apiKeys = void 0;
3138
4642
  persistProviders(cfg, deps);
3139
4643
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3140
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
4644
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3141
4645
  }
3142
4646
  async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
3143
4647
  if (!id) return writeJsonError(res, 400, "provider id required in path");
@@ -3151,7 +4655,7 @@ async function handleToggleProviderKey(req, res, id, keyId, cfg, deps) {
3151
4655
  row.apiKeys[keyIdx].enabled = Boolean(body["enabled"]);
3152
4656
  persistProviders(cfg, deps);
3153
4657
  const cooldown = await deps.apiKeyPool.getKeyHealth(id);
3154
- return writeJson2(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
4658
+ return writeJson3(res, 200, { keys: toPoolKeyView(row, cooldown, deps) });
3155
4659
  }
3156
4660
  function parseApiKeysInput(raw, existing) {
3157
4661
  if (!Array.isArray(raw)) return existing;
@@ -3269,7 +4773,9 @@ function parseProviderInput(body, existing) {
3269
4773
  const baseUrl = body["baseUrl"];
3270
4774
  if (!id) return null;
3271
4775
  const name = typeof body["name"] === "string" && body["name"].length > 0 ? body["name"] : body["name"] === null ? void 0 : existing?.name;
3272
- if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini") return null;
4776
+ if (apiFormat !== "openai" && apiFormat !== "anthropic" && apiFormat !== "gemini" && apiFormat !== "openai-response") {
4777
+ return null;
4778
+ }
3273
4779
  if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
3274
4780
  const rawKey = body["apiKey"];
3275
4781
  let apiKey = typeof rawKey === "string" && rawKey.length > 0 ? rawKey : existing?.apiKey ?? "";
@@ -3291,10 +4797,11 @@ function parseProviderInput(body, existing) {
3291
4797
  apiKey = mode.apiKey;
3292
4798
  }
3293
4799
  }
4800
+ const migrated = migrateFormatAxis(apiFormat, transformer);
3294
4801
  return {
3295
4802
  id,
3296
4803
  name,
3297
- apiFormat,
4804
+ apiFormat: migrated.apiFormat,
3298
4805
  baseUrl: baseUrl.trim(),
3299
4806
  apiKey,
3300
4807
  models,
@@ -3305,7 +4812,7 @@ function parseProviderInput(body, existing) {
3305
4812
  apiVersion,
3306
4813
  maxConcurrency,
3307
4814
  modelsEndpoint,
3308
- transformer,
4815
+ transformer: migrated.transformer,
3309
4816
  codingPlan,
3310
4817
  apiModes,
3311
4818
  selectedApiModeId
@@ -3322,13 +4829,13 @@ function handlePresets(res, method) {
3322
4829
  baseUrl: p.baseUrl,
3323
4830
  models: p.models
3324
4831
  }));
3325
- return writeJson2(res, 200, { presets, excluded });
4832
+ return writeJson3(res, 200, { presets, excluded });
3326
4833
  }
3327
4834
  async function handleKeys(req, res, method, rest, deps) {
3328
4835
  if (method === "GET" && rest.length === 0) {
3329
4836
  const rows = await deps.keyDb.outboundApiKeysList();
3330
4837
  const reader = deps.keySpendReader;
3331
- if (!reader) return writeJson2(res, 200, { keys: rows.map(toKeyInfo) });
4838
+ if (!reader) return writeJson3(res, 200, { keys: rows.map(toKeyInfo) });
3332
4839
  const now = Date.now();
3333
4840
  const keys = await Promise.all(
3334
4841
  rows.map(async (row) => {
@@ -3340,13 +4847,13 @@ async function handleKeys(req, res, method, rest, deps) {
3340
4847
  return info;
3341
4848
  })
3342
4849
  );
3343
- return writeJson2(res, 200, { keys });
4850
+ return writeJson3(res, 200, { keys });
3344
4851
  }
3345
4852
  if (method === "POST" && rest.length === 0) {
3346
4853
  const body = await readJsonBody3(req);
3347
4854
  const name = typeof body["name"] === "string" && body["name"].trim() ? body["name"].trim() : "key";
3348
4855
  const created = await createNamedKey(deps.keyDb, name);
3349
- return writeJson2(res, 201, {
4856
+ return writeJson3(res, 201, {
3350
4857
  id: created.id,
3351
4858
  name: created.name,
3352
4859
  keyPrefix: created.keyPrefix,
@@ -3358,13 +4865,13 @@ async function handleKeys(req, res, method, rest, deps) {
3358
4865
  const action = rest[1];
3359
4866
  if (method === "POST" && id && action === "revoke") {
3360
4867
  const ok = await deps.keyDb.outboundApiKeysRevoke(id);
3361
- return writeJson2(res, ok ? 200 : 404, { ok });
4868
+ return writeJson3(res, ok ? 200 : 404, { ok });
3362
4869
  }
3363
4870
  if (method === "POST" && id && action === "enabled") {
3364
4871
  const body = await readJsonBody3(req);
3365
4872
  const enabled = body["enabled"] === true;
3366
4873
  const ok = await deps.keyDb.outboundApiKeysSetEnabled(id, enabled);
3367
- return writeJson2(res, ok ? 200 : 404, { ok, enabled });
4874
+ return writeJson3(res, ok ? 200 : 404, { ok, enabled });
3368
4875
  }
3369
4876
  if (method === "POST" && id && action === "max-concurrency") {
3370
4877
  const body = await readJsonBody3(req);
@@ -3382,14 +4889,14 @@ async function handleKeys(req, res, method, rest, deps) {
3382
4889
  );
3383
4890
  }
3384
4891
  const ok = await deps.keyDb.outboundApiKeysSetMaxConcurrency(id, value);
3385
- return writeJson2(res, ok ? 200 : 404, { ok, maxConcurrency: value });
4892
+ return writeJson3(res, ok ? 200 : 404, { ok, maxConcurrency: value });
3386
4893
  }
3387
4894
  if (method === "POST" && id && action === "policy") {
3388
4895
  const body = await readJsonBody3(req);
3389
4896
  const parsed = parseKeyPolicyBody(body);
3390
4897
  if (!parsed.ok) return writeJsonError(res, 400, parsed.message);
3391
4898
  const ok = await deps.keyDb.outboundApiKeysSetPolicy(id, parsed.policy);
3392
- return writeJson2(res, ok ? 200 : 404, { ok });
4899
+ return writeJson3(res, ok ? 200 : 404, { ok });
3393
4900
  }
3394
4901
  return writeJsonError(res, 405, `method ${method} not allowed on keys`);
3395
4902
  }
@@ -3400,10 +4907,10 @@ function validateQueueSegments(patch) {
3400
4907
  errors.push(`${label} must be a number ${min}..${max}`);
3401
4908
  }
3402
4909
  };
3403
- const isPlainObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4910
+ const isPlainObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3404
4911
  const umq = patch.userMessageQueue;
3405
4912
  if (umq !== void 0) {
3406
- if (!isPlainObject4(umq)) {
4913
+ if (!isPlainObject5(umq)) {
3407
4914
  errors.push("userMessageQueue must be an object");
3408
4915
  } else {
3409
4916
  if (typeof umq.enabled !== "boolean") {
@@ -3415,7 +4922,7 @@ function validateQueueSegments(patch) {
3415
4922
  }
3416
4923
  const cq = patch.concurrencyQueue;
3417
4924
  if (cq !== void 0) {
3418
- if (!isPlainObject4(cq)) {
4925
+ if (!isPlainObject5(cq)) {
3419
4926
  errors.push("concurrencyQueue must be an object");
3420
4927
  } else {
3421
4928
  checkNum("concurrencyQueue.maxQueueSizeFactor", cq.maxQueueSizeFactor, 1, 10);
@@ -3425,7 +4932,7 @@ function validateQueueSegments(patch) {
3425
4932
  }
3426
4933
  const ah = patch.accountHealth;
3427
4934
  if (ah !== void 0) {
3428
- if (!isPlainObject4(ah)) {
4935
+ if (!isPlainObject5(ah)) {
3429
4936
  errors.push("accountHealth must be an object");
3430
4937
  } else {
3431
4938
  if (typeof ah.overloadCooldownEnabled !== "boolean") {
@@ -3436,6 +4943,31 @@ function validateQueueSegments(patch) {
3436
4943
  }
3437
4944
  return errors;
3438
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
+ }
3439
4971
  async function handleServer(req, res, method, deps) {
3440
4972
  if (method === "GET") {
3441
4973
  const config = await loadServerConfig2(deps.settingsStore);
@@ -3443,7 +4975,7 @@ async function handleServer(req, res, method, deps) {
3443
4975
  if (config.proxy) server = { ...server, proxy: redactOutboundProxy(config.proxy) };
3444
4976
  if (config.webhook) server = { ...server, webhook: redactWebhookConfig(config.webhook) };
3445
4977
  if (config.billing) server = { ...server, billing: redactBillingConfig(config.billing) };
3446
- return writeJson2(res, 200, { server });
4978
+ return writeJson3(res, 200, { server });
3447
4979
  }
3448
4980
  if (method === "PUT") {
3449
4981
  const patch = await readJsonBody3(req);
@@ -3451,6 +4983,18 @@ async function handleServer(req, res, method, deps) {
3451
4983
  if (queueErrors.length > 0) {
3452
4984
  return writeJsonError(res, 400, `invalid queue config: ${queueErrors.join("; ")}`);
3453
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
+ }
3454
4998
  const webhookErrors = validateWebhookSegment(patch);
3455
4999
  if (webhookErrors.length > 0) {
3456
5000
  return writeJsonError(res, 400, `invalid webhook config: ${webhookErrors.join("; ")}`);
@@ -3477,66 +5021,113 @@ async function handleServer(req, res, method, deps) {
3477
5021
  const merged = mergeServerConfig(current, effectivePatch);
3478
5022
  await saveServerConfig(deps.settingsStore, merged);
3479
5023
  setServerProxyConfig(merged.proxy);
5024
+ getSharedAccountAllowanceScheduling2().configure(merged.allowanceScheduling);
5025
+ deps.allowanceRefreshScheduler?.configure(merged.allowanceScheduling);
3480
5026
  applyWebhookConfig(merged.webhook);
3481
5027
  applyAuditConfig(merged.audit);
3482
5028
  applyBillingConfig(merged.billing);
3483
- if (merged.enabled) {
3484
- const missing = validateServerModelConfig(merged);
3485
- if (missing.length > 0) {
3486
- if (deps.outboundApiServer.getStatus().running) {
3487
- await deps.outboundApiServer.stop();
3488
- }
3489
- return writeJson2(res, 200, {
3490
- server: merged,
3491
- error: { code: "incomplete-model-config", missing }
3492
- });
3493
- }
3494
- }
3495
- try {
3496
- await deps.outboundApiServer.applyConfig({
3497
- enabled: merged.enabled,
3498
- networkBinding: merged.networkBinding,
3499
- endpoints: merged.endpoints,
3500
- port: merged.port,
3501
- userMessageQueue: merged.userMessageQueue,
3502
- concurrencyQueue: merged.concurrencyQueue,
3503
- // voucher-redemption #9: hot-apply the voucher flag so enabling the product
3504
- // takes effect without a restart.
3505
- voucher: merged.voucher
3506
- });
3507
- } catch (err5) {
3508
- const missing = incompleteConfigMissing(err5);
3509
- if (missing) {
3510
- return writeJson2(res, 200, {
3511
- server: merged,
3512
- error: { code: "incomplete-model-config", missing }
3513
- });
3514
- }
3515
- throw err5;
3516
- }
3517
- 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 });
3518
5042
  }
3519
5043
  return writeJsonError(res, 405, `method ${method} not allowed on server`);
3520
5044
  }
3521
- function incompleteConfigMissing(err5) {
3522
- if (typeof err5 !== "object" || err5 === null) return null;
3523
- const missing = err5.missing;
3524
- return Array.isArray(missing) ? missing : null;
3525
- }
3526
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
+ }
3527
5055
  if (method === "GET" && rest.length === 0) {
3528
5056
  const accounts = await deps.subscriptionAccounts.listAll();
3529
5057
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
3530
5058
  const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
3531
- 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 });
3532
5079
  }
3533
5080
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
3534
5081
  const result = handleCodexOAuthStatus(rest[2], deps);
3535
- return writeJson2(res, result.status, result.body);
5082
+ return writeJson3(res, result.status, result.body);
3536
5083
  }
3537
5084
  if (method === "DELETE" && rest[0] === "codex" && rest[1] === "oauth" && rest[2]) {
3538
5085
  const result = handleCodexOAuthCancel(rest[2], deps);
3539
- 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 });
3540
5131
  }
3541
5132
  if (method === "PUT" || method === "POST" || method === "DELETE") {
3542
5133
  const providerId = asSubscriptionProviderId(rest[0]);
@@ -3545,12 +5136,12 @@ async function handleAccounts(req, res, method, rest, deps) {
3545
5136
  }
3546
5137
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "start") {
3547
5138
  const result = providerId === "codex" ? handleCodexOAuthStart(deps) : handleOAuthStart(providerId, deps);
3548
- return writeJson2(res, result.status, result.body);
5139
+ return writeJson3(res, result.status, result.body);
3549
5140
  }
3550
5141
  if (method === "POST" && rest[1] === "oauth" && rest[2] === "complete") {
3551
5142
  const body2 = await readJsonBody3(req);
3552
5143
  const result = await handleOAuthComplete(providerId, body2, deps);
3553
- return writeJson2(res, result.status, result.body);
5144
+ return writeJson3(res, result.status, result.body);
3554
5145
  }
3555
5146
  if (method === "POST" && rest[1] === "accounts") {
3556
5147
  const body2 = await readJsonBody3(req);
@@ -3561,7 +5152,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3561
5152
  const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
3562
5153
  await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
3563
5154
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3564
- return writeJson2(res, 200, status2 ? { account: status2 } : { ok: true });
5155
+ return writeJson3(res, 200, status2 ? { account: status2 } : { ok: true });
3565
5156
  }
3566
5157
  if (method === "POST" && rest[1] === "import-external") {
3567
5158
  if (providerId !== "claude" && providerId !== "codex") {
@@ -3574,7 +5165,13 @@ async function handleAccounts(req, res, method, rest, deps) {
3574
5165
  return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
3575
5166
  }
3576
5167
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3577
- 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
+ });
3578
5175
  }
3579
5176
  if (method === "POST" && rest[1] === "refresh") {
3580
5177
  if (providerId === "opencodego") {
@@ -3583,7 +5180,17 @@ async function handleAccounts(req, res, method, rest, deps) {
3583
5180
  const writer2 = deps.subscriptionTokenWriter;
3584
5181
  const ok = providerId === "claude" ? await writer2.refreshClaudeToken() : providerId === "codex" ? await writer2.refreshCodexToken() : await writer2.refreshGeminiToken();
3585
5182
  const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
3586
- 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 });
3587
5194
  }
3588
5195
  if (method === "POST" && rest[2] === "label") {
3589
5196
  const accountId = rest[1];
@@ -3591,7 +5198,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3591
5198
  const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
3592
5199
  const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
3593
5200
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3594
- return writeJson2(res, 200, { ok: true });
5201
+ return writeJson3(res, 200, { ok: true });
3595
5202
  }
3596
5203
  if (method === "POST" && rest[2] === "priority") {
3597
5204
  const accountId = rest[1];
@@ -3603,7 +5210,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3603
5210
  }
3604
5211
  const result = await deps.subscriptionTokenWriter.setAccountPriority(providerId, accountId, priority);
3605
5212
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3606
- return writeJson2(res, 200, { ok: true });
5213
+ return writeJson3(res, 200, { ok: true });
3607
5214
  }
3608
5215
  if (method === "POST" && rest[2] === "proxy") {
3609
5216
  const accountId = rest[1];
@@ -3616,7 +5223,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3616
5223
  }
3617
5224
  const result = await deps.subscriptionTokenWriter.setAccountProxy(providerId, accountId, proxy);
3618
5225
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3619
- return writeJson2(res, 200, { ok: true });
5226
+ return writeJson3(res, 200, { ok: true });
3620
5227
  }
3621
5228
  if (method === "POST" && rest[2] === "supported-models") {
3622
5229
  const accountId = rest[1];
@@ -3625,7 +5232,7 @@ async function handleAccounts(req, res, method, rest, deps) {
3625
5232
  if (!parsed.ok) return writeJsonError(res, 400, "invalid supportedModels");
3626
5233
  const result = await deps.subscriptionTokenWriter.setAccountSupportedModels(providerId, accountId, parsed.value);
3627
5234
  if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
3628
- return writeJson2(res, 200, { ok: true });
5235
+ return writeJson3(res, 200, { ok: true });
3629
5236
  }
3630
5237
  if (method === "PUT" && rest[1] === "active") {
3631
5238
  const body2 = await readJsonBody3(req);
@@ -3633,17 +5240,22 @@ async function handleAccounts(req, res, method, rest, deps) {
3633
5240
  if (!id) return writeJsonError(res, 400, "active switch requires { id }");
3634
5241
  const result = await deps.subscriptionTokenWriter.setActiveAccount(providerId, id);
3635
5242
  if (!result.ok) return writeJsonError(res, 404, `account '${id}' not found`);
3636
- return writeJson2(res, 200, { ok: true });
5243
+ return writeJson3(res, 200, { ok: true });
3637
5244
  }
3638
- if (method === "DELETE" && rest.length >= 2) {
5245
+ if (method === "DELETE" && rest.length === 2) {
3639
5246
  const accountId = rest[1];
3640
5247
  const result = await deps.subscriptionTokenWriter.removeAccount(providerId, accountId);
3641
5248
  if (!result.removed) return writeJsonError(res, 404, `account '${accountId}' not found`);
3642
- return writeJson2(res, 200, { ok: true });
5249
+ deps.accountAllowanceService?.removeAccountSnapshot?.(providerId, accountId);
5250
+ return writeJson3(res, 200, { ok: true });
3643
5251
  }
3644
- if (method === "DELETE") {
5252
+ if (method === "DELETE" && rest.length === 1) {
3645
5253
  await deps.subscriptionTokenWriter.clearProvider(providerId);
3646
- 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");
3647
5259
  }
3648
5260
  const body = await readJsonBody3(req);
3649
5261
  const config = validateTokenBody(providerId, body);
@@ -3652,22 +5264,22 @@ async function handleAccounts(req, res, method, rest, deps) {
3652
5264
  }
3653
5265
  await deps.subscriptionTokenWriter.writeProviderTokens(providerId, config);
3654
5266
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
3655
- return writeJson2(res, 200, status ? { account: status } : { ok: true });
5267
+ return writeJson3(res, 200, status ? { account: status } : { ok: true });
3656
5268
  }
3657
5269
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
3658
5270
  }
3659
5271
  async function handleCli(req, res, method, rest, deps) {
3660
5272
  if (method === "GET" && rest.length === 0) {
3661
5273
  const result = handleCliList(process.platform, deps.cliPathProbe);
3662
- return writeJson2(res, result.status, result.body);
5274
+ return writeJson3(res, result.status, result.body);
3663
5275
  }
3664
5276
  if (method === "GET" && rest[0] === "sessions") {
3665
5277
  const result = handleCliSessions();
3666
- return writeJson2(res, result.status, result.body);
5278
+ return writeJson3(res, result.status, result.body);
3667
5279
  }
3668
5280
  if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
3669
5281
  const result = handleCliStop(rest[1]);
3670
- return writeJson2(res, result.status, result.body);
5282
+ return writeJson3(res, result.status, result.body);
3671
5283
  }
3672
5284
  if (method === "POST" && rest[1] === "install") {
3673
5285
  const cli = rest[0];
@@ -3675,7 +5287,7 @@ async function handleCli(req, res, method, rest, deps) {
3675
5287
  return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
3676
5288
  }
3677
5289
  const result = await handleCliInstall(cli, deps.cliCommandRunner);
3678
- return writeJson2(res, result.status, result.body);
5290
+ return writeJson3(res, result.status, result.body);
3679
5291
  }
3680
5292
  if (method === "POST" && rest[1] === "launch") {
3681
5293
  const cli = rest[0];
@@ -3690,28 +5302,100 @@ async function handleCli(req, res, method, rest, deps) {
3690
5302
  opener: deps.cliTerminalOpener,
3691
5303
  probe: deps.cliPathProbe
3692
5304
  });
3693
- return writeJson2(res, result.status, result.body);
5305
+ return writeJson3(res, result.status, result.body);
3694
5306
  }
3695
5307
  return writeJsonError(res, 405, `method ${method} not allowed on cli`);
3696
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
+ }
3697
5365
  async function handleStatus(res, method, deps) {
3698
5366
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
3699
5367
  const status = deps.outboundApiServer.getStatus();
3700
5368
  const serverConfig = await loadServerConfig2(deps.settingsStore);
3701
- const endpoints = serverConfig.endpoints.map((e) => {
3702
- if (isKindMappedEndpoint(e.endpoint)) {
3703
- 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 };
3704
5380
  }
3705
- if (e.endpoint === "chat") {
3706
- 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
+ };
3707
5387
  }
3708
- 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
+ };
3709
5393
  });
3710
5394
  if (status.running) {
3711
5395
  const queueStatus = deps.outboundApiServer.getQueueStatus();
3712
- return writeJson2(res, 200, { ...status, endpoints, queueStatus });
5396
+ return writeJson3(res, 200, { ...status, endpoints, queueStatus });
3713
5397
  }
3714
- return writeJson2(res, 200, { ...status, endpoints });
5398
+ return writeJson3(res, 200, { ...status, endpoints });
3715
5399
  }
3716
5400
  function resolvePlaygroundPath(endpoint, body) {
3717
5401
  switch (endpoint) {
@@ -3737,16 +5421,16 @@ async function handlePlayground(req, res, method, deps) {
3737
5421
  const payload = body["body"];
3738
5422
  const status = deps.outboundApiServer.getStatus();
3739
5423
  if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
3740
- const path2 = resolvePlaygroundPath(endpoint, isRecord(payload) ? payload : {});
5424
+ const path2 = resolvePlaygroundPath(endpoint, isRecord2(payload) ? payload : {});
3741
5425
  if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
3742
5426
  const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
3743
5427
  await proxyToOutbound(res, status.port, path2, key, upstreamBody);
3744
5428
  }
3745
- function isRecord(v) {
5429
+ function isRecord2(v) {
3746
5430
  return !!v && typeof v === "object" && !Array.isArray(v);
3747
5431
  }
3748
5432
  function proxyToOutbound(res, outboundPort, path2, key, body) {
3749
- return new Promise((resolve) => {
5433
+ return new Promise((resolve2) => {
3750
5434
  const upstream = http.request(
3751
5435
  {
3752
5436
  host: "127.0.0.1",
@@ -3767,14 +5451,14 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
3767
5451
  proxRes.on("data", (chunk) => res.write(chunk));
3768
5452
  proxRes.on("end", () => {
3769
5453
  res.end();
3770
- resolve();
5454
+ resolve2();
3771
5455
  });
3772
5456
  }
3773
5457
  );
3774
5458
  upstream.on("error", (err5) => {
3775
5459
  if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
3776
5460
  else res.end();
3777
- resolve();
5461
+ resolve2();
3778
5462
  });
3779
5463
  upstream.write(body);
3780
5464
  upstream.end();
@@ -3782,7 +5466,7 @@ function proxyToOutbound(res, outboundPort, path2, key, body) {
3782
5466
  }
3783
5467
 
3784
5468
  // src/admin/uiStatic.ts
3785
- import { existsSync as existsSync3, statSync } from "fs";
5469
+ import { existsSync as existsSync6, statSync as statSync2 } from "fs";
3786
5470
  import { readFile } from "fs/promises";
3787
5471
  import { createRequire } from "module";
3788
5472
  import path from "path";
@@ -3805,13 +5489,13 @@ var CONTENT_TYPES = {
3805
5489
  function resolveUiDist() {
3806
5490
  const fromEnv = process.env["OMNICROSS_UI_DIST"];
3807
5491
  if (fromEnv) {
3808
- return existsSync3(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
5492
+ return existsSync6(path.join(fromEnv, "index.html")) ? path.resolve(fromEnv) : null;
3809
5493
  }
3810
5494
  try {
3811
5495
  const req = createRequire(typeof __filename !== "undefined" ? __filename : import.meta.url);
3812
5496
  const pkgJson = req.resolve("@omnicross/ui/package.json");
3813
5497
  const dist = path.join(path.dirname(pkgJson), "dist");
3814
- return existsSync3(path.join(dist, "index.html")) ? dist : null;
5498
+ return existsSync6(path.join(dist, "index.html")) ? dist : null;
3815
5499
  } catch {
3816
5500
  return null;
3817
5501
  }
@@ -3860,7 +5544,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
3860
5544
  return true;
3861
5545
  }
3862
5546
  let target = filePath;
3863
- if (!existsSync3(target) || statSync(target).isDirectory()) {
5547
+ if (!existsSync6(target) || statSync2(target).isDirectory()) {
3864
5548
  if (path.extname(rel) === "") {
3865
5549
  target = path.join(uiDist, "index.html");
3866
5550
  } else {
@@ -3877,7 +5561,7 @@ async function handleUiStatic(req, res, urlPath, uiDist) {
3877
5561
  }
3878
5562
 
3879
5563
  // src/admin/version.ts
3880
- var DAEMON_VERSION = true ? "0.1.4" : "0.0.0-dev";
5564
+ var DAEMON_VERSION = true ? "0.1.6" : "0.0.0-dev";
3881
5565
 
3882
5566
  // src/admin/AdminServer.ts
3883
5567
  var LOOPBACK_ADDR = "127.0.0.1";
@@ -3916,14 +5600,14 @@ var AdminServer = class {
3916
5600
  }
3917
5601
  /** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
3918
5602
  listen(bindAddr, port) {
3919
- return new Promise((resolve, reject) => {
5603
+ return new Promise((resolve2, reject) => {
3920
5604
  const server = http2.createServer((req, res) => {
3921
5605
  this.onRequest(req, res);
3922
5606
  });
3923
5607
  const onError = (err5) => {
3924
5608
  if (err5.code === "EADDRINUSE" && port !== 0) {
3925
5609
  server.removeListener("error", onError);
3926
- this.listen(bindAddr, 0).then(resolve, reject);
5610
+ this.listen(bindAddr, 0).then(resolve2, reject);
3927
5611
  return;
3928
5612
  }
3929
5613
  reject(err5);
@@ -3935,7 +5619,7 @@ var AdminServer = class {
3935
5619
  server.removeListener("error", onError);
3936
5620
  server.on("error", (e) => this.deps.logger.error("[AdminServer] server error", e));
3937
5621
  this.server = server;
3938
- resolve(addr.port);
5622
+ resolve2(addr.port);
3939
5623
  } else {
3940
5624
  reject(new Error("Failed to get admin server address"));
3941
5625
  }
@@ -4016,8 +5700,8 @@ var AdminServer = class {
4016
5700
  if (!server) return;
4017
5701
  this.server = null;
4018
5702
  this.boundPort = 0;
4019
- return new Promise((resolve) => {
4020
- server.close(() => resolve());
5703
+ return new Promise((resolve2) => {
5704
+ server.close(() => resolve2());
4021
5705
  });
4022
5706
  }
4023
5707
  /** A live status snapshot. */
@@ -4133,7 +5817,7 @@ function pageHtml(message) {
4133
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>`;
4134
5818
  }
4135
5819
  function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
4136
- return new Promise((resolve, reject) => {
5820
+ return new Promise((resolve2, reject) => {
4137
5821
  let settled = false;
4138
5822
  const finish = (server2, fn) => {
4139
5823
  if (settled) return;
@@ -4164,7 +5848,7 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal
4164
5848
  }
4165
5849
  res.writeHead(200, { "Content-Type": "text/html" });
4166
5850
  res.end(pageHtml("Login complete."));
4167
- finish(server, () => resolve(code));
5851
+ finish(server, () => resolve2(code));
4168
5852
  });
4169
5853
  const abort = () => finish(server, () => reject(new Error("login: cancelled")));
4170
5854
  if (signal?.aborted) {
@@ -4259,21 +5943,30 @@ function createPoolKeysLoader(getProviderRow, autoDisabled) {
4259
5943
  }
4260
5944
 
4261
5945
  // src/commands/paths.ts
4262
- import { dirname as dirname2, join as join3 } from "path";
5946
+ import { dirname as dirname5, join as join5 } from "path";
4263
5947
  function defaultVouchersPath(configPath) {
4264
- return join3(dirname2(configPath), "vouchers.json");
5948
+ return join5(dirname5(configPath), "vouchers.json");
5949
+ }
5950
+ function defaultIntegrationsPath(configPath) {
5951
+ return join5(dirname5(configPath), "integrations.json");
4265
5952
  }
4266
5953
  function defaultPricingPath(configPath) {
4267
- return join3(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");
4268
5961
  }
4269
5962
  function defaultUsageEventsPath(configPath) {
4270
- return join3(dirname2(configPath), "usage-events.jsonl");
5963
+ return join5(dirname5(configPath), "usage-events.jsonl");
4271
5964
  }
4272
5965
  function defaultAuditDir(configPath) {
4273
- return join3(dirname2(configPath), "audit");
5966
+ return join5(dirname5(configPath), "audit");
4274
5967
  }
4275
5968
  function defaultBillingDir(configPath) {
4276
- return join3(dirname2(configPath), "billing");
5969
+ return join5(dirname5(configPath), "billing");
4277
5970
  }
4278
5971
 
4279
5972
  // src/ports/ConfigFileProviderConfigSource.ts
@@ -4286,8 +5979,10 @@ var EMPTY_CHAIN = {
4286
5979
  modelTransformers: []
4287
5980
  };
4288
5981
  var FORMAT_TRANSFORMER = {
5982
+ openai: "openai",
4289
5983
  anthropic: "anthropic",
4290
- gemini: "gemini"
5984
+ gemini: "gemini",
5985
+ "openai-response": "openai-response"
4291
5986
  };
4292
5987
  var ConfigFileProviderConfigSource = class {
4293
5988
  providers = /* @__PURE__ */ new Map();
@@ -4356,7 +6051,7 @@ var ConfigFileProviderConfigSource = class {
4356
6051
  }
4357
6052
  async getMainTransformer(providerId) {
4358
6053
  const row = this.providers.get(providerId);
4359
- if (!row || row.apiFormat === "openai") return null;
6054
+ if (!row) return null;
4360
6055
  const name = FORMAT_TRANSFORMER[row.apiFormat];
4361
6056
  const instances = this.transformerService.resolveTransformerReferences([name]);
4362
6057
  return instances[0] ?? null;
@@ -4366,11 +6061,8 @@ var ConfigFileProviderConfigSource = class {
4366
6061
  if (!row) return EMPTY_CHAIN;
4367
6062
  const customRefs = row.transformer?.use ?? [];
4368
6063
  if (customRefs.length === 0) return EMPTY_CHAIN;
4369
- const formatName = row.apiFormat === "openai" ? void 0 : FORMAT_TRANSFORMER[row.apiFormat];
4370
- const effectiveRefs = formatName ? customRefs.filter((ref) => (typeof ref === "string" ? ref : ref[0]) !== formatName) : customRefs;
4371
- if (effectiveRefs.length === 0) return EMPTY_CHAIN;
4372
6064
  return {
4373
- providerTransformers: this.transformerService.resolveTransformerReferences(effectiveRefs),
6065
+ providerTransformers: this.transformerService.resolveTransformerReferences(customRefs),
4374
6066
  modelTransformers: []
4375
6067
  };
4376
6068
  }
@@ -4401,7 +6093,7 @@ function resolvePreferredApiKey(row) {
4401
6093
  }
4402
6094
  function toLLMProvider(row) {
4403
6095
  const apiFormat = row.apiFormat === "gemini" ? "google" : row.apiFormat;
4404
- const transformer = row.apiFormat === "openai" ? void 0 : { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
6096
+ const transformer = { use: [FORMAT_TRANSFORMER[row.apiFormat]] };
4405
6097
  const allModels = row.models ?? [];
4406
6098
  const models = row.modelConfigs ? allModels.filter((id) => row.modelConfigs.find((c) => c.id === id)?.enabled !== false) : allModels;
4407
6099
  return {
@@ -4471,7 +6163,7 @@ var ConfigurableLogger = class {
4471
6163
  const stream = this.fileStream;
4472
6164
  this.fileStream = null;
4473
6165
  if (!stream) return Promise.resolve();
4474
- return new Promise((resolve) => stream.end(() => resolve()));
6166
+ return new Promise((resolve2) => stream.end(() => resolve2()));
4475
6167
  }
4476
6168
  emit(level, message, error, meta) {
4477
6169
  if (LEVEL_ORDER[level] > this.threshold) return;
@@ -4582,7 +6274,7 @@ function safeStringify(value) {
4582
6274
  }
4583
6275
 
4584
6276
  // src/ports/JsonApiServerSettingsStore.ts
4585
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
6277
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
4586
6278
  import { OUTBOUND_API_SERVER_CONFIG_KEY } from "@omnicross/core/outbound-api";
4587
6279
  var JsonApiServerSettingsStore = class {
4588
6280
  /**
@@ -4609,7 +6301,7 @@ var JsonApiServerSettingsStore = class {
4609
6301
  if (key !== OUTBOUND_API_SERVER_CONFIG_KEY) return;
4610
6302
  const file = this.readFile();
4611
6303
  file.server = this.encryptSecrets(value);
4612
- writeFileSync3(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
6304
+ writeFileSync5(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
4613
6305
  }
4614
6306
  /** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
4615
6307
  encryptSecrets(config) {
@@ -4632,7 +6324,7 @@ var JsonApiServerSettingsStore = class {
4632
6324
  /** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
4633
6325
  readFile() {
4634
6326
  try {
4635
- const raw = readFileSync3(this.configPath, "utf8");
6327
+ const raw = readFileSync6(this.configPath, "utf8");
4636
6328
  const parsed = JSON.parse(raw);
4637
6329
  if (parsed && typeof parsed === "object") return parsed;
4638
6330
  } catch {
@@ -4642,8 +6334,8 @@ var JsonApiServerSettingsStore = class {
4642
6334
  };
4643
6335
 
4644
6336
  // src/ports/JsonlUsageEventStore.ts
4645
- import { randomUUID as randomUUID3 } from "crypto";
4646
- 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";
4647
6339
  var JsonlUsageEventStore = class {
4648
6340
  constructor(eventsPath, isPriced) {
4649
6341
  this.eventsPath = eventsPath;
@@ -4655,7 +6347,7 @@ var JsonlUsageEventStore = class {
4655
6347
  async insert(input) {
4656
6348
  const row = {
4657
6349
  ...input,
4658
- id: randomUUID3(),
6350
+ id: randomUUID4(),
4659
6351
  ts: input.ts ?? Date.now()
4660
6352
  };
4661
6353
  appendFileSync(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
@@ -4753,15 +6445,15 @@ var JsonlUsageEventStore = class {
4753
6445
  * Used to lazily seed the outbound key-policy spend tracker (once per key). A
4754
6446
  * key with no attributed events yields all zeros.
4755
6447
  */
4756
- async getSpendByKey(query) {
6448
+ async getSpendByKey(query2) {
4757
6449
  let totalUsd = 0;
4758
6450
  let dailyUsd = 0;
4759
6451
  let weeklyUsd = 0;
4760
- for (const row of this.readRows({ startTs: 0, endTs: query.endTs })) {
4761
- 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;
4762
6454
  totalUsd += row.costUsd;
4763
- if (row.ts >= query.dayStartTs) dailyUsd += row.costUsd;
4764
- 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;
4765
6457
  }
4766
6458
  return { totalUsd, dailyUsd, weeklyUsd };
4767
6459
  }
@@ -4846,10 +6538,10 @@ var JsonlUsageEventStore = class {
4846
6538
  }
4847
6539
  /** Parse every line, skipping malformed/torn lines defensively. */
4848
6540
  readAllRows() {
4849
- if (!existsSync4(this.eventsPath)) return [];
6541
+ if (!existsSync7(this.eventsPath)) return [];
4850
6542
  let raw;
4851
6543
  try {
4852
- raw = readFileSync4(this.eventsPath, "utf8");
6544
+ raw = readFileSync7(this.eventsPath, "utf8");
4853
6545
  } catch {
4854
6546
  return [];
4855
6547
  }
@@ -4933,7 +6625,7 @@ function isUsageEventRecord(parsed) {
4933
6625
  }
4934
6626
 
4935
6627
  // src/ports/JsonOutboundKeyDb.ts
4936
- 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";
4937
6629
  var JsonOutboundKeyDb = class {
4938
6630
  constructor(keysPath) {
4939
6631
  this.keysPath = keysPath;
@@ -4959,7 +6651,10 @@ var JsonOutboundKeyDb = class {
4959
6651
  enabled: true,
4960
6652
  createdAt: input.createdAt ?? Date.now(),
4961
6653
  lastUsedAt: null,
4962
- revokedAt: null
6654
+ revokedAt: null,
6655
+ kind: input.kind,
6656
+ allowedEndpoints: input.allowedEndpoints,
6657
+ loopbackOnly: input.loopbackOnly
4963
6658
  };
4964
6659
  rows.push(row);
4965
6660
  this.writeRows(rows);
@@ -5036,16 +6731,16 @@ var JsonOutboundKeyDb = class {
5036
6731
  }
5037
6732
  /** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
5038
6733
  readRows() {
5039
- if (!existsSync5(this.keysPath)) return [];
6734
+ if (!existsSync8(this.keysPath)) return [];
5040
6735
  try {
5041
- const parsed = JSON.parse(readFileSync5(this.keysPath, "utf8"));
6736
+ const parsed = JSON.parse(readFileSync8(this.keysPath, "utf8"));
5042
6737
  return Array.isArray(parsed) ? parsed : [];
5043
6738
  } catch {
5044
6739
  return [];
5045
6740
  }
5046
6741
  }
5047
6742
  writeRows(rows) {
5048
- writeFileSync4(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
6743
+ writeFileSync6(this.keysPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
5049
6744
  }
5050
6745
  };
5051
6746
  function applyPolicyField(row, field, value) {
@@ -5055,12 +6750,29 @@ function applyPolicyField(row, field, value) {
5055
6750
  }
5056
6751
 
5057
6752
  // src/ports/JsonPricingStore.ts
5058
- 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";
5059
6755
  var JsonPricingStore = class {
5060
6756
  constructor(pricingPath) {
5061
6757
  this.pricingPath = pricingPath;
5062
6758
  }
5063
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
+ }
5064
6776
  async getAll() {
5065
6777
  return this.readRows();
5066
6778
  }
@@ -5073,17 +6785,17 @@ var JsonPricingStore = class {
5073
6785
  */
5074
6786
  async upsert(input, asUserEdit) {
5075
6787
  const rows = this.readRows();
5076
- const entry = this.applyUpsert(rows, input, asUserEdit);
6788
+ const entry = this.applyUpsert(rows, input, asUserEdit, "litellm");
5077
6789
  this.writeRows(rows);
5078
6790
  return entry;
5079
6791
  }
5080
6792
  /**
5081
6793
  * Apply a batch fetched from a pricing source. Rows whose local copy is
5082
6794
  * user-edited are NOT applied — they come back as `{ current, incoming }`
5083
- * conflicts; everything else is upserted (source 'litellm'). ONE file write
5084
- * for the whole batch.
6795
+ * conflicts; everything else is upserted with the supplied automatic source.
6796
+ * ONE file write for the whole batch.
5085
6797
  */
5086
- async bulkApplyFromSource(entries) {
6798
+ async bulkApplyFromSource(entries, source = "litellm") {
5087
6799
  const rows = this.readRows();
5088
6800
  const applied = [];
5089
6801
  const conflicts = [];
@@ -5099,7 +6811,8 @@ var JsonPricingStore = class {
5099
6811
  rows,
5100
6812
  incoming,
5101
6813
  /* asUserEdit */
5102
- false
6814
+ false,
6815
+ source
5103
6816
  ));
5104
6817
  }
5105
6818
  if (applied.length > 0) this.writeRows(rows);
@@ -5122,7 +6835,8 @@ var JsonPricingStore = class {
5122
6835
  rows,
5123
6836
  r.incoming,
5124
6837
  /* asUserEdit */
5125
- false
6838
+ false,
6839
+ "litellm"
5126
6840
  );
5127
6841
  overwrittenCount += 1;
5128
6842
  }
@@ -5143,7 +6857,7 @@ var JsonPricingStore = class {
5143
6857
  return true;
5144
6858
  }
5145
6859
  /** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
5146
- applyUpsert(rows, input, asUserEdit) {
6860
+ applyUpsert(rows, input, asUserEdit, automaticSource) {
5147
6861
  const now = Date.now();
5148
6862
  const entry = {
5149
6863
  providerId: input.providerId,
@@ -5152,7 +6866,7 @@ var JsonPricingStore = class {
5152
6866
  outputPricePer1m: input.outputPricePer1m,
5153
6867
  cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
5154
6868
  cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
5155
- source: asUserEdit ? "user" : "litellm",
6869
+ source: asUserEdit ? "user" : automaticSource,
5156
6870
  userEdited: asUserEdit,
5157
6871
  editedAt: asUserEdit ? now : null,
5158
6872
  updatedAt: now
@@ -5166,21 +6880,142 @@ var JsonPricingStore = class {
5166
6880
  }
5167
6881
  /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
5168
6882
  readRows() {
5169
- if (!existsSync6(this.pricingPath)) return [];
6883
+ if (!existsSync9(this.pricingPath)) return [];
5170
6884
  try {
5171
- const parsed = JSON.parse(readFileSync6(this.pricingPath, "utf8"));
6885
+ const parsed = JSON.parse(readFileSync9(this.pricingPath, "utf8"));
5172
6886
  return Array.isArray(parsed) ? parsed : [];
5173
6887
  } catch {
5174
6888
  return [];
5175
6889
  }
5176
6890
  }
5177
6891
  writeRows(rows) {
5178
- 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);
5179
7011
  }
5180
7012
  };
7013
+ function finiteOrNull(value) {
7014
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
7015
+ }
5181
7016
 
5182
7017
  // src/ports/JsonVoucherDb.ts
5183
- 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";
5184
7019
  var JsonVoucherDb = class {
5185
7020
  constructor(vouchersPath) {
5186
7021
  this.vouchersPath = vouchersPath;
@@ -5258,25 +7093,26 @@ var JsonVoucherDb = class {
5258
7093
  }
5259
7094
  /** Read the voucher rows, tolerating a missing/corrupt file (→ empty list). */
5260
7095
  readRows() {
5261
- if (!existsSync7(this.vouchersPath)) return [];
7096
+ if (!existsSync11(this.vouchersPath)) return [];
5262
7097
  try {
5263
- const parsed = JSON.parse(readFileSync7(this.vouchersPath, "utf8"));
7098
+ const parsed = JSON.parse(readFileSync11(this.vouchersPath, "utf8"));
5264
7099
  return Array.isArray(parsed) ? parsed : [];
5265
7100
  } catch {
5266
7101
  return [];
5267
7102
  }
5268
7103
  }
5269
7104
  writeRows(rows) {
5270
- writeFileSync6(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
7105
+ writeFileSync9(this.vouchersPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
5271
7106
  }
5272
7107
  };
5273
7108
 
5274
7109
  // src/ports/JsonSubscriptionCredentialStore.ts
5275
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
5276
- import { dirname as dirname4 } from "path";
5277
- import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
5278
- import { fetchUpstream as fetchUpstream2 } from "@omnicross/core/pipeline/upstreamFetch";
5279
- 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";
5280
7116
  import {
5281
7117
  claudeOAuth as claudeOAuth2,
5282
7118
  codexOAuth as codexOAuth2,
@@ -5284,33 +7120,9 @@ import {
5284
7120
  } from "@omnicross/subscriptions";
5285
7121
 
5286
7122
  // src/ports/account-sync.ts
5287
- var IMPORT_EXPIRY_MARGIN_MS = 6e4;
5288
7123
  function viewOf(tokens) {
5289
7124
  return tokens;
5290
7125
  }
5291
- function decideExternalImport(captured, external, now = Date.now()) {
5292
- if (!external?.accessToken) return "no-credential";
5293
- const capturedRt = viewOf(captured).refreshToken;
5294
- const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
5295
- const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
5296
- return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
5297
- }
5298
- function buildImportedTokens(captured, external) {
5299
- const imported = {
5300
- ...captured,
5301
- accessToken: external.accessToken,
5302
- status: "authorized",
5303
- errorMessage: void 0,
5304
- syncWarning: void 0,
5305
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
5306
- };
5307
- if (external.refreshToken) imported.refreshToken = external.refreshToken;
5308
- if (external.expiresAt) imported.expiresAt = external.expiresAt;
5309
- else delete imported.expiresAt;
5310
- if (external.idToken) imported.idToken = external.idToken;
5311
- if (external.scopes) imported.scopes = external.scopes;
5312
- return imported;
5313
- }
5314
7126
  function buildTokensFromExternal(provider, external) {
5315
7127
  const base = {
5316
7128
  authMethod: "oauth",
@@ -5331,14 +7143,6 @@ function buildTokensFromExternal(provider, external) {
5331
7143
  if (external.idToken) tokens.idToken = external.idToken;
5332
7144
  return tokens;
5333
7145
  }
5334
- function isExternalDivergent(stored, external) {
5335
- if (!external?.accessToken || !external.refreshToken) return false;
5336
- const view = viewOf(stored);
5337
- if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
5338
- const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
5339
- const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
5340
- return !Number.isFinite(storedExp) || externalExp > storedExp;
5341
- }
5342
7146
  function findDuplicateCredentialIds(accounts) {
5343
7147
  const byCredential = /* @__PURE__ */ new Map();
5344
7148
  for (const account of accounts) {
@@ -5357,11 +7161,11 @@ function findDuplicateCredentialIds(accounts) {
5357
7161
  }
5358
7162
 
5359
7163
  // src/ports/external-cli-credentials.ts
5360
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
5361
- import { homedir as homedir2 } from "os";
5362
- import { join as join4 } from "path";
5363
- function externalStorePath(provider, home = homedir2()) {
5364
- return provider === "claude" ? join4(home, ".claude", ".credentials.json") : join4(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");
5365
7169
  }
5366
7170
  function decodeJwtExpiryMs(token) {
5367
7171
  try {
@@ -5408,12 +7212,12 @@ function parseCodexTokensEnvelope(raw) {
5408
7212
  }
5409
7213
  return parsed;
5410
7214
  }
5411
- function readExternalCliCredentials(provider, home = homedir2()) {
7215
+ function readExternalCliCredentials(provider, home = homedir3()) {
5412
7216
  const path2 = externalStorePath(provider, home);
5413
- if (!existsSync8(path2)) return null;
7217
+ if (!existsSync12(path2)) return null;
5414
7218
  let raw;
5415
7219
  try {
5416
- const parsed = JSON.parse(readFileSync8(path2, "utf8"));
7220
+ const parsed = JSON.parse(readFileSync12(path2, "utf8"));
5417
7221
  raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
5418
7222
  } catch {
5419
7223
  return null;
@@ -5421,84 +7225,6 @@ function readExternalCliCredentials(provider, home = homedir2()) {
5421
7225
  return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
5422
7226
  }
5423
7227
 
5424
- // src/ports/external-cli-store.ts
5425
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync7 } from "fs";
5426
- import { homedir as homedir3 } from "os";
5427
- import { dirname as dirname3 } from "path";
5428
- function markerPath(provider, home) {
5429
- return `${externalStorePath(provider, home)}.omnicross-managed`;
5430
- }
5431
- function backupPath(provider, home) {
5432
- return `${externalStorePath(provider, home)}.omnicross-backup`;
5433
- }
5434
- function buildClaudeOAuthEnvelope(tokens) {
5435
- if (!tokens.accessToken) return null;
5436
- const envelope = { accessToken: tokens.accessToken };
5437
- if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
5438
- if (tokens.expiresAt) {
5439
- const ms = Date.parse(tokens.expiresAt);
5440
- if (Number.isFinite(ms)) envelope.expiresAt = ms;
5441
- }
5442
- if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
5443
- return envelope;
5444
- }
5445
- function buildCodexTokensEnvelope(tokens) {
5446
- if (!tokens.accessToken && !tokens.idToken) return null;
5447
- const envelope = { access_token: tokens.accessToken ?? "" };
5448
- if (tokens.idToken) envelope.id_token = tokens.idToken;
5449
- if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
5450
- return envelope;
5451
- }
5452
- function readExistingObject(path2) {
5453
- if (!existsSync9(path2)) return {};
5454
- try {
5455
- const parsed = JSON.parse(readFileSync9(path2, "utf8"));
5456
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5457
- } catch {
5458
- return {};
5459
- }
5460
- }
5461
- function writeAtomic(path2, content) {
5462
- mkdirSync2(dirname3(path2), { recursive: true });
5463
- const temp = `${path2}.omnicross-tmp`;
5464
- writeFileSync7(temp, content, "utf8");
5465
- renameSync(temp, path2);
5466
- }
5467
- function createExternalCliStore(home = homedir3()) {
5468
- return {
5469
- readMarkerAccountId(provider) {
5470
- const path2 = markerPath(provider, home);
5471
- if (!existsSync9(path2)) return void 0;
5472
- try {
5473
- const parsed = JSON.parse(readFileSync9(path2, "utf8"));
5474
- return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
5475
- } catch {
5476
- return void 0;
5477
- }
5478
- },
5479
- writeMarker(provider, accountId) {
5480
- writeAtomic(
5481
- markerPath(provider, home),
5482
- JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
5483
- );
5484
- },
5485
- writeBack(provider, accountId, tokens) {
5486
- const owner = this.readMarkerAccountId(provider);
5487
- if (owner !== accountId) return false;
5488
- const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
5489
- if (!envelope) return false;
5490
- const storePath = externalStorePath(provider, home);
5491
- if (existsSync9(storePath) && !existsSync9(backupPath(provider, home))) {
5492
- copyFileSync(storePath, backupPath(provider, home));
5493
- }
5494
- const existing = readExistingObject(storePath);
5495
- const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
5496
- writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
5497
- return true;
5498
- }
5499
- };
5500
- }
5501
-
5502
7228
  // src/ports/JsonSubscriptionCredentialStore.ts
5503
7229
  var ACCOUNT_REFRESH_LEAD_MS = 5 * 6e4;
5504
7230
  var JsonSubscriptionCredentialStore = class {
@@ -5511,32 +7237,30 @@ var JsonSubscriptionCredentialStore = class {
5511
7237
  * proxy-aware {@link fetchUpstream} that threads the
5512
7238
  * `{ providerId, accountId }` ctx (upstream-proxy M1) so a
5513
7239
  * per-account/per-provider proxy is honored on refresh exactly
5514
- * 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
5515
7241
  * account's traffic. NOT used by any read/write path.
5516
7242
  */
5517
- constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
7243
+ constructor(tokensPath, box, fetchImpl = void 0, externalCliReader = readExternalCliCredentials) {
5518
7244
  this.tokensPath = tokensPath;
5519
7245
  this.box = box;
5520
7246
  this.fetchImpl = fetchImpl;
5521
7247
  this.externalCliReader = externalCliReader;
5522
- this.externalCliStore = externalCliStore;
5523
7248
  }
5524
7249
  tokensPath;
5525
7250
  box;
5526
7251
  fetchImpl;
5527
7252
  externalCliReader;
5528
- externalCliStore;
5529
7253
  /**
5530
7254
  * The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
5531
7255
  * TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
5532
7256
  * through {@link fetchUpstream} with the account's `{ providerId, accountId }`
5533
- * 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.
5534
7258
  */
5535
7259
  buildRefreshFetch(providerId, accountId) {
5536
- return this.fetchImpl ?? ((url, init) => fetchUpstream2(url, init, { providerId, accountId }));
7260
+ return this.fetchImpl ?? ((url, init) => fetchUpstream3(url, init, { providerId, accountId }));
5537
7261
  }
5538
7262
  /**
5539
- * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
7263
+ * In-flight refresh coalescing. OAuth refresh tokens are
5540
7264
  * SINGLE-USE: two concurrent refreshes of one account each spend the same
5541
7265
  * token and the loser bricks a healthy account. Every refresh entry point
5542
7266
  * (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
@@ -5551,13 +7275,13 @@ var JsonSubscriptionCredentialStore = class {
5551
7275
  return run;
5552
7276
  }
5553
7277
  /** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
5554
- * 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
5555
7279
  * strategies pull `accessToken` / `expiresAt` / `status` from it. */
5556
7280
  async getFullConfig() {
5557
7281
  return this.readConfig();
5558
7282
  }
5559
7283
  /** Current Claude OAuth access token, or `null` when none is stored. No inline
5560
- * 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
5561
7285
  * subscription auth strategy, which calls `refreshClaudeToken` (now real). */
5562
7286
  async getValidClaudeAccessToken() {
5563
7287
  return this.readConfig().claude?.accessToken ?? null;
@@ -5582,13 +7306,14 @@ var JsonSubscriptionCredentialStore = class {
5582
7306
  /**
5583
7307
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
5584
7308
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
5585
- * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
7309
+ * shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
5586
7310
  * Used by the admin accounts GET (secret-IN-never-OUT).
5587
7311
  */
5588
7312
  async listSanitizedAccounts() {
5589
7313
  const config = this.readConfig();
5590
- const health2 = getSharedAccountHealth();
5591
- const identityStore = getSharedIdentityStore();
7314
+ const health2 = getSharedAccountHealth2();
7315
+ const allowanceScheduling = getSharedAccountAllowanceScheduling3();
7316
+ const identityStore = getSharedIdentityStore2();
5592
7317
  const fingerprintOn = identityStore.isEnabled();
5593
7318
  const now = Date.now();
5594
7319
  const out = {};
@@ -5597,7 +7322,13 @@ var JsonSubscriptionCredentialStore = class {
5597
7322
  if (sanitized.length === 0) continue;
5598
7323
  for (const account of sanitized) {
5599
7324
  const status = health2.getStatus(provider, account.id, now);
7325
+ const allowance = allowanceScheduling.preview(provider, account.id, account.priority ?? 50, now);
5600
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;
5601
7332
  account.cooldownUntil = status.cooldownUntil !== void 0 ? new Date(status.cooldownUntil).toISOString() : void 0;
5602
7333
  if (fingerprintOn && provider === "claude") {
5603
7334
  account.identityCaptured = identityStore.hasIdentity(provider, account.id);
@@ -5605,31 +7336,25 @@ var JsonSubscriptionCredentialStore = class {
5605
7336
  account.identityCapturedAt = capturedAt !== void 0 ? new Date(capturedAt).toISOString() : void 0;
5606
7337
  }
5607
7338
  }
5608
- out[provider] = this.attachSyncWarnings(config, provider, sanitized);
7339
+ out[provider] = this.attachDuplicateWarnings(config, provider, sanitized);
5609
7340
  }
5610
7341
  return out;
5611
7342
  }
5612
7343
  /**
5613
- * List-time credential-conflict warnings (external-cli-sync). Computed, not
5614
- * persisted: (a) `duplicate-token` when two accounts of one provider share a
5615
- * credential, (b) `external-divergent` when the external CLI native store has
5616
- * rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
5617
- * a failed refresh (`external-not-rotated`) takes precedence — it is the most
5618
- * 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.
5619
7347
  */
5620
- attachSyncWarnings(config, provider, sanitized) {
7348
+ attachDuplicateWarnings(config, provider, sanitized) {
5621
7349
  const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
5622
- let divergentId;
5623
- if (provider === "claude" || provider === "codex") {
5624
- const active = getActiveAccount(config, provider);
5625
- if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
5626
- divergentId = active.id;
5627
- }
5628
- }
5629
- if (duplicates.size === 0 && !divergentId) return sanitized;
5630
7350
  return sanitized.map((account) => {
5631
- const computed = account.id === divergentId ? "external-divergent" : duplicates.has(account.id) ? "duplicate-token" : void 0;
5632
- 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 };
5633
7358
  });
5634
7359
  }
5635
7360
  /** Read the external CLI store, never letting an fs/parse error escape. */
@@ -5642,11 +7367,11 @@ var JsonSubscriptionCredentialStore = class {
5642
7367
  }
5643
7368
  /**
5644
7369
  * Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
5645
- * 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
5646
7371
  * block is untouched. Otherwise mint via the shared claude refresh flow and
5647
7372
  * write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
5648
- * On failure status:expired +
5649
- * errorMessage `false`.
7373
+ * On failure status:expired +
7374
+ * errorMessage `false`.
5650
7375
  */
5651
7376
  async refreshClaudeToken() {
5652
7377
  return this.coalesce("claude:active", async () => {
@@ -5671,19 +7396,8 @@ var JsonSubscriptionCredentialStore = class {
5671
7396
  syncWarning: void 0
5672
7397
  };
5673
7398
  this.writeBackById("claude", capturedId, next);
5674
- this.resyncExternal("claude", capturedId, next);
5675
7399
  return true;
5676
7400
  } catch (error) {
5677
- if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
5678
- const r = await claudeOAuth2.refreshAccessToken(rt, refreshFetch);
5679
- return {
5680
- accessToken: r.accessToken,
5681
- refreshToken: r.refreshToken,
5682
- expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
5683
- };
5684
- })) {
5685
- return true;
5686
- }
5687
7401
  this.markExpiredById("claude", capturedId, claude, error);
5688
7402
  return false;
5689
7403
  }
@@ -5718,20 +7432,8 @@ var JsonSubscriptionCredentialStore = class {
5718
7432
  syncWarning: void 0
5719
7433
  };
5720
7434
  this.writeBackById("codex", capturedId, next);
5721
- this.resyncExternal("codex", capturedId, next);
5722
7435
  return true;
5723
7436
  } catch (error) {
5724
- if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
5725
- const r = await codexOAuth2.refreshAccessToken(rt, refreshFetch);
5726
- return {
5727
- accessToken: r.accessToken,
5728
- refreshToken: r.refreshToken,
5729
- idToken: r.idToken,
5730
- expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
5731
- };
5732
- })) {
5733
- return true;
5734
- }
5735
7437
  this.markExpiredById("codex", capturedId, codex, error);
5736
7438
  return false;
5737
7439
  }
@@ -5774,11 +7476,10 @@ var JsonSubscriptionCredentialStore = class {
5774
7476
  });
5775
7477
  }
5776
7478
  /**
5777
- * Refresh a SPECIFIC account by id (background scheduler sweep,
5778
- * external-cli-sync). Unlike the active-account refreshers it does NOT
5779
- * attempt the external-import fallback the external CLI file's lineage can
5780
- * only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
5781
- * 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`.
5782
7483
  */
5783
7484
  async refreshAccountById(provider, id) {
5784
7485
  return this.coalesce(`${provider}:${id}`, async () => {
@@ -5792,7 +7493,7 @@ var JsonSubscriptionCredentialStore = class {
5792
7493
  const next = {
5793
7494
  ...captured,
5794
7495
  accessToken: refreshed.accessToken,
5795
- // 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.
5796
7497
  refreshToken: refreshed.refreshToken ?? captured.refreshToken,
5797
7498
  expiresAt: refreshed.expiresAt,
5798
7499
  status: "authorized",
@@ -5802,7 +7503,6 @@ var JsonSubscriptionCredentialStore = class {
5802
7503
  };
5803
7504
  if (refreshed.idToken) next.idToken = refreshed.idToken;
5804
7505
  this.writeBackById(provider, id, next);
5805
- if (provider !== "gemini") this.resyncExternal(provider, id, next);
5806
7506
  return true;
5807
7507
  } catch (error) {
5808
7508
  this.markExpiredById(provider, id, captured, error);
@@ -5810,7 +7510,7 @@ var JsonSubscriptionCredentialStore = class {
5810
7510
  }
5811
7511
  });
5812
7512
  }
5813
- // ── By-id account-pool surface (subscription-account-scheduling, design D6) ──
7513
+ // By-id account-pool surface (subscription-account-scheduling, design D6)
5814
7514
  /**
5815
7515
  * Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
5816
7516
  * provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
@@ -5843,7 +7543,7 @@ var JsonSubscriptionCredentialStore = class {
5843
7543
  /**
5844
7544
  * Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
5845
7545
  * `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
5846
- * `false` (no refresh affordance).
7546
+ * `false` (no refresh affordance).
5847
7547
  */
5848
7548
  async refreshAccountToken(providerId, accountId) {
5849
7549
  if (providerId === "opencodego") return false;
@@ -5865,7 +7565,7 @@ var JsonSubscriptionCredentialStore = class {
5865
7565
  * (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
5866
7566
  * whitelisted fingerprint headers; the token mirror is untouched); a no-op for
5867
7567
  * an unknown id. Called by the identity store's persistence port on a first-seen
5868
- * 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
5869
7569
  * store's port wrapper swallows a rejection so the relay hot path is unaffected.
5870
7570
  */
5871
7571
  async setAccountIdentity(providerId, accountId, identity) {
@@ -5890,7 +7590,7 @@ var JsonSubscriptionCredentialStore = class {
5890
7590
  * DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
5891
7591
  * the port). Passing `undefined` clears the override. Write-only password: when
5892
7592
  * the incoming structured proxy omits the password but the account already had
5893
- * one, the current (decrypted) password is preserved editing host/port never
7593
+ * one, the current (decrypted) password is preserved editing host/port never
5894
7594
  * wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
5895
7595
  */
5896
7596
  async setAccountProxy(providerId, accountId, proxy) {
@@ -5925,75 +7625,25 @@ var JsonSubscriptionCredentialStore = class {
5925
7625
  expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
5926
7626
  };
5927
7627
  }
5928
- /**
5929
- * External-import fallback for a FAILED active-account refresh
5930
- * (external-cli-sync). Reads the CLI native store; imports when the external
5931
- * lineage ROTATED (different refresh token) or its access token is still
5932
- * valid. When the imported access token is already expired it refreshes once
5933
- * with the rotated refresh token. A `not-rotated` outcome persists the
5934
- * `external-not-rotated` warning on the (about-to-be-expired) account so the
5935
- * UI can tell "genuine revocation" apart from a plain refresh failure.
5936
- */
5937
- async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
5938
- const markerOwner = this.safeReadMarker(provider);
5939
- if (markerOwner && markerOwner !== capturedId) return false;
5940
- const external = this.safeReadExternal(provider);
5941
- const decision = decideExternalImport(captured, external);
5942
- if (decision === "not-rotated") {
5943
- captured.syncWarning = "external-not-rotated";
5944
- return false;
5945
- }
5946
- if (decision !== "import" || !external) return false;
5947
- let imported = buildImportedTokens(
5948
- captured,
5949
- external
5950
- );
5951
- const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
5952
- if (!accessStillValid) {
5953
- try {
5954
- const refreshed = await refreshWithToken(external.refreshToken);
5955
- imported = {
5956
- ...imported,
5957
- accessToken: refreshed.accessToken,
5958
- refreshToken: refreshed.refreshToken ?? imported.refreshToken,
5959
- expiresAt: refreshed.expiresAt,
5960
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
5961
- };
5962
- if (refreshed.idToken) imported.idToken = refreshed.idToken;
5963
- } catch {
5964
- return false;
5965
- }
5966
- }
5967
- this.writeBackById(provider, capturedId, imported);
5968
- this.resyncExternal(provider, capturedId, imported);
5969
- return true;
5970
- }
5971
- /**
5972
- * Marker-gated external write-back (external-cli-sync). After a successful
5973
- * refresh of the account that OWNS the provider's native CLI store (imported
5974
- * via `importExternalCliAccount`), push the rotated credential back into the
5975
- * file — otherwise the daemon's refresh invalidates the single-use refresh
5976
- * token and silently logs the bare CLI out. NON-FATAL: the internal store is
5977
- * already persisted; a failed external write only leaves the file stale,
5978
- * which the `external-divergent` warning surfaces.
5979
- */
5980
- resyncExternal(provider, accountId, tokens) {
5981
- try {
5982
- this.externalCliStore.writeBack(provider, accountId, tokens);
5983
- } catch {
5984
- }
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;
5985
7635
  }
5986
- /** Read the marker's owning account id, never letting an fs error escape. */
5987
- safeReadMarker(provider) {
5988
- try {
5989
- return this.externalCliStore.readMarkerAccountId(provider);
5990
- } catch {
5991
- return void 0;
5992
- }
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;
5993
7643
  }
5994
7644
  /**
5995
7645
  * DAEMON-ONLY (admin import button): which providers have a usable external
5996
- * CLI credential on THIS machine. Pure detection reads the native files,
7646
+ * CLI credential on THIS machine. Pure detection reads the native files,
5997
7647
  * never mutates anything, never returns a token.
5998
7648
  */
5999
7649
  async listExternalCliAvailability() {
@@ -6004,21 +7654,22 @@ var JsonSubscriptionCredentialStore = class {
6004
7654
  }
6005
7655
  /**
6006
7656
  * DAEMON-ONLY (admin import button): import the external CLI's current login
6007
- * as a NEW account (+ activate), and take MANAGED ownership of the native
6008
- * store (marker) so subsequent refreshes write back keeping the bare CLI
6009
- * and the daemon on the same live credential instead of silently killing one
6010
- * 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.
6011
7661
  */
6012
7662
  async importExternalCliAccount(provider, label) {
6013
7663
  const external = this.safeReadExternal(provider);
6014
7664
  if (!external?.accessToken) return { ok: false, reason: "no-credential" };
6015
7665
  const tokens = buildTokensFromExternal(provider, external);
6016
7666
  const result = await this.appendProviderAccount(provider, tokens, label);
6017
- try {
6018
- this.externalCliStore.writeMarker(provider, result.id);
6019
- } catch {
6020
- }
6021
- return { ok: true, id: result.id };
7667
+ return {
7668
+ ok: true,
7669
+ id: result.id,
7670
+ nativeCredentialMode: "read-only",
7671
+ refreshWritesNativeCredentials: false
7672
+ };
6022
7673
  }
6023
7674
  /**
6024
7675
  * Materialize a lazily-synthesized account id to disk (design D3). On a legacy
@@ -6051,7 +7702,8 @@ var JsonSubscriptionCredentialStore = class {
6051
7702
  this.writeBackById(providerId, capturedId, {
6052
7703
  ...block,
6053
7704
  status: "expired",
6054
- errorMessage
7705
+ errorMessage,
7706
+ syncWarning: "syncWarning" in block && block.syncWarning === "duplicate-token" ? "duplicate-token" : void 0
6055
7707
  });
6056
7708
  }
6057
7709
  /**
@@ -6060,7 +7712,7 @@ var JsonSubscriptionCredentialStore = class {
6060
7712
  * `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
6061
7713
  * OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
6062
7714
  * tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
6063
- * 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
6064
7716
  * sees this write.
6065
7717
  */
6066
7718
  async writeProviderTokens(providerId, config) {
@@ -6070,7 +7722,7 @@ var JsonSubscriptionCredentialStore = class {
6070
7722
  }
6071
7723
  /**
6072
7724
  * DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
6073
- * (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
6074
7726
  * `omnicross login <provider> --label` to add an account instead of overwriting.
6075
7727
  */
6076
7728
  async appendProviderAccount(providerId, config, label) {
@@ -6104,7 +7756,7 @@ var JsonSubscriptionCredentialStore = class {
6104
7756
  }
6105
7757
  /**
6106
7758
  * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
6107
- * 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
6108
7760
  * (the secret-free invariant holds).
6109
7761
  */
6110
7762
  async renameAccount(providerId, id, label) {
@@ -6127,12 +7779,12 @@ var JsonSubscriptionCredentialStore = class {
6127
7779
  }
6128
7780
  /** Write the merged config to disk as pretty JSON (mkdir parent if needed).
6129
7781
  * Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
6130
- * `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
6131
- * 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. */
6132
7784
  persist(config) {
6133
- mkdirSync3(dirname4(this.tokensPath), { recursive: true });
7785
+ mkdirSync4(dirname6(this.tokensPath), { recursive: true });
6134
7786
  const encrypted = encryptTokens(config, this.box);
6135
- writeFileSync8(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
7787
+ writeFileSync10(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
6136
7788
  }
6137
7789
  /**
6138
7790
  * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
@@ -6140,18 +7792,18 @@ var JsonSubscriptionCredentialStore = class {
6140
7792
  * subscription bearer path is byte-identical).
6141
7793
  *
6142
7794
  * The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
6143
- * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
7795
+ * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
6144
7796
  * wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
6145
- * box's clear, secret-free error (secrets spec "错误密钥 / 篡改的解密失败 UX":
6146
- * SHALL fail-fast, SHALL NOT 静默降级 — a swallowed decrypt would report "no
6147
- * 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
6148
7800
  * `config.ts loadConfig`, which decrypts outside its parse try.
6149
7801
  */
6150
7802
  readConfig() {
6151
- if (!existsSync10(this.tokensPath)) return { updatedAt: "" };
7803
+ if (!existsSync13(this.tokensPath)) return { updatedAt: "" };
6152
7804
  let parsed;
6153
7805
  try {
6154
- const raw = JSON.parse(readFileSync10(this.tokensPath, "utf8"));
7806
+ const raw = JSON.parse(readFileSync13(this.tokensPath, "utf8"));
6155
7807
  parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
6156
7808
  } catch {
6157
7809
  parsed = null;
@@ -6163,7 +7815,7 @@ var JsonSubscriptionCredentialStore = class {
6163
7815
  };
6164
7816
 
6165
7817
  // src/AccountHealthProbeScheduler.ts
6166
- import { fetchUpstream as fetchUpstream3 } from "@omnicross/core/pipeline/upstreamFetch";
7818
+ import { fetchUpstream as fetchUpstream4 } from "@omnicross/core/pipeline/upstreamFetch";
6167
7819
 
6168
7820
  // src/probe/ProbeStrategy.ts
6169
7821
  var PROVIDER_PROBE_PLANS = {
@@ -6206,7 +7858,7 @@ var AccountHealthProbeScheduler = class {
6206
7858
  this.logger = logger;
6207
7859
  this.config = config;
6208
7860
  this.now = opts.now ?? Date.now;
6209
- this.fetchImpl = opts.fetchImpl ?? fetchUpstream3;
7861
+ this.fetchImpl = opts.fetchImpl ?? fetchUpstream4;
6210
7862
  this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6211
7863
  this.planFor = opts.planFor ?? probePlanFor;
6212
7864
  }
@@ -6479,8 +8131,8 @@ var AccountHealthSweeper = class {
6479
8131
  };
6480
8132
 
6481
8133
  // src/audit/AuditPruneSweeper.ts
6482
- import { existsSync as existsSync11, readdirSync, unlinkSync } from "fs";
6483
- import { join as join5 } from "path";
8134
+ import { existsSync as existsSync14, readdirSync, unlinkSync as unlinkSync3 } from "fs";
8135
+ import { join as join7 } from "path";
6484
8136
 
6485
8137
  // src/audit/auditFiles.ts
6486
8138
  var AUDIT_FILE_RE = /^audit-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
@@ -6506,8 +8158,8 @@ function auditFileDateMs(fileName) {
6506
8158
  var DAY_MS = 24 * 60 * 6e4;
6507
8159
  var SWEEP_INTERVAL_MS2 = 60 * 6e4;
6508
8160
  var AuditPruneSweeper = class {
6509
- constructor(auditDir, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
6510
- this.auditDir = auditDir;
8161
+ constructor(auditDir2, logger, config, intervalMs = SWEEP_INTERVAL_MS2, now = Date.now) {
8162
+ this.auditDir = auditDir2;
6511
8163
  this.logger = logger;
6512
8164
  this.config = config;
6513
8165
  this.intervalMs = intervalMs;
@@ -6554,7 +8206,7 @@ var AuditPruneSweeper = class {
6554
8206
  if (!this.config.enabled || this.sweeping) return 0;
6555
8207
  this.sweeping = true;
6556
8208
  try {
6557
- if (!existsSync11(this.auditDir)) return 0;
8209
+ if (!existsSync14(this.auditDir)) return 0;
6558
8210
  const today = new Date(this.now());
6559
8211
  const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
6560
8212
  const cutoff = todayMidnight - (this.config.retentionDays - 1) * DAY_MS;
@@ -6563,7 +8215,7 @@ var AuditPruneSweeper = class {
6563
8215
  const dateMs = auditFileDateMs(file);
6564
8216
  if (dateMs === null || dateMs >= cutoff) continue;
6565
8217
  try {
6566
- unlinkSync(join5(this.auditDir, file));
8218
+ unlinkSync3(join7(this.auditDir, file));
6567
8219
  removed += 1;
6568
8220
  } catch (error) {
6569
8221
  this.logger.warn("[AuditPruneSweeper] failed to unlink expired audit file", {
@@ -6586,26 +8238,26 @@ var AuditPruneSweeper = class {
6586
8238
  };
6587
8239
 
6588
8240
  // src/audit/auditReader.ts
6589
- import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
6590
- import { join as join6 } from "path";
8241
+ import { existsSync as existsSync15, readdirSync as readdirSync2, readFileSync as readFileSync14 } from "fs";
8242
+ import { join as join8 } from "path";
6591
8243
  var DEFAULT_LIMIT = 200;
6592
8244
  var MAX_LIMIT = 2e3;
6593
- function readAuditRecords(auditDir, query = {}) {
6594
- if (!existsSync12(auditDir)) return [];
8245
+ function readAuditRecords(auditDir2, query2 = {}) {
8246
+ if (!existsSync15(auditDir2)) return [];
6595
8247
  let files;
6596
8248
  try {
6597
- files = readdirSync2(auditDir).filter((f) => AUDIT_FILE_RE.test(f));
8249
+ files = readdirSync2(auditDir2).filter((f) => AUDIT_FILE_RE.test(f));
6598
8250
  } catch {
6599
8251
  return [];
6600
8252
  }
6601
- const from = typeof query.from === "number" ? query.from : -Infinity;
6602
- const to = typeof query.to === "number" ? query.to : Infinity;
6603
- 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)));
6604
8256
  const matched = [];
6605
8257
  for (const file of files.sort().reverse()) {
6606
8258
  let raw;
6607
8259
  try {
6608
- raw = readFileSync11(join6(auditDir, file), "utf8");
8260
+ raw = readFileSync14(join8(auditDir2, file), "utf8");
6609
8261
  } catch {
6610
8262
  continue;
6611
8263
  }
@@ -6619,7 +8271,7 @@ function readAuditRecords(auditDir, query = {}) {
6619
8271
  continue;
6620
8272
  }
6621
8273
  if (!isAuditRecord(rec)) continue;
6622
- if (query.keyId !== void 0 && rec.keyId !== query.keyId) continue;
8274
+ if (query2.keyId !== void 0 && rec.keyId !== query2.keyId) continue;
6623
8275
  if (rec.ts < from || rec.ts > to) continue;
6624
8276
  matched.push(rec);
6625
8277
  }
@@ -6634,11 +8286,11 @@ function isAuditRecord(value) {
6634
8286
  }
6635
8287
 
6636
8288
  // src/audit/AuditWriter.ts
6637
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "fs";
6638
- import { join as join7 } from "path";
8289
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
8290
+ import { join as join9 } from "path";
6639
8291
  var AuditWriter = class {
6640
- constructor(auditDir, logger, defer = (fn) => setTimeout(fn, 0)) {
6641
- this.auditDir = auditDir;
8292
+ constructor(auditDir2, logger, defer = (fn) => setTimeout(fn, 0)) {
8293
+ this.auditDir = auditDir2;
6642
8294
  this.logger = logger;
6643
8295
  this.defer = defer;
6644
8296
  }
@@ -6668,19 +8320,19 @@ var AuditWriter = class {
6668
8320
  */
6669
8321
  appendNow(record) {
6670
8322
  if (!this.dirEnsured) {
6671
- mkdirSync4(this.auditDir, { recursive: true });
8323
+ mkdirSync5(this.auditDir, { recursive: true });
6672
8324
  this.dirEnsured = true;
6673
8325
  }
6674
- const file = join7(this.auditDir, auditFileName(record.ts));
8326
+ const file = join9(this.auditDir, auditFileName(record.ts));
6675
8327
  appendFileSync2(file, JSON.stringify(record) + "\n", "utf8");
6676
8328
  }
6677
8329
  };
6678
8330
 
6679
8331
  // src/billing/BillingPublisher.ts
6680
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync5 } from "fs";
8332
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync6 } from "fs";
6681
8333
  import { createHmac } from "crypto";
6682
- import { join as join8 } from "path";
6683
- 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";
6684
8336
 
6685
8337
  // src/billing/billingFiles.ts
6686
8338
  var BILLING_FILE_RE = /^billing-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
@@ -6703,7 +8355,7 @@ var BillingPublisher = class {
6703
8355
  constructor(billingDir, logger, opts = {}) {
6704
8356
  this.billingDir = billingDir;
6705
8357
  this.logger = logger;
6706
- this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream4(url, init));
8358
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
6707
8359
  this.defer = opts.defer ?? ((fn) => setTimeout(fn, 0));
6708
8360
  this.timeoutMs = opts.timeoutMs ?? BILLING_POST_TIMEOUT_MS;
6709
8361
  this.now = opts.now ?? Date.now;
@@ -6750,7 +8402,7 @@ var BillingPublisher = class {
6750
8402
  */
6751
8403
  appendNow(event) {
6752
8404
  this.ensureDir();
6753
- const file = join8(this.billingDir, billingFileName(event.ts));
8405
+ const file = join10(this.billingDir, billingFileName(event.ts));
6754
8406
  appendFileSync3(file, JSON.stringify(event) + "\n", "utf8");
6755
8407
  }
6756
8408
  /**
@@ -6800,7 +8452,7 @@ var BillingPublisher = class {
6800
8452
  markDelivered(event) {
6801
8453
  try {
6802
8454
  this.ensureDir();
6803
- const file = join8(this.billingDir, deliveredFileName(event.ts));
8455
+ const file = join10(this.billingDir, deliveredFileName(event.ts));
6804
8456
  appendFileSync3(file, JSON.stringify({ id: event.id, deliveredAt: this.now() }) + "\n", "utf8");
6805
8457
  } catch (error) {
6806
8458
  this.logger.warn("[BillingPublisher] failed to append delivery marker", {
@@ -6810,17 +8462,17 @@ var BillingPublisher = class {
6810
8462
  }
6811
8463
  ensureDir() {
6812
8464
  if (this.dirEnsured) return;
6813
- mkdirSync5(this.billingDir, { recursive: true });
8465
+ mkdirSync6(this.billingDir, { recursive: true });
6814
8466
  this.dirEnsured = true;
6815
8467
  }
6816
8468
  };
6817
8469
 
6818
8470
  // src/billing/billingReader.ts
6819
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync12 } from "fs";
6820
- import { join as join9 } from "path";
8471
+ import { existsSync as existsSync16, readdirSync as readdirSync3, readFileSync as readFileSync15 } from "fs";
8472
+ import { join as join11 } from "path";
6821
8473
  function readBillingLedger(billingDir) {
6822
8474
  const view = { events: [], deliveredIds: /* @__PURE__ */ new Set() };
6823
- if (!existsSync13(billingDir)) return view;
8475
+ if (!existsSync16(billingDir)) return view;
6824
8476
  let files;
6825
8477
  try {
6826
8478
  files = readdirSync3(billingDir);
@@ -6854,7 +8506,7 @@ function readBillingStatus(billingDir) {
6854
8506
  function parseLines(dir, file) {
6855
8507
  let raw;
6856
8508
  try {
6857
- raw = readFileSync12(join9(dir, file), "utf8");
8509
+ raw = readFileSync15(join11(dir, file), "utf8");
6858
8510
  } catch {
6859
8511
  return [];
6860
8512
  }
@@ -7009,8 +8661,9 @@ var TokenRefreshScheduler = class {
7009
8661
  const expiresAt = Date.parse(t.expiresAt);
7010
8662
  return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
7011
8663
  }
7012
- /** Refresh one account; failures are logged, never thrown (the store has
7013
- * 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
+ */
7014
8667
  async refreshOne(provider, id, isActive) {
7015
8668
  try {
7016
8669
  const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
@@ -7041,7 +8694,7 @@ var TokenRefreshScheduler = class {
7041
8694
 
7042
8695
  // src/webhook/WebhookDispatcher.ts
7043
8696
  import { createHmac as createHmac2 } from "crypto";
7044
- import { fetchUpstream as fetchUpstream5 } from "@omnicross/core/pipeline/upstreamFetch";
8697
+ import { fetchUpstream as fetchUpstream6 } from "@omnicross/core/pipeline/upstreamFetch";
7045
8698
  var WEBHOOK_MAX_ATTEMPTS = 3;
7046
8699
  var WEBHOOK_QUEUE_MAX = 1e3;
7047
8700
  var WEBHOOK_SEND_TIMEOUT_MS = 1e4;
@@ -7061,7 +8714,7 @@ var WebhookDispatcher = class {
7061
8714
  sleep;
7062
8715
  now;
7063
8716
  constructor(opts = {}) {
7064
- this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream5(url, init));
8717
+ this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetchUpstream6(url, init));
7065
8718
  this.logger = opts.logger;
7066
8719
  this.maxAttempts = opts.maxAttempts ?? WEBHOOK_MAX_ATTEMPTS;
7067
8720
  this.queueMax = opts.queueMax ?? WEBHOOK_QUEUE_MAX;
@@ -7215,11 +8868,32 @@ function buildDaemon(config, paths) {
7215
8868
  setSecretBox(secretBox3);
7216
8869
  setSecretBox2(secretBox3);
7217
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
+ );
7218
8880
  const llmConfig = new ConfigFileProviderConfigSource(decryptedConfig);
7219
8881
  const keyDb = new JsonOutboundKeyDb(paths.keysPath);
7220
8882
  const voucherDb = new JsonVoucherDb(defaultVouchersPath(paths.configPath));
7221
8883
  const settingsStore = new JsonApiServerSettingsStore(paths.configPath, secretBox3);
8884
+ const integrationStateStore = new IntegrationStateStore(
8885
+ defaultIntegrationsPath(paths.configPath),
8886
+ secretBox3
8887
+ );
7222
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
+ );
7223
8897
  const subscriptionAccounts = new SubscriptionAccountService(credentialStore);
7224
8898
  setSubscriptionAccountService(subscriptionAccounts);
7225
8899
  const subscriptionRegistry = new SubscriptionProviderRegistry(
@@ -7248,7 +8922,17 @@ function buildDaemon(config, paths) {
7248
8922
  }
7249
8923
  );
7250
8924
  const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
7251
- 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
+ );
7252
8936
  const usageEventStore = new JsonlUsageEventStore(
7253
8937
  defaultUsageEventsPath(paths.configPath),
7254
8938
  async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
@@ -7261,7 +8945,7 @@ function buildDaemon(config, paths) {
7261
8945
  llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
7262
8946
  const accountHealthProbeScheduler = new AccountHealthProbeScheduler(
7263
8947
  credentialStore,
7264
- getSharedAccountHealth2(),
8948
+ getSharedAccountHealth3(),
7265
8949
  logger,
7266
8950
  DEFAULT_ACCOUNT_PROBE
7267
8951
  );
@@ -7295,7 +8979,7 @@ function buildDaemon(config, paths) {
7295
8979
  // lines through the injected logger (honors level/format/file sink).
7296
8980
  logger
7297
8981
  });
7298
- const auditDir = defaultAuditDir(paths.configPath);
8982
+ const auditDir2 = defaultAuditDir(paths.configPath);
7299
8983
  const billingDir = defaultBillingDir(paths.configPath);
7300
8984
  const adminServer = new AdminServer({
7301
8985
  configPath: paths.configPath,
@@ -7309,6 +8993,9 @@ function buildDaemon(config, paths) {
7309
8993
  settingsStore,
7310
8994
  outboundApiServer,
7311
8995
  subscriptionAccounts,
8996
+ accountAllowanceService,
8997
+ allowanceRefreshScheduler: claudeAllowanceRefreshScheduler,
8998
+ accountProbeService: accountHealthProbeScheduler,
7312
8999
  // Least-authority token WRITER (design D4) — the concrete credential store
7313
9000
  // exposes `writeProviderTokens` / `clearProvider` as daemon-only methods (NOT
7314
9001
  // on the `SubscriptionCredentialStore` port). The admin API sees ONLY these two
@@ -7329,7 +9016,7 @@ function buildDaemon(config, paths) {
7329
9016
  // inject a mock so no real token endpoint is hit.
7330
9017
  // upstream-proxy: default the OAuth token-exchange fetch to the proxy-aware
7331
9018
  // helper so interactive login honors a configured proxy (global/env layers).
7332
- oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream6(url, init)),
9019
+ oauthExchangeFetch: paths.oauthExchangeFetch ?? ((url, init) => fetchUpstream7(url, init)),
7333
9020
  subscriptionAccountAppender: credentialStore,
7334
9021
  // Codex interactive OAuth (app-parity-2 child 5) — the async loopback flow store
7335
9022
  // + the one-shot 127.0.0.1:1455 listener. Token captured + persisted daemon-side;
@@ -7347,6 +9034,16 @@ function buildDaemon(config, paths) {
7347
9034
  cliTerminalOpener: paths.cliTerminalOpener,
7348
9035
  cliPathProbe: paths.cliPathProbe,
7349
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
+ },
7350
9047
  // Usage/pricing admin surface (usage-pricing child): stats queries go
7351
9048
  // through the recorder facade, pricing mutations through the engine, and
7352
9049
  // the row DELETE through the concrete store (delete is store-local — the
@@ -7371,19 +9068,19 @@ function buildDaemon(config, paths) {
7371
9068
  // date-rotated audit store. Bound to the store dir here so the AdminServer
7372
9069
  // carries no path/store coupling. Records hold IP/UA/bodies → admin-only,
7373
9070
  // NEVER unauth, NEVER on `/health`. Routed in `AdminServer` (not `adminApi.ts`).
7374
- auditReader: (query) => readAuditRecords(auditDir, query),
9071
+ auditReader: (query2) => readAuditRecords(auditDir2, query2),
7375
9072
  // billing-event-stream: the AUTHED `GET /admin/api/billing-status` returns the
7376
9073
  // secret-free total/delivered/pending counts of the durable ledger.
7377
9074
  billingStatusReader: () => readBillingStatus(billingDir)
7378
9075
  });
7379
9076
  const webhookDispatcher = new WebhookDispatcher({
7380
9077
  logger,
7381
- fetchImpl: (url, init) => fetchUpstream6(url, init)
9078
+ fetchImpl: (url, init) => fetchUpstream7(url, init)
7382
9079
  });
7383
- setWebhookRuntime(webhookDispatcher, getSharedAccountHealth2());
7384
- const auditWriter = new AuditWriter(auditDir, logger);
7385
- const auditPruneSweeper = new AuditPruneSweeper(auditDir, logger, DEFAULT_AUDIT_CONFIG);
7386
- setAuditRuntime(auditWriter, auditPruneSweeper);
9080
+ setWebhookRuntime(webhookDispatcher, getSharedAccountHealth3());
9081
+ const auditWriter = new AuditWriter(auditDir2, logger);
9082
+ const auditPruneSweeper = new AuditPruneSweeper(auditDir2, logger, DEFAULT_AUDIT_CONFIG);
9083
+ setAuditRuntime(auditWriter, auditPruneSweeper, auditDir2);
7387
9084
  const billingPublisher = new BillingPublisher(billingDir, logger);
7388
9085
  const billingRetrySweeper = new BillingRetrySweeper(
7389
9086
  billingDir,
@@ -7395,7 +9092,7 @@ function buildDaemon(config, paths) {
7395
9092
  const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
7396
9093
  const accountHealthSweeper = new AccountHealthSweeper(
7397
9094
  credentialStore,
7398
- getSharedAccountHealth2(),
9095
+ getSharedAccountHealth3(),
7399
9096
  logger
7400
9097
  );
7401
9098
  return {
@@ -7410,8 +9107,11 @@ function buildDaemon(config, paths) {
7410
9107
  credentialStore,
7411
9108
  subscriptionRegistry,
7412
9109
  subscriptionAccounts,
9110
+ accountAllowanceService,
9111
+ claudeAllowanceRefreshScheduler,
7413
9112
  pricingStore,
7414
9113
  pricingEngine,
9114
+ pricingRefreshScheduler,
7415
9115
  usageRecorder,
7416
9116
  adminServer,
7417
9117
  tokenRefreshScheduler,
@@ -7439,10 +9139,12 @@ function resetDaemonSingletonsForTests() {
7439
9139
  resetAuditRuntimeForTests();
7440
9140
  resetBillingRuntimeForTests();
7441
9141
  __resetSharedIdentityStoreForTests();
9142
+ __resetSharedAccountAllowanceStoreForTests();
9143
+ __resetSharedAccountAllowanceSchedulingForTests();
7442
9144
  }
7443
9145
  function isTokensStoreReadable(tokensPath) {
7444
9146
  try {
7445
- if (!existsSync14(tokensPath)) return true;
9147
+ if (!existsSync17(tokensPath)) return true;
7446
9148
  accessSync(tokensPath, fsConstants.R_OK);
7447
9149
  return true;
7448
9150
  } catch {
@@ -7489,6 +9191,9 @@ function inferApiFormat(provider) {
7489
9191
  if (hay.includes("generativelanguage") || hay.includes("gemini") || hay.includes("google")) {
7490
9192
  return { format: "gemini", ambiguous: false };
7491
9193
  }
9194
+ if (hay.includes("/responses")) {
9195
+ return { format: "openai-response", ambiguous: false };
9196
+ }
7492
9197
  if (hay.includes("openai") || hay.includes("/v1") || hay.includes("chat/completions")) {
7493
9198
  return { format: "openai", ambiguous: false };
7494
9199
  }