@omnicross/daemon 0.1.2 → 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,14 +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';
4
5
  import { UsageRecorder, PricingEngine } from '@omnicross/core/usage';
5
6
  import { SubscriptionCredentialStore, FetchLike, SubscriptionProviderRegistry, SubscriptionAccountService } from '@omnicross/subscriptions';
6
- import { OutboundApiServerConfig, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, PricingStore, 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';
7
14
  import http from 'node:http';
8
15
  import { LLMProvider, AgentDefaultModels, GlobalModelParameters } from '@omnicross/contracts/llm-config';
9
16
  import { PricingEntry, PricingEntryInput, PricingResolution } from '@omnicross/contracts/pricing-types';
10
- import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
11
- import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, SubscriptionAccountSanitized } from '@omnicross/contracts/account-tokens-types';
17
+ import { WebhookConfig, WebhookEvent } from '@omnicross/contracts/webhook-types';
12
18
 
13
19
  /**
14
20
  * SecretBox.ts — a 32-byte master key wrapped with the tri-state secret rules
@@ -376,6 +382,12 @@ interface DaemonConfig {
376
382
  server?: OutboundApiServerConfig;
377
383
  /** Optional admin-dashboard config (RT3). */
378
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;
379
391
  }
380
392
  /** Apply defaults to a (possibly absent) admin block: enabled, port 8766,
381
393
  * loopback, no token. NOTE: an EXPLICIT `port: 0` is honored as "bind an
@@ -551,16 +563,27 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
551
563
  /**
552
564
  * @param tokensPath on-disk `tokens.json` location.
553
565
  * @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
554
- * @param fetchImpl injectable HTTP port for the OAuth refresh round-trips
555
- * (oauth design D4). Defaults to the global `fetch` so boot
556
- * is unchanged; tests inject a mock fetch. NOT used by any
557
- * read/write path only by `refresh*Token`.
558
- */
559
- constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike,
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,
560
576
  /** Injectable external CLI native-store reader (external-cli-sync). */
561
577
  externalCliReader?: ExternalCliReader,
562
578
  /** Injectable external CLI native-store WRITER (marker-gated write-back). */
563
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;
564
587
  /**
565
588
  * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
566
589
  * SINGLE-USE: two concurrent refreshes of one account each spend the same
@@ -580,6 +603,14 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
580
603
  getValidClaudeAccessToken(): Promise<string | null>;
581
604
  /** Current OpenCodeGo static API key, or `null` when none is stored. */
582
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;
583
614
  /**
584
615
  * DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
585
616
  * each provider's accounts to the secret-free `SubscriptionAccountSanitized`
@@ -629,6 +660,63 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
629
660
  * failure flags ONLY that account `expired`.
630
661
  */
631
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
+ }>;
632
720
  /** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
633
721
  private refreshUpstream;
634
722
  /**
@@ -798,6 +886,24 @@ interface SubscriptionTokenWriter {
798
886
  renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
799
887
  ok: boolean;
800
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
+ }>;
801
907
  listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
802
908
  refreshClaudeToken(): Promise<boolean>;
803
909
  refreshCodexToken(): Promise<boolean>;
@@ -973,6 +1079,239 @@ declare class CodexOAuthSessionStore {
973
1079
  private sweep;
974
1080
  }
975
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
+
976
1315
  /**
977
1316
  * autoDisableStore.ts — the daemon's PROCESS-IN-MEMORY auto-disable store.
978
1317
  *
@@ -1114,9 +1453,22 @@ declare class ConfigFileProviderConfigSource implements ProviderConfigSource {
1114
1453
 
1115
1454
  declare class JsonApiServerSettingsStore implements ApiServerSettingsStore {
1116
1455
  private readonly configPath;
1117
- 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);
1118
1466
  get<T = unknown>(key: string): Promise<T | undefined>;
1119
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;
1120
1472
  /** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
1121
1473
  private readFile;
1122
1474
  }
@@ -1310,6 +1662,18 @@ interface AdminApiDeps {
1310
1662
  readonly llmConfig: ConfigFileProviderConfigSource;
1311
1663
  /** Named outbound-key store. */
1312
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;
1313
1677
  /** Outbound server settings store (server config persistence). */
1314
1678
  readonly settingsStore: JsonApiServerSettingsStore;
1315
1679
  /** The running outbound server (status + live applyConfig). */
