@omnicross/daemon 0.1.5 → 0.1.7
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 +3041 -702
- package/dist/cli.js +3062 -677
- package/dist/index.cjs +2903 -666
- package/dist/index.d.cts +546 -213
- package/dist/index.d.ts +546 -213
- package/dist/index.js +2924 -641
- package/package.json +63 -63
package/dist/index.d.cts
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as _omnicross_core from '@omnicross/core';
|
|
2
|
+
import { OutboundApiServerConfig, Logger, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, PricingStore, AutomaticPricingSource, OutboundKeyDb, OutboundKeyDbRow, OutboundKeyPolicy, PricingEngine as PricingEngine$1 } from '@omnicross/core';
|
|
2
3
|
import { ApiKeyPoolService } from '@omnicross/core/completion/ApiKeyPoolService';
|
|
3
|
-
import { AccountProbeConfig, OutboundKeyDb, VoucherDb, KeySpendReader, OutboundApiServer } from '@omnicross/core/outbound-api';
|
|
4
|
+
import { AllowanceSchedulingConfig, AccountProbeConfig, OutboundKeyDb as OutboundKeyDb$1, VoucherDb, KeySpendReader, OutboundApiServer } from '@omnicross/core/outbound-api';
|
|
4
5
|
import { ProviderProxy } from '@omnicross/core/provider-proxy';
|
|
5
6
|
import { UsageRecorder, PricingEngine } from '@omnicross/core/usage';
|
|
6
7
|
import { SubscriptionCredentialStore, FetchLike, SubscriptionProviderRegistry, SubscriptionAccountService } from '@omnicross/subscriptions';
|
|
7
|
-
import {
|
|
8
|
-
import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, ProxyConfig, SubscriptionAccountSanitized, AccountClientIdentity } from '@omnicross/contracts/account-tokens-types';
|
|
8
|
+
import { AccountAllowanceSnapshot } from '@omnicross/contracts/account-allowance-types';
|
|
9
|
+
import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, ProxyConfig, SubscriptionAccountSanitized, AccountClientIdentity, SubscriptionAccountEntry } from '@omnicross/contracts/account-tokens-types';
|
|
10
|
+
import * as _omnicross_contracts_subscription_types from '@omnicross/contracts/subscription-types';
|
|
9
11
|
import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
|
|
12
|
+
import { AccountAllowanceStore } from '@omnicross/core/pipeline/AccountAllowanceStore';
|
|
13
|
+
import { AllowanceSchedulingDecision } from '@omnicross/core/pipeline/AccountAllowanceScheduling';
|
|
14
|
+
import { SubscriptionIdentityStore } from '@omnicross/core/provider-proxy/identity/SubscriptionIdentityStore';
|
|
15
|
+
import { LoggingConfig, HealthReport } from '@omnicross/contracts/health-logging-types';
|
|
10
16
|
import { SubscriptionAccountHealth } from '@omnicross/core/pipeline/SubscriptionAccountHealth';
|
|
11
17
|
import { fetchUpstream } from '@omnicross/core/pipeline/upstreamFetch';
|
|
12
|
-
import { AuditRecord, AuditConfig } from '@omnicross/contracts/audit-types';
|
|
18
|
+
import { AuditRecord, AuditStats, AuditConfig } from '@omnicross/contracts/audit-types';
|
|
13
19
|
import { BillingDeliveryStatus, BillingConfig, BillingEvent } from '@omnicross/contracts/billing-types';
|
|
14
20
|
import http from 'node:http';
|
|
15
21
|
import { LLMProvider, AgentDefaultModels, GlobalModelParameters } from '@omnicross/contracts/llm-config';
|
|
16
|
-
import { PricingEntry, PricingEntryInput, PricingResolution } from '@omnicross/contracts/pricing-types';
|
|
22
|
+
import { PricingEntry, PricingEntryInput, PricingResolution, PricingSourceRefreshResult } from '@omnicross/contracts/pricing-types';
|
|
17
23
|
import { WebhookConfig, WebhookEvent } from '@omnicross/contracts/webhook-types';
|
|
18
24
|
|
|
19
25
|
/**
|
|
@@ -104,8 +110,17 @@ declare class SecretBox {
|
|
|
104
110
|
* @module @omnicross/daemon/config
|
|
105
111
|
*/
|
|
106
112
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The wire formats the daemon's BYO providers can speak.
|
|
115
|
+
*
|
|
116
|
+
* Each value names EXACTLY ONE provider-slot format transformer (see
|
|
117
|
+
* `ConfigFileProviderConfigSource.FORMAT_TRANSFORMER`). `openai-response` used
|
|
118
|
+
* to be inexpressible here, so a Responses-wire upstream had to smuggle its
|
|
119
|
+
* format through `transformer.use[]` — which put TWO format transformers in
|
|
120
|
+
* reach of the provider slot. Naming it here keeps `use[]` purely a modifier
|
|
121
|
+
* list. `validateProvider` migrates the old shape on load.
|
|
122
|
+
*/
|
|
123
|
+
type DaemonApiFormat = 'openai' | 'anthropic' | 'gemini' | 'openai-response';
|
|
109
124
|
/**
|
|
110
125
|
* One pool key on a provider row (design D1). Structurally compatible with
|
|
111
126
|
* core's `ApiKeyEntry` (`@omnicross/contracts/llm-config`) — a hand-authored SUBSET: only
|
|
@@ -406,21 +421,54 @@ declare function loadConfig(path: string): DaemonConfig;
|
|
|
406
421
|
declare function saveConfig(path: string, cfg: DaemonConfig): void;
|
|
407
422
|
|
|
408
423
|
/**
|
|
409
|
-
*
|
|
424
|
+
* account-multi — daemon-side pure helpers for the subscription multi-account
|
|
425
|
+
* layout.
|
|
426
|
+
*
|
|
427
|
+
* Host-clean: no I/O, no encryption — callers persist through their own
|
|
428
|
+
* encrypted writers.
|
|
429
|
+
*
|
|
430
|
+
* Load-bearing invariant: the top-level per-provider block is ALWAYS a byte-equal
|
|
431
|
+
* mirror of the active account's `tokens`; every mutator re-derives it last.
|
|
432
|
+
*
|
|
433
|
+
* @module @omnicross/daemon/ports/account-multi
|
|
434
|
+
*/
|
|
435
|
+
|
|
436
|
+
/** Provider id → owned contract field names. */
|
|
437
|
+
type DaemonProvider = 'claude' | 'codex' | 'gemini' | 'opencodego';
|
|
438
|
+
interface AccountMetadataPatch {
|
|
439
|
+
label?: string;
|
|
440
|
+
enabled?: boolean;
|
|
441
|
+
priority?: number;
|
|
442
|
+
group?: string | null;
|
|
443
|
+
tags?: string[];
|
|
444
|
+
}
|
|
445
|
+
interface AccountRef {
|
|
446
|
+
providerId: DaemonProvider;
|
|
447
|
+
accountId: string;
|
|
448
|
+
}
|
|
449
|
+
type AccountBatchMutation = {
|
|
450
|
+
action: 'enable' | 'disable';
|
|
451
|
+
} | {
|
|
452
|
+
action: 'set-group';
|
|
453
|
+
group: string | null;
|
|
454
|
+
} | {
|
|
455
|
+
action: 'delete';
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* external-cli-credentials read-only access to the external CLI native
|
|
410
460
|
* credential stores (external-cli-sync).
|
|
411
461
|
*
|
|
412
|
-
* The daemon never WRITES these files (they belong to the CLIs); it
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* refresh token out from under us, and the external file then holds the only
|
|
416
|
-
* live credential — and (b) detect divergence for the account-list warning.
|
|
462
|
+
* The daemon never WRITES these files (they belong to the CLIs); it reads them
|
|
463
|
+
* only for the explicit admin availability check and copy-as-new-account import
|
|
464
|
+
* flow. Normal account reads and refreshes never call this reader.
|
|
417
465
|
*
|
|
418
466
|
* File shapes (mirrors the shapes the CLIs themselves write):
|
|
419
467
|
* claude `~/.claude/.credentials.json`
|
|
420
|
-
*
|
|
468
|
+
* `{ claudeAiOauth: { accessToken, refreshToken, expiresAt(number ms),
|
|
421
469
|
* scopes? } }`
|
|
422
470
|
* codex `~/.codex/auth.json`
|
|
423
|
-
*
|
|
471
|
+
* `{ tokens: { id_token, access_token, refresh_token } }` no explicit
|
|
424
472
|
* expiry; the access token's JWT `exp` claim is the only expiry signal.
|
|
425
473
|
*
|
|
426
474
|
* Gemini is deliberately excluded: the gemini CLI's oauth store is not a
|
|
@@ -445,56 +493,9 @@ interface ExternalCliCredentials {
|
|
|
445
493
|
/** claude only. */
|
|
446
494
|
scopes?: string[];
|
|
447
495
|
}
|
|
448
|
-
/** Reader port
|
|
496
|
+
/** Reader port injectable so tests never touch the real home directory. */
|
|
449
497
|
type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentials | null;
|
|
450
498
|
|
|
451
|
-
/**
|
|
452
|
-
* external-cli-store — WRITE side of the external CLI native credential stores
|
|
453
|
-
* (external-cli-sync, import + write-back).
|
|
454
|
-
*
|
|
455
|
-
* Counterpart of `external-cli-credentials` (the read side). The daemon only
|
|
456
|
-
* ever writes a file it MANAGES: an explicit "import existing CLI login" puts
|
|
457
|
-
* an `.omnicross-managed` marker (recording the owning account id) next to the
|
|
458
|
-
* native store, and every subsequent successful refresh of THAT account writes
|
|
459
|
-
* the rotated credential back into the file. Without the write-back, the
|
|
460
|
-
* daemon's refresh would rotate the single-use refresh token and silently log
|
|
461
|
-
* the user's bare CLI out — the write-back keeps both sides on the same live
|
|
462
|
-
* credential.
|
|
463
|
-
*
|
|
464
|
-
* Safety properties:
|
|
465
|
-
* - NEVER writes without a matching marker (an unmanaged / foreign-account
|
|
466
|
-
* file is untouched);
|
|
467
|
-
* - read-then-merge: unrelated top-level keys in the native file (e.g.
|
|
468
|
-
* claude `email`, codex `OPENAI_API_KEY`) are preserved;
|
|
469
|
-
* - one-time `.omnicross-backup` of the original file before the FIRST
|
|
470
|
-
* overwrite (restorable by hand if the user wants the daemon out);
|
|
471
|
-
* - atomic write (temp file → rename) so a crash never leaves a torn file.
|
|
472
|
-
*
|
|
473
|
-
* The envelope shapes mirror what the CLIs themselves write — the round-trip
|
|
474
|
-
* test parses a written file back through `external-cli-credentials` to keep
|
|
475
|
-
* the two sides from drifting.
|
|
476
|
-
*
|
|
477
|
-
* @module @omnicross/daemon/ports/external-cli-store
|
|
478
|
-
*/
|
|
479
|
-
|
|
480
|
-
/** Token blocks the write-back accepts (the two external-store providers). */
|
|
481
|
-
type ExternalWritableTokens = ClaudeTokenConfig | CodexTokenConfig;
|
|
482
|
-
/**
|
|
483
|
-
* Injectable port over the external store writes (tests use an in-memory fake;
|
|
484
|
-
* the store wires `realExternalCliStore`).
|
|
485
|
-
*/
|
|
486
|
-
interface ExternalCliStorePort {
|
|
487
|
-
/** The owning account id recorded by the marker, or undefined when unmanaged. */
|
|
488
|
-
readMarkerAccountId(provider: ExternalCliProvider): string | undefined;
|
|
489
|
-
/** Record (or move) ownership of the provider's native store to an account. */
|
|
490
|
-
writeMarker(provider: ExternalCliProvider, accountId: string): void;
|
|
491
|
-
/**
|
|
492
|
-
* Write the refreshed tokens back into the native store — ONLY when the
|
|
493
|
-
* marker names `accountId`. Returns true when a write happened.
|
|
494
|
-
*/
|
|
495
|
-
writeBack(provider: ExternalCliProvider, accountId: string, tokens: ExternalWritableTokens): boolean;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
499
|
/**
|
|
499
500
|
* JsonSubscriptionCredentialStore — the daemon's file-backed
|
|
500
501
|
* `SubscriptionCredentialStore` port impl (design D1).
|
|
@@ -503,7 +504,7 @@ interface ExternalCliStorePort {
|
|
|
503
504
|
* over a sibling `tokens.json` holding an `AccountTokensConfig`-shaped object
|
|
504
505
|
* (`{ claude?, codex?, gemini?, opencodego?, updatedAt }`). Modeled on
|
|
505
506
|
* `JsonOutboundKeyDb`: the constructor takes the path; reads are
|
|
506
|
-
* `existsSync`
|
|
507
|
+
* `existsSync` `readFileSync` `JSON.parse`, tolerating a missing/corrupt
|
|
507
508
|
* file by returning a minimal `{ updatedAt }` config (the strategies already
|
|
508
509
|
* guard `?.accessToken`, so a partial/empty config never crashes dispatch).
|
|
509
510
|
*
|
|
@@ -513,7 +514,7 @@ interface ExternalCliStorePort {
|
|
|
513
514
|
* (strategies only consume + refresh, never log in).
|
|
514
515
|
*
|
|
515
516
|
* DAEMON-ONLY WRITE PATH (token-paste, design D1): `writeProviderTokens` /
|
|
516
|
-
* `clearProvider` are CONCRETE-CLASS methods
|
|
517
|
+
* `clearProvider` are CONCRETE-CLASS methods NOT part of the
|
|
517
518
|
* `SubscriptionCredentialStore` port. The registry / auth strategies / account
|
|
518
519
|
* service never see them (they hold the port type), so a mutation can never leak
|
|
519
520
|
* into the subscription block. Only the daemon admin API (which holds the
|
|
@@ -523,7 +524,7 @@ interface ExternalCliStorePort {
|
|
|
523
524
|
*
|
|
524
525
|
* AT-REST ENCRYPTION (secrets design D6/D7): the constructor takes a `SecretBox`.
|
|
525
526
|
* `readConfig` decrypts the token-material fields on read (so every getter +
|
|
526
|
-
* `getFullConfig` returns PLAINTEXT tokens
|
|
527
|
+
* `getFullConfig` returns PLAINTEXT tokens the subscription bearer path is
|
|
527
528
|
* byte-identical), and `persist` encrypts them before writing. Because EVERY
|
|
528
529
|
* write funnels through `persist`, the OAuth-refresh writes below are encrypted
|
|
529
530
|
* at-rest with NO extra work (the store API guarantees it). The "re-read on every
|
|
@@ -531,16 +532,16 @@ interface ExternalCliStorePort {
|
|
|
531
532
|
*
|
|
532
533
|
* REAL TOKEN REFRESH (oauth design D4): `refresh{Claude,Codex,Gemini}Token` mint
|
|
533
534
|
* a new access token via the shared host-clean OAuth refresh functions
|
|
534
|
-
* (`@omnicross/subscriptions/oauth`, injected `FetchLike`
|
|
535
|
+
* (`@omnicross/subscriptions/oauth`, injected `FetchLike` default global
|
|
535
536
|
* `fetch`), then read-merge the refreshed fields into the provider block and
|
|
536
|
-
* write back through `persist` (
|
|
537
|
+
* write back through `persist` (encrypted). Field-writes:
|
|
537
538
|
* claude/codex write access+refresh(+codex idToken)
|
|
538
539
|
* +expiresAt+status:authorized+lastRefreshedAt; gemini writes ONLY access+
|
|
539
|
-
* expiresAt (its refresh response omits refresh_token
|
|
540
|
+
* expiresAt (its refresh response omits refresh_token the OLD value is reused,
|
|
540
541
|
* never overwritten). On any failure the block is marked `status:'expired'` +
|
|
541
542
|
* errorMessage and `false` is returned. When the block has NO refresh_token
|
|
542
543
|
* (claude setup-token, manual token), it is an HONEST `false` BEFORE any upstream
|
|
543
|
-
* call
|
|
544
|
+
* call the block is not touched and no refresh_token is invented.
|
|
544
545
|
*
|
|
545
546
|
* @module @omnicross/daemon/ports/JsonSubscriptionCredentialStore
|
|
546
547
|
*/
|
|
@@ -548,7 +549,7 @@ interface ExternalCliStorePort {
|
|
|
548
549
|
/**
|
|
549
550
|
* The per-provider token block accepted by `writeProviderTokens`. Mirrors the
|
|
550
551
|
* `AccountTokensConfig` per-provider field types (one of the four contract token
|
|
551
|
-
* shapes), keyed by `SubscriptionProviderId`
|
|
552
|
+
* shapes), keyed by `SubscriptionProviderId` the daemon admin layer validates
|
|
552
553
|
* the wire body to one of these before calling the writer.
|
|
553
554
|
*/
|
|
554
555
|
type SubscriptionTokenBlock = ClaudeTokenConfig | CodexTokenConfig | GeminiTokenConfig | OpenCodeGoTokenConfig;
|
|
@@ -556,10 +557,8 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
556
557
|
private readonly tokensPath;
|
|
557
558
|
private readonly box;
|
|
558
559
|
private readonly fetchImpl;
|
|
559
|
-
/** Injectable external CLI native-store reader
|
|
560
|
+
/** Injectable, strictly read-only external CLI native-store reader. */
|
|
560
561
|
private readonly externalCliReader;
|
|
561
|
-
/** Injectable external CLI native-store WRITER (marker-gated write-back). */
|
|
562
|
-
private readonly externalCliStore;
|
|
563
562
|
/**
|
|
564
563
|
* @param tokensPath on-disk `tokens.json` location.
|
|
565
564
|
* @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
|
|
@@ -569,23 +568,27 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
569
568
|
* proxy-aware {@link fetchUpstream} that threads the
|
|
570
569
|
* `{ providerId, accountId }` ctx (upstream-proxy M1) so a
|
|
571
570
|
* per-account/per-provider proxy is honored on refresh exactly
|
|
572
|
-
* as on relay
|
|
571
|
+
* as on relay refresh egresses from the SAME proxy IP as the
|
|
573
572
|
* account's traffic. NOT used by any read/write path.
|
|
574
573
|
*/
|
|
575
574
|
constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike | undefined,
|
|
576
|
-
/** Injectable external CLI native-store reader
|
|
577
|
-
externalCliReader?: ExternalCliReader
|
|
578
|
-
/** Injectable external CLI native-store WRITER (marker-gated write-back). */
|
|
579
|
-
externalCliStore?: ExternalCliStorePort);
|
|
575
|
+
/** Injectable, strictly read-only external CLI native-store reader. */
|
|
576
|
+
externalCliReader?: ExternalCliReader);
|
|
580
577
|
/**
|
|
581
578
|
* The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
|
|
582
579
|
* TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
|
|
583
580
|
* through {@link fetchUpstream} with the account's `{ providerId, accountId }`
|
|
584
|
-
* ctx so the per-account/provider proxy applies. `@internal`
|
|
581
|
+
* ctx so the per-account/provider proxy applies. `@internal` also a test seam.
|
|
582
|
+
*
|
|
583
|
+
* `redactBodies` is REQUIRED here: this round-trip sends the refresh_token and
|
|
584
|
+
* receives a fresh access/refresh token pair. Carrying a `providerId` opts the
|
|
585
|
+
* call into the upstream trace (so a failing refresh is diagnosable), and the
|
|
586
|
+
* trace captures bodies verbatim — without this flag every refresh would write
|
|
587
|
+
* a plaintext token pair into `upstream-trace.jsonl`.
|
|
585
588
|
*/
|
|
586
589
|
buildRefreshFetch(providerId: string, accountId?: string): FetchLike;
|
|
587
590
|
/**
|
|
588
|
-
* In-flight refresh coalescing
|
|
591
|
+
* In-flight refresh coalescing. OAuth refresh tokens are
|
|
589
592
|
* SINGLE-USE: two concurrent refreshes of one account each spend the same
|
|
590
593
|
* token and the loser bricks a healthy account. Every refresh entry point
|
|
591
594
|
* (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
|
|
@@ -594,11 +597,11 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
594
597
|
private readonly inFlightRefreshes;
|
|
595
598
|
private coalesce;
|
|
596
599
|
/** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
|
|
597
|
-
* file is absent/corrupt). This is the hot read
|
|
600
|
+
* file is absent/corrupt). This is the hot read the codex / gemini auth
|
|
598
601
|
* strategies pull `accessToken` / `expiresAt` / `status` from it. */
|
|
599
602
|
getFullConfig(): Promise<AccountTokensConfig>;
|
|
600
603
|
/** Current Claude OAuth access token, or `null` when none is stored. No inline
|
|
601
|
-
* refresh here
|
|
604
|
+
* refresh here the lead-window / 401-retry refresh is driven by the
|
|
602
605
|
* subscription auth strategy, which calls `refreshClaudeToken` (now real). */
|
|
603
606
|
getValidClaudeAccessToken(): Promise<string | null>;
|
|
604
607
|
/** Current OpenCodeGo static API key, or `null` when none is stored. */
|
|
@@ -614,28 +617,25 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
614
617
|
/**
|
|
615
618
|
* DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
|
|
616
619
|
* each provider's accounts to the secret-free `SubscriptionAccountSanitized`
|
|
617
|
-
* shape (id/label/status/expiresAt/hasAccessToken/isActive)
|
|
620
|
+
* shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
|
|
618
621
|
* Used by the admin accounts GET (secret-IN-never-OUT).
|
|
619
622
|
*/
|
|
620
623
|
listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
|
|
621
624
|
/**
|
|
622
|
-
* List-time credential
|
|
623
|
-
*
|
|
624
|
-
* credential
|
|
625
|
-
* rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
|
|
626
|
-
* a failed refresh (`external-not-rotated`) takes precedence — it is the most
|
|
627
|
-
* actionable state.
|
|
625
|
+
* List-time managed-credential conflict warnings. Computed, not persisted:
|
|
626
|
+
* `duplicate-token` is projected when two accounts of one provider share a
|
|
627
|
+
* credential. This deliberately does not inspect either native CLI file.
|
|
628
628
|
*/
|
|
629
|
-
private
|
|
629
|
+
private attachDuplicateWarnings;
|
|
630
630
|
/** Read the external CLI store, never letting an fs/parse error escape. */
|
|
631
631
|
private safeReadExternal;
|
|
632
632
|
/**
|
|
633
633
|
* Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
|
|
634
|
-
* the block has no refresh_token (setup-token / manual)
|
|
634
|
+
* the block has no refresh_token (setup-token / manual) no upstream call, the
|
|
635
635
|
* block is untouched. Otherwise mint via the shared claude refresh flow and
|
|
636
636
|
* write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
|
|
637
|
-
* On failure
|
|
638
|
-
* errorMessage
|
|
637
|
+
* On failure status:expired +
|
|
638
|
+
* errorMessage `false`.
|
|
639
639
|
*/
|
|
640
640
|
refreshClaudeToken(): Promise<boolean>;
|
|
641
641
|
/**
|
|
@@ -653,11 +653,10 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
653
653
|
*/
|
|
654
654
|
refreshGeminiToken(): Promise<boolean>;
|
|
655
655
|
/**
|
|
656
|
-
* Refresh a SPECIFIC account by id (background scheduler sweep
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
* failure flags ONLY that account `expired`.
|
|
656
|
+
* Refresh a SPECIFIC managed account by id (background scheduler sweep and
|
|
657
|
+
* account-pool resolution). It uses only that account's stored refresh
|
|
658
|
+
* token. Coalesced per `provider:id`; on failure flags ONLY that account
|
|
659
|
+
* `expired`.
|
|
661
660
|
*/
|
|
662
661
|
refreshAccountById(provider: 'claude' | 'codex' | 'gemini', id: string): Promise<boolean>;
|
|
663
662
|
/**
|
|
@@ -672,7 +671,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
672
671
|
/**
|
|
673
672
|
* Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
|
|
674
673
|
* `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
|
|
675
|
-
*
|
|
674
|
+
* `false` (no refresh affordance).
|
|
676
675
|
*/
|
|
677
676
|
refreshAccountToken(providerId: SubscriptionProviderId, accountId: string): Promise<boolean>;
|
|
678
677
|
/**
|
|
@@ -686,7 +685,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
686
685
|
* (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
|
|
687
686
|
* whitelisted fingerprint headers; the token mirror is untouched); a no-op for
|
|
688
687
|
* 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
|
|
688
|
+
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
|
|
690
689
|
* store's port wrapper swallows a rejection so the relay hot path is unaffected.
|
|
691
690
|
*/
|
|
692
691
|
setAccountIdentity(providerId: SubscriptionProviderId, accountId: string, identity: AccountClientIdentity): Promise<void>;
|
|
@@ -702,7 +701,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
702
701
|
* DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
|
|
703
702
|
* the port). Passing `undefined` clears the override. Write-only password: when
|
|
704
703
|
* the incoming structured proxy omits the password but the account already had
|
|
705
|
-
* one, the current (decrypted) password is preserved
|
|
704
|
+
* one, the current (decrypted) password is preserved editing host/port never
|
|
706
705
|
* wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
|
|
707
706
|
*/
|
|
708
707
|
setAccountProxy(providerId: SubscriptionProviderId, accountId: string, proxy: ProxyConfig | undefined): Promise<{
|
|
@@ -719,44 +718,36 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
719
718
|
}>;
|
|
720
719
|
/** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
|
|
721
720
|
private refreshUpstream;
|
|
722
|
-
/**
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
* refresh of the account that OWNS the provider's native CLI store (imported
|
|
735
|
-
* via `importExternalCliAccount`), push the rotated credential back into the
|
|
736
|
-
* file — otherwise the daemon's refresh invalidates the single-use refresh
|
|
737
|
-
* token and silently logs the bare CLI out. NON-FATAL: the internal store is
|
|
738
|
-
* already persisted; a failed external write only leaves the file stale,
|
|
739
|
-
* which the `external-divergent` warning surfaces.
|
|
740
|
-
*/
|
|
741
|
-
private resyncExternal;
|
|
742
|
-
/** Read the marker's owning account id, never letting an fs error escape. */
|
|
743
|
-
private safeReadMarker;
|
|
721
|
+
/** Atomically patch one account's non-secret management metadata. */
|
|
722
|
+
patchAccountMetadata(providerId: SubscriptionProviderId, accountId: string, patch: AccountMetadataPatch): Promise<{
|
|
723
|
+
ok: boolean;
|
|
724
|
+
}>;
|
|
725
|
+
/** Validate every target, then persist one all-or-nothing batch mutation. */
|
|
726
|
+
batchManageAccounts(refs: AccountRef[], mutation: AccountBatchMutation): Promise<{
|
|
727
|
+
ok: true;
|
|
728
|
+
affected: number;
|
|
729
|
+
} | {
|
|
730
|
+
ok: false;
|
|
731
|
+
missing: AccountRef;
|
|
732
|
+
}>;
|
|
744
733
|
/**
|
|
745
734
|
* DAEMON-ONLY (admin import button): which providers have a usable external
|
|
746
|
-
* CLI credential on THIS machine. Pure detection
|
|
735
|
+
* CLI credential on THIS machine. Pure detection reads the native files,
|
|
747
736
|
* never mutates anything, never returns a token.
|
|
748
737
|
*/
|
|
749
738
|
listExternalCliAvailability(): Promise<Record<ExternalCliProvider, boolean>>;
|
|
750
739
|
/**
|
|
751
740
|
* DAEMON-ONLY (admin import button): import the external CLI's current login
|
|
752
|
-
* as a NEW account (+ activate)
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
741
|
+
* as a NEW account (+ activate). This is a COPY-ONLY import: Omnicross never
|
|
742
|
+
* claims, writes, moves, restores, or deletes the native CLI credential file
|
|
743
|
+
* or any legacy `.omnicross-managed` marker/backup beside it. Subsequent
|
|
744
|
+
* refreshes persist only Omnicross's encrypted token store.
|
|
756
745
|
*/
|
|
757
746
|
importExternalCliAccount(provider: ExternalCliProvider, label?: string): Promise<{
|
|
758
747
|
ok: true;
|
|
759
748
|
id: string;
|
|
749
|
+
nativeCredentialMode: 'read-only';
|
|
750
|
+
refreshWritesNativeCredentials: false;
|
|
760
751
|
} | {
|
|
761
752
|
ok: false;
|
|
762
753
|
reason: 'no-credential';
|
|
@@ -788,13 +779,13 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
788
779
|
* `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
|
|
789
780
|
* OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
|
|
790
781
|
* tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
|
|
791
|
-
* so a first-ever write still produces a valid config. No cache
|
|
782
|
+
* so a first-ever write still produces a valid config. No cache the next read
|
|
792
783
|
* sees this write.
|
|
793
784
|
*/
|
|
794
785
|
writeProviderTokens(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock): Promise<void>;
|
|
795
786
|
/**
|
|
796
787
|
* DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
|
|
797
|
-
* (optional label) and set it active, then re-derive the mirror
|
|
788
|
+
* (optional label) and set it active, then re-derive the mirror used by
|
|
798
789
|
* `omnicross login <provider> --label` to add an account instead of overwriting.
|
|
799
790
|
*/
|
|
800
791
|
appendProviderAccount(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock, label?: string): Promise<{
|
|
@@ -817,7 +808,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
817
808
|
}>;
|
|
818
809
|
/**
|
|
819
810
|
* DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
|
|
820
|
-
* rejects an unknown id. Label-only
|
|
811
|
+
* rejects an unknown id. Label-only no token material is read or written
|
|
821
812
|
* (the secret-free invariant holds).
|
|
822
813
|
*/
|
|
823
814
|
renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
|
|
@@ -832,8 +823,8 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
832
823
|
clearProvider(providerId: SubscriptionProviderId): Promise<void>;
|
|
833
824
|
/** Write the merged config to disk as pretty JSON (mkdir parent if needed).
|
|
834
825
|
* Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
|
|
835
|
-
*
|
|
836
|
-
* write
|
|
826
|
+
* `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
827
|
+
* write incl. child 4's future refresh writes lands encrypted. */
|
|
837
828
|
private persist;
|
|
838
829
|
/**
|
|
839
830
|
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
@@ -841,11 +832,11 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
841
832
|
* subscription bearer path is byte-identical).
|
|
842
833
|
*
|
|
843
834
|
* The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
|
|
844
|
-
* file
|
|
835
|
+
* file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
|
|
845
836
|
* wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
|
|
846
|
-
* box's clear, secret-free error (secrets spec "
|
|
847
|
-
* SHALL fail-fast, SHALL NOT
|
|
848
|
-
* tokens" and silently send the WRONG bearer upstream
|
|
837
|
+
* box's clear, secret-free error (secrets spec "/ UX":
|
|
838
|
+
* SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
|
|
839
|
+
* tokens" and silently send the WRONG bearer upstream 401). Mirrors
|
|
849
840
|
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
850
841
|
*/
|
|
851
842
|
private readConfig;
|
|
@@ -891,6 +882,18 @@ interface SubscriptionTokenWriter {
|
|
|
891
882
|
setAccountPriority(providerId: SubscriptionProviderId, id: string, priority: number): Promise<{
|
|
892
883
|
ok: boolean;
|
|
893
884
|
}>;
|
|
885
|
+
/** Patch non-secret account management metadata in one write. */
|
|
886
|
+
patchAccountMetadata(providerId: SubscriptionProviderId, id: string, patch: AccountMetadataPatch): Promise<{
|
|
887
|
+
ok: boolean;
|
|
888
|
+
}>;
|
|
889
|
+
/** All-or-nothing multi-provider account management mutation. */
|
|
890
|
+
batchManageAccounts(refs: AccountRef[], mutation: AccountBatchMutation): Promise<{
|
|
891
|
+
ok: true;
|
|
892
|
+
affected: number;
|
|
893
|
+
} | {
|
|
894
|
+
ok: false;
|
|
895
|
+
missing: AccountRef;
|
|
896
|
+
}>;
|
|
894
897
|
/** Set (or CLEAR, with `undefined`) one account's per-account proxy override
|
|
895
898
|
* (upstream-proxy). The `proxy.password` is a secret (encrypted at rest, masked
|
|
896
899
|
* in the sanitized view). Rejects an unknown id. */
|
|
@@ -912,6 +915,8 @@ interface SubscriptionTokenWriter {
|
|
|
912
915
|
importExternalCliAccount(providerId: 'claude' | 'codex', label?: string): Promise<{
|
|
913
916
|
ok: true;
|
|
914
917
|
id: string;
|
|
918
|
+
nativeCredentialMode: 'read-only';
|
|
919
|
+
refreshWritesNativeCredentials: false;
|
|
915
920
|
} | {
|
|
916
921
|
ok: false;
|
|
917
922
|
reason: 'no-credential';
|
|
@@ -924,11 +929,12 @@ interface SubscriptionTokenWriter {
|
|
|
924
929
|
* app-parity child 4, design D1).
|
|
925
930
|
*
|
|
926
931
|
* `start` mints a crypto-random `sessionId` and stashes the per-session PKCE
|
|
927
|
-
* `{ providerId, codeVerifier, state }` here; `complete`
|
|
928
|
-
* `
|
|
932
|
+
* `{ providerId, codeVerifier, state }` here; `complete` `peek`s the session,
|
|
933
|
+
* exchanges the code, and `consume`s it ONLY once a token was minted — so a
|
|
934
|
+
* failed exchange leaves the session retryable instead of burning it. The map is
|
|
929
935
|
* NEVER serialized to the client — only the opaque `sessionId` + the public
|
|
930
936
|
* `authUrl` cross the wire. Sessions are short-lived (OQ3 = 10-min TTL); a sweep
|
|
931
|
-
* reaps abandoned sessions, and `
|
|
937
|
+
* reaps abandoned sessions, and `peek` re-checks the TTL so an expired-but-not-
|
|
932
938
|
* yet-swept session is still rejected. A daemon restart simply drops in-flight
|
|
933
939
|
* logins (correct fail-safe — no partial token is ever written).
|
|
934
940
|
*
|
|
@@ -964,13 +970,25 @@ declare class OAuthSessionStore {
|
|
|
964
970
|
*/
|
|
965
971
|
put(session: Omit<PendingOAuthSession, 'createdAt'>): string;
|
|
966
972
|
/**
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
* dropped). A `null` return means the completer must reject (no
|
|
970
|
-
* write).
|
|
973
|
+
* NON-DESTRUCTIVE lookup: return the session for `sessionId`, or `null` when
|
|
974
|
+
* it is unknown, already consumed, or past its TTL (an expired entry is
|
|
975
|
+
* dropped here). A `null` return means the completer must reject (no
|
|
976
|
+
* exchange, no write).
|
|
977
|
+
*
|
|
978
|
+
* Deliberately NOT a consume: the completer peeks, runs the token exchange,
|
|
979
|
+
* and only {@link consume}s once a token has actually been minted. Consuming
|
|
980
|
+
* up-front burned the session on EVERY failed exchange (a mistyped/expired
|
|
981
|
+
* pasted code, a proxy hiccup), so the user's natural retry hit
|
|
982
|
+
* "session is unknown, expired, or already used" and the login became
|
|
983
|
+
* unrecoverable without restarting the whole flow.
|
|
984
|
+
*/
|
|
985
|
+
peek(sessionId: string): PendingOAuthSession | null;
|
|
986
|
+
/**
|
|
987
|
+
* SINGLE-USE burn: drop the session so the same `sessionId` can never be
|
|
988
|
+
* completed twice. Called ONLY after a successful token exchange.
|
|
971
989
|
*/
|
|
972
|
-
|
|
973
|
-
/** Drop every session past its TTL. Called on each put/
|
|
990
|
+
consume(sessionId: string): void;
|
|
991
|
+
/** Drop every session past its TTL. Called on each put/peek. */
|
|
974
992
|
private sweep;
|
|
975
993
|
}
|
|
976
994
|
|
|
@@ -984,10 +1002,12 @@ declare class OAuthSessionStore {
|
|
|
984
1002
|
* provider's authorize params and stashes the per-session `{ codeVerifier, state }`
|
|
985
1003
|
* in the `OAuthSessionStore` keyed by a minted opaque `sessionId`, returning ONLY
|
|
986
1004
|
* `{ authUrl, sessionId }` (the `authUrl` carries client_id + PKCE challenge +
|
|
987
|
-
* state — all public). `complete`
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
* it active, and responds ONLY
|
|
1005
|
+
* state — all public). `complete` `peek`s the session, validates state (claude's
|
|
1006
|
+
* `code#state`), `exchangeCodeForTokens(...)`, then — and ONLY then — `consume`s
|
|
1007
|
+
* the session (single-use), persists the minted token through the encrypted
|
|
1008
|
+
* credential store (`appendProviderAccount`) + marks it active, and responds ONLY
|
|
1009
|
+
* the sanitized `SubscriptionListEntry`. A FAILED exchange leaves the session
|
|
1010
|
+
* intact so the user can retry within its TTL instead of being locked out by a 410.
|
|
991
1011
|
*
|
|
992
1012
|
* SECRET SPINE (the load-bearing invariant): the minted access/refresh token
|
|
993
1013
|
* NEVER appears in any response body or log; the `codeVerifier` / session map is
|
|
@@ -1084,6 +1104,127 @@ declare class CodexOAuthSessionStore {
|
|
|
1084
1104
|
private sweep;
|
|
1085
1105
|
}
|
|
1086
1106
|
|
|
1107
|
+
/**
|
|
1108
|
+
* Claude OAuth usage collector.
|
|
1109
|
+
*
|
|
1110
|
+
* Fetches one account's five-hour, seven-day, and seven-day Sonnet windows,
|
|
1111
|
+
* coalesces concurrent refreshes by account, and caches every result for five
|
|
1112
|
+
* minutes. Tokens and raw upstream payloads never leave this module.
|
|
1113
|
+
*/
|
|
1114
|
+
|
|
1115
|
+
interface ClaudeAllowanceCredentialReader {
|
|
1116
|
+
getAccessTokenForAccount(providerId: 'claude', accountId: string): Promise<string | null>;
|
|
1117
|
+
refreshAccountToken(providerId: 'claude', accountId: string): Promise<boolean>;
|
|
1118
|
+
}
|
|
1119
|
+
type ClaudeAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
|
|
1120
|
+
interface ClaudeAllowanceCollectOptions {
|
|
1121
|
+
force?: boolean;
|
|
1122
|
+
/**
|
|
1123
|
+
* Treat an otherwise valid cache entry as due when it will expire within this
|
|
1124
|
+
* window. The resident background scheduler uses this to refresh shortly
|
|
1125
|
+
* before expiry without bypassing the normal cache on every tick.
|
|
1126
|
+
*/
|
|
1127
|
+
refreshAheadMs?: number;
|
|
1128
|
+
}
|
|
1129
|
+
declare class ClaudeAllowanceCollector {
|
|
1130
|
+
private readonly credentials;
|
|
1131
|
+
private readonly store;
|
|
1132
|
+
private readonly fetchImpl;
|
|
1133
|
+
private readonly identityStore;
|
|
1134
|
+
private readonly now;
|
|
1135
|
+
private readonly inFlight;
|
|
1136
|
+
constructor(credentials: ClaudeAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: ClaudeAllowanceFetch, identityStore?: SubscriptionIdentityStore, now?: () => number);
|
|
1137
|
+
collectMany(accounts: readonly SubscriptionAccountEntry<ClaudeTokenConfig>[], options?: ClaudeAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
|
|
1138
|
+
collect(account: SubscriptionAccountEntry<ClaudeTokenConfig>, options?: ClaudeAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
|
|
1139
|
+
private isCacheValid;
|
|
1140
|
+
private fetchAccount;
|
|
1141
|
+
private request;
|
|
1142
|
+
private failureSnapshot;
|
|
1143
|
+
private unsupportedSnapshot;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/** Secret-free account allowance query/refresh facade used by the admin API. */
|
|
1147
|
+
|
|
1148
|
+
interface AccountAllowanceCredentialReader extends ClaudeAllowanceCredentialReader {
|
|
1149
|
+
getFullConfig(): Promise<AccountTokensConfig>;
|
|
1150
|
+
}
|
|
1151
|
+
interface AccountAllowanceFilter {
|
|
1152
|
+
providerId?: SubscriptionProviderId;
|
|
1153
|
+
accountId?: string;
|
|
1154
|
+
}
|
|
1155
|
+
interface AccountAllowanceSchedulingStatus {
|
|
1156
|
+
config: AllowanceSchedulingConfig;
|
|
1157
|
+
history: AllowanceSchedulingDecision[];
|
|
1158
|
+
}
|
|
1159
|
+
declare class AccountAllowanceService {
|
|
1160
|
+
private readonly credentials;
|
|
1161
|
+
private readonly store;
|
|
1162
|
+
private readonly now;
|
|
1163
|
+
readonly claudeCollector: ClaudeAllowanceCollector;
|
|
1164
|
+
constructor(credentials: AccountAllowanceCredentialReader, store?: AccountAllowanceStore, collector?: ClaudeAllowanceCollector, now?: () => number);
|
|
1165
|
+
/**
|
|
1166
|
+
* Read all/filtered snapshots. Claude's five-minute cache is refreshed lazily;
|
|
1167
|
+
* Codex remains passive and reports not-observed until a real model response.
|
|
1168
|
+
*/
|
|
1169
|
+
list(filter?: AccountAllowanceFilter): Promise<AccountAllowanceSnapshot[]>;
|
|
1170
|
+
/** Force-refresh Claude usage for one account or every stored Claude account. */
|
|
1171
|
+
refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
1172
|
+
/**
|
|
1173
|
+
* Keep Claude snapshots warm for allowance-aware routing. This deliberately
|
|
1174
|
+
* excludes Codex (whose quota is learned from real response headers) and
|
|
1175
|
+
* preserves the collector's cache + per-account in-flight coalescing.
|
|
1176
|
+
*/
|
|
1177
|
+
maintainClaudeCache(refreshAheadMs: number): Promise<void>;
|
|
1178
|
+
/** Remove a cache row as soon as an account is deleted by the admin path. */
|
|
1179
|
+
removeAccountSnapshot(providerId: SubscriptionProviderId, accountId: string): void;
|
|
1180
|
+
/** Remove all allowance rows for a provider block that was deleted. */
|
|
1181
|
+
removeProviderSnapshots(providerId: SubscriptionProviderId): void;
|
|
1182
|
+
/** Secret-free policy diagnostics for the settings/accounts UI. */
|
|
1183
|
+
getSchedulingStatus(): AccountAllowanceSchedulingStatus;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Low-frequency, non-blocking cache maintenance for Claude allowance snapshots.
|
|
1188
|
+
*
|
|
1189
|
+
* The routing policy intentionally ignores stale quota data. Without a resident
|
|
1190
|
+
* UI poll, the five-minute Claude snapshot would therefore age out and silently
|
|
1191
|
+
* stop influencing account selection. This worker checks once a minute and asks
|
|
1192
|
+
* the existing collector to refresh entries that are close to expiry. The
|
|
1193
|
+
* collector remains the cache/coalescing authority, so a tick normally performs
|
|
1194
|
+
* no network I/O and can share an in-flight request with the admin UI.
|
|
1195
|
+
*
|
|
1196
|
+
* Zero-regression invariant: the worker does not arm a timer or perform an
|
|
1197
|
+
* initial sweep unless `server.allowanceScheduling.enabled` is true.
|
|
1198
|
+
*/
|
|
1199
|
+
|
|
1200
|
+
interface ClaudeAllowanceCacheMaintainer {
|
|
1201
|
+
maintainClaudeCache(refreshAheadMs: number): Promise<void>;
|
|
1202
|
+
}
|
|
1203
|
+
declare class ClaudeAllowanceRefreshScheduler {
|
|
1204
|
+
private readonly service;
|
|
1205
|
+
private readonly logger;
|
|
1206
|
+
private readonly intervalMs;
|
|
1207
|
+
private readonly refreshAheadMs;
|
|
1208
|
+
private timer;
|
|
1209
|
+
private started;
|
|
1210
|
+
private enabled;
|
|
1211
|
+
private sweeping;
|
|
1212
|
+
constructor(service: ClaudeAllowanceCacheMaintainer, logger: Logger, intervalMs?: number, refreshAheadMs?: number);
|
|
1213
|
+
/**
|
|
1214
|
+
* Apply live server policy. Once started, enable/disable changes arm or disarm
|
|
1215
|
+
* immediately; the initial enabled sweep is fire-and-forget.
|
|
1216
|
+
*/
|
|
1217
|
+
configure(config: AllowanceSchedulingConfig | undefined): void;
|
|
1218
|
+
/** Start the lifecycle. Disabled policy remains completely inert. */
|
|
1219
|
+
start(): void;
|
|
1220
|
+
/** Stop all future checks. Idempotent and safe during an in-flight refresh. */
|
|
1221
|
+
dispose(): void;
|
|
1222
|
+
/** One non-overlapping cache-maintenance pass. Exposed for focused tests. */
|
|
1223
|
+
sweep(): Promise<void>;
|
|
1224
|
+
private arm;
|
|
1225
|
+
private disarm;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1087
1228
|
/**
|
|
1088
1229
|
* ProbeStrategy — the per-provider two-tier probe plan
|
|
1089
1230
|
* (subscription-account-probe #8, design D1).
|
|
@@ -1163,7 +1304,14 @@ interface ProbeRecord {
|
|
|
1163
1304
|
/** Upstream round-trip latency (ms); absent for a local-tier record. */
|
|
1164
1305
|
latencyMs?: number;
|
|
1165
1306
|
/** Which tier produced this record. */
|
|
1166
|
-
tier: 'local' | 'upstream';
|
|
1307
|
+
tier: 'local' | 'upstream' | 'generation';
|
|
1308
|
+
}
|
|
1309
|
+
/** Secret-free outcome returned by the manual account connection-test route. */
|
|
1310
|
+
interface AccountConnectionProbeOutcome {
|
|
1311
|
+
ok: boolean;
|
|
1312
|
+
marked: boolean;
|
|
1313
|
+
tier: ProbeRecord['tier'];
|
|
1314
|
+
model?: string;
|
|
1167
1315
|
}
|
|
1168
1316
|
/** Per-account probe history for the authed admin surface (names account ids). */
|
|
1169
1317
|
interface AccountProbeHistorySnapshot {
|
|
@@ -1183,6 +1331,8 @@ interface AccountProbeHistoryReader {
|
|
|
1183
1331
|
interface ProbeCredentialStore {
|
|
1184
1332
|
getFullConfig(): Promise<AccountTokensConfig>;
|
|
1185
1333
|
getAccessTokenForAccount(providerId: SubscriptionProviderId, accountId: string): Promise<string | null>;
|
|
1334
|
+
/** Optional serving-parity retry seam for an upstream Codex 401. */
|
|
1335
|
+
refreshAccountToken?(providerId: SubscriptionProviderId, accountId: string): Promise<boolean>;
|
|
1186
1336
|
}
|
|
1187
1337
|
/** Proxy-aware upstream fetch signature (#3 `fetchUpstream`). */
|
|
1188
1338
|
type ProbeFetch = typeof fetchUpstream;
|
|
@@ -1232,10 +1382,13 @@ declare class AccountHealthProbeScheduler implements AccountProbeHistoryReader {
|
|
|
1232
1382
|
* no upstream); else the upstream tier when a verified endpoint exists. Records
|
|
1233
1383
|
* the rolling history entry either way; returns whether the tracker was MARKED.
|
|
1234
1384
|
*/
|
|
1235
|
-
probeAccount(providerId: SubscriptionProviderId, accountId: string): Promise<
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1385
|
+
probeAccount(providerId: SubscriptionProviderId, accountId: string): Promise<AccountConnectionProbeOutcome>;
|
|
1386
|
+
/**
|
|
1387
|
+
* Manual connection test. Codex performs a real, quota-consuming generation;
|
|
1388
|
+
* every other provider keeps its existing cheap probe. Scheduled sweeps never
|
|
1389
|
+
* call this method, so they remain non-billable.
|
|
1390
|
+
*/
|
|
1391
|
+
testAccountConnection(providerId: SubscriptionProviderId, accountId: string): Promise<AccountConnectionProbeOutcome>;
|
|
1239
1392
|
/** Per-account rolling history for the authed admin surface (design D5). */
|
|
1240
1393
|
getAllHistory(): AccountProbeHistorySnapshot[];
|
|
1241
1394
|
/**
|
|
@@ -1254,6 +1407,7 @@ declare class AccountHealthProbeScheduler implements AccountProbeHistoryReader {
|
|
|
1254
1407
|
private record;
|
|
1255
1408
|
/** Read a bounded slice of the response body for the 403-ban sniff (never throws). */
|
|
1256
1409
|
private readBounded;
|
|
1410
|
+
private runCodexGenerationAttempt;
|
|
1257
1411
|
private key;
|
|
1258
1412
|
private parseKey;
|
|
1259
1413
|
}
|
|
@@ -1280,6 +1434,18 @@ interface AuditQuery {
|
|
|
1280
1434
|
limit?: number;
|
|
1281
1435
|
}
|
|
1282
1436
|
|
|
1437
|
+
/**
|
|
1438
|
+
* Metadata-only audit aggregation. The overview needs request/error counts, not
|
|
1439
|
+
* multi-gigabyte request/response bodies. A compact per-day sidecar is updated
|
|
1440
|
+
* with each new record; legacy files are scanned once with a bounded prefix per
|
|
1441
|
+
* JSONL row and then cached in the same sidecar.
|
|
1442
|
+
*/
|
|
1443
|
+
|
|
1444
|
+
interface AuditStatsQuery {
|
|
1445
|
+
from?: number;
|
|
1446
|
+
to?: number;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1283
1449
|
/**
|
|
1284
1450
|
* auditQueryApi — the AUTHED `GET /admin/api/audit?keyId=&from=&to=&limit=`
|
|
1285
1451
|
* handler (request-audit-log, design D6).
|
|
@@ -1299,6 +1465,8 @@ interface AuditQuery {
|
|
|
1299
1465
|
|
|
1300
1466
|
/** The read surface the AdminServer consumes (bootstrap binds it to the store). */
|
|
1301
1467
|
type AuditQueryReader = (query: AuditQuery) => AuditRecord[];
|
|
1468
|
+
/** Metadata-only aggregate reader used by the overview. */
|
|
1469
|
+
type AuditStatsReader = (query: AuditStatsQuery) => Promise<AuditStats> | AuditStats;
|
|
1302
1470
|
|
|
1303
1471
|
/**
|
|
1304
1472
|
* billingStatusApi — the AUTHED `GET /admin/api/billing-status` handler
|
|
@@ -1498,6 +1666,14 @@ declare class JsonApiServerSettingsStore implements ApiServerSettingsStore {
|
|
|
1498
1666
|
declare class JsonPricingStore implements PricingStore {
|
|
1499
1667
|
private readonly pricingPath;
|
|
1500
1668
|
constructor(pricingPath: string);
|
|
1669
|
+
/**
|
|
1670
|
+
* Return whether the durable snapshot can actually serve at least one price.
|
|
1671
|
+
*
|
|
1672
|
+
* This intentionally checks the file itself instead of relying on refresh
|
|
1673
|
+
* metadata: a recent `lastSuccessAt` must not hide a deleted, truncated, or
|
|
1674
|
+
* otherwise unusable pricing table after a crash or manual file edit.
|
|
1675
|
+
*/
|
|
1676
|
+
hasUsableSnapshot(): boolean;
|
|
1501
1677
|
getAll(): Promise<PricingEntry[]>;
|
|
1502
1678
|
/**
|
|
1503
1679
|
* Insert or update one row keyed (providerId, modelId). `asUserEdit` stamps
|
|
@@ -1510,10 +1686,10 @@ declare class JsonPricingStore implements PricingStore {
|
|
|
1510
1686
|
/**
|
|
1511
1687
|
* Apply a batch fetched from a pricing source. Rows whose local copy is
|
|
1512
1688
|
* user-edited are NOT applied — they come back as `{ current, incoming }`
|
|
1513
|
-
* conflicts; everything else is upserted
|
|
1514
|
-
* for the whole batch.
|
|
1689
|
+
* conflicts; everything else is upserted with the supplied automatic source.
|
|
1690
|
+
* ONE file write for the whole batch.
|
|
1515
1691
|
*/
|
|
1516
|
-
bulkApplyFromSource(entries: PricingEntryInput[]): Promise<{
|
|
1692
|
+
bulkApplyFromSource(entries: PricingEntryInput[], source?: AutomaticPricingSource): Promise<{
|
|
1517
1693
|
applied: PricingEntry[];
|
|
1518
1694
|
conflicts: Array<{
|
|
1519
1695
|
current: PricingEntry;
|
|
@@ -1539,6 +1715,98 @@ declare class JsonPricingStore implements PricingStore {
|
|
|
1539
1715
|
/** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
|
|
1540
1716
|
private readRows;
|
|
1541
1717
|
private writeRows;
|
|
1718
|
+
/** Isolated for deterministic failure testing; never removes the target. */
|
|
1719
|
+
private replaceFile;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
type IntegrationClientId = 'codex' | 'claude';
|
|
1723
|
+
type IntegrationStatusKind = 'not-installed' | 'enabled' | 'configuration-drift' | 'configuration-missing' | 'key-missing';
|
|
1724
|
+
interface IntegrationClientStatus {
|
|
1725
|
+
client: IntegrationClientId;
|
|
1726
|
+
status: IntegrationStatusKind;
|
|
1727
|
+
configPath: string;
|
|
1728
|
+
installedAt?: number;
|
|
1729
|
+
gatewayBaseUrl?: string;
|
|
1730
|
+
message?: string;
|
|
1731
|
+
}
|
|
1732
|
+
interface IntegrationChangePlan {
|
|
1733
|
+
client: IntegrationClientId;
|
|
1734
|
+
configPath: string;
|
|
1735
|
+
action: 'install' | 'none' | 'repair';
|
|
1736
|
+
canApply: boolean;
|
|
1737
|
+
/** Redacted logical fields only; never file contents or credential values. */
|
|
1738
|
+
changes: string[];
|
|
1739
|
+
warnings: string[];
|
|
1740
|
+
}
|
|
1741
|
+
interface IntegrationInstallRecord {
|
|
1742
|
+
client: IntegrationClientId;
|
|
1743
|
+
configPath: string;
|
|
1744
|
+
originalExisted: boolean;
|
|
1745
|
+
/** Encrypted by IntegrationStateStore before it reaches disk. */
|
|
1746
|
+
originalContent: string;
|
|
1747
|
+
originalHash: string;
|
|
1748
|
+
installedHash: string;
|
|
1749
|
+
installedAt: number;
|
|
1750
|
+
gatewayBaseUrl: string;
|
|
1751
|
+
/** Codex auth.json snapshot and installed hash; absent on legacy records. */
|
|
1752
|
+
credentialFile?: IntegrationManagedFileRecord;
|
|
1753
|
+
}
|
|
1754
|
+
interface IntegrationManagedFileRecord {
|
|
1755
|
+
path: string;
|
|
1756
|
+
originalExisted: boolean;
|
|
1757
|
+
/** Encrypted by IntegrationStateStore before it reaches disk. */
|
|
1758
|
+
originalContent: string;
|
|
1759
|
+
originalHash: string;
|
|
1760
|
+
installedHash: string;
|
|
1761
|
+
}
|
|
1762
|
+
interface IntegrationGatewayKeyRecord {
|
|
1763
|
+
id: string;
|
|
1764
|
+
/** Encrypted by IntegrationStateStore before it reaches disk. */
|
|
1765
|
+
secret: string;
|
|
1766
|
+
createdAt: number;
|
|
1767
|
+
}
|
|
1768
|
+
interface IntegrationState {
|
|
1769
|
+
version: 1;
|
|
1770
|
+
gatewayKey?: IntegrationGatewayKeyRecord;
|
|
1771
|
+
clients: Partial<Record<IntegrationClientId, IntegrationInstallRecord>>;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/** Encrypted, Omnicross-owned state for reversible native CLI configuration. */
|
|
1775
|
+
declare class IntegrationStateStore {
|
|
1776
|
+
readonly path: string;
|
|
1777
|
+
private readonly box;
|
|
1778
|
+
constructor(path: string, box: SecretBox);
|
|
1779
|
+
load(): IntegrationState;
|
|
1780
|
+
save(state: IntegrationState): void;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
interface IntegrationManagerOptions {
|
|
1784
|
+
configPath: string;
|
|
1785
|
+
gatewayBaseUrl: string;
|
|
1786
|
+
keyDb: OutboundKeyDb;
|
|
1787
|
+
stateStore: IntegrationStateStore;
|
|
1788
|
+
homeDir?: string;
|
|
1789
|
+
}
|
|
1790
|
+
/** Coordinates a least-privilege gateway key with reversible native CLI config edits. */
|
|
1791
|
+
declare class IntegrationManager {
|
|
1792
|
+
private readonly options;
|
|
1793
|
+
private readonly homeDir;
|
|
1794
|
+
constructor(options: IntegrationManagerOptions);
|
|
1795
|
+
listStatus(): Promise<IntegrationClientStatus[]>;
|
|
1796
|
+
plan(client: IntegrationClientId, configPath?: string): Promise<IntegrationChangePlan>;
|
|
1797
|
+
install(client: IntegrationClientId, configPath?: string): Promise<IntegrationClientStatus>;
|
|
1798
|
+
repair(client: IntegrationClientId): Promise<IntegrationClientStatus>;
|
|
1799
|
+
remove(client: IntegrationClientId): Promise<IntegrationClientStatus>;
|
|
1800
|
+
rotateGatewayKey(): Promise<{
|
|
1801
|
+
keyId: string;
|
|
1802
|
+
}>;
|
|
1803
|
+
getGatewayToken(): Promise<string>;
|
|
1804
|
+
private ensureGatewayKey;
|
|
1805
|
+
private isKeyUsable;
|
|
1806
|
+
private statusFor;
|
|
1807
|
+
private defaultConfigPath;
|
|
1808
|
+
private codexAuthPathForConfig;
|
|
1809
|
+
private renderInstalled;
|
|
1542
1810
|
}
|
|
1543
1811
|
|
|
1544
1812
|
/**
|
|
@@ -1623,22 +1891,18 @@ interface MigrationCredentialStore extends SubscriptionAccountAppender {
|
|
|
1623
1891
|
getFullConfig(): Promise<AccountTokensConfig>;
|
|
1624
1892
|
}
|
|
1625
1893
|
|
|
1626
|
-
/**
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
* the invariant lives in one spot.
|
|
1639
|
-
*
|
|
1640
|
-
* @module @omnicross/daemon/admin/adminApi
|
|
1641
|
-
*/
|
|
1894
|
+
/** Minimal, auth-gated admin API for secret-free account allowance snapshots. */
|
|
1895
|
+
|
|
1896
|
+
interface AccountAllowanceAdminReader {
|
|
1897
|
+
list(filter?: {
|
|
1898
|
+
providerId?: SubscriptionProviderId;
|
|
1899
|
+
accountId?: string;
|
|
1900
|
+
}): Promise<AccountAllowanceSnapshot[]>;
|
|
1901
|
+
refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
1902
|
+
removeAccountSnapshot?(providerId: SubscriptionProviderId, accountId: string): void;
|
|
1903
|
+
removeProviderSnapshots?(providerId: SubscriptionProviderId): void;
|
|
1904
|
+
getSchedulingStatus?(): AccountAllowanceSchedulingStatus;
|
|
1905
|
+
}
|
|
1642
1906
|
|
|
1643
1907
|
/** Token-free subscription account list entry (passthrough from core's service). */
|
|
1644
1908
|
interface AdminAccountsLister {
|
|
@@ -1666,7 +1930,7 @@ interface AdminApiDeps {
|
|
|
1666
1930
|
/** Live provider catalog (hot-reload target). */
|
|
1667
1931
|
readonly llmConfig: ConfigFileProviderConfigSource;
|
|
1668
1932
|
/** Named outbound-key store. */
|
|
1669
|
-
readonly keyDb: OutboundKeyDb;
|
|
1933
|
+
readonly keyDb: OutboundKeyDb$1;
|
|
1670
1934
|
/**
|
|
1671
1935
|
* OPTIONAL voucher (redemption-card) store (voucher-redemption #9). When wired,
|
|
1672
1936
|
* the `/admin/api/voucher` surface can generate/list/revoke cards. Absent ⇒ the
|
|
@@ -1685,6 +1949,26 @@ interface AdminApiDeps {
|
|
|
1685
1949
|
readonly outboundApiServer: OutboundApiServer;
|
|
1686
1950
|
/** Subscription accounts (token-free `listAll`). */
|
|
1687
1951
|
readonly subscriptionAccounts: AdminAccountsLister;
|
|
1952
|
+
/**
|
|
1953
|
+
* Secret-free upstream allowance facade. Optional for lightweight embedders;
|
|
1954
|
+
* the standalone daemon wires it and the route returns 501 when absent.
|
|
1955
|
+
*/
|
|
1956
|
+
readonly accountAllowanceService?: AccountAllowanceAdminReader;
|
|
1957
|
+
/** Live Claude cache worker; hot-reconfigured with allowance scheduling. */
|
|
1958
|
+
readonly allowanceRefreshScheduler?: Pick<ClaudeAllowanceRefreshScheduler, 'configure'>;
|
|
1959
|
+
/** Optional secret-free account connection probe + rolling history surface. */
|
|
1960
|
+
readonly accountProbeService?: AccountProbeHistoryReader & {
|
|
1961
|
+
probeAccount(providerId: _omnicross_contracts_subscription_types.SubscriptionProviderId, accountId: string): Promise<{
|
|
1962
|
+
ok: boolean;
|
|
1963
|
+
marked: boolean;
|
|
1964
|
+
}>;
|
|
1965
|
+
testAccountConnection(providerId: _omnicross_contracts_subscription_types.SubscriptionProviderId, accountId: string): Promise<{
|
|
1966
|
+
ok: boolean;
|
|
1967
|
+
marked: boolean;
|
|
1968
|
+
tier: 'local' | 'upstream' | 'generation';
|
|
1969
|
+
model?: string;
|
|
1970
|
+
}>;
|
|
1971
|
+
};
|
|
1688
1972
|
/**
|
|
1689
1973
|
* Least-authority subscription-token WRITER (design D4) — ONLY the mutation
|
|
1690
1974
|
* methods (`writeProviderTokens` / `clearProvider`), never a token-returning
|
|
@@ -1706,11 +1990,13 @@ interface AdminApiDeps {
|
|
|
1706
1990
|
*/
|
|
1707
1991
|
readonly oauthSessions: OAuthSessionStore;
|
|
1708
1992
|
/**
|
|
1709
|
-
* Injected token-exchange `FetchLike` (oauth design D2-a) —
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1993
|
+
* Injected token-exchange `FetchLike` FACTORY (oauth design D2-a) — built per
|
|
1994
|
+
* provider in `bootstrap.ts` so the exchange carries a `{ providerId }` egress
|
|
1995
|
+
* ctx (per-provider proxy layer + upstream trace, bodies redacted); tests
|
|
1996
|
+
* inject a mock so no real token endpoint is hit. Mirrors how `login.ts`
|
|
1997
|
+
* injects its exchange fetch.
|
|
1712
1998
|
*/
|
|
1713
|
-
readonly oauthExchangeFetch: FetchLike;
|
|
1999
|
+
readonly oauthExchangeFetch: (providerId: SubscriptionProviderId) => FetchLike;
|
|
1714
2000
|
/**
|
|
1715
2001
|
* NARROW append handle (oauth design D2-a) — the OAuth complete handler needs
|
|
1716
2002
|
* `appendProviderAccount` (NOT on the least-authority `SubscriptionTokenWriter`).
|
|
@@ -1765,6 +2051,8 @@ interface AdminApiDeps {
|
|
|
1765
2051
|
* actually runs.
|
|
1766
2052
|
*/
|
|
1767
2053
|
readonly cliCommandRunner?: CommandRunner;
|
|
2054
|
+
/** Factory so each request observes the outbound server's current loopback port. */
|
|
2055
|
+
readonly integrationManagerFactory?: () => IntegrationManager;
|
|
1768
2056
|
}
|
|
1769
2057
|
/**
|
|
1770
2058
|
* Dispatch one `/admin/api/*` request. `path` is the already-extracted pathname
|
|
@@ -1830,6 +2118,8 @@ interface AdminServerDeps extends AdminApiDeps {
|
|
|
1830
2118
|
* unauthenticated, NEVER on `/health`.
|
|
1831
2119
|
*/
|
|
1832
2120
|
auditReader?: AuditQueryReader;
|
|
2121
|
+
/** Metadata-only audit aggregate used by the overview error-rate metric. */
|
|
2122
|
+
auditStatsReader?: AuditStatsReader;
|
|
1833
2123
|
/**
|
|
1834
2124
|
* OPTIONAL billing delivery-status reader (billing-event-stream, design D5).
|
|
1835
2125
|
* When wired (bootstrap → the ledger dir), the AUTHED `GET /admin/api/billing-status`
|
|
@@ -1872,23 +2162,18 @@ declare class AdminServer {
|
|
|
1872
2162
|
getStatus(): AdminServerStatus;
|
|
1873
2163
|
}
|
|
1874
2164
|
|
|
1875
|
-
|
|
1876
|
-
* JsonOutboundKeyDb — the daemon's file-backed `OutboundKeyDb` port impl
|
|
1877
|
-
* (design D3).
|
|
1878
|
-
*
|
|
1879
|
-
* Durable storage for named outbound API keys, backed by a json file (a sibling
|
|
1880
|
-
* of `config.json`, e.g. `keys.json`) holding an `OutboundKeyDbRow[]`. This port
|
|
1881
|
-
* provides ONLY storage — it never generates secrets nor hashes. Core's
|
|
1882
|
-
* `createNamedKey(db, name)` calls `outboundApiKeysCreate` with the sha256
|
|
1883
|
-
* `keyHash` + display `keyPrefix` and returns the one-time plaintext; the hot
|
|
1884
|
-
* auth path uses core's `hashKey(presented)` + `outboundApiKeysGetByHash`.
|
|
1885
|
-
*
|
|
1886
|
-
* @module @omnicross/daemon/ports/JsonOutboundKeyDb
|
|
1887
|
-
*/
|
|
1888
|
-
|
|
1889
|
-
declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
|
|
2165
|
+
declare class JsonOutboundKeyDb implements OutboundKeyDb {
|
|
1890
2166
|
private readonly keysPath;
|
|
1891
|
-
|
|
2167
|
+
private readonly secretBox?;
|
|
2168
|
+
/**
|
|
2169
|
+
* @param secretBox OPTIONAL reversible-secret codec. When present, a created
|
|
2170
|
+
* key's plaintext is persisted as a `keySecret` `enc:` envelope (enabling the
|
|
2171
|
+
* operator "view key" affordance via `outboundApiKeysReveal`). When absent the
|
|
2172
|
+
* store stays hash-only (byte-identical to the legacy behavior) and reveal
|
|
2173
|
+
* always returns `null`. Existing 1-arg call sites (tests, lightweight
|
|
2174
|
+
* embedders) keep working.
|
|
2175
|
+
*/
|
|
2176
|
+
constructor(keysPath: string, secretBox?: SecretBox | undefined);
|
|
1892
2177
|
outboundApiKeysList(): Promise<OutboundKeyDbRow[]>;
|
|
1893
2178
|
outboundApiKeysGetByHash(hash: string): Promise<OutboundKeyDbRow | null>;
|
|
1894
2179
|
outboundApiKeysCreate(input: {
|
|
@@ -1897,7 +2182,13 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
|
|
|
1897
2182
|
keyHash: string;
|
|
1898
2183
|
keyPrefix: string;
|
|
1899
2184
|
createdAt?: number;
|
|
2185
|
+
kind?: 'client' | 'integration';
|
|
2186
|
+
allowedEndpoints?: _omnicross_core.OutboundEndpoint[];
|
|
2187
|
+
loopbackOnly?: boolean;
|
|
2188
|
+
plaintext?: string;
|
|
1900
2189
|
}): Promise<OutboundKeyDbRow>;
|
|
2190
|
+
outboundApiKeysReveal(id: string): Promise<string | null>;
|
|
2191
|
+
outboundApiKeysDelete(id: string): Promise<boolean>;
|
|
1901
2192
|
outboundApiKeysRevoke(id: string): Promise<boolean>;
|
|
1902
2193
|
outboundApiKeysTouchLastUsed(id: string): Promise<boolean>;
|
|
1903
2194
|
outboundApiKeysSetEnabled(id: string, enabled: boolean): Promise<boolean>;
|
|
@@ -1911,6 +2202,41 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
|
|
|
1911
2202
|
private writeRows;
|
|
1912
2203
|
}
|
|
1913
2204
|
|
|
2205
|
+
interface PricingRefreshState {
|
|
2206
|
+
lastAttemptAt: number | null;
|
|
2207
|
+
lastSuccessAt: number | null;
|
|
2208
|
+
lastError: string | null;
|
|
2209
|
+
sources: PricingSourceRefreshResult[];
|
|
2210
|
+
}
|
|
2211
|
+
interface PricingRefreshSchedulerOptions {
|
|
2212
|
+
staleAfterMs?: number;
|
|
2213
|
+
intervalMs?: number;
|
|
2214
|
+
now?: () => number;
|
|
2215
|
+
}
|
|
2216
|
+
interface PricingCatalogSnapshot {
|
|
2217
|
+
hasUsableSnapshot(): boolean;
|
|
2218
|
+
}
|
|
2219
|
+
declare class PricingRefreshScheduler {
|
|
2220
|
+
private readonly engine;
|
|
2221
|
+
private readonly catalog;
|
|
2222
|
+
private readonly statePath;
|
|
2223
|
+
private readonly logger;
|
|
2224
|
+
private readonly staleAfterMs;
|
|
2225
|
+
private readonly intervalMs;
|
|
2226
|
+
private readonly now;
|
|
2227
|
+
private timer;
|
|
2228
|
+
private inFlight;
|
|
2229
|
+
constructor(engine: PricingEngine$1, catalog: PricingCatalogSnapshot, statePath: string, logger: Logger, options?: PricingRefreshSchedulerOptions);
|
|
2230
|
+
/** Fire one stale check immediately and arm an unref'ed periodic check. */
|
|
2231
|
+
start(): void;
|
|
2232
|
+
dispose(): void;
|
|
2233
|
+
getState(): PricingRefreshState;
|
|
2234
|
+
/** Public for admin/manual tests; concurrent checks share one promise. */
|
|
2235
|
+
refreshIfStale(force?: boolean): Promise<void>;
|
|
2236
|
+
private runRefresh;
|
|
2237
|
+
private writeState;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
1914
2240
|
/**
|
|
1915
2241
|
* AccountHealthSweeper — proactive account-health recovery tick
|
|
1916
2242
|
* (subscription-account-health, design D6).
|
|
@@ -2181,24 +2507,20 @@ declare class BillingRetrySweeper {
|
|
|
2181
2507
|
}
|
|
2182
2508
|
|
|
2183
2509
|
/**
|
|
2184
|
-
* TokenRefreshScheduler
|
|
2185
|
-
* (external-cli-sync).
|
|
2510
|
+
* TokenRefreshScheduler proactive background OAuth token refresh.
|
|
2186
2511
|
*
|
|
2187
|
-
* The auth strategies already refresh
|
|
2188
|
-
* request + 401 retry), but
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2191
|
-
* and refreshes any token entering the expiry lead window.
|
|
2512
|
+
* The auth strategies already refresh lazily (lead-window check before each
|
|
2513
|
+
* request + 401 retry), but an idle daemon can still reach token expiry
|
|
2514
|
+
* before its next request. This scheduler sweeps managed OAuth accounts on
|
|
2515
|
+
* an interval and refreshes tokens entering the expiry lead window.
|
|
2192
2516
|
*
|
|
2193
2517
|
* Safety properties:
|
|
2194
|
-
* - the store coalesces in-flight refreshes per account, so a sweep
|
|
2518
|
+
* - the store coalesces in-flight refreshes per account, so a sweep cannot
|
|
2195
2519
|
* double-spend a single-use refresh token against a concurrent lazy refresh;
|
|
2196
|
-
* - accounts already flagged `expired` are skipped
|
|
2197
|
-
*
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2200
|
-
* carries the external CLI import fallback); non-active accounts refresh
|
|
2201
|
-
* by id;
|
|
2520
|
+
* - accounts already flagged `expired` are skipped and remain expired until
|
|
2521
|
+
* their own managed credential is repaired or re-login occurs;
|
|
2522
|
+
* - the active account uses its managed active refresher, while non-active
|
|
2523
|
+
* accounts refresh by id; both paths use only managed stored credentials;
|
|
2202
2524
|
* - one sweep runs at a time (a long sweep never overlaps the next tick).
|
|
2203
2525
|
*
|
|
2204
2526
|
* Modeled on `ApiKeyPoolService`'s interval lifecycle: `start()` arms an
|
|
@@ -2223,8 +2545,9 @@ declare class TokenRefreshScheduler {
|
|
|
2223
2545
|
sweep(now?: number): Promise<void>;
|
|
2224
2546
|
/** Expiring within the lead window, refreshable, and not already dead. */
|
|
2225
2547
|
private needsRefresh;
|
|
2226
|
-
/** Refresh one account; failures are logged, never thrown
|
|
2227
|
-
*
|
|
2548
|
+
/** Refresh one managed account; failures are logged, never thrown. The
|
|
2549
|
+
* store marks only the targeted account `expired` on a failed refresh.
|
|
2550
|
+
*/
|
|
2228
2551
|
private refreshOne;
|
|
2229
2552
|
private refreshActive;
|
|
2230
2553
|
}
|
|
@@ -2406,10 +2729,20 @@ interface Daemon {
|
|
|
2406
2729
|
/** Subscription account service (token-free `listAll`) — now exposed for the
|
|
2407
2730
|
* admin dashboard's read-only accounts panel (RT3). */
|
|
2408
2731
|
readonly subscriptionAccounts: SubscriptionAccountService;
|
|
2732
|
+
/** Secret-free upstream subscription allowance cache/collector facade. */
|
|
2733
|
+
readonly accountAllowanceService: AccountAllowanceService;
|
|
2734
|
+
/**
|
|
2735
|
+
* Claude allowance cache maintenance for allowance-aware routing. Constructed
|
|
2736
|
+
* armed-off; the resident `start` command starts it only when the persisted
|
|
2737
|
+
* scheduling policy is enabled.
|
|
2738
|
+
*/
|
|
2739
|
+
readonly claudeAllowanceRefreshScheduler: ClaudeAllowanceRefreshScheduler;
|
|
2409
2740
|
/** File-backed pricing table (`pricing.json`; concrete for the admin DELETE). */
|
|
2410
2741
|
readonly pricingStore: JsonPricingStore;
|
|
2411
2742
|
/** Pricing engine (cost calc + source refresh + conflict resolution). */
|
|
2412
2743
|
readonly pricingEngine: PricingEngine;
|
|
2744
|
+
/** Non-blocking stale-while-revalidate catalog worker (armed by `start`). */
|
|
2745
|
+
readonly pricingRefreshScheduler: PricingRefreshScheduler;
|
|
2413
2746
|
/** Usage recorder over `usage-events.jsonl` — also the admin stats query facade. */
|
|
2414
2747
|
readonly usageRecorder: UsageRecorder;
|
|
2415
2748
|
/** The localhost admin/dashboard HTTP listener (RT3). Started by `start.ts`. */
|