@omnicross/daemon 0.1.0 → 0.1.3

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.d.cts CHANGED
@@ -1,12 +1,20 @@
1
+ import { OutboundApiServerConfig, Logger, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, PricingStore, OutboundKeyDb as OutboundKeyDb$1, OutboundKeyDbRow, OutboundKeyPolicy } from '@omnicross/core';
1
2
  import { ApiKeyPoolService } from '@omnicross/core/completion/ApiKeyPoolService';
2
- import { OutboundKeyDb, OutboundApiServer } from '@omnicross/core/outbound-api';
3
+ import { AccountProbeConfig, OutboundKeyDb, VoucherDb, KeySpendReader, OutboundApiServer } from '@omnicross/core/outbound-api';
3
4
  import { ProviderProxy } from '@omnicross/core/provider-proxy';
5
+ import { UsageRecorder, PricingEngine } from '@omnicross/core/usage';
4
6
  import { SubscriptionCredentialStore, FetchLike, SubscriptionProviderRegistry, SubscriptionAccountService } from '@omnicross/subscriptions';
5
- import { OutboundApiServerConfig, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, Logger, OutboundKeyDb as OutboundKeyDb$1, OutboundKeyDbRow } from '@omnicross/core';
7
+ import { LoggingConfig, HealthReport } from '@omnicross/contracts/health-logging-types';
8
+ import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, ProxyConfig, SubscriptionAccountSanitized, AccountClientIdentity } from '@omnicross/contracts/account-tokens-types';
9
+ import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
10
+ import { SubscriptionAccountHealth } from '@omnicross/core/pipeline/SubscriptionAccountHealth';
11
+ import { fetchUpstream } from '@omnicross/core/pipeline/upstreamFetch';
12
+ import { AuditRecord, AuditConfig } from '@omnicross/contracts/audit-types';
13
+ import { BillingDeliveryStatus, BillingConfig, BillingEvent } from '@omnicross/contracts/billing-types';
6
14
  import http from 'node:http';
7
15
  import { LLMProvider, AgentDefaultModels, GlobalModelParameters } from '@omnicross/contracts/llm-config';
8
- import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
9
- import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, SubscriptionAccountSanitized } from '@omnicross/contracts/account-tokens-types';
16
+ import { PricingEntry, PricingEntryInput, PricingResolution } from '@omnicross/contracts/pricing-types';
17
+ import { WebhookConfig, WebhookEvent } from '@omnicross/contracts/webhook-types';
10
18
 
11
19
  /**
12
20
  * SecretBox.ts — a 32-byte master key wrapped with the tri-state secret rules
@@ -374,6 +382,12 @@ interface DaemonConfig {
374
382
  server?: OutboundApiServerConfig;
375
383
  /** Optional admin-dashboard config (RT3). */
376
384
  admin?: DaemonAdminConfig;
385
+ /**
386
+ * Optional logging config (configurable-logging). Absent ⇒ the zero-regression
387
+ * default (console + all levels + text). `file` is a PLAIN value — NOT a secret,
388
+ * so it is never walked by `decryptConfigSecrets`/`encryptConfigSecrets`.
389
+ */
390
+ logging?: LoggingConfig;
377
391
  }
378
392
  /** Apply defaults to a (possibly absent) admin block: enabled, port 8766,
379
393
  * loopback, no token. NOTE: an EXPLICIT `port: 0` is honored as "bind an
@@ -391,6 +405,96 @@ declare function loadConfig(path: string): DaemonConfig;
391
405
  * `enc:` untouched) before serializing. */
392
406
  declare function saveConfig(path: string, cfg: DaemonConfig): void;
393
407
 
408
+ /**
409
+ * external-cli-credentials — read-only access to the external CLI native
410
+ * credential stores (external-cli-sync).
411
+ *
412
+ * The daemon never WRITES these files (they belong to the CLIs); it only reads
413
+ * them back to (a) recover from the rotating-refresh-token race — when e.g.
414
+ * Claude Code refreshes `~/.claude/.credentials.json` it rotates OUR stored
415
+ * refresh token out from under us, and the external file then holds the only
416
+ * live credential — and (b) detect divergence for the account-list warning.
417
+ *
418
+ * File shapes (mirrors the shapes the CLIs themselves write):
419
+ * claude `~/.claude/.credentials.json`
420
+ * → `{ claudeAiOauth: { accessToken, refreshToken?, expiresAt(number ms),
421
+ * scopes? } }`
422
+ * codex `~/.codex/auth.json`
423
+ * → `{ tokens: { id_token?, access_token, refresh_token? } }` — no explicit
424
+ * expiry; the access token's JWT `exp` claim is the only expiry signal.
425
+ *
426
+ * Gemini is deliberately excluded: the gemini CLI's oauth store is not a
427
+ * supported import source (parity with the host app's external-sync scope).
428
+ *
429
+ * @module @omnicross/daemon/ports/external-cli-credentials
430
+ */
431
+ /** The two external CLI providers with a readable native store. */
432
+ type ExternalCliProvider = 'claude' | 'codex';
433
+ /**
434
+ * External credentials parsed OUT of a native store file. Field axes match the
435
+ * internal `*TokenConfig` shapes: `expiresAt` is an ISO STRING here (converted
436
+ * from the native ms number / codex JWT `exp`).
437
+ */
438
+ interface ExternalCliCredentials {
439
+ accessToken?: string;
440
+ refreshToken?: string;
441
+ /** ISO string. */
442
+ expiresAt?: string;
443
+ /** codex only. */
444
+ idToken?: string;
445
+ /** claude only. */
446
+ scopes?: string[];
447
+ }
448
+ /** Reader port — injectable so tests never touch the real home directory. */
449
+ type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentials | null;
450
+
451
+ /**
452
+ * external-cli-store — WRITE side of the external CLI native credential stores
453
+ * (external-cli-sync, import + write-back).
454
+ *
455
+ * Counterpart of `external-cli-credentials` (the read side). The daemon only
456
+ * ever writes a file it MANAGES: an explicit "import existing CLI login" puts
457
+ * an `.omnicross-managed` marker (recording the owning account id) next to the
458
+ * native store, and every subsequent successful refresh of THAT account writes
459
+ * the rotated credential back into the file. Without the write-back, the
460
+ * daemon's refresh would rotate the single-use refresh token and silently log
461
+ * the user's bare CLI out — the write-back keeps both sides on the same live
462
+ * credential.
463
+ *
464
+ * Safety properties:
465
+ * - NEVER writes without a matching marker (an unmanaged / foreign-account
466
+ * file is untouched);
467
+ * - read-then-merge: unrelated top-level keys in the native file (e.g.
468
+ * claude `email`, codex `OPENAI_API_KEY`) are preserved;
469
+ * - one-time `.omnicross-backup` of the original file before the FIRST
470
+ * overwrite (restorable by hand if the user wants the daemon out);
471
+ * - atomic write (temp file → rename) so a crash never leaves a torn file.
472
+ *
473
+ * The envelope shapes mirror what the CLIs themselves write — the round-trip
474
+ * test parses a written file back through `external-cli-credentials` to keep
475
+ * the two sides from drifting.
476
+ *
477
+ * @module @omnicross/daemon/ports/external-cli-store
478
+ */
479
+
480
+ /** Token blocks the write-back accepts (the two external-store providers). */
481
+ type ExternalWritableTokens = ClaudeTokenConfig | CodexTokenConfig;
482
+ /**
483
+ * Injectable port over the external store writes (tests use an in-memory fake;
484
+ * the store wires `realExternalCliStore`).
485
+ */
486
+ interface ExternalCliStorePort {
487
+ /** The owning account id recorded by the marker, or undefined when unmanaged. */
488
+ readMarkerAccountId(provider: ExternalCliProvider): string | undefined;
489
+ /** Record (or move) ownership of the provider's native store to an account. */
490
+ writeMarker(provider: ExternalCliProvider, accountId: string): void;
491
+ /**
492
+ * Write the refreshed tokens back into the native store — ONLY when the
493
+ * marker names `accountId`. Returns true when a write happened.
494
+ */
495
+ writeBack(provider: ExternalCliProvider, accountId: string, tokens: ExternalWritableTokens): boolean;
496
+ }
497
+
394
498
  /**
395
499
  * JsonSubscriptionCredentialStore — the daemon's file-backed
396
500
  * `SubscriptionCredentialStore` port impl (design D1).
@@ -452,15 +556,43 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
452
556
  private readonly tokensPath;
453
557
  private readonly box;
454
558
  private readonly fetchImpl;
559
+ /** Injectable external CLI native-store reader (external-cli-sync). */
560
+ private readonly externalCliReader;
561
+ /** Injectable external CLI native-store WRITER (marker-gated write-back). */
562
+ private readonly externalCliStore;
455
563
  /**
456
564
  * @param tokensPath on-disk `tokens.json` location.
457
565
  * @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
458
- * @param fetchImpl injectable HTTP port for the OAuth refresh round-trips
459
- * (oauth design D4). Defaults to the global `fetch` so boot
460
- * is unchanged; tests inject a mock fetch. NOT used by any
461
- * read/write path only by `refresh*Token`.
566
+ * @param fetchImpl OPTIONAL injectable HTTP port for the OAuth refresh
567
+ * round-trips (oauth design D4). A TEST-injected transport is
568
+ * used verbatim. When ABSENT (production), each refresh uses a
569
+ * proxy-aware {@link fetchUpstream} that threads the
570
+ * `{ providerId, accountId }` ctx (upstream-proxy M1) so a
571
+ * per-account/per-provider proxy is honored on refresh exactly
572
+ * as on relay — refresh egresses from the SAME proxy IP as the
573
+ * account's traffic. NOT used by any read/write path.
574
+ */
575
+ constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike | undefined,
576
+ /** Injectable external CLI native-store reader (external-cli-sync). */
577
+ externalCliReader?: ExternalCliReader,
578
+ /** Injectable external CLI native-store WRITER (marker-gated write-back). */
579
+ externalCliStore?: ExternalCliStorePort);
580
+ /**
581
+ * The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
582
+ * TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
583
+ * through {@link fetchUpstream} with the account's `{ providerId, accountId }`
584
+ * ctx so the per-account/provider proxy applies. `@internal` — also a test seam.
585
+ */
586
+ buildRefreshFetch(providerId: string, accountId?: string): FetchLike;
587
+ /**
588
+ * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
589
+ * SINGLE-USE: two concurrent refreshes of one account each spend the same
590
+ * token and the loser bricks a healthy account. Every refresh entry point
591
+ * (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
592
+ * through `coalesce`, so overlapping callers share ONE upstream round-trip.
462
593
  */
463
- constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike);
594
+ private readonly inFlightRefreshes;
595
+ private coalesce;
464
596
  /** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
465
597
  * file is absent/corrupt). This is the hot read — the codex / gemini auth
466
598
  * strategies pull `accessToken` / `expiresAt` / `status` from it. */
@@ -471,6 +603,14 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
471
603
  getValidClaudeAccessToken(): Promise<string | null>;
472
604
  /** Current OpenCodeGo static API key, or `null` when none is stored. */
473
605
  getValidOpenCodeGoApiKey(): Promise<string | null>;
606
+ /**
607
+ * DAEMON-ONLY per-account proxy lookup by id (upstream-proxy). Returns the
608
+ * DECRYPTED `ProxyConfig` for the account (`readConfig` decrypts on read), or
609
+ * `undefined` for an unknown provider/account or no per-account proxy. Feeds the
610
+ * winning per-account layer of the upstream-proxy resolver. Synchronous like the
611
+ * other hot reads. Never returns token material.
612
+ */
613
+ getAccountProxy(providerId: string, accountId: string): ProxyConfig | undefined;
474
614
  /**
475
615
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
476
616
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
@@ -478,6 +618,17 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
478
618
  * Used by the admin accounts GET (secret-IN-never-OUT).
479
619
  */
