@omnicross/daemon 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2425 -660
- package/dist/cli.js +2416 -618
- package/dist/index.cjs +2286 -622
- package/dist/index.d.cts +452 -189
- package/dist/index.d.ts +452 -189
- package/dist/index.js +2273 -576
- package/package.json +1 -1
package/dist/index.d.ts
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
18
|
import { AuditRecord, 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,21 @@ 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.
|
|
585
582
|
*/
|
|
586
583
|
buildRefreshFetch(providerId: string, accountId?: string): FetchLike;
|
|
587
584
|
/**
|
|
588
|
-
* In-flight refresh coalescing
|
|
585
|
+
* In-flight refresh coalescing. OAuth refresh tokens are
|
|
589
586
|
* SINGLE-USE: two concurrent refreshes of one account each spend the same
|
|
590
587
|
* token and the loser bricks a healthy account. Every refresh entry point
|
|
591
588
|
* (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
|
|
@@ -594,11 +591,11 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
594
591
|
private readonly inFlightRefreshes;
|
|
595
592
|
private coalesce;
|
|
596
593
|
/** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
|
|
597
|
-
* file is absent/corrupt). This is the hot read
|
|
594
|
+
* file is absent/corrupt). This is the hot read the codex / gemini auth
|
|
598
595
|
* strategies pull `accessToken` / `expiresAt` / `status` from it. */
|
|
599
596
|
getFullConfig(): Promise<AccountTokensConfig>;
|
|
600
597
|
/** Current Claude OAuth access token, or `null` when none is stored. No inline
|
|
601
|
-
* refresh here
|
|
598
|
+
* refresh here the lead-window / 401-retry refresh is driven by the
|
|
602
599
|
* subscription auth strategy, which calls `refreshClaudeToken` (now real). */
|
|
603
600
|
getValidClaudeAccessToken(): Promise<string | null>;
|
|
604
601
|
/** Current OpenCodeGo static API key, or `null` when none is stored. */
|
|
@@ -614,28 +611,25 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
614
611
|
/**
|
|
615
612
|
* DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
|
|
616
613
|
* each provider's accounts to the secret-free `SubscriptionAccountSanitized`
|
|
617
|
-
* shape (id/label/status/expiresAt/hasAccessToken/isActive)
|
|
614
|
+
* shape (id/label/status/expiresAt/hasAccessToken/isActive) NEVER a token.
|
|
618
615
|
* Used by the admin accounts GET (secret-IN-never-OUT).
|
|
619
616
|
*/
|
|
620
617
|
listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
|
|
621
618
|
/**
|
|
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.
|
|
619
|
+
* List-time managed-credential conflict warnings. Computed, not persisted:
|
|
620
|
+
* `duplicate-token` is projected when two accounts of one provider share a
|
|
621
|
+
* credential. This deliberately does not inspect either native CLI file.
|
|
628
622
|
*/
|
|
629
|
-
private
|
|
623
|
+
private attachDuplicateWarnings;
|
|
630
624
|
/** Read the external CLI store, never letting an fs/parse error escape. */
|
|
631
625
|
private safeReadExternal;
|
|
632
626
|
/**
|
|
633
627
|
* Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
|
|
634
|
-
* the block has no refresh_token (setup-token / manual)
|
|
628
|
+
* the block has no refresh_token (setup-token / manual) no upstream call, the
|
|
635
629
|
* block is untouched. Otherwise mint via the shared claude refresh flow and
|
|
636
630
|
* write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
|
|
637
|
-
* On failure
|
|
638
|
-
* errorMessage
|
|
631
|
+
* On failure status:expired +
|
|
632
|
+
* errorMessage `false`.
|
|
639
633
|
*/
|
|
640
634
|
refreshClaudeToken(): Promise<boolean>;
|
|
641
635
|
/**
|
|
@@ -653,11 +647,10 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
653
647
|
*/
|
|
654
648
|
refreshGeminiToken(): Promise<boolean>;
|
|
655
649
|
/**
|
|
656
|
-
* Refresh a SPECIFIC account by id (background scheduler sweep
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
* failure flags ONLY that account `expired`.
|
|
650
|
+
* Refresh a SPECIFIC managed account by id (background scheduler sweep and
|
|
651
|
+
* account-pool resolution). It uses only that account's stored refresh
|
|
652
|
+
* token. Coalesced per `provider:id`; on failure flags ONLY that account
|
|
653
|
+
* `expired`.
|
|
661
654
|
*/
|
|
662
655
|
refreshAccountById(provider: 'claude' | 'codex' | 'gemini', id: string): Promise<boolean>;
|
|
663
656
|
/**
|
|
@@ -672,7 +665,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
672
665
|
/**
|
|
673
666
|
* Refresh a SPECIFIC account's OAuth token by id (design D6/D7). Delegates to
|
|
674
667
|
* `refreshAccountById` (coalesced per `provider:id`); opencodego is a static key
|
|
675
|
-
*
|
|
668
|
+
* `false` (no refresh affordance).
|
|
676
669
|
*/
|
|
677
670
|
refreshAccountToken(providerId: SubscriptionProviderId, accountId: string): Promise<boolean>;
|
|
678
671
|
/**
|
|
@@ -686,7 +679,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
686
679
|
* (subscription-client-fingerprint #7, P2). Entry-metadata only (NON-secret
|
|
687
680
|
* whitelisted fingerprint headers; the token mirror is untouched); a no-op for
|
|
688
681
|
* 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
|
|
682
|
+
* freeze / TTL refresh, so it stays infrequent. Never throws to the caller the
|
|
690
683
|
* store's port wrapper swallows a rejection so the relay hot path is unaffected.
|
|
691
684
|
*/
|
|
692
685
|
setAccountIdentity(providerId: SubscriptionProviderId, accountId: string, identity: AccountClientIdentity): Promise<void>;
|
|
@@ -702,7 +695,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
702
695
|
* DAEMON-ONLY set/clear per-account proxy (upstream-proxy, admin write, NOT on
|
|
703
696
|
* the port). Passing `undefined` clears the override. Write-only password: when
|
|
704
697
|
* the incoming structured proxy omits the password but the account already had
|
|
705
|
-
* one, the current (decrypted) password is preserved
|
|
698
|
+
* one, the current (decrypted) password is preserved editing host/port never
|
|
706
699
|
* wipes the secret. Persist re-encrypts `proxy.password` via the tokens SecretBox.
|
|
707
700
|
*/
|
|
708
701
|
setAccountProxy(providerId: SubscriptionProviderId, accountId: string, proxy: ProxyConfig | undefined): Promise<{
|
|
@@ -719,44 +712,36 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
719
712
|
}>;
|
|
720
713
|
/** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
|
|
721
714
|
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;
|
|
715
|
+
/** Atomically patch one account's non-secret management metadata. */
|
|
716
|
+
patchAccountMetadata(providerId: SubscriptionProviderId, accountId: string, patch: AccountMetadataPatch): Promise<{
|
|
717
|
+
ok: boolean;
|
|
718
|
+
}>;
|
|
719
|
+
/** Validate every target, then persist one all-or-nothing batch mutation. */
|
|
720
|
+
batchManageAccounts(refs: AccountRef[], mutation: AccountBatchMutation): Promise<{
|
|
721
|
+
ok: true;
|
|
722
|
+
affected: number;
|
|
723
|
+
} | {
|
|
724
|
+
ok: false;
|
|
725
|
+
missing: AccountRef;
|
|
726
|
+
}>;
|
|
744
727
|
/**
|
|
745
728
|
* DAEMON-ONLY (admin import button): which providers have a usable external
|
|
746
|
-
* CLI credential on THIS machine. Pure detection
|
|
729
|
+
* CLI credential on THIS machine. Pure detection reads the native files,
|
|
747
730
|
* never mutates anything, never returns a token.
|
|
748
731
|
*/
|
|
749
732
|
listExternalCliAvailability(): Promise<Record<ExternalCliProvider, boolean>>;
|
|
750
733
|
/**
|
|
751
734
|
* DAEMON-ONLY (admin import button): import the external CLI's current login
|
|
752
|
-
* as a NEW account (+ activate)
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
735
|
+
* as a NEW account (+ activate). This is a COPY-ONLY import: Omnicross never
|
|
736
|
+
* claims, writes, moves, restores, or deletes the native CLI credential file
|
|
737
|
+
* or any legacy `.omnicross-managed` marker/backup beside it. Subsequent
|
|
738
|
+
* refreshes persist only Omnicross's encrypted token store.
|
|
756
739
|
*/
|
|
757
740
|
importExternalCliAccount(provider: ExternalCliProvider, label?: string): Promise<{
|
|
758
741
|
ok: true;
|
|
759
742
|
id: string;
|
|
743
|
+
nativeCredentialMode: 'read-only';
|
|
744
|
+
refreshWritesNativeCredentials: false;
|
|
760
745
|
} | {
|
|
761
746
|
ok: false;
|
|
762
747
|
reason: 'no-credential';
|
|
@@ -788,13 +773,13 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
788
773
|
* `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
|
|
789
774
|
* OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
|
|
790
775
|
* tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
|
|
791
|
-
* so a first-ever write still produces a valid config. No cache
|
|
776
|
+
* so a first-ever write still produces a valid config. No cache the next read
|
|
792
777
|
* sees this write.
|
|
793
778
|
*/
|
|
794
779
|
writeProviderTokens(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock): Promise<void>;
|
|
795
780
|
/**
|
|
796
781
|
* 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
|
|
782
|
+
* (optional label) and set it active, then re-derive the mirror used by
|
|
798
783
|
* `omnicross login <provider> --label` to add an account instead of overwriting.
|
|
799
784
|
*/
|
|
800
785
|
appendProviderAccount(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock, label?: string): Promise<{
|
|
@@ -817,7 +802,7 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
817
802
|
}>;
|
|
818
803
|
/**
|
|
819
804
|
* DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
|
|
820
|
-
* rejects an unknown id. Label-only
|
|
805
|
+
* rejects an unknown id. Label-only no token material is read or written
|
|
821
806
|
* (the secret-free invariant holds).
|
|
822
807
|
*/
|
|
823
808
|
renameAccount(providerId: SubscriptionProviderId, id: string, label: string): Promise<{
|
|
@@ -832,8 +817,8 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
832
817
|
clearProvider(providerId: SubscriptionProviderId): Promise<void>;
|
|
833
818
|
/** Write the merged config to disk as pretty JSON (mkdir parent if needed).
|
|
834
819
|
* Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
|
|
835
|
-
*
|
|
836
|
-
* write
|
|
820
|
+
* `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
821
|
+
* write incl. child 4's future refresh writes lands encrypted. */
|
|
837
822
|
private persist;
|
|
838
823
|
/**
|
|
839
824
|
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
@@ -841,11 +826,11 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
841
826
|
* subscription bearer path is byte-identical).
|
|
842
827
|
*
|
|
843
828
|
* The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
|
|
844
|
-
* file
|
|
829
|
+
* file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
|
|
845
830
|
* 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
|
|
831
|
+
* box's clear, secret-free error (secrets spec "/ UX":
|
|
832
|
+
* SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
|
|
833
|
+
* tokens" and silently send the WRONG bearer upstream 401). Mirrors
|
|
849
834
|
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
850
835
|
*/
|
|
851
836
|
private readConfig;
|
|
@@ -891,6 +876,18 @@ interface SubscriptionTokenWriter {
|
|
|
891
876
|
setAccountPriority(providerId: SubscriptionProviderId, id: string, priority: number): Promise<{
|
|
892
877
|
ok: boolean;
|
|
893
878
|
}>;
|
|
879
|
+
/** Patch non-secret account management metadata in one write. */
|
|
880
|
+
patchAccountMetadata(providerId: SubscriptionProviderId, id: string, patch: AccountMetadataPatch): Promise<{
|
|
881
|
+
ok: boolean;
|
|
882
|
+
}>;
|
|
883
|
+
/** All-or-nothing multi-provider account management mutation. */
|
|
884
|
+
batchManageAccounts(refs: AccountRef[], mutation: AccountBatchMutation): Promise<{
|
|
885
|
+
ok: true;
|
|
886
|
+
affected: number;
|
|
887
|
+
} | {
|
|
888
|
+
ok: false;
|
|
889
|
+
missing: AccountRef;
|
|
890
|
+
}>;
|
|
894
891
|
/** Set (or CLEAR, with `undefined`) one account's per-account proxy override
|
|
895
892
|
* (upstream-proxy). The `proxy.password` is a secret (encrypted at rest, masked
|
|
896
893
|
* in the sanitized view). Rejects an unknown id. */
|
|
@@ -912,6 +909,8 @@ interface SubscriptionTokenWriter {
|
|
|
912
909
|
importExternalCliAccount(providerId: 'claude' | 'codex', label?: string): Promise<{
|
|
913
910
|
ok: true;
|
|
914
911
|
id: string;
|
|
912
|
+
nativeCredentialMode: 'read-only';
|
|
913
|
+
refreshWritesNativeCredentials: false;
|
|
915
914
|
} | {
|
|
916
915
|
ok: false;
|
|
917
916
|
reason: 'no-credential';
|
|
@@ -1084,6 +1083,127 @@ declare class CodexOAuthSessionStore {
|
|
|
1084
1083
|
private sweep;
|
|
1085
1084
|
}
|
|
1086
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* Claude OAuth usage collector.
|
|
1088
|
+
*
|
|
1089
|
+
* Fetches one account's five-hour, seven-day, and seven-day Sonnet windows,
|
|
1090
|
+
* coalesces concurrent refreshes by account, and caches every result for five
|
|
1091
|
+
* minutes. Tokens and raw upstream payloads never leave this module.
|
|
1092
|
+
*/
|
|
1093
|
+
|
|
1094
|
+
interface ClaudeAllowanceCredentialReader {
|
|
1095
|
+
getAccessTokenForAccount(providerId: 'claude', accountId: string): Promise<string | null>;
|
|
1096
|
+
refreshAccountToken(providerId: 'claude', accountId: string): Promise<boolean>;
|
|
1097
|
+
}
|
|
1098
|
+
type ClaudeAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
|
|
1099
|
+
interface ClaudeAllowanceCollectOptions {
|
|
1100
|
+
force?: boolean;
|
|
1101
|
+
/**
|
|
1102
|
+
* Treat an otherwise valid cache entry as due when it will expire within this
|
|
1103
|
+
* window. The resident background scheduler uses this to refresh shortly
|
|
1104
|
+
* before expiry without bypassing the normal cache on every tick.
|
|
1105
|
+
*/
|
|
1106
|
+
refreshAheadMs?: number;
|
|
1107
|
+
}
|
|
1108
|
+
declare class ClaudeAllowanceCollector {
|
|
1109
|
+
private readonly credentials;
|
|
1110
|
+
private readonly store;
|
|
1111
|
+
private readonly fetchImpl;
|
|
1112
|
+
private readonly identityStore;
|
|
1113
|
+
private readonly now;
|
|
1114
|
+
private readonly inFlight;
|
|
1115
|
+
constructor(credentials: ClaudeAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: ClaudeAllowanceFetch, identityStore?: SubscriptionIdentityStore, now?: () => number);
|
|
1116
|
+
collectMany(accounts: readonly SubscriptionAccountEntry<ClaudeTokenConfig>[], options?: ClaudeAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
|
|
1117
|
+
collect(account: SubscriptionAccountEntry<ClaudeTokenConfig>, options?: ClaudeAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
|
|
1118
|
+
private isCacheValid;
|
|
1119
|
+
private fetchAccount;
|
|
1120
|
+
private request;
|
|
1121
|
+
private failureSnapshot;
|
|
1122
|
+
private unsupportedSnapshot;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** Secret-free account allowance query/refresh facade used by the admin API. */
|
|
1126
|
+
|
|
1127
|
+
interface AccountAllowanceCredentialReader extends ClaudeAllowanceCredentialReader {
|
|
1128
|
+
getFullConfig(): Promise<AccountTokensConfig>;
|
|
1129
|
+
}
|
|
1130
|
+
interface AccountAllowanceFilter {
|
|
1131
|
+
providerId?: SubscriptionProviderId;
|
|
1132
|
+
accountId?: string;
|
|
1133
|
+
}
|
|
1134
|
+
interface AccountAllowanceSchedulingStatus {
|
|
1135
|
+
config: AllowanceSchedulingConfig;
|
|
1136
|
+
history: AllowanceSchedulingDecision[];
|
|
1137
|
+
}
|
|
1138
|
+
declare class AccountAllowanceService {
|
|
1139
|
+
private readonly credentials;
|
|
1140
|
+
private readonly store;
|
|
1141
|
+
private readonly now;
|
|
1142
|
+
readonly claudeCollector: ClaudeAllowanceCollector;
|
|
1143
|
+
constructor(credentials: AccountAllowanceCredentialReader, store?: AccountAllowanceStore, collector?: ClaudeAllowanceCollector, now?: () => number);
|
|
1144
|
+
/**
|
|
1145
|
+
* Read all/filtered snapshots. Claude's five-minute cache is refreshed lazily;
|
|
1146
|
+
* Codex remains passive and reports not-observed until a real model response.
|
|
1147
|
+
*/
|
|
1148
|
+
list(filter?: AccountAllowanceFilter): Promise<AccountAllowanceSnapshot[]>;
|
|
1149
|
+
/** Force-refresh Claude usage for one account or every stored Claude account. */
|
|
1150
|
+
refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
1151
|
+
/**
|
|
1152
|
+
* Keep Claude snapshots warm for allowance-aware routing. This deliberately
|
|
1153
|
+
* excludes Codex (whose quota is learned from real response headers) and
|
|
1154
|
+
* preserves the collector's cache + per-account in-flight coalescing.
|
|
1155
|
+
*/
|
|
1156
|
+
maintainClaudeCache(refreshAheadMs: number): Promise<void>;
|
|
1157
|
+
/** Remove a cache row as soon as an account is deleted by the admin path. */
|
|
1158
|
+
removeAccountSnapshot(providerId: SubscriptionProviderId, accountId: string): void;
|
|
1159
|
+
/** Remove all allowance rows for a provider block that was deleted. */
|
|
1160
|
+
removeProviderSnapshots(providerId: SubscriptionProviderId): void;
|
|
1161
|
+
/** Secret-free policy diagnostics for the settings/accounts UI. */
|
|
1162
|
+
getSchedulingStatus(): AccountAllowanceSchedulingStatus;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Low-frequency, non-blocking cache maintenance for Claude allowance snapshots.
|
|
1167
|
+
*
|
|
1168
|
+
* The routing policy intentionally ignores stale quota data. Without a resident
|
|
1169
|
+
* UI poll, the five-minute Claude snapshot would therefore age out and silently
|
|
1170
|
+
* stop influencing account selection. This worker checks once a minute and asks
|
|
1171
|
+
* the existing collector to refresh entries that are close to expiry. The
|
|
1172
|
+
* collector remains the cache/coalescing authority, so a tick normally performs
|
|
1173
|
+
* no network I/O and can share an in-flight request with the admin UI.
|
|
1174
|
+
*
|
|
1175
|
+
* Zero-regression invariant: the worker does not arm a timer or perform an
|
|
1176
|
+
* initial sweep unless `server.allowanceScheduling.enabled` is true.
|
|
1177
|
+
*/
|
|
1178
|
+
|
|
1179
|
+
interface ClaudeAllowanceCacheMaintainer {
|
|
1180
|
+
maintainClaudeCache(refreshAheadMs: number): Promise<void>;
|
|
1181
|
+
}
|
|
1182
|
+
declare class ClaudeAllowanceRefreshScheduler {
|
|
1183
|
+
private readonly service;
|
|
1184
|
+
private readonly logger;
|
|
1185
|
+
private readonly intervalMs;
|
|
1186
|
+
private readonly refreshAheadMs;
|
|
1187
|
+
private timer;
|
|
1188
|
+
private started;
|
|
1189
|
+
private enabled;
|
|
1190
|
+
private sweeping;
|
|
1191
|
+
constructor(service: ClaudeAllowanceCacheMaintainer, logger: Logger, intervalMs?: number, refreshAheadMs?: number);
|
|
1192
|
+
/**
|
|
1193
|
+
* Apply live server policy. Once started, enable/disable changes arm or disarm
|
|
1194
|
+
* immediately; the initial enabled sweep is fire-and-forget.
|
|
1195
|
+
*/
|
|
1196
|
+
configure(config: AllowanceSchedulingConfig | undefined): void;
|
|
1197
|
+
/** Start the lifecycle. Disabled policy remains completely inert. */
|
|
1198
|
+
start(): void;
|
|
1199
|
+
/** Stop all future checks. Idempotent and safe during an in-flight refresh. */
|
|
1200
|
+
dispose(): void;
|
|
1201
|
+
/** One non-overlapping cache-maintenance pass. Exposed for focused tests. */
|
|
1202
|
+
sweep(): Promise<void>;
|
|
1203
|
+
private arm;
|
|
1204
|
+
private disarm;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1087
1207
|
/**
|
|
1088
1208
|
* ProbeStrategy — the per-provider two-tier probe plan
|
|
1089
1209
|
* (subscription-account-probe #8, design D1).
|
|
@@ -1498,6 +1618,14 @@ declare class JsonApiServerSettingsStore implements ApiServerSettingsStore {
|
|
|
1498
1618
|
declare class JsonPricingStore implements PricingStore {
|
|
1499
1619
|
private readonly pricingPath;
|
|
1500
1620
|
constructor(pricingPath: string);
|
|
1621
|
+
/**
|
|
1622
|
+
* Return whether the durable snapshot can actually serve at least one price.
|
|
1623
|
+
*
|
|
1624
|
+
* This intentionally checks the file itself instead of relying on refresh
|
|
1625
|
+
* metadata: a recent `lastSuccessAt` must not hide a deleted, truncated, or
|
|
1626
|
+
* otherwise unusable pricing table after a crash or manual file edit.
|
|
1627
|
+
*/
|
|
1628
|
+
hasUsableSnapshot(): boolean;
|
|
1501
1629
|
getAll(): Promise<PricingEntry[]>;
|
|
1502
1630
|
/**
|
|
1503
1631
|
* Insert or update one row keyed (providerId, modelId). `asUserEdit` stamps
|
|
@@ -1510,10 +1638,10 @@ declare class JsonPricingStore implements PricingStore {
|
|
|
1510
1638
|
/**
|
|
1511
1639
|
* Apply a batch fetched from a pricing source. Rows whose local copy is
|
|
1512
1640
|
* user-edited are NOT applied — they come back as `{ current, incoming }`
|
|
1513
|
-
* conflicts; everything else is upserted
|
|
1514
|
-
* for the whole batch.
|
|
1641
|
+
* conflicts; everything else is upserted with the supplied automatic source.
|
|
1642
|
+
* ONE file write for the whole batch.
|
|
1515
1643
|
*/
|
|
1516
|
-
bulkApplyFromSource(entries: PricingEntryInput[]): Promise<{
|
|
1644
|
+
bulkApplyFromSource(entries: PricingEntryInput[], source?: AutomaticPricingSource): Promise<{
|
|
1517
1645
|
applied: PricingEntry[];
|
|
1518
1646
|
conflicts: Array<{
|
|
1519
1647
|
current: PricingEntry;
|
|
@@ -1539,6 +1667,98 @@ declare class JsonPricingStore implements PricingStore {
|
|
|
1539
1667
|
/** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
|
|
1540
1668
|
private readRows;
|
|
1541
1669
|
private writeRows;
|
|
1670
|
+
/** Isolated for deterministic failure testing; never removes the target. */
|
|
1671
|
+
private replaceFile;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
type IntegrationClientId = 'codex' | 'claude';
|
|
1675
|
+
type IntegrationStatusKind = 'not-installed' | 'enabled' | 'configuration-drift' | 'configuration-missing' | 'key-missing';
|
|
1676
|
+
interface IntegrationClientStatus {
|
|
1677
|
+
client: IntegrationClientId;
|
|
1678
|
+
status: IntegrationStatusKind;
|
|
1679
|
+
configPath: string;
|
|
1680
|
+
installedAt?: number;
|
|
1681
|
+
gatewayBaseUrl?: string;
|
|
1682
|
+
message?: string;
|
|
1683
|
+
}
|
|
1684
|
+
interface IntegrationChangePlan {
|
|
1685
|
+
client: IntegrationClientId;
|
|
1686
|
+
configPath: string;
|
|
1687
|
+
action: 'install' | 'none' | 'repair';
|
|
1688
|
+
canApply: boolean;
|
|
1689
|
+
/** Redacted logical fields only; never file contents or credential values. */
|
|
1690
|
+
changes: string[];
|
|
1691
|
+
warnings: string[];
|
|
1692
|
+
}
|
|
1693
|
+
interface IntegrationInstallRecord {
|
|
1694
|
+
client: IntegrationClientId;
|
|
1695
|
+
configPath: string;
|
|
1696
|
+
originalExisted: boolean;
|
|
1697
|
+
/** Encrypted by IntegrationStateStore before it reaches disk. */
|
|
1698
|
+
originalContent: string;
|
|
1699
|
+
originalHash: string;
|
|
1700
|
+
installedHash: string;
|
|
1701
|
+
installedAt: number;
|
|
1702
|
+
gatewayBaseUrl: string;
|
|
1703
|
+
/** Codex auth.json snapshot and installed hash; absent on legacy records. */
|
|
1704
|
+
credentialFile?: IntegrationManagedFileRecord;
|
|
1705
|
+
}
|
|
1706
|
+
interface IntegrationManagedFileRecord {
|
|
1707
|
+
path: string;
|
|
1708
|
+
originalExisted: boolean;
|
|
1709
|
+
/** Encrypted by IntegrationStateStore before it reaches disk. */
|
|
1710
|
+
originalContent: string;
|
|
1711
|
+
originalHash: string;
|
|
1712
|
+
installedHash: string;
|
|
1713
|
+
}
|
|
1714
|
+
interface IntegrationGatewayKeyRecord {
|
|
1715
|
+
id: string;
|
|
1716
|
+
/** Encrypted by IntegrationStateStore before it reaches disk. */
|
|
1717
|
+
secret: string;
|
|
1718
|
+
createdAt: number;
|
|
1719
|
+
}
|
|
1720
|
+
interface IntegrationState {
|
|
1721
|
+
version: 1;
|
|
1722
|
+
gatewayKey?: IntegrationGatewayKeyRecord;
|
|
1723
|
+
clients: Partial<Record<IntegrationClientId, IntegrationInstallRecord>>;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/** Encrypted, Omnicross-owned state for reversible native CLI configuration. */
|
|
1727
|
+
declare class IntegrationStateStore {
|
|
1728
|
+
readonly path: string;
|
|
1729
|
+
private readonly box;
|
|
1730
|
+
constructor(path: string, box: SecretBox);
|
|
1731
|
+
load(): IntegrationState;
|
|
1732
|
+
save(state: IntegrationState): void;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
interface IntegrationManagerOptions {
|
|
1736
|
+
configPath: string;
|
|
1737
|
+
gatewayBaseUrl: string;
|
|
1738
|
+
keyDb: OutboundKeyDb;
|
|
1739
|
+
stateStore: IntegrationStateStore;
|
|
1740
|
+
homeDir?: string;
|
|
1741
|
+
}
|
|
1742
|
+
/** Coordinates a least-privilege gateway key with reversible native CLI config edits. */
|
|
1743
|
+
declare class IntegrationManager {
|
|
1744
|
+
private readonly options;
|
|
1745
|
+
private readonly homeDir;
|
|
1746
|
+
constructor(options: IntegrationManagerOptions);
|
|
1747
|
+
listStatus(): Promise<IntegrationClientStatus[]>;
|
|
1748
|
+
plan(client: IntegrationClientId, configPath?: string): Promise<IntegrationChangePlan>;
|
|
1749
|
+
install(client: IntegrationClientId, configPath?: string): Promise<IntegrationClientStatus>;
|
|
1750
|
+
repair(client: IntegrationClientId): Promise<IntegrationClientStatus>;
|
|
1751
|
+
remove(client: IntegrationClientId): Promise<IntegrationClientStatus>;
|
|
1752
|
+
rotateGatewayKey(): Promise<{
|
|
1753
|
+
keyId: string;
|
|
1754
|
+
}>;
|
|
1755
|
+
getGatewayToken(): Promise<string>;
|
|
1756
|
+
private ensureGatewayKey;
|
|
1757
|
+
private isKeyUsable;
|
|
1758
|
+
private statusFor;
|
|
1759
|
+
private defaultConfigPath;
|
|
1760
|
+
private codexAuthPathForConfig;
|
|
1761
|
+
private renderInstalled;
|
|
1542
1762
|
}
|
|
1543
1763
|
|
|
1544
1764
|
/**
|
|
@@ -1623,22 +1843,18 @@ interface MigrationCredentialStore extends SubscriptionAccountAppender {
|
|
|
1623
1843
|
getFullConfig(): Promise<AccountTokensConfig>;
|
|
1624
1844
|
}
|
|
1625
1845
|
|
|
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
|
-
*/
|
|
1846
|
+
/** Minimal, auth-gated admin API for secret-free account allowance snapshots. */
|
|
1847
|
+
|
|
1848
|
+
interface AccountAllowanceAdminReader {
|
|
1849
|
+
list(filter?: {
|
|
1850
|
+
providerId?: SubscriptionProviderId;
|
|
1851
|
+
accountId?: string;
|
|
1852
|
+
}): Promise<AccountAllowanceSnapshot[]>;
|
|
1853
|
+
refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
1854
|
+
removeAccountSnapshot?(providerId: SubscriptionProviderId, accountId: string): void;
|
|
1855
|
+
removeProviderSnapshots?(providerId: SubscriptionProviderId): void;
|
|
1856
|
+
getSchedulingStatus?(): AccountAllowanceSchedulingStatus;
|
|
1857
|
+
}
|
|
1642
1858
|
|
|
1643
1859
|
/** Token-free subscription account list entry (passthrough from core's service). */
|
|
1644
1860
|
interface AdminAccountsLister {
|
|
@@ -1666,7 +1882,7 @@ interface AdminApiDeps {
|
|
|
1666
1882
|
/** Live provider catalog (hot-reload target). */
|
|
1667
1883
|
readonly llmConfig: ConfigFileProviderConfigSource;
|
|
1668
1884
|
/** Named outbound-key store. */
|
|
1669
|
-
readonly keyDb: OutboundKeyDb;
|
|
1885
|
+
readonly keyDb: OutboundKeyDb$1;
|
|
1670
1886
|
/**
|
|
1671
1887
|
* OPTIONAL voucher (redemption-card) store (voucher-redemption #9). When wired,
|
|
1672
1888
|
* the `/admin/api/voucher` surface can generate/list/revoke cards. Absent ⇒ the
|
|
@@ -1685,6 +1901,20 @@ interface AdminApiDeps {
|
|
|
1685
1901
|
readonly outboundApiServer: OutboundApiServer;
|
|
1686
1902
|
/** Subscription accounts (token-free `listAll`). */
|
|
1687
1903
|
readonly subscriptionAccounts: AdminAccountsLister;
|
|
1904
|
+
/**
|
|
1905
|
+
* Secret-free upstream allowance facade. Optional for lightweight embedders;
|
|
1906
|
+
* the standalone daemon wires it and the route returns 501 when absent.
|
|
1907
|
+
*/
|
|
1908
|
+
readonly accountAllowanceService?: AccountAllowanceAdminReader;
|
|
1909
|
+
/** Live Claude cache worker; hot-reconfigured with allowance scheduling. */
|
|
1910
|
+
readonly allowanceRefreshScheduler?: Pick<ClaudeAllowanceRefreshScheduler, 'configure'>;
|
|
1911
|
+
/** Optional secret-free account connection probe + rolling history surface. */
|
|
1912
|
+
readonly accountProbeService?: AccountProbeHistoryReader & {
|
|
1913
|
+
probeAccount(providerId: _omnicross_contracts_subscription_types.SubscriptionProviderId, accountId: string): Promise<{
|
|
1914
|
+
ok: boolean;
|
|
1915
|
+
marked: boolean;
|
|
1916
|
+
}>;
|
|
1917
|
+
};
|
|
1688
1918
|
/**
|
|
1689
1919
|
* Least-authority subscription-token WRITER (design D4) — ONLY the mutation
|
|
1690
1920
|
* methods (`writeProviderTokens` / `clearProvider`), never a token-returning
|
|
@@ -1765,6 +1995,8 @@ interface AdminApiDeps {
|
|
|
1765
1995
|
* actually runs.
|
|
1766
1996
|
*/
|
|
1767
1997
|
readonly cliCommandRunner?: CommandRunner;
|
|
1998
|
+
/** Factory so each request observes the outbound server's current loopback port. */
|
|
1999
|
+
readonly integrationManagerFactory?: () => IntegrationManager;
|
|
1768
2000
|
}
|
|
1769
2001
|
/**
|
|
1770
2002
|
* Dispatch one `/admin/api/*` request. `path` is the already-extracted pathname
|
|
@@ -1872,21 +2104,7 @@ declare class AdminServer {
|
|
|
1872
2104
|
getStatus(): AdminServerStatus;
|
|
1873
2105
|
}
|
|
1874
2106
|
|
|
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 {
|
|
2107
|
+
declare class JsonOutboundKeyDb implements OutboundKeyDb {
|
|
1890
2108
|
private readonly keysPath;
|
|
1891
2109
|
constructor(keysPath: string);
|
|
1892
2110
|
outboundApiKeysList(): Promise<OutboundKeyDbRow[]>;
|
|
@@ -1897,6 +2115,9 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
|
|
|
1897
2115
|
keyHash: string;
|
|
1898
2116
|
keyPrefix: string;
|
|
1899
2117
|
createdAt?: number;
|
|
2118
|
+
kind?: 'client' | 'integration';
|
|
2119
|
+
allowedEndpoints?: _omnicross_core.OutboundEndpoint[];
|
|
2120
|
+
loopbackOnly?: boolean;
|
|
1900
2121
|
}): Promise<OutboundKeyDbRow>;
|
|
1901
2122
|
outboundApiKeysRevoke(id: string): Promise<boolean>;
|
|
1902
2123
|
outboundApiKeysTouchLastUsed(id: string): Promise<boolean>;
|
|
@@ -1911,6 +2132,41 @@ declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
|
|
|
1911
2132
|
private writeRows;
|
|
1912
2133
|
}
|
|
1913
2134
|
|
|
2135
|
+
interface PricingRefreshState {
|
|
2136
|
+
lastAttemptAt: number | null;
|
|
2137
|
+
lastSuccessAt: number | null;
|
|
2138
|
+
lastError: string | null;
|
|
2139
|
+
sources: PricingSourceRefreshResult[];
|
|
2140
|
+
}
|
|
2141
|
+
interface PricingRefreshSchedulerOptions {
|
|
2142
|
+
staleAfterMs?: number;
|
|
2143
|
+
intervalMs?: number;
|
|
2144
|
+
now?: () => number;
|
|
2145
|
+
}
|
|
2146
|
+
interface PricingCatalogSnapshot {
|
|
2147
|
+
hasUsableSnapshot(): boolean;
|
|
2148
|
+
}
|
|
2149
|
+
declare class PricingRefreshScheduler {
|
|
2150
|
+
private readonly engine;
|
|
2151
|
+
private readonly catalog;
|
|
2152
|
+
private readonly statePath;
|
|
2153
|
+
private readonly logger;
|
|
2154
|
+
private readonly staleAfterMs;
|
|
2155
|
+
private readonly intervalMs;
|
|
2156
|
+
private readonly now;
|
|
2157
|
+
private timer;
|
|
2158
|
+
private inFlight;
|
|
2159
|
+
constructor(engine: PricingEngine$1, catalog: PricingCatalogSnapshot, statePath: string, logger: Logger, options?: PricingRefreshSchedulerOptions);
|
|
2160
|
+
/** Fire one stale check immediately and arm an unref'ed periodic check. */
|
|
2161
|
+
start(): void;
|
|
2162
|
+
dispose(): void;
|
|
2163
|
+
getState(): PricingRefreshState;
|
|
2164
|
+
/** Public for admin/manual tests; concurrent checks share one promise. */
|
|
2165
|
+
refreshIfStale(force?: boolean): Promise<void>;
|
|
2166
|
+
private runRefresh;
|
|
2167
|
+
private writeState;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
1914
2170
|
/**
|
|
1915
2171
|
* AccountHealthSweeper — proactive account-health recovery tick
|
|
1916
2172
|
* (subscription-account-health, design D6).
|
|
@@ -2181,24 +2437,20 @@ declare class BillingRetrySweeper {
|
|
|
2181
2437
|
}
|
|
2182
2438
|
|
|
2183
2439
|
/**
|
|
2184
|
-
* TokenRefreshScheduler
|
|
2185
|
-
* (external-cli-sync).
|
|
2440
|
+
* TokenRefreshScheduler proactive background OAuth token refresh.
|
|
2186
2441
|
*
|
|
2187
|
-
* The auth strategies already refresh
|
|
2188
|
-
* request + 401 retry), but
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2191
|
-
* and refreshes any token entering the expiry lead window.
|
|
2442
|
+
* The auth strategies already refresh lazily (lead-window check before each
|
|
2443
|
+
* request + 401 retry), but an idle daemon can still reach token expiry
|
|
2444
|
+
* before its next request. This scheduler sweeps managed OAuth accounts on
|
|
2445
|
+
* an interval and refreshes tokens entering the expiry lead window.
|
|
2192
2446
|
*
|
|
2193
2447
|
* Safety properties:
|
|
2194
|
-
* - the store coalesces in-flight refreshes per account, so a sweep
|
|
2448
|
+
* - the store coalesces in-flight refreshes per account, so a sweep cannot
|
|
2195
2449
|
* 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;
|
|
2450
|
+
* - accounts already flagged `expired` are skipped and remain expired until
|
|
2451
|
+
* their own managed credential is repaired or re-login occurs;
|
|
2452
|
+
* - the active account uses its managed active refresher, while non-active
|
|
2453
|
+
* accounts refresh by id; both paths use only managed stored credentials;
|
|
2202
2454
|
* - one sweep runs at a time (a long sweep never overlaps the next tick).
|
|
2203
2455
|
*
|
|
2204
2456
|
* Modeled on `ApiKeyPoolService`'s interval lifecycle: `start()` arms an
|
|
@@ -2223,8 +2475,9 @@ declare class TokenRefreshScheduler {
|
|
|
2223
2475
|
sweep(now?: number): Promise<void>;
|
|
2224
2476
|
/** Expiring within the lead window, refreshable, and not already dead. */
|
|
2225
2477
|
private needsRefresh;
|
|
2226
|
-
/** Refresh one account; failures are logged, never thrown
|
|
2227
|
-
*
|
|
2478
|
+
/** Refresh one managed account; failures are logged, never thrown. The
|
|
2479
|
+
* store marks only the targeted account `expired` on a failed refresh.
|
|
2480
|
+
*/
|
|
2228
2481
|
private refreshOne;
|
|
2229
2482
|
private refreshActive;
|
|
2230
2483
|
}
|
|
@@ -2406,10 +2659,20 @@ interface Daemon {
|
|
|
2406
2659
|
/** Subscription account service (token-free `listAll`) — now exposed for the
|
|
2407
2660
|
* admin dashboard's read-only accounts panel (RT3). */
|
|
2408
2661
|
readonly subscriptionAccounts: SubscriptionAccountService;
|
|
2662
|
+
/** Secret-free upstream subscription allowance cache/collector facade. */
|
|
2663
|
+
readonly accountAllowanceService: AccountAllowanceService;
|
|
2664
|
+
/**
|
|
2665
|
+
* Claude allowance cache maintenance for allowance-aware routing. Constructed
|
|
2666
|
+
* armed-off; the resident `start` command starts it only when the persisted
|
|
2667
|
+
* scheduling policy is enabled.
|
|
2668
|
+
*/
|
|
2669
|
+
readonly claudeAllowanceRefreshScheduler: ClaudeAllowanceRefreshScheduler;
|
|
2409
2670
|
/** File-backed pricing table (`pricing.json`; concrete for the admin DELETE). */
|
|
2410
2671
|
readonly pricingStore: JsonPricingStore;
|
|
2411
2672
|
/** Pricing engine (cost calc + source refresh + conflict resolution). */
|
|
2412
2673
|
readonly pricingEngine: PricingEngine;
|
|
2674
|
+
/** Non-blocking stale-while-revalidate catalog worker (armed by `start`). */
|
|
2675
|
+
readonly pricingRefreshScheduler: PricingRefreshScheduler;
|
|
2413
2676
|
/** Usage recorder over `usage-events.jsonl` — also the admin stats query facade. */
|
|
2414
2677
|
readonly usageRecorder: UsageRecorder;
|
|
2415
2678
|
/** The localhost admin/dashboard HTTP listener (RT3). Started by `start.ts`. */
|