@@ -1433,6 +1797,40 @@ declare function handleAdminApi(req: http.IncomingMessage, res: http.ServerRespo
1433
1797
  interface AdminServerDeps extends AdminApiDeps {
1434
1798
  /** Read the resolved admin config (enabled/port/networkBinding/token). */
1435
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;
1436
1834
  }
1437
1835
  /** A live status snapshot for the admin listener. */
1438
1836
  interface AdminServerStatus {
@@ -1469,24 +1867,6 @@ declare class AdminServer {
1469
1867
  getStatus(): AdminServerStatus;
1470
1868
  }
1471
1869
 
1472
- /**
1473
- * ConsoleLogger — the daemon's file-less default `Logger` port impl (design D5).
1474
- *
1475
- * A thin `console.*` wrapper. The serving core depends on the `Logger` port
1476
- * (never a host class), so this trivial implementation is the only logger the
1477
- * standalone daemon needs. `error` uses the WIDEST `(message, error?, meta?)`
1478
- * signature so every core call site stays assignable.
1479
- *
1480
- * @module @omnicross/daemon/ports/ConsoleLogger
1481
- */
1482
-
1483
- declare class ConsoleLogger implements Logger {
1484
- info(message: string, meta?: Record<string, unknown> | Error | object): void;
1485
- warn(message: string, meta?: Record<string, unknown> | Error | object): void;
1486
- error(message: string, error?: unknown, meta?: Record<string, unknown> | object): void;
1487
- debug(message: string, meta?: Record<string, unknown> | Error | object): void;
1488
- }
1489
-
1490
1870
  /**
1491
1871
  * JsonOutboundKeyDb — the daemon's file-backed `OutboundKeyDb` port impl
1492
1872
  * (design D3).
@@ -1516,6 +1896,9 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1516
1896
  outboundApiKeysRevoke(id: string): Promise<boolean>;
1517
1897
  outboundApiKeysTouchLastUsed(id: string): Promise<boolean>;
1518
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>;
1519
1902
  /** Apply `fn` to the row with `id`, persisting when it returns true. */
1520
1903
  private mutateRow;
1521
1904
  /** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
@@ -1523,6 +1906,275 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
1523
1906
  private writeRows;
1524
1907
  }
1525
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
+
1526
2178
  /**
1527
2179
  * TokenRefreshScheduler — proactive background OAuth token refresh
1528
2180
  * (external-cli-sync).
@@ -1572,6 +2224,96 @@ declare class TokenRefreshScheduler {
1572
2224
  private refreshActive;
1573
2225
  }
1574
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
+
1575
2317
  /**
1576
2318
  * bootstrap.ts — `buildDaemon` wires `@omnicross/core`'s `ProviderProxy` +
1577
2319
  * `OutboundApiServer` STANDALONE (design D6).
@@ -1636,7 +2378,8 @@ interface DaemonPaths {
1636
2378
  }
1637
2379
  /** The constructed daemon handles the CLI commands operate on. */
1638
2380
  interface Daemon {
1639
- readonly logger: ConsoleLogger;
2381
+ /** The injected `Logger` port (a `ConfigurableLogger` built from `config.logging`). */
2382
+ readonly logger: Logger;
1640
2383
  readonly llmConfig: ConfigFileProviderConfigSource;
1641
2384
  readonly keyDb: JsonOutboundKeyDb;
1642
2385
  readonly settingsStore: JsonApiServerSettingsStore;
@@ -1673,6 +2416,54 @@ interface Daemon {
1673
2416
  * still disposes it in cleanup.
1674
2417
  */
1675
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;
1676
2467
  }
1677
2468
  /**
1678
2469
  * Construct the standalone daemon from a loaded config + on-disk paths. Does NOT
@@ -1698,6 +2489,137 @@ declare function buildDaemon(config: DaemonConfig, paths: DaemonPaths): Daemon;
1698
2489
  * via `daemon.adminServer.stop()`. */
1699
2490
  declare function resetDaemonSingletonsForTests(): void;
1700
2491
 
2492
+ /**
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).
2607
+ *
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.
2612
+ *
2613
+ * @module @omnicross/daemon/ports/ConsoleLogger
2614
+ */
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
+ }
2622
+
1701
2623
  /**
1702
2624
  * ccr-import.ts — translate a `claude-code-router` (CCR) `config.json` into an
1703
2625
  * omnicross daemon config (design D9). Pure + testable: `parseCcrConfig(raw)` +
@@ -1761,4 +2683,4 @@ declare function mapCcrToOmnicross(ccr: CcrConfig): {
1761
2683
  notes: string[];
1762
2684
  };
1763
2685
 
1764
- export { type AdminApiDeps, AdminServer, type AdminServerDeps, type AdminServerStatus, type CcrConfig, type CcrProvider, type CcrRouter, ConfigFileProviderConfigSource, ConsoleLogger, 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 };