@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/cli.cjs +3206 -328
- package/dist/cli.js +3210 -314
- package/dist/index.cjs +3114 -260
- package/dist/index.d.cts +953 -31
- package/dist/index.d.ts +953 -31
- package/dist/index.js +3083 -213
- package/package.json +1 -1
package/dist/index.d.ts
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 {
|
|
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 {
|
|
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
|
|
555
|
-
* (oauth design D4).
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|