480
620
  listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
621
+ /**
622
+ * List-time credential-conflict warnings (external-cli-sync). Computed, not
623
+ * persisted: (a) `duplicate-token` when two accounts of one provider share a
624
+ * credential, (b) `external-divergent` when the external CLI native store has
625
+ * rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
626
+ * a failed refresh (`external-not-rotated`) takes precedence — it is the most
627
+ * actionable state.
628
+ */
629
+ private attachSyncWarnings;
630
+ /** Read the external CLI store, never letting an fs/parse error escape. */
631
+ private safeReadExternal;
481
632
  /**
482
633
  * Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
483
634
  * the block has no refresh_token (setup-token / manual) — no upstream call, the
@@ -501,6 +652,115 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
501
652
  * destroy the ability to refresh again). HONEST `false` when no refresh_token.
502
653
  */
503
654
  refreshGeminiToken(): Promise<boolean>;
655
+ /**
656
+ * Refresh a SPECIFIC account by id (background scheduler sweep,
657
+ * external-cli-sync). Unlike the active-account refreshers it does NOT
658
+ * attempt the external-import fallback — the external CLI file's lineage can
659
+ * only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
660
+ * failure flags ONLY that account `expired`.
661
+ */
662
+ refreshAccountById(provider: 'claude' | 'codex' | 'gemini', id: string): Promise<boolean>;
663
+ /**
664
+ * Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
665
+ * provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
666
+ * (refresh is 401-driven, like `getValidClaudeAccessToken`); codex/gemini refresh
667
+ * a near-expiry token via `refreshAccountById` (like `resolveAccessToken`);
668
+ * opencodego returns the account's static key. `null` when unknown/expired/
669
+ * tokenless.
670
+ */
671
+ getAccessTokenForAccount(providerId: SubscriptionProviderId, accountId: string): Promise<string | null>;
672
+ /**
673
+ * Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
674
+ * `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
675
+ * → `false` (no refresh affordance).
676
+ */
677
+ refreshAccountToken(providerId: SubscriptionProviderId, accountId: string): Promise<boolean>;
678
+ /**
679
+ * Best-effort record of a selection time onto the account's `lastUsedAt` by id
680
+ * (design D4). Entry-metadata only (the token mirror is untouched); a no-op for
681
+ * an unknown id. The selector throttles the call frequency, so this stays cheap.
682
+ */
683
+ touchAccountLastUsed(providerId: SubscriptionProviderId, accountId: string, iso: string): Promise<void>;
684
+ /**
685
+ * Best-effort write-through of a per-account client `identity`
686
+ * (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
687
+ * whitelisted fingerprint headers; the token mirror is untouched); a no-op for
688
+ * an unknown id. Called by the identity store's persistence port on a first-seen
689
+ * freeze / TTL refresh, so it stays infrequent. Never throws to the caller — the
690
+ * store's port wrapper swallows a rejection so the relay hot path is unaffected.
691
+ */
692
+ setAccountIdentity(providerId: SubscriptionProviderId, accountId: string, identity: AccountClientIdentity): Promise<void>;
693
+ /**
694
+ * DAEMON-ONLY set-priority (subscription-account-scheduling, admin write, NOT on
695
+ * the port). Set one account's scheduling `priority` by id. Secret-free
696
+ * (entry-metadata only; the mirror invariant is untouched). Rejects an unknown id.
697
+ */
698
+ setAccountPriority(providerId: SubscriptionProviderId, accountId: string, priority: number): Promise<{
699
+ ok: boolean;
700
+ }>;
701
+ /**
702
+ * DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
703
+ * the port). Passing `undefined` clears the override. Write-only password: when
704
+ * the incoming structured proxy omits the password but the account already had
705
+ * one, the current (decrypted) password is preserved — editing host/port never
706
+ * wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
707
+ */
708
+ setAccountProxy(providerId: SubscriptionProviderId, accountId: string, proxy: ProxyConfig | undefined): Promise<{
709
+ ok: boolean;
710
+ }>;
711
+ /**
712
+ * DAEMON-ONLY set/clear per-account `supportedModels` (subscription-account-
713
+ * model-map, admin write, NOT on the port). Passing `undefined` clears it.
714
+ * Secret-free (model ids only; the mirror invariant is untouched). Rejects an
715
+ * unknown id.
716
+ */
717
+ setAccountSupportedModels(providerId: SubscriptionProviderId, accountId: string, supportedModels: string[] | Record<string, string> | undefined): Promise<{
718
+ ok: boolean;
719
+ }>;
720
+ /** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
721
+ private refreshUpstream;
722
+ /**
723
+ * External-import fallback for a FAILED active-account refresh
724
+ * (external-cli-sync). Reads the CLI native store; imports when the external
725
+ * lineage ROTATED (different refresh token) or its access token is still
726
+ * valid. When the imported access token is already expired it refreshes once
727
+ * with the rotated refresh token. A `not-rotated` outcome persists the
728
+ * `external-not-rotated` warning on the (about-to-be-expired) account so the
729
+ * UI can tell "genuine revocation" apart from a plain refresh failure.
730
+ */
731
+ private tryExternalImport;
732
+ /**
733
+ * Marker-gated external write-back (external-cli-sync). After a successful
734
+ * refresh of the account that OWNS the provider's native CLI store (imported
735
+ * via `importExternalCliAccount`), push the rotated credential back into the
736
+ * file — otherwise the daemon's refresh invalidates the single-use refresh
737
+ * token and silently logs the bare CLI out. NON-FATAL: the internal store is
738
+ * already persisted; a failed external write only leaves the file stale,
739
+ * which the `external-divergent` warning surfaces.
740
+ */
741
+ private resyncExternal;
742
+ /** Read the marker's owning account id, never letting an fs error escape. */
743
+ private safeReadMarker;
744
+ /**
745
+ * DAEMON-ONLY (admin import button): which providers have a usable external
746
+ * CLI credential on THIS machine. Pure detection — reads the native files,
747
+ * never mutates anything, never returns a token.
748
+ */
749
+ listExternalCliAvailability(): Promise<Record<ExternalCliProvider, boolean>>;
750
+ /**
751
+ * DAEMON-ONLY (admin import button): import the external CLI's current login
752
+ * as a NEW account (+ activate), and take MANAGED ownership of the native
753
+ * store (marker) so subsequent refreshes write back — keeping the bare CLI
754
+ * and the daemon on the same live credential instead of silently killing one
755
+ * side's single-use refresh token.
756
+ */
757
+ importExternalCliAccount(provider: ExternalCliProvider, label?: string): Promise<{
758
+ ok: true;
759
+ id: string;
760
+ } | {
761
+ ok: false;
762
+ reason: 'no-credential';
763
+ }>;
504
764
  /**
505
765
  * Materialize a lazily-synthesized account id to disk (design D3). On a legacy
506
766
  * single-slot file, `readConfig` synthesizes a NON-deterministic account id
@@ -555,6 +815,14 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
555
815
  removeAccount(providerId: SubscriptionProviderId, id: string): Promise<{
556
816
  removed: boolean;
557
817
  }>;
818
+ /**
819
+ * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
820
+ * rejects an unknown id. Label-only — no token material is read or written
821
+ * (the secret-free invariant holds).
822
+ */
823
+ renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
824
+ ok: boolean;
825
+ }>;
558
826
  /**
559
827
  * DAEMON-ONLY CLEAR (design D1/D3, NOT on the port). Remove a single provider's
560
828
  * block from `tokens.json` and re-persist (the strategies already tolerate an
@@ -614,7 +882,40 @@ interface SubscriptionTokenWriter {
614
882
  removeAccount(providerId: SubscriptionProviderId, id: string): Promise<{
615
883
  removed: boolean;
616
884
  }>;
885
+ /** Rename one account's label (label-only; rejects an unknown id). */
886
+ renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
887
+ ok: boolean;
888
+ }>;
889
+ /** Set one account's scheduling priority (secret-free; rejects an unknown id).
890
+ * subscription-account-scheduling — lets an operator order a pool. */
891
+ setAccountPriority(providerId: SubscriptionProviderId, id: string, priority: number): Promise<{
892
+ ok: boolean;
893
+ }>;
894
+ /** Set (or CLEAR, with `undefined`) one account's per-account proxy override
895
+ * (upstream-proxy). The `proxy.password` is a secret (encrypted at rest, masked
896
+ * in the sanitized view). Rejects an unknown id. */
897
+ setAccountProxy(providerId: SubscriptionProviderId, id: string, proxy: ProxyConfig | undefined): Promise<{
898
+ ok: boolean;
899
+ }>;
900
+ /** Set (or CLEAR, with `undefined`) one account's `supportedModels`
901
+ * (subscription-account-model-map). Secret-free (model ids only). Rejects an
902
+ * unknown id. An array = allow-list (skip-only); an object = allow-list keys +
903
+ * logical→actual remap values. */
904
+ setAccountSupportedModels(providerId: SubscriptionProviderId, id: string, supportedModels: string[] | Record<string, string> | undefined): Promise<{
905
+ ok: boolean;
906
+ }>;
617
907
  listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
908
+ refreshClaudeToken(): Promise<boolean>;
909
+ refreshCodexToken(): Promise<boolean>;
910
+ refreshGeminiToken(): Promise<boolean>;
911
+ listExternalCliAvailability(): Promise<Record<'claude' | 'codex', boolean>>;
912
+ importExternalCliAccount(providerId: 'claude' | 'codex', label?: string): Promise<{
913
+ ok: true;
914
+ id: string;
915
+ } | {
916
+ ok: false;
917
+ reason: 'no-credential';
918
+ }>;
618
919
  }
619
920
 
