@omnicross/daemon 0.1.4 → 0.1.6

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