@omnicross/daemon 0.1.5 → 0.1.6

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