620
921
  /**
@@ -778,6 +1079,239 @@ declare class CodexOAuthSessionStore {
778
1079
  private sweep;
779
1080
  }
780
1081
 
1082
+ /**
1083
+ * ProbeStrategy — the per-provider two-tier probe plan
1084
+ * (subscription-account-probe #8, design D1).
1085
+ *
1086
+ * A `ProbePlan` says HOW to probe one provider's account, cheapest signal first:
1087
+ * - `{ kind: 'local' }` — no upstream call. The scheduler's free local tier reads
1088
+ * the account's token via the credential store; a missing/expired-unrefreshable
1089
+ * token is a dead account (recorded as a synthesized 401). A provider is
1090
+ * local-only until a cheap authed GET endpoint is VERIFIED (the omnicross
1091
+ * `// UNVERIFIED` convention) — Phase 1 ships codex/gemini/opencodego local-only
1092
+ * (LEAD OQ1: never GUESS a billable/wrong endpoint).
1093
+ * - `{ kind: 'upstream'; url; buildInit(token) }` — a minimal AUTHED GET the
1094
+ * scheduler issues through #3's proxy-aware `fetchUpstream`. Phase 1 wires ONLY
1095
+ * claude → `GET https://api.anthropic.com/v1/models` (a verified free list;
1096
+ * NEVER a billable completion). The scheduler adds the timeout `signal` +
1097
+ * `{ providerId, accountId }` ctx; `buildInit` supplies method + auth header.
1098
+ *
1099
+ * NEVER put a body / max_tokens here — a probe must cost nothing (design D1).
1100
+ *
1101
+ * @module @omnicross/daemon/probe/ProbeStrategy
1102
+ */
1103
+
1104
+ /** How to probe one provider's account (design D1). */
1105
+ type ProbePlan = {
1106
+ kind: 'local';
1107
+ } | {
1108
+ kind: 'upstream';
1109
+ /** The cheap, free, authed GET endpoint. */
1110
+ url: string;
1111
+ /** Build the minimal request init (method + Authorization only — no body). */
1112
+ buildInit(token: string): RequestInit;
1113
+ };
1114
+
1115
+ /**
1116
+ * AccountHealthProbeScheduler — the scheduled ACTIVE account-health probe
1117
+ * (subscription-account-probe #8, design D1–D6).
1118
+ *
1119
+ * #2's health machine is PASSIVE — an account is only found dead when a REAL
1120
+ * request hits it and fails. This scheduler is the active complement: on a plain
1121
+ * `unref()`ed interval (omnicross has no cron dep) it runs a CHEAP per-account
1122
+ * probe and feeds the outcome into #2's EXISTING shared tracker
1123
+ * (`recordUpstreamOutcome`) — inventing no new marking path. Modeled EXACTLY on
1124
+ * `AccountHealthSweeper`: `start()` arms the timer, `dispose()` clears it, a
1125
+ * single-sweep re-entrancy guard prevents overlap.
1126
+ *
1127
+ * TWO-TIER, cheapest-first (design D1):
1128
+ * 1. FREE local — read the account's token via the credential store; no usable
1129
+ * token ⇒ dead ⇒ a synthesized `401` outcome, NO upstream call.
1130
+ * 2. Minimal AUTHED upstream GET (only providers with a VERIFIED cheap endpoint)
1131
+ * via #3's proxy-aware `fetchUpstream` with a short timeout.
1132
+ *
1133
+ * SAFE tracker mapping (LEAD constraint, mirrors #2's bare-429 discipline):
1134
+ * **401/403 → mark; 2xx → clear transient; 429 / 5xx / thrown/timeout → NEVER a
1135
+ * mark** (a probe rate-limit or upstream blip must not blacklist a healthy
1136
+ * account). So only 401/403/2xx are forwarded to the tracker; the rest are
1137
+ * history-only. NEVER a billable probe — the upstream tier is a free authed GET.
1138
+ *
1139
+ * NEVER-STRAND: marking flows through #2, whose ≥2-account gate (in the strategy's
1140
+ * schedulable derivation) keeps a marked SOLE account schedulable — so a probe can
1141
+ * never strand a single-account user. `onlyMultiAccount` (default) additionally
1142
+ * skips single-account providers entirely.
1143
+ *
1144
+ * ZERO REGRESSION: default `enabled:false` ⇒ `start()` never arms + `sweep()`
1145
+ * early-returns ⇒ no probes, no `/health` boolean, byte-identical.
1146
+ *
1147
+ * @module @omnicross/daemon/AccountHealthProbeScheduler
1148
+ */
1149
+
1150
+ /** One rolling probe result (design D4; in-memory, cleared on restart). */
1151
+ interface ProbeRecord {
1152
+ /** Epoch ms of the probe. */
1153
+ ts: number;
1154
+ /** Whether the probe observed a HEALTHY signal (2xx upstream / token-present local). */
1155
+ ok: boolean;
1156
+ /** The HTTP status (`null` = thrown/timeout); absent for a token-present local record. */
1157
+ status?: number | null;
1158
+ /** Upstream round-trip latency (ms); absent for a local-tier record. */
1159
+ latencyMs?: number;
1160
+ /** Which tier produced this record. */
1161
+ tier: 'local' | 'upstream';
1162
+ }
1163
+ /** Per-account probe history for the authed admin surface (names account ids). */
1164
+ interface AccountProbeHistorySnapshot {
1165
+ providerId: string;
1166
+ accountId: string;
1167
+ records: ProbeRecord[];
1168
+ }
1169
+ /**
1170
+ * The read surface the AUTHED admin route consumes (subscription-account-probe,
1171
+ * design D5). Structurally satisfied by {@link AccountHealthProbeScheduler}; typed
1172
+ * narrow so the admin layer carries no scheduler coupling.
1173
+ */
1174
+ interface AccountProbeHistoryReader {
1175
+ getAllHistory(): AccountProbeHistorySnapshot[];
1176
+ }
1177
+ /** The narrow credential-store surface the scheduler reads (#1 seams). */
1178
+ interface ProbeCredentialStore {
1179
+ getFullConfig(): Promise<AccountTokensConfig>;
1180
+ getAccessTokenForAccount(providerId: SubscriptionProviderId, accountId: string): Promise<string | null>;
1181
+ }
1182
+ /** Proxy-aware upstream fetch signature (#3 `fetchUpstream`). */
1183
+ type ProbeFetch = typeof fetchUpstream;
1184
+ /** Injectable test seams (all default to production behavior). */
1185
+ interface ProbeSchedulerOptions {
1186
+ /** Injectable clock (ms). Default `Date.now`. */
1187
+ now?: () => number;
1188
+ /** Injectable proxy-aware fetch (#3). Default `fetchUpstream`. */
1189
+ fetchImpl?: ProbeFetch;
1190
+ /** Injectable inter-probe delay (stagger). Default a real `setTimeout`. */
1191
+ sleep?: (ms: number) => Promise<void>;
1192
+ /** Injectable per-provider probe-plan resolver. Default {@link probePlanFor}. */
1193
+ planFor?: (providerId: string) => ProbePlan;
1194
+ }
1195
+ declare class AccountHealthProbeScheduler implements AccountProbeHistoryReader {
1196
+ private readonly store;
1197
+ private readonly health;
1198
+ private readonly logger;
1199
+ private config;
1200
+ private timer;
1201
+ private sweeping;
1202
+ private readonly history;
1203
+ private readonly now;
1204
+ private readonly fetchImpl;
1205
+ private readonly sleep;
1206
+ private readonly planFor;
1207
+ constructor(store: ProbeCredentialStore, health: SubscriptionAccountHealth, logger: Logger, config: AccountProbeConfig, opts?: ProbeSchedulerOptions);
1208
+ /** Whether probing is enabled by the current config. */
1209
+ get enabled(): boolean;
1210
+ /**
1211
+ * Re-apply config to the live instance (the async `start.ts` loads the persisted
1212
+ * `accountProbe` segment after `buildDaemon`). Call BEFORE `start()`.
1213
+ */
1214
+ configure(config: AccountProbeConfig): void;
1215
+ /** Arm the probe interval. No-op when disabled (zero regression). Idempotent. */
1216
+ start(): void;
1217
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
1218
+ dispose(): void;
1219
+ /**
1220
+ * One sweep: probe every ELIGIBLE account SEQUENTIALLY with a `staggerMs` gap.
1221
+ * Disabled ⇒ no-op. `onlyMultiAccount` skips single-account providers. Exposed
1222
+ * for tests; never throws.
1223
+ */
1224
+ sweep(): Promise<void>;
1225
+ /**
1226
+ * Probe ONE account (design D1). Local tier first (dead token → synthesized 401,
1227
+ * no upstream); else the upstream tier when a verified endpoint exists. Records
1228
+ * the rolling history entry either way; returns whether the tracker was MARKED.
1229
+ */
1230
+ probeAccount(providerId: SubscriptionProviderId, accountId: string): Promise<{
1231
+ ok: boolean;
1232
+ marked: boolean;
1233
+ }>;
1234
+ /** Per-account rolling history for the authed admin surface (design D5). */
1235
+ getAllHistory(): AccountProbeHistorySnapshot[];
1236
+ /**
1237
+ * The coarse, account-ANONYMOUS `/health` signal (design D5): `true` when no
1238
+ * probed account is currently unhealthy (per #2's tracker). No ids, no counts —
1239
+ * safe for the unauthenticated `/health`. Vacuously `true` when nothing probed.
1240
+ */
1241
+ probedAccountsHealthy(now?: number): boolean;
1242
+ /**
1243
+ * Feed ONLY the account/auth-decisive statuses to #2 (LEAD constraint):
1244
+ * 401/403 → mark; 2xx → clear transient; 429 / 5xx / other 4xx / null →
1245
+ * NOT forwarded (never a mark). Returns whether a NEGATIVE mark was applied.
1246
+ */
1247
+ private applyOutcome;
1248
+ /** Append a record, capping the ring at `historySize` (drop oldest). */
1249
+ private record;
1250
+ /** Read a bounded slice of the response body for the 403-ban sniff (never throws). */
1251
+ private readBounded;
1252
+ private key;
1253
+ private parseKey;
1254
+ }
1255
+
1256
+ /**
1257
+ * auditReader — read + filter the date-rotated audit store (request-audit-log,
1258
+ * design D4/D6). Backs the AUTHED admin query only (the records carry IP/UA +
1259
+ * possibly bodies). Reads the relevant `audit-*.jsonl` files, parses defensively
1260
+ * (a torn final line never poisons a query), filters by key id + time window, and
1261
+ * returns NEWEST-FIRST up to a bounded limit.
1262
+ *
1263
+ * @module @omnicross/daemon/audit/auditReader
1264
+ */
1265
+
1266
+ /** Filters for an audit query (all optional). */
1267
+ interface AuditQuery {
1268
+ /** Restrict to one outbound key id. */
1269
+ keyId?: string;
1270
+ /** Inclusive lower bound (epoch ms). */
1271
+ from?: number;
1272
+ /** Inclusive upper bound (epoch ms). */
1273
+ to?: number;
1274
+ /** Max rows (default 200, capped 2000). */
1275
+ limit?: number;
1276
+ }
1277
+
1278
+ /**
1279
+ * auditQueryApi — the AUTHED `GET /admin/api/audit?keyId=&from=&to=&limit=`
1280
+ * handler (request-audit-log, design D6).
1281
+ *
1282
+ * Audit records carry client IP / user-agent (PII) and, when body capture is on,
1283
+ * redacted bodies — so unlike the coarse `/health` boolean they are served ONLY
1284
+ * behind the admin auth gate. This lives in its OWN helper module (the
1285
+ * #4/#8/#10 helper-module convention) so `adminApi.ts` — at its line cap — is not
1286
+ * touched: `AdminServer.dispatch` routes the path here directly, AFTER its auth
1287
+ * gate. NEVER unauthenticated, NEVER surfaced on `/health`.
1288
+ *
1289
+ * SECRET-FREE by construction: it returns exactly the stored records, which never
1290
+ * hold key material / tokens / Authorization (headers are never captured).
1291
+ *
1292
+ * @module @omnicross/daemon/admin/auditQueryApi
1293
+ */
1294
+
1295
+ /** The read surface the AdminServer consumes (bootstrap binds it to the store). */
1296
+ type AuditQueryReader = (query: AuditQuery) => AuditRecord[];
1297
+
1298
+ /**
1299
+ * billingStatusApi — the AUTHED `GET /admin/api/billing-status` handler
1300
+ * (billing-event-stream, design D5/P2).
1301
+ *
1302
+ * Returns the SECRET-FREE aggregate delivery status of the durable billing ledger
1303
+ * (total / delivered / pending counts) so the admin UI can show a delivery
1304
+ * indicator. Lives in its OWN helper module (the #4/#8/#10/#13 convention) so
1305
+ * `adminApi.ts` — at its line cap — is not touched: `AdminServer.dispatch` routes
1306
+ * the path here directly, AFTER its auth gate. Carries no secret and no event
1307
+ * payload — only counts.
1308
+ *
1309
+ * @module @omnicross/daemon/admin/billingStatusApi
1310
+ */
1311
+
1312
+ /** The read surface the AdminServer consumes (bootstrap binds it to the ledger dir). */
1313
+ type BillingStatusReader = () => BillingDeliveryStatus;
1314
+
781
1315
  /**
782
1316
  * autoDisableStore.ts — the daemon's PROCESS-IN-MEMORY auto-disable store.
783
1317
  *
@@ -919,13 +1453,129 @@ declare class ConfigFileProviderConfigSource implements ProviderConfigSource {
919
1453
 
920
1454
  declare class JsonApiServerSettingsStore implements ApiServerSettingsStore {
921
1455
  private readonly configPath;
922
- constructor(configPath: string);
1456
+ private readonly box;
1457
+ /**
1458
+ * @param configPath the daemon config.json whose `server` field is backed.
1459
+ * @param box OPTIONAL at-rest `SecretBox` (upstream-proxy). When set, the
1460
+ * `server.proxy.*` passwords are encrypted-on-`set` /
1461
+ * decrypted-on-`get` (the settings-store path is otherwise not
1462
+ * secret-aware — every OTHER server field is non-secret). Null
1463
+ * ⇒ passthrough (legacy/pure tests unchanged).
1464
+ */
1465
+ constructor(configPath: string, box?: SecretBox | null);
923
1466
  get<T = unknown>(key: string): Promise<T | undefined>;
924
1467
  set<T = unknown>(key: string, value: T): Promise<void>;
1468
+ /** Encrypt the proxy passwords + webhook + billing secrets before persisting (no-op without a box). */
1469
+ private encryptSecrets;
1470
+ /** Decrypt the proxy passwords + webhook + billing secrets on read (no-op without a box). */
1471
+ private decryptSecrets;
925
1472
  /** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
926
1473
  private readFile;
927
1474
  }
928
1475
 
1476
+ /**
1477
+ * JsonPricingStore — the daemon's file-backed `PricingStore` port impl.
1478
+ *
1479
+ * Durable storage for the model pricing table, backed by a pretty-printed json
1480
+ * file (a sibling of `config.json`, `pricing.json` by convention) holding a
1481
+ * `PricingEntry[]`. Reads tolerate a missing/corrupt file (→ empty table);
1482
+ * every mutation rewrites the full array (the table is at most a few thousand
1483
+ * rows — same trade-off as `JsonOutboundKeyDb`). The table starts EMPTY: no
1484
+ * seeding — the first pricing-source refresh (or a manual upsert) populates it.
1485
+ *
1486
+ * Beyond the core port, the store exposes a STORE-LOCAL `delete` (the port is
1487
+ * frozen; the admin DELETE route calls the concrete class and then invalidates
1488
+ * the engine cache).
1489
+ *
1490
+ * @module @omnicross/daemon/ports/JsonPricingStore
1491
+ */
1492
+
1493
+ declare class JsonPricingStore implements PricingStore {
1494
+ private readonly pricingPath;
1495
+ constructor(pricingPath: string);
1496
+ getAll(): Promise<PricingEntry[]>;
1497
+ /**
1498
+ * Insert or update one row keyed (providerId, modelId). `asUserEdit` stamps
1499
+ * user provenance (source 'user', userEdited, editedAt now) so the row is
1500
+ * protected from auto-overwrite during source refreshes; a non-user upsert
1501
+ * stamps source 'litellm' and clears nothing it should not (a plain source
1502
+ * upsert through this method overwrites the row wholesale).
1503
+ */
1504
+ upsert(input: PricingEntryInput, asUserEdit: boolean): Promise<PricingEntry>;
1505
+ /**
1506
+ * Apply a batch fetched from a pricing source. Rows whose local copy is
1507
+ * user-edited are NOT applied — they come back as `{ current, incoming }`
1508
+ * conflicts; everything else is upserted (source 'litellm'). ONE file write
1509
+ * for the whole batch.
1510
+ */
1511
+ bulkApplyFromSource(entries: PricingEntryInput[]): Promise<{
1512
+ applied: PricingEntry[];
1513
+ conflicts: Array<{
1514
+ current: PricingEntry;
1515
+ incoming: PricingEntryInput;
1516
+ }>;
1517
+ }>;
1518
+ /**
1519
+ * Apply per-row conflict decisions: 'overwrite' replaces the local row with
1520
+ * the incoming values (clearing the user-edited mark), 'skip' counts only.
1521
+ */
1522
+ applyResolutions(resolutions: Array<{
1523
+ incoming: PricingEntryInput;
1524
+ action: 'overwrite' | 'skip';
1525
+ }>): Promise<PricingResolution>;
1526
+ /**
1527
+ * STORE-LOCAL (not on the core port): remove one row. Returns whether a row
1528
+ * was actually removed. The admin DELETE handler calls this then invalidates
1529
+ * the engine cache.
1530
+ */
1531
+ delete(providerId: string, modelId: string): Promise<boolean>;
1532
+ /** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
1533
+ private applyUpsert;
1534
+ /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
1535
+ private readRows;
1536
+ private writeRows;
1537
+ }
1538
+
1539
+ /**
1540
+ * cliLaunch — the admin API's "launch a coding CLI in a terminal, pointed at the
1541
+ * daemon" surface (dashboard parity with the desktop app's Code CLI tab).
1542
+ *
1543
+ * This is the EXTERNAL-terminal analogue of `commands/launch.ts`: it reuses the
1544
+ * same `@omnicross/cli-launcher` builders (which register one route on the
1545
+ * RESIDENT `ProviderProxy` and return the redirect env — `ANTHROPIC_BASE_URL` +
1546
+ * a one-shot ROUTE token, codex's `-c base_url=…` overrides, etc.), then opens a
1547
+ * NEW terminal window running the CLI with that env injected. The route token —
1548
+ * NOT an upstream credential — is the only secret in the env; it is removed when
1549
+ * the session is stopped (`onSessionEnd`).
1550
+ *
1551
+ * SECRET DISCIPLINE: the env carries a route token (proxy-scoped, revocable),
1552
+ * never a provider key. On win32 the token rides the spawned process environment
1553
+ * (inherited by the terminal), never the command line / a file on disk.
1554
+ *
1555
+ * @module @omnicross/daemon/admin/cliLaunch
1556
+ */
1557
+
1558
+ /** Injectable PATH probe (tests stub this; default scans `process.env.PATH`). */
1559
+ type PathProbe = (candidate: string) => string | null;
1560
+ /** Open a NEW terminal window running `command [extraArgs…]` with `env` injected. */
1561
+ type TerminalOpener = (input: {
1562
+ cli: string;
1563
+ command: string;
1564
+ extraArgs: string[];
1565
+ env: Record<string, string>;
1566
+ cwd?: string;
1567
+ platform: NodeJS.Platform;
1568
+ }) => void;
1569
+ /**
1570
+ * Injectable shell runner for `POST /cli/:cli/install` (tests stub this; the
1571
+ * default execs the install command with a bounded timeout). Returns the host's
1572
+ * honest install outcome — `error` carries stderr/the failure reason.
1573
+ */
1574
+ type CommandRunner = (command: string) => Promise<{
1575
+ ok: boolean;
1576
+ error?: string;
1577
+ }>;
1578
+
929
1579
  /**
930
1580
  * migration.ts — the export gather + import apply logic for the passphrase pack
931
1581
  * (app-parity child 6, design D2/D3/D5).
@@ -1012,6 +1662,18 @@ interface AdminApiDeps {
1012
1662
  readonly llmConfig: ConfigFileProviderConfigSource;
1013
1663
  /** Named outbound-key store. */
1014
1664
  readonly keyDb: OutboundKeyDb;
1665
+ /**
1666
+ * OPTIONAL voucher (redemption-card) store (voucher-redemption #9). When wired,
1667
+ * the `/admin/api/voucher` surface can generate/list/revoke cards. Absent ⇒ the
1668
+ * surface returns 501 (feature not available in this build).
1669
+ */
1670
+ readonly voucherDb?: VoucherDb;
1671
+ /**
1672
+ * OPTIONAL per-key spend reader (outbound-key-policy). When wired, the key list
1673
+ * surfaces each key's OWN accumulated spend (daily/weekly/total) so the admin
1674
+ * can see spend-vs-limit. Leak-safe: only the key's own numbers are exposed.
1675
+ */
1676
+ readonly keySpendReader?: KeySpendReader;
1015
1677
  /** Outbound server settings store (server config persistence). */
1016
1678
  readonly settingsStore: JsonApiServerSettingsStore;
1017
1679
  /** The running outbound server (status + live applyConfig). */
@@ -1072,6 +1734,32 @@ interface AdminApiDeps {
1072
1734
  * Wired from the concrete `credentialStore` in `bootstrap.ts`.
1073
1735
  */
1074
1736
  readonly migrationCredentialStore: MigrationCredentialStore;
1737
+ /**
1738
+ * Usage-stats query facade (usage-pricing child) — delegates to the JSONL
1739
+ * usage-event store. Aggregates only; carries no key material.
1740
+ */
1741
+ readonly usageRecorder: UsageRecorder;
1742
+ /** Pricing engine (upsert / source refresh / conflict resolution). */
1743
+ readonly pricingEngine: PricingEngine;
1744
+ /**
1745
+ * CONCRETE pricing store — ONLY for the store-local row `delete` (the core
1746
+ * `PricingStore` port is frozen; delete is a daemon-local extra).
1747
+ */
1748
+ readonly pricingStore: JsonPricingStore;
1749
+ /**
1750
+ * External-terminal opener for the Code CLI launch route (dashboard parity).
1751
+ * Optional — defaults to the real `defaultTerminalOpener`; tests inject a spy so
1752
+ * no terminal window is actually spawned.
1753
+ */
1754
+ readonly cliTerminalOpener?: TerminalOpener;
1755
+ /** Injectable PATH probe for CLI detection (tests fake "installed"). */
1756
+ readonly cliPathProbe?: PathProbe;
1757
+ /**
1758
+ * Injectable shell runner for the Code CLI install route. Optional — defaults
1759
+ * to the real `exec`-based runner; tests inject a stub so no package manager
1760
+ * actually runs.
1761
+ */
1762
+ readonly cliCommandRunner?: CommandRunner;
1075
1763
  }
1076
1764
  /**
1077
1765
  * Dispatch one `/admin/api/*` request. `path` is the already-extracted pathname
@@ -1098,8 +1786,9 @@ declare function handleAdminApi(req: http.IncomingMessage, res: http.ServerRespo
1098
1786
  * - HARD SAFETY GATE: `networkBinding` (LAN/`0.0.0.0`) without a non-empty
1099
1787
  * `admin.token` → `start` REFUSES to bind (logs + stays down, fail closed).
1100
1788
  *
1101
- * Routing: `GET /` (and `GET /admin`) → `DASHBOARD_HTML`; `* /admin/api/*` → the
1102
- * management API (`handleAdminApi`); everything else`404`.
1789
+ * Routing: `GET /` (and `GET /admin`) → `302 /ui/`; `* /admin/api/*` → the
1790
+ * management API (`handleAdminApi`); `GET /ui[/...]`the Control Panel static
1791
+ * UI (`handleUiStatic`, from `@omnicross/ui`); everything else → `404`.
1103
1792
  *
1104
1793
  * @module @omnicross/daemon/admin/AdminServer
1105
1794
  */
@@ -1108,6 +1797,40 @@ declare function handleAdminApi(req: http.IncomingMessage, res: http.ServerRespo
1108
1797
  interface AdminServerDeps extends AdminApiDeps {
1109
1798
  /** Read the resolved admin config (enabled/port/networkBinding/token). */
1110
1799
  getAdminConfig: () => ResolvedAdminConfig;
1800
+ /**
1801
+ * Build the coarse, secret-free `/health` report (daemon-health-endpoint). A
1802
+ * shared closure over live handles (bootstrap wires the SAME builder into the
1803
+ * outbound server), served UNAUTHENTICATED — before the admin auth gate.
1804
+ */
1805
+ getHealthReport: () => HealthReport;
1806
+ /**
1807
+ * Injected logger (configurable-logging) — the admin listener's OWN lifecycle
1808
+ * lines (bind/refuse/error) route through it so they honor the configured
1809
+ * level / format / file sink.
1810
+ */
1811
+ logger: Logger;
1812
+ /**
1813
+ * OPTIONAL per-account probe-history reader (subscription-account-probe #8,
1814
+ * design D5). When wired (bootstrap → the `AccountHealthProbeScheduler`), the
1815
+ * AUTHED `GET /admin/api/account-probes` returns per-account probe history.
1816
+ * Absent ⇒ the route serves an empty list (byte-safe for embedders/tests that
1817
+ * do not wire it). Read-only + secret-free (ids + status labels, no tokens).
1818
+ */
1819
+ probeHistoryReader?: AccountProbeHistoryReader;
1820
+ /**
1821
+ * OPTIONAL audit query reader (request-audit-log, design D6). When wired
1822
+ * (bootstrap → the date-rotated store), the AUTHED `GET /admin/api/audit`
1823
+ * returns filtered records. Absent ⇒ the route serves an empty list. The
1824
+ * records carry IP/UA/bodies → this route is behind the auth gate ONLY, NEVER
1825
+ * unauthenticated, NEVER on `/health`.
1826
+ */
1827
+ auditReader?: AuditQueryReader;
1828
+ /**
1829
+ * OPTIONAL billing delivery-status reader (billing-event-stream, design D5).
1830
+ * When wired (bootstrap → the ledger dir), the AUTHED `GET /admin/api/billing-status`
1831
+ * returns secret-free total/delivered/pending counts. Absent ⇒ zeroed counts.
1832
+ */
1833
+ billingStatusReader?: BillingStatusReader;
1111
1834
  }
1112
1835
  /** A live status snapshot for the admin listener. */
1113
1836
  interface AdminServerStatus {
@@ -1122,6 +1845,8 @@ declare class AdminServer {
1122
1845
  private server;
1123
1846
  private boundPort;
1124
1847
  private boundAddr;
1848
+ /** Control Panel dist dir (resolved once at first request; null = no UI). */
1849
+ private uiDist;
1125
1850
  constructor(deps: AdminServerDeps);
1126
1851
  /**
1127
1852
  * Start the admin listener honoring the resolved admin config. Returns the
@@ -1142,24 +1867,6 @@ declare class AdminServer {
1142
1867
  getStatus(): AdminServerStatus;
1143
1868
  }
1144
1869
 
1145
- /**
1146
- * ConsoleLogger — the daemon's file-less default `Logger` port impl (design D5).
1147
- *
1148
- * A thin `console.*` wrapper. The serving core depends on the `Logger` port
1149
- * (never a host class), so this trivial implementation is the only logger the
1150
- * standalone daemon needs. `error` uses the WIDEST `(message, error?, meta?)`
1151
- * signature so every core call site stays assignable.
1152
- *
1153
- * @module @omnicross/daemon/ports/ConsoleLogger
1154
- */
1155
-
1156
- declare class ConsoleLogger implements Logger {
1157
- info(message: string, meta?: Record<string, unknown> | Error | object): void;
1158
- warn(message: string, meta?: Record<string, unknown> | Error | object): void;
1159
- error(message: string, error?: unknown, meta?: Record<string, unknown> | object): void;
1160
- debug(message: string, meta?: Record<string, unknown> | Error | object): void;
1161
- }
1162
-
1163
1870
  /**
1164
1871
  * JsonOutboundKeyDb — the daemon's file-backed `OutboundKeyDb` port impl
1165
1872
  * (design D3).
@@ -1189,6 +1896,9 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1189
1896
  outboundApiKeysRevoke(id: string): Promise<boolean>;
1190
1897
  outboundApiKeysTouchLastUsed(id: string): Promise<boolean>;
1191
1898
  outboundApiKeysSetEnabled(id: string, enabled: boolean): Promise<boolean>;
1899
+ outboundApiKeysSetMaxConcurrency(id: string, maxConcurrency: number | null): Promise<boolean>;
1900
+ outboundApiKeysSetPolicy(id: string, policy: OutboundKeyPolicy): Promise<boolean>;
1901
+ outboundApiKeysMarkActivated(id: string, activatedAt: number): Promise<boolean>;
1192
1902
  /** Apply `fn` to the row with `id`, persisting when it returns true. */
1193
1903
  private mutateRow;
1194
1904
  /** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
@@ -1196,17 +1906,428 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1196
1906
  private writeRows;
1197
1907
  }
1198
1908
 
1909
+ /**
1910
+ * AccountHealthSweeper — proactive account-health recovery tick
1911
+ * (subscription-account-health, design D6).
1912
+ *
1913
+ * The health tracker (`@omnicross/core` `SubscriptionAccountHealth`) already
1914
+ * self-heals LAZILY: an elapsed cooldown restores an account on the next
1915
+ * `isSchedulable` read, so CORRECTNESS never depends on this sweeper. What the
1916
+ * tick adds is PROACTIVITY for IDLE accounts (no traffic to trigger a lazy read):
1917
+ * - it fires the tracker's recovery SIGNAL (the seam #5 webhooks + #8
1918
+ * health-cron consume — this child only emits it), and
1919
+ * - it optionally nudges a fresh token for a recovered OAuth account so it
1920
+ * resumes instantly instead of paying refresh latency on its first request.
1921
+ *
1922
+ * Modeled EXACTLY on `TokenRefreshScheduler`: `start()` arms an `unref()`ed 60s
1923
+ * timer, `dispose()` clears it, and a single-sweep re-entrancy guard means a
1924
+ * long sweep never overlaps the next tick.
1925
+ *
1926
+ * @module @omnicross/daemon/AccountHealthSweeper
1927
+ */
1928
+
1929
+ declare class AccountHealthSweeper {
1930
+ private readonly store;
1931
+ private readonly health;
1932
+ private readonly logger;
1933
+ private readonly intervalMs;
1934
+ private readonly leadMs;
1935
+ private timer;
1936
+ private sweeping;
1937
+ constructor(store: JsonSubscriptionCredentialStore, health: SubscriptionAccountHealth, logger: Logger, intervalMs?: number, leadMs?: number);
1938
+ /** Arm the sweep interval. Idempotent. The timer never holds the loop open. */
1939
+ start(): void;
1940
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
1941
+ dispose(): void;
1942
+ /**
1943
+ * One sweep: surface accounts that just recovered (emits the recovery signal
1944
+ * through the tracker's hook) and nudge a fresh token for any recovered OAuth
1945
+ * account whose token is near expiry. Exposed for tests. Never throws.
1946
+ */
1947
+ sweep(now?: number): Promise<void>;
1948
+ /** Expiring within the lead window, refreshable, and not already dead. */
1949
+ private needsRefresh;
1950
+ /** Refresh one recovered account by id; failures are logged, never thrown. */
1951
+ private refreshOne;
1952
+ }
1953
+
1954
+ /**
1955
+ * AuditPruneSweeper — the TTL prune for the audit store (request-audit-log,
1956
+ * design D4). Deletes whole `audit-YYYY-MM-DD.jsonl` files whose date is older
1957
+ * than `retentionDays` — a cheap file UNLINK, never a line-level rewrite of a
1958
+ * live file (which jsonl makes awkward). So the store never grows unbounded and
1959
+ * TTL is O(files).
1960
+ *
1961
+ * Modeled on the #8 `AccountHealthProbeScheduler` / `AccountHealthSweeper`:
1962
+ * `start()` arms an `unref()`ed interval, `dispose()` clears it, a single-sweep
1963
+ * re-entrancy guard prevents overlap. A prune ALSO runs once at boot (`start`
1964
+ * fires an immediate sweep). Disabled/zero-retention config ⇒ armed-off ⇒ no-op
1965
+ * (byte-identical zero regression). Never throws.
1966
+ *
1967
+ * @module @omnicross/daemon/audit/AuditPruneSweeper
1968
+ */
1969
+
1970
+ declare class AuditPruneSweeper {
1971
+ private readonly auditDir;
1972
+ private readonly logger;
1973
+ private config;
1974
+ private readonly intervalMs;
1975
+ /** Injectable clock (ms) for deterministic tests. */
1976
+ private readonly now;
1977
+ private timer;
1978
+ private sweeping;
1979
+ constructor(auditDir: string, logger: Logger, config: AuditConfig, intervalMs?: number,
1980
+ /** Injectable clock (ms) for deterministic tests. */
1981
+ now?: () => number);
1982
+ /** Whether pruning is active (audit enabled). */
1983
+ get enabled(): boolean;
1984
+ /** Re-apply config to the live instance (boot + admin PUT hot-reload). */
1985
+ configure(config: AuditConfig): void;
1986
+ /**
1987
+ * Arm the prune interval AND run one prune immediately (boot cleanup). No-op
1988
+ * when audit is disabled (zero regression). Idempotent.
1989
+ */
1990
+ start(): void;
1991
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
1992
+ dispose(): void;
1993
+ /**
1994
+ * One prune: unlink every audit date file strictly OLDER than the retention
1995
+ * cutoff (`now - retentionDays` days, at local-midnight granularity). Exposed
1996
+ * for tests; never throws. Returns the number of files removed.
1997
+ */
1998
+ sweep(): Promise<number>;
1999
+ }
2000
+
2001
+ /**
2002
+ * AuditWriter — the daemon's file-backed audit sink (request-audit-log, design
2003
+ * D4/D5). Registered as `@omnicross/core`'s audit sink when audit is enabled; its
2004
+ * {@link record} is what `recordAudit` hands each assembled record to.
2005
+ *
2006
+ * FIRE-AND-FORGET (hard constraint): {@link record} DEFERS the fs append off the
2007
+ * caller's stack (an injectable `defer`, default a zero-delay timer — the
2008
+ * `UsageRecorder` precedent) and returns immediately, so the relay response path
2009
+ * never waits on disk I/O. A write error is swallowed + logged (a failing audit
2010
+ * store must never affect a relay). Each record is appended as ONE JSON line to
2011
+ * `audit/audit-YYYY-MM-DD.jsonl` (the record's LOCAL date), matching the
2012
+ * `usage-events.jsonl` pattern — no new dependency, TTL is a whole-file unlink.
2013
+ *
2014
+ * @module @omnicross/daemon/audit/AuditWriter
2015
+ */
2016
+
2017
+ declare class AuditWriter {
2018
+ private readonly auditDir;
2019
+ private readonly logger;
2020
+ /** Deferral used by `record()` to schedule the append off the caller's path. */
2021
+ private readonly defer;
2022
+ private dirEnsured;
2023
+ constructor(auditDir: string, logger: Logger,
2024
+ /** Deferral used by `record()` to schedule the append off the caller's path. */
2025
+ defer?: (fn: () => void) => void);
2026
+ /**
2027
+ * Enqueue one record for append. Returns IMMEDIATELY (fire-and-forget); the fs
2028
+ * write happens on the deferred tick. A failure is logged, never thrown.
2029
+ */
2030
+ record(record: AuditRecord): void;
2031
+ /**
2032
+ * Append synchronously — the awaitable form tests use to assert the line landed.
2033
+ * Ensures the `audit/` directory exists on first write (lazy, like the usage
2034
+ * store's lazy file creation).
2035
+ */
2036
+ appendNow(record: AuditRecord): void;
2037
+ }
2038
+
2039
+ /**
2040
+ * BillingPublisher — the daemon's durable-first billing sink (billing-event-stream,
2041
+ * design D2/D4). Registered as `@omnicross/core`'s billing sink when billing is
2042
+ * enabled; its {@link record} is what `publishBillingEvent` hands each assembled
2043
+ * event.
2044
+ *
2045
+ * DURABLE-FIRST (THE key decision, design D2): {@link record}
2046
+ * 1. APPENDS the event as one JSON line to `billing/billing-YYYY-MM-DD.jsonl` —
2047
+ * SYNCHRONOUSLY, BEFORE any delivery attempt. This is the DURABLE source of
2048
+ * truth: once appended, the event is NEVER lost, even if the process crashes
2049
+ * or every delivery attempt fails. A billing ledger is a financial record.
2050
+ * 2. then, only when an `endpoint` is configured, schedules a best-effort POST
2051
+ * OFF the caller's stack (an injectable `defer`, default a zero-delay timer)
2052
+ * so {@link record} RETURNS IMMEDIATELY — a slow/failing endpoint never blocks
2053
+ * the caller (which is already off the relay response path). Ledger-only mode
2054
+ * (no `endpoint`) simply appends — an external tailer consumes the jsonl.
2055
+ * 3. on a POST ack, appends a delivery marker (`delivered-YYYY-MM-DD.jsonl`); on
2056
+ * failure the event stays UNdelivered in the ledger for the retry sweep +
2057
+ * external reconciliation. A delivery failure NEVER drops the event.
2058
+ *
2059
+ * At-least-once: the consumer dedupes on the event `id` (the request id). The
2060
+ * built-in POST optionally signs the body with `X-Omnicross-Billing-Signature:
2061
+ * sha256=<hmac hex>` (node `crypto`, no new dep). The signing `secret` is used
2062
+ * ONLY to sign — it NEVER appears in the payload or a log line. Egress is #3's
2063
+ * proxy-aware `fetchUpstream` (global proxy).
2064
+ *
2065
+ * @module @omnicross/daemon/billing/BillingPublisher
2066
+ */
2067
+
2068
+ /** The minimal `fetch` shape the publisher POSTs through (proxy-aware by default). */
2069
+ type BillingFetch = (url: string, init: RequestInit) => Promise<Response>;
2070
+ /** Constructor knobs (all optional; test seams for fetch/defer/clock). */
2071
+ interface BillingPublisherOptions {
2072
+ /** Egress fn; defaults to #3's proxy-aware `fetchUpstream` (global proxy only). */
2073
+ fetchImpl?: BillingFetch;
2074
+ /** Deferral used by `record()` to schedule the POST off the caller's path. */
2075
+ defer?: (fn: () => void) => void;
2076
+ timeoutMs?: number;
2077
+ /** Clock seam for delivery-marker timestamps (tests fix it). */
2078
+ now?: () => number;
2079
+ }
2080
+ declare class BillingPublisher {
2081
+ private readonly billingDir;
2082
+ private readonly logger;
2083
+ private config;
2084
+ private dirEnsured;
2085
+ private readonly fetchImpl;
2086
+ private readonly defer;
2087
+ private readonly timeoutMs;
2088
+ private readonly now;
2089
+ constructor(billingDir: string, logger: Logger, opts?: BillingPublisherOptions);
2090
+ /** Install/replace the live billing config (endpoint + secret + retry bound). */
2091
+ setConfig(config: BillingConfig | undefined): void;
2092
+ /**
2093
+ * Record one billing event. DURABLE-FIRST: append synchronously (the event is
2094
+ * now on disk, never lost), THEN schedule a best-effort POST off the caller's
2095
+ * stack (non-blocking; ledger-only when no endpoint). Returns IMMEDIATELY and
2096
+ * NEVER throws — a failing append/POST is logged, never propagated.
2097
+ */
2098
+ record(event: BillingEvent): void;
2099
+ /**
2100
+ * Append the event as one JSON line to `billing-YYYY-MM-DD.jsonl` (the event's
2101
+ * LOCAL date). Synchronous — the awaitable form tests use to assert the ledger
2102
+ * line landed BEFORE any delivery. Ensures the `billing/` directory on first write.
2103
+ */
2104
+ appendNow(event: BillingEvent): void;
2105
+ /**
2106
+ * One best-effort delivery attempt for an event ALREADY in the ledger. POSTs the
2107
+ * event JSON to the configured endpoint (optionally HMAC-signed); on a 2xx ack
2108
+ * appends a delivery marker and returns `true`. Any non-2xx / thrown / timed-out
2109
+ * attempt returns `false` — the event stays UNdelivered in the ledger (never
2110
+ * lost). NEVER rejects. A no-op `false` when no endpoint is configured.
2111
+ */
2112
+ deliverNow(event: BillingEvent): Promise<boolean>;
2113
+ /**
2114
+ * Append a delivery marker `{ id, deliveredAt }` to `delivered-YYYY-MM-DD.jsonl`
2115
+ * (keyed by the EVENT's date so the reader finds both together). Idempotent at
2116
+ * the reconciliation layer — the reader unions marker ids into a delivered set,
2117
+ * so a duplicate marker is harmless. A marker-write failure is logged, never thrown.
2118
+ */
2119
+ markDelivered(event: BillingEvent): void;
2120
+ private ensureDir;
2121
+ }
2122
+
2123
+ /**
2124
+ * BillingRetrySweeper — the bounded retry + reconciliation sweep for the billing
2125
+ * ledger (billing-event-stream, design D5). Periodically re-POSTs UNdelivered
2126
+ * ledger events (via the publisher's built-in delivery) with a bounded age:
2127
+ * - an undelivered event WITHIN `maxRetryAgeMs` of its timestamp is re-POSTed
2128
+ * (the request id makes the re-POST safe — the consumer dedupes);
2129
+ * - an undelivered event PAST `maxRetryAgeMs` is LEFT in the ledger for external
2130
+ * reconciliation — it is NEVER deleted (a delivery failure must never drop a
2131
+ * billing record; the ledger is a financial record, so there is NO prune here,
2132
+ * unlike the #13 audit TTL);
2133
+ * - a DELIVERED event (has a marker) is never re-sent (delivery-marking prevents
2134
+ * double delivery).
2135
+ *
2136
+ * Modeled on the #8/#13 sweepers: `start()` arms an `unref()`ed interval, a
2137
+ * single-sweep re-entrancy guard prevents overlap, `dispose()` clears it. A sweep
2138
+ * ALSO runs once at boot (`start` fires an immediate sweep). Disabled/ledger-only
2139
+ * (no endpoint) ⇒ armed-off ⇒ no-op. Never throws.
2140
+ *
2141
+ * @module @omnicross/daemon/billing/BillingRetrySweeper
2142
+ */
2143
+
2144
+ declare class BillingRetrySweeper {
2145
+ private readonly billingDir;
2146
+ private readonly publisher;
2147
+ private readonly logger;
2148
+ private config;
2149
+ private readonly intervalMs;
2150
+ /** Injectable clock (ms) for deterministic tests. */
2151
+ private readonly now;
2152
+ private timer;
2153
+ private sweeping;
2154
+ constructor(billingDir: string, publisher: BillingPublisher, logger: Logger, config: BillingConfig, intervalMs?: number,
2155
+ /** Injectable clock (ms) for deterministic tests. */
2156
+ now?: () => number);
2157
+ /** Whether retrying is active: billing enabled AND an endpoint is configured. */
2158
+ get enabled(): boolean;
2159
+ /** Re-apply config to the live instance (boot + admin PUT hot-reload). */
2160
+ configure(config: BillingConfig): void;
2161
+ /**
2162
+ * Arm the retry interval AND run one sweep immediately (boot catch-up for events
2163
+ * that failed to deliver while the daemon was down). No-op when disabled or in
2164
+ * ledger-only mode (no endpoint to POST to). Idempotent.
2165
+ */
2166
+ start(): void;
2167
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
2168
+ dispose(): void;
2169
+ /**
2170
+ * One sweep: re-POST every UNdelivered ledger event still within
2171
+ * `maxRetryAgeMs`; leave over-age undelivered events for reconciliation (NEVER
2172
+ * deleted). Exposed for tests; never throws. Returns the number of events a
2173
+ * re-POST was attempted for.
2174
+ */
2175
+ sweep(): Promise<number>;
2176
+ }
2177
+
2178
+ /**
2179
+ * TokenRefreshScheduler — proactive background OAuth token refresh
2180
+ * (external-cli-sync).
2181
+ *
2182
+ * The auth strategies already refresh LAZILY (lead-window check before each
2183
+ * request + 401 retry), but a daemon that sits idle past a token's lifetime
2184
+ * pays the refresh latency — or a dead rotated token — on the first request.
2185
+ * This scheduler sweeps every account of every OAuth provider on an interval
2186
+ * and refreshes any token entering the expiry lead window.
2187
+ *
2188
+ * Safety properties:
2189
+ * - the store coalesces in-flight refreshes per account, so a sweep can never
2190
+ * double-spend a single-use refresh token against a concurrent lazy refresh;
2191
+ * - accounts already flagged `expired` are skipped (a dead refresh token is
2192
+ * not retried every tick — recovery is the external-import fallback or a
2193
+ * re-login);
2194
+ * - the ACTIVE account routes through the provider's active refresher (which
2195
+ * carries the external CLI import fallback); non-active accounts refresh
2196
+ * by id;
2197
+ * - one sweep runs at a time (a long sweep never overlaps the next tick).
2198
+ *
2199
+ * Modeled on `ApiKeyPoolService`'s interval lifecycle: `start()` arms an
2200
+ * `unref()`ed timer, `dispose()` clears it.
2201
+ *
2202
+ * @module @omnicross/daemon/TokenRefreshScheduler
2203
+ */
2204
+
2205
+ declare class TokenRefreshScheduler {
2206
+ private readonly store;
2207
+ private readonly logger;
2208
+ private readonly intervalMs;
2209
+ private readonly leadMs;
2210
+ private timer;
2211
+ private sweeping;
2212
+ constructor(store: JsonSubscriptionCredentialStore, logger: Logger, intervalMs?: number, leadMs?: number);
2213
+ /** Arm the sweep interval. Idempotent. The timer never holds the loop open. */
2214
+ start(): void;
2215
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
2216
+ dispose(): void;
2217
+ /** One sweep over every account of every OAuth provider. Exposed for tests. */
2218
+ sweep(now?: number): Promise<void>;
2219
+ /** Expiring within the lead window, refreshable, and not already dead. */
2220
+ private needsRefresh;
2221
+ /** Refresh one account; failures are logged, never thrown (the store has
2222
+ * already flagged the account `expired`). */
2223
+ private refreshOne;
2224
+ private refreshActive;
2225
+ }
2226
+
2227
+ /**
2228
+ * WebhookDispatcher — the daemon-side fire-and-forget webhook sender
2229
+ * (webhook-notifications, design D4/D5/D6).
2230
+ *
2231
+ * Registered as the core emit sink at bootstrap. {@link emit} pushes the event
2232
+ * onto a bounded in-memory queue and RETURNS IMMEDIATELY — it never awaits a
2233
+ * send and never throws, so a slow/failing/throwing destination can NEVER block
2234
+ * or delay a relay request (the HARD contract). An async drain loop then:
2235
+ * - matches each event to every enabled destination whose `events` filter
2236
+ * allows it (absent/empty ⇒ all kinds),
2237
+ * - sends to the matching destinations CONCURRENTLY,
2238
+ * - retries a failed send with bounded exponential backoff up to
2239
+ * `maxAttempts`, then LOGS (via the injected #10 logger) and DROPS it.
2240
+ * The queue is bounded (drop-OLDEST + a one-shot warn) so a runaway source can't
2241
+ * OOM the process.
2242
+ *
2243
+ * Egress is #3's proxy-aware `fetchUpstream` with NO ctx → the GLOBAL proxy only
2244
+ * (webhooks aren't per-account). Signing uses node `crypto` (no new deps):
2245
+ * `custom` → optional `X-Omnicross-Signature: sha256=<hex hmac of body>`;
2246
+ * `feishu` → Feishu's `timestamp` + `sign` (HMAC-SHA256 base64 of
2247
+ * `timestamp\nsecret`) envelope. The destination `secret` is used ONLY to sign;
2248
+ * it is NEVER placed in a payload or a log line.
2249
+ *
2250
+ * @module @omnicross/daemon/webhook/WebhookDispatcher
2251
+ */
2252
+
2253
+ /** The minimal `fetch` shape the dispatcher POSTs through (proxy-aware by default). */
2254
+ type WebhookFetch = (url: string, init: RequestInit) => Promise<Response>;
2255
+ /** Outcome of a single delivery attempt (used by the admin test path). */
2256
+ interface WebhookDeliveryResult {
2257
+ ok: boolean;
2258
+ status?: number;
2259
+ error?: string;
2260
+ }
2261
+ /** Constructor knobs (all optional; test seams for fetch/logger/sleep/clock). */
2262
+ interface WebhookDispatcherOptions {
2263
+ /** Egress fn; defaults to #3's proxy-aware `fetchUpstream` (global proxy only). */
2264
+ fetchImpl?: WebhookFetch;
2265
+ /** Injected #10 logger for drop/debug lines (never logs a secret). */
2266
+ logger?: Logger;
2267
+ maxAttempts?: number;
2268
+ queueMax?: number;
2269
+ timeoutMs?: number;
2270
+ baseBackoffMs?: number;
2271
+ /** Backoff sleep seam (tests inject an instant/fake sleep). */
2272
+ sleep?: (ms: number) => Promise<void>;
2273
+ /** Clock seam for the `test` event `at` + Feishu `timestamp` (tests fix it). */
2274
+ now?: () => number;
2275
+ }
2276
+ declare class WebhookDispatcher {
2277
+ private config;
2278
+ private readonly queue;
2279
+ private draining;
2280
+ private warnedFull;
2281
+ private readonly fetchImpl;
2282
+ private readonly logger;
2283
+ private readonly maxAttempts;
2284
+ private readonly queueMax;
2285
+ private readonly timeoutMs;
2286
+ private readonly baseBackoffMs;
2287
+ private readonly sleep;
2288
+ private readonly now;
2289
+ constructor(opts?: WebhookDispatcherOptions);
2290
+ /** Install/replace the live webhook config (destinations + master switch). */
2291
+ setConfig(config: WebhookConfig | undefined): void;
2292
+ /**
2293
+ * Enqueue an event and return IMMEDIATELY (fire-and-forget). NEVER awaits a
2294
+ * send, NEVER throws — the drain loop does all sending on a side channel. A
2295
+ * full queue drops the OLDEST event (with a one-shot warn) so a runaway source
2296
+ * can't OOM the process.
2297
+ */
2298
+ emit(event: WebhookEvent): void;
2299
+ /** Drain the queue, sending each event to its matching destinations concurrently. */
2300
+ private drain;
2301
+ /** The enabled destinations whose event filter admits this kind (empty ⇒ all). */
2302
+ private matchingDestinations;
2303
+ /** Send with bounded exponential backoff; log-and-drop after `maxAttempts`. */
2304
+ private sendWithRetry;
2305
+ /** One POST attempt. Returns an outcome; a thrown error becomes `{ ok:false }`. */
2306
+ private sendOnce;
2307
+ /**
2308
+ * ADMIN test path (design D8): deliver a `test` event to ONE destination and
2309
+ * AWAIT the single-attempt result. This is the ONLY awaited send — it runs on
2310
+ * the admin request path (an operator clicking "Test"), NEVER on a relay path,
2311
+ * so awaiting it is safe. Finds the destination regardless of its `enabled`
2312
+ * flag or the master switch (an explicit operator action).
2313
+ */
2314
+ deliverTest(destinationId: string): Promise<WebhookDeliveryResult>;
2315
+ }
2316
+
1199
2317
  /**
1200
2318
  * bootstrap.ts — `buildDaemon` wires `@omnicross/core`'s `ProviderProxy` +
1201
2319
  * `OutboundApiServer` STANDALONE (design D6).
1202
2320
  *
1203
2321
  * A DB-backed embedder wires the same `@omnicross/core` surface differently;
1204
- * this standalone wiring makes three SUBSTITUTIONS (file-backed ports replace
1205
- * DB-backed ones) and three SUBTRACTIONS:
2322
+ * this standalone wiring makes SUBSTITUTIONS (file-backed ports replace
2323
+ * DB-backed ones) and SUBTRACTIONS:
1206
2324
  * - no `CompletionService` (the BYO proxy path doesn't need it),
1207
- * - no `apiKeyPool` / `usageRecorder` (optional `ProviderProxyDeps`),
1208
2325
  * - no `anthropicIngressHandlerFactory` (→ `/v1/messages` returns 502 by core's
1209
2326
  * existing contract — no daemon code needed).
2327
+ * (`apiKeyPool` and `usageRecorder` are NO LONGER subtracted: the pool is wired
2328
+ * for multi-key load balancing, and the usage recorder is wired over the
2329
+ * file-backed pricing/usage stores so every served request is cost-stamped and
2330
+ * persisted to `usage-events.jsonl`.)
1210
2331
  *
1211
2332
  * `getProviderProxy` / `getOutboundApiServer` are module singletons, so the boot
1212
2333
  * smoke test calls `__resetProviderProxyForTests` / `__resetOutboundApiServerForTests`
@@ -1239,10 +2360,26 @@ interface DaemonPaths {
1239
2360
  * hit a real token endpoint. Absent → the global `fetch`.
1240
2361
  */
1241
2362
  oauthExchangeFetch?: FetchLike;
2363
+ /**
2364
+ * TEST SEAM (optional): override the Code CLI external-terminal opener so tests
2365
+ * never spawn a window. Absent → the real `defaultTerminalOpener`.
2366
+ */
2367
+ cliTerminalOpener?: TerminalOpener;
2368
+ /**
2369
+ * TEST SEAM (optional): override the Code CLI PATH probe so tests can fake an
2370
+ * installed CLI. Absent → the real PATH scan.
2371
+ */
2372
+ cliPathProbe?: PathProbe;
2373
+ /**
2374
+ * TEST SEAM (optional): override the Code CLI install command runner so tests
2375
+ * never invoke a real package manager. Absent → the real `exec`-based runner.
2376
+ */
2377
+ cliCommandRunner?: CommandRunner;
1242
2378
  }
1243
2379
  /** The constructed daemon handles the CLI commands operate on. */
1244
2380
  interface Daemon {
1245
- readonly logger: ConsoleLogger;
2381
+ /** The injected `Logger` port (a `ConfigurableLogger` built from `config.logging`). */
2382
+ readonly logger: Logger;
1246
2383
  readonly llmConfig: ConfigFileProviderConfigSource;
1247
2384
  readonly keyDb: JsonOutboundKeyDb;
1248
2385
  readonly settingsStore: JsonApiServerSettingsStore;
@@ -1264,8 +2401,69 @@ interface Daemon {
1264
2401
  /** Subscription account service (token-free `listAll`) — now exposed for the
1265
2402
  * admin dashboard's read-only accounts panel (RT3). */
1266
2403
  readonly subscriptionAccounts: SubscriptionAccountService;
2404
+ /** File-backed pricing table (`pricing.json`; concrete for the admin DELETE). */
2405
+ readonly pricingStore: JsonPricingStore;
2406
+ /** Pricing engine (cost calc + source refresh + conflict resolution). */
2407
+ readonly pricingEngine: PricingEngine;
2408
+ /** Usage recorder over `usage-events.jsonl` — also the admin stats query facade. */
2409
+ readonly usageRecorder: UsageRecorder;
1267
2410
  /** The localhost admin/dashboard HTTP listener (RT3). Started by `start.ts`. */
1268
2411
  readonly adminServer: AdminServer;
2412
+ /**
2413
+ * Proactive background OAuth refresh sweep (external-cli-sync). NOT started
2414
+ * here — `start.ts` arms it for the resident daemon; the short-lived `launch`
2415
+ * boot leaves it off (the lazy strategy refresh covers a single session) but
2416
+ * still disposes it in cleanup.
2417
+ */
2418
+ readonly tokenRefreshScheduler: TokenRefreshScheduler;
2419
+ /**
2420
+ * Proactive account-health recovery sweep (subscription-account-health, D6).
2421
+ * NOT started here — `start.ts` arms it for the resident daemon; disposed in
2422
+ * cleanup. Correctness never depends on it (health self-heals lazily on read).
2423
+ */
2424
+ readonly accountHealthSweeper: AccountHealthSweeper;
2425
+ /**
2426
+ * Scheduled ACTIVE account-health probe (subscription-account-probe #8).
2427
+ * Constructed armed-off with default config (`enabled:false`); `start.ts`
2428
+ * `configure(...)`s it from the persisted `accountProbe` segment and starts it
2429
+ * ONLY when enabled. Disposed in cleanup.
2430
+ */
2431
+ readonly accountHealthProbeScheduler: AccountHealthProbeScheduler;
2432
+ /**
2433
+ * Fire-and-forget webhook sender (webhook-notifications). Wired into the core
2434
+ * emit sink + the #2 health signals by `start.ts`/admin PUT via
2435
+ * `applyWebhookConfig`. INERT until a config enables it (zero regression).
2436
+ */
2437
+ readonly webhookDispatcher: WebhookDispatcher;
2438
+ /**
2439
+ * File-backed audit sink (request-audit-log) — appends each captured record to
2440
+ * `audit/audit-YYYY-MM-DD.jsonl` fire-and-forget. Registered as the core sink
2441
+ * (via the audit runtime slot) by `start.ts`/admin PUT ONLY when the `audit`
2442
+ * segment is enabled. INERT until then (no sink ⇒ capture hook is a no-op).
2443
+ */
2444
+ readonly auditWriter: AuditWriter;
2445
+ /**
2446
+ * TTL prune for the audit store (request-audit-log) — unlinks date files past
2447
+ * `retentionDays`. Armed-off; `start.ts` configures from the persisted `audit`
2448
+ * segment + starts it (running one prune at boot) ONLY when enabled. Disposed
2449
+ * in cleanup.
2450
+ */
2451
+ readonly auditPruneSweeper: AuditPruneSweeper;
2452
+ /**
2453
+ * Durable-first billing publisher (billing-event-stream) — appends each event
2454
+ * to `billing/billing-YYYY-MM-DD.jsonl` FIRST, then best-effort POSTs it.
2455
+ * Registered as the core billing sink (via the billing runtime slot) by
2456
+ * `start.ts`/admin PUT ONLY when the `billing` segment is enabled. INERT until
2457
+ * then (no sink ⇒ `publishBillingEvent` is a no-op).
2458
+ */
2459
+ readonly billingPublisher: BillingPublisher;
2460
+ /**
2461
+ * Bounded retry + reconciliation sweep for the billing ledger
2462
+ * (billing-event-stream) — re-POSTs undelivered events within `maxRetryAgeMs`,
2463
+ * NEVER deletes. Armed-off; `start.ts` configures from the persisted `billing`
2464
+ * segment + starts it ONLY when enabled with an endpoint. Disposed in cleanup.
2465
+ */
2466
+ readonly billingRetrySweeper: BillingRetrySweeper;
1269
2467
  }
1270
2468
  /**
1271
2469
  * Construct the standalone daemon from a loaded config + on-disk paths. Does NOT
@@ -1292,22 +2490,135 @@ declare function buildDaemon(config: DaemonConfig, paths: DaemonPaths): Daemon;
1292
2490
  declare function resetDaemonSingletonsForTests(): void;
1293
2491
 
1294
2492
  /**
1295
- * html.ts — the embedded vanilla-JS admin dashboard (RT3, design D7).
1296
- *
1297
- * A SINGLE `text/html` template literal served by `AdminServer` on `GET /`. No
1298
- * framework, no bundler, no new dependency vanilla `fetch` + DOM. Panels:
1299
- * providers (table + add/edit form), keys (table + create modal showing the
1300
- * one-time plaintext with a copy button + a "shown once" warning, never
1301
- * persisted), server config, read-only accounts status, and a playground
1302
- * (endpoint select + key + request textarea + Send + response area).
2493
+ * health.ts — the pure `/health` report builder (daemon-health-endpoint, D2).
2494
+ *
2495
+ * `buildHealthReport(deps)` returns a COARSE, SECRET-FREE {@link HealthReport}
2496
+ * from cheap SYNCHRONOUS probes. It NEVER hits an upstream, NEVER blocks, and
2497
+ * NEVER embeds a token/email/config-value/record-count the body is served
2498
+ * UNAUTHENTICATED (before the admin auth gate, and optionally before the outbound
2499
+ * key-auth), so it must expose nothing sensitive.
2500
+ *
2501
+ * Each check is a caller-supplied boolean thunk; a thunk that THROWS collapses to
2502
+ * `false` (a health probe must never crash the process). Status math:
2503
+ * - CRITICAL (`config`, `credentialStore`): a false critical → `error`.
2504
+ * - READINESS (`outboundServer`): the serving-path signal — false → `degraded`.
2505
+ * - INFORMATIONAL (`adminServer`): reported in `checks` but does NOT affect
2506
+ * `status` — a disabled/loopback dashboard must not fail the TRAFFIC-port
2507
+ * probe (the whole point of the outbound secondary mount).
2508
+ * `error` and `degraded` both map to HTTP 503 (see `healthHttpStatus`), so a
2509
+ * probe treats "not fully ready" as not-ready.
2510
+ *
2511
+ * @module @omnicross/daemon/admin/health
2512
+ */
2513
+
2514
+ /** The coarse dependency probes + process-stat seams the builder reads. */
2515
+ interface HealthReportDeps {
2516
+ /** The daemon package version (non-secret; also on the identity header). */
2517
+ version: string;
2518
+ /** CRITICAL: the bootstrap config is present/loaded. */
2519
+ configPresent: () => boolean;
2520
+ /** CRITICAL: the credential store is constructed + its file readable (no decrypt). */
2521
+ credentialStoreReadable: () => boolean;
2522
+ /** Non-critical: the outbound `/v1/*` serving listener is running. */
2523
+ outboundServerRunning: () => boolean;
2524
+ /** Non-critical: the admin listener is running. */
2525
+ adminServerRunning: () => boolean;
2526
+ /**
2527
+ * OPTIONAL coarse account-probe signal (subscription-account-probe #8, design
2528
+ * D5). Returns `true` when no PROBED account is currently unhealthy, `false`
2529
+ * when one is, or `undefined` when probing is DISABLED (→ the check is OMITTED,
2530
+ * keeping the `/health` body byte-identical when the feature is off).
2531
+ * INFORMATIONAL: it never affects `status` (a marked account is an operational
2532
+ * signal, not a daemon-readiness failure). Account-anonymous — no ids/counts.
2533
+ */
2534
+ subscriptionAccountsHealthy?: () => boolean | undefined;
2535
+ /** TEST SEAM: process memory snapshot (defaults to `process.memoryUsage`). */
2536
+ memoryUsage?: () => NodeJS.MemoryUsage;
2537
+ /** TEST SEAM: process uptime seconds (defaults to `process.uptime`). */
2538
+ uptimeSeconds?: () => number;
2539
+ /** TEST SEAM: wall clock ms (defaults to `Date.now`). */
2540
+ now?: () => number;
2541
+ }
2542
+ /** Build the coarse, secret-free health report (design D2). */
2543
+ declare function buildHealthReport(deps: HealthReportDeps): HealthReport;
2544
+
2545
+ /**
2546
+ * ConfigurableLogger — a `Logger` port impl with level / format / file sink
2547
+ * (configurable-logging, design D3). Supersedes `ConsoleLogger` as the injected
2548
+ * daemon logger.
2549
+ *
2550
+ * - LEVEL: numeric severity `error(0) < warn(1) < info(2) < debug(3)`; a message
2551
+ * whose level is BELOW the configured threshold (higher ordinal) is dropped.
2552
+ * Default threshold = `debug` (prints everything).
2553
+ * - FORMAT: `text` (the legacy `console.*(message, meta)` shape) | `json` (one
2554
+ * structured line `{ ts, level, msg, ...meta }`). Default `text`.
2555
+ * - SINK: always the console; PLUS an optional append-only file stream when
2556
+ * `file` is set (lazy-open; a write/open error is swallowed → the daemon never
2557
+ * crashes on a logging failure, it just falls back to the console).
2558
+ *
2559
+ * ZERO-REGRESSION DEFAULT: `new ConfigurableLogger()` (no config) = console +
2560
+ * all levels + text = behaviorally byte-identical to the legacy `ConsoleLogger`
2561
+ * (same `console` method per level, same `(message[, meta])` / error arg shape).
2562
+ *
2563
+ * CAUTION (per the #3 host:port-only-logging precedent): the JSON serializer
2564
+ * reduces an `Error` to `{ message, stack }` and spreads a plain `meta` object,
2565
+ * but it is NOT a secret redactor — call sites remain responsible for not passing
2566
+ * secret-bearing objects.
2567
+ *
2568
+ * @module @omnicross/daemon/ports/ConfigurableLogger
2569
+ */
2570
+
2571
+ declare class ConfigurableLogger implements Logger {
2572
+ private readonly threshold;
2573
+ private readonly format;
2574
+ private readonly filePath;
2575
+ private fileStream;
2576
+ private fileDisabled;
2577
+ constructor(cfg?: LoggingConfig);
2578
+ info(message: string, meta?: Record<string, unknown> | Error | object): void;
2579
+ warn(message: string, meta?: Record<string, unknown> | Error | object): void;
2580
+ error(message: string, error?: unknown, meta?: Record<string, unknown> | object): void;
2581
+ debug(message: string, meta?: Record<string, unknown> | Error | object): void;
2582
+ /**
2583
+ * Flush + close the file sink (tests / graceful shutdown). Resolves once the
2584
+ * append stream has finished flushing to disk. No-op when no file sink is open.
2585
+ */
2586
+ close(): Promise<void>;
2587
+ private emit;
2588
+ /**
2589
+ * Console sink. In `text` format this reproduces the legacy `ConsoleLogger`
2590
+ * EXACTLY (same method + arg shape) so the unconfigured default is a byte-for-
2591
+ * byte drop-in; in `json` format it prints the structured line.
2592
+ */
2593
+ private writeConsole;
2594
+ /** Append one line to the file sink; a failure disables the sink (swallowed). */
2595
+ private writeFile;
2596
+ /** Lazily open the append-only file stream; disable the sink on any error. */
2597
+ private getFileStream;
2598
+ private consoleFn;
2599
+ /** `{ ts, level, msg, ...meta }` (+ `error` when present) as a single line. */
2600
+ private jsonLine;
2601
+ /** Human-readable file line: `ISO [level] message {metaJson}`. */
2602
+ private textLine;
2603
+ }
2604
+
2605
+ /**
2606
+ * ConsoleLogger — the daemon's file-less default `Logger` port impl (design D5).
1303
2607
  *
1304
- * SECURITY: the create-key plaintext is held only in a local variable inside the
1305
- * modal flow and cleared on dismiss never written to a field, list, or storage.
2608
+ * A thin `console.*` wrapper. The serving core depends on the `Logger` port
2609
+ * (never a host class), so this trivial implementation is the only logger the
2610
+ * standalone daemon needs. `error` uses the WIDEST `(message, error?, meta?)`
2611
+ * signature so every core call site stays assignable.
1306
2612
  *
1307
- * @module @omnicross/daemon/admin/html
2613
+ * @module @omnicross/daemon/ports/ConsoleLogger
1308
2614
  */
1309
- /** The full dashboard document (style + body + the vanilla client script). */
1310
- declare const DASHBOARD_HTML: string;
2615
+
2616
+ declare class ConsoleLogger implements Logger {
2617
+ info(message: string, meta?: Record<string, unknown> | Error | object): void;
2618
+ warn(message: string, meta?: Record<string, unknown> | Error | object): void;
2619
+ error(message: string, error?: unknown, meta?: Record<string, unknown> | object): void;
2620
+ debug(message: string, meta?: Record<string, unknown> | Error | object): void;
2621
+ }
1311
2622
 
1312
2623
  /**
1313
2624
  * ccr-import.ts — translate a `claude-code-router` (CCR) `config.json` into an
@@ -1372,4 +2683,4 @@ declare function mapCcrToOmnicross(ccr: CcrConfig): {
1372
2683
  notes: string[];
1373
2684
  };
1374
2685
 
1375
- export { type AdminApiDeps, AdminServer, type AdminServerDeps, type AdminServerStatus, type CcrConfig, type CcrProvider, type CcrRouter, ConfigFileProviderConfigSource, ConsoleLogger, DASHBOARD_HTML, DEFAULT_ADMIN_PORT, type Daemon, type DaemonAdminConfig, type DaemonApiFormat, type DaemonConfig, type DaemonPaths, type DaemonProviderConfig, JsonApiServerSettingsStore, JsonOutboundKeyDb, JsonSubscriptionCredentialStore, type ResolvedAdminConfig, buildDaemon, handleAdminApi, inferApiFormat, loadConfig, mapCcrToOmnicross, parseCcrConfig, resetDaemonSingletonsForTests, resolveAdminConfig, saveConfig, validateConfig };
2686
+ export { type AdminApiDeps, AdminServer, type AdminServerDeps, type AdminServerStatus, type CcrConfig, type CcrProvider, type CcrRouter, ConfigFileProviderConfigSource, ConfigurableLogger, ConsoleLogger, DEFAULT_ADMIN_PORT, type Daemon, type DaemonAdminConfig, type DaemonApiFormat, type DaemonConfig, type DaemonPaths, type DaemonProviderConfig, type HealthReportDeps, JsonApiServerSettingsStore, JsonOutboundKeyDb, JsonSubscriptionCredentialStore, type ResolvedAdminConfig, buildDaemon, buildHealthReport, handleAdminApi, inferApiFormat, loadConfig, mapCcrToOmnicross, parseCcrConfig, resetDaemonSingletonsForTests, resolveAdminConfig, saveConfig, validateConfig };