@velum-labs/routekit-accounts 1.3.2 → 1.5.0

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.
Files changed (106) hide show
  1. package/README.md +4 -5
  2. package/dist/account-set/reset-credits.d.ts +4 -4
  3. package/dist/account-set/status-service.d.ts +2 -2
  4. package/dist/account-set/status-service.js +19 -19
  5. package/dist/account-set/types.d.ts +16 -0
  6. package/dist/account-set.d.ts +17 -4
  7. package/dist/account-set.js +164 -96
  8. package/dist/account-source.js +2 -2
  9. package/dist/activity.d.ts +2 -4
  10. package/dist/activity.js +12 -6
  11. package/dist/admission.d.ts +5 -2
  12. package/dist/admission.js +49 -5
  13. package/dist/auth-health.d.ts +8 -4
  14. package/dist/auth-health.js +31 -8
  15. package/dist/backend.d.ts +5 -3
  16. package/dist/backend.js +30 -13
  17. package/dist/codex-originator.d.ts +9 -0
  18. package/dist/codex-originator.js +29 -0
  19. package/dist/connector.js +3 -2
  20. package/dist/credential-store.d.ts +48 -0
  21. package/dist/credential-store.js +23 -0
  22. package/dist/credentials.d.ts +3 -1
  23. package/dist/credentials.js +28 -44
  24. package/dist/directory-credential-store.d.ts +14 -0
  25. package/dist/directory-credential-store.js +127 -0
  26. package/dist/effect/stream.d.ts +1 -1
  27. package/dist/effect/stream.js +5 -3
  28. package/dist/gateway-port.d.ts +10 -0
  29. package/dist/gateway.d.ts +20 -109
  30. package/dist/gateway.js +74 -77
  31. package/dist/index.d.ts +25 -16
  32. package/dist/index.js +17 -13
  33. package/dist/managed-login.d.ts +70 -26
  34. package/dist/managed-login.js +625 -167
  35. package/dist/pool-api.d.ts +9 -0
  36. package/dist/pool-api.js +4 -0
  37. package/dist/pool-state-store.d.ts +28 -0
  38. package/dist/provider/index.d.ts +0 -4
  39. package/dist/provider/index.js +0 -12
  40. package/dist/provider/shared.d.ts +26 -2
  41. package/dist/provider/shared.js +187 -8
  42. package/dist/provider-api.d.ts +5 -0
  43. package/dist/provider-api.js +3 -0
  44. package/dist/provider-port.d.ts +15 -1
  45. package/dist/proxy.d.ts +6 -2
  46. package/dist/proxy.js +2 -0
  47. package/dist/rate-limit-tracker-codec.d.ts +1 -0
  48. package/dist/rate-limit-tracker-codec.js +195 -0
  49. package/dist/rate-limit-tracker.d.ts +5 -0
  50. package/dist/rate-limit-tracker.js +12 -36
  51. package/dist/refresh-lock.d.ts +8 -0
  52. package/dist/refresh-lock.js +15 -0
  53. package/dist/relay-api.d.ts +4 -4
  54. package/dist/relay-api.js +1 -1
  55. package/dist/relay.d.ts +2 -19
  56. package/dist/relay.js +0 -100
  57. package/dist/subscription-pool-selection.d.ts +9 -6
  58. package/dist/subscription-pool-selection.js +136 -25
  59. package/dist/subscription-request-executor.d.ts +10 -3
  60. package/dist/subscription-request-executor.js +145 -49
  61. package/dist/subscription-stream.d.ts +5 -3
  62. package/dist/subscription-stream.js +20 -18
  63. package/dist/test/account-auth-health.test.js +22 -0
  64. package/dist/test/admission.test.js +77 -1
  65. package/dist/test/cliproxy.test.js +46 -22
  66. package/dist/test/codex-originator.test.js +16 -0
  67. package/dist/test/credential-store.test.d.ts +1 -0
  68. package/dist/test/credential-store.test.js +232 -0
  69. package/dist/test/credentials.test.js +103 -3
  70. package/dist/test/effect-lifecycle.test.js +52 -4
  71. package/dist/test/fixtures/anthropic-usage.d.ts +76 -0
  72. package/dist/test/fixtures/anthropic-usage.js +80 -0
  73. package/dist/test/foundry-provider-exposure.test.d.ts +1 -0
  74. package/dist/test/foundry-provider-exposure.test.js +305 -0
  75. package/dist/test/http-submission.test.d.ts +1 -0
  76. package/dist/test/http-submission.test.js +506 -0
  77. package/dist/test/managed-login.test.js +583 -97
  78. package/dist/{provider → test/provider-fixtures}/anthropic.d.ts +1 -1
  79. package/dist/{provider → test/provider-fixtures}/anthropic.js +14 -5
  80. package/dist/{provider → test/provider-fixtures}/codex.d.ts +1 -1
  81. package/dist/{provider → test/provider-fixtures}/codex.js +17 -5
  82. package/dist/test/provider-fixtures/index.d.ts +13 -0
  83. package/dist/test/provider-fixtures/index.js +28 -0
  84. package/dist/test/subscription-account-source.test.js +4 -3
  85. package/dist/test/subscription-backend.test.js +5 -4
  86. package/dist/test/subscription-pool-discovery.test.js +67 -0
  87. package/dist/test/subscription-pool-execution-streaming.test.js +186 -13
  88. package/dist/test/subscription-pool-fixtures.d.ts +2 -2
  89. package/dist/test/subscription-pool-fixtures.js +14 -2
  90. package/dist/test/subscription-pool-usage.test.js +374 -2
  91. package/dist/test/subscription-provider.test.js +188 -20
  92. package/dist/test/subscription-retry-safety.test.d.ts +1 -0
  93. package/dist/test/subscription-retry-safety.test.js +162 -0
  94. package/dist/test/subscription-sdk.test.js +147 -3
  95. package/dist/types.d.ts +45 -0
  96. package/dist/usage.d.ts +74 -5
  97. package/dist/usage.js +2 -2
  98. package/dist/wire.d.ts +44 -0
  99. package/dist/wire.js +35 -0
  100. package/package.json +11 -5
  101. package/dist/codex-relay.d.ts +0 -124
  102. package/dist/codex-relay.js +0 -248
  103. package/dist/test/codex-relay.test.js +0 -493
  104. package/dist/test/subscription-relay.test.js +0 -297
  105. /package/dist/{test/codex-relay.test.d.ts → pool-state-store.js} +0 -0
  106. /package/dist/test/{subscription-relay.test.d.ts → codex-originator.test.d.ts} +0 -0
package/README.md CHANGED
@@ -10,11 +10,10 @@ runs against every healthy account; the provider publishes the union of models
10
10
  and tracks which accounts are eligible for each model.
11
11
 
12
12
  The first-launch RouteKit subscription kinds are `claude-code` and `codex`.
13
- They run the official provider login in isolated temporary state
14
- (`captureLoginCredential`) before enrolling. Any number of named accounts may
15
- join a provider; the first enrollment enables that provider in the effective
16
- router config. The RouteKit tool command is still `routekit claude`; tool names
17
- and subscription kinds are separate contracts.
13
+ They run first-party OAuth (`captureLoginCredential`) before enrolling. Any
14
+ number of named accounts may join a provider; the first enrollment enables that
15
+ provider in the effective router config. The RouteKit tool command is still
16
+ `routekit claude`; tool names and subscription kinds are separate contracts.
18
17
 
19
18
  Other registry entries and exported connector APIs are internal, unqualified,
20
19
  and non-contractual; they are not RouteKit onboarding or support.
@@ -2,15 +2,15 @@ import type { SubscriptionMode } from "@velum-labs/routekit-registry";
2
2
  import { type RouteKitPlatform } from "@velum-labs/routekit-runtime/effect";
3
3
  import { Effect } from "effect";
4
4
  import type { SubscriptionProvider } from "../provider.js";
5
- import type { RateLimitTracker } from "../rate-limit-tracker.js";
5
+ import type { PoolStateStore } from "../pool-state-store.js";
6
6
  import type { SubscriptionPoolMember } from "../subscription-pool-selection.js";
7
7
  import type { AccountLimits } from "../types.js";
8
8
  import type { RedeemResetCreditInput } from "./types.js";
9
9
  export declare class ResetCreditService<M extends SubscriptionMode> {
10
10
  private readonly provider;
11
11
  private readonly tracker;
12
- constructor(provider: SubscriptionProvider<M>, tracker: RateLimitTracker);
13
- list(member: SubscriptionPoolMember, signal?: AbortSignal): Effect.Effect<import("../types.js").ResetCreditSnapshot, import("effect/PlatformError").PlatformError | Error, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/unstable/http/HttpClient").HttpClient>;
12
+ constructor(provider: SubscriptionProvider<M>, tracker: PoolStateStore);
13
+ list(member: SubscriptionPoolMember, signal?: AbortSignal): Effect.Effect<import("../types.js").ResetCreditSnapshot, Error, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
14
14
  attach(member: SubscriptionPoolMember, limits: AccountLimits, signal?: AbortSignal): Effect.Effect<AccountLimits, never, import("effect/unstable/http/HttpClient").HttpClient>;
15
15
  redeem(input: RedeemResetCreditInput, member: SubscriptionPoolMember, fetchUsage: (member: SubscriptionPoolMember, signal?: AbortSignal) => Effect.Effect<AccountLimits, Error, RouteKitPlatform>, signal?: AbortSignal): Effect.Effect<{
16
16
  creditId?: string;
@@ -20,5 +20,5 @@ export declare class ResetCreditService<M extends SubscriptionMode> {
20
20
  code: string;
21
21
  redeemRequestId: string;
22
22
  windowsReset?: number;
23
- }, import("effect/PlatformError").PlatformError | Error, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
23
+ }, Error, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
24
24
  }
@@ -1,7 +1,7 @@
1
1
  import type { SubscriptionMode } from "@velum-labs/routekit-registry";
2
2
  import type { AccountActivityService } from "../activity.js";
3
3
  import type { AccountAuthService } from "../auth-health.js";
4
- import type { RateLimitTracker } from "../rate-limit-tracker.js";
4
+ import type { PoolStateStore } from "../pool-state-store.js";
5
5
  import type { SubscriptionPoolMember, SubscriptionPoolSelector } from "../subscription-pool-selection.js";
6
6
  import type { SubscriptionAccountSetSnapshot, SubscriptionSelectionStrategy } from "../types.js";
7
7
  export declare class AccountSetStatusService<M extends SubscriptionMode> {
@@ -14,7 +14,7 @@ export declare class AccountSetStatusService<M extends SubscriptionMode> {
14
14
  private readonly authHealth;
15
15
  private readonly selector;
16
16
  private readonly catalogReady;
17
- constructor(mode: M, strategy: SubscriptionSelectionStrategy, switchThreshold: number, members: SubscriptionPoolMember[], tracker: RateLimitTracker, activity: AccountActivityService, authHealth: AccountAuthService, selector: SubscriptionPoolSelector, catalogReady: () => boolean);
17
+ constructor(mode: M, strategy: SubscriptionSelectionStrategy, switchThreshold: number, members: SubscriptionPoolMember[], tracker: PoolStateStore, activity: AccountActivityService, authHealth: AccountAuthService, selector: SubscriptionPoolSelector, catalogReady: () => boolean);
18
18
  snapshot(): SubscriptionAccountSetSnapshot;
19
19
  statusSnapshot(): SubscriptionAccountSetSnapshot;
20
20
  private memberStatus;
@@ -30,12 +30,13 @@ export class AccountSetStatusService {
30
30
  };
31
31
  }
32
32
  statusSnapshot() {
33
- const snapshot = this.snapshot();
34
33
  const now = Date.now() / 1000;
35
34
  return {
36
- ...snapshot,
37
- members: snapshot.members.map((status) => {
38
- const member = this.members.find((candidate) => candidate.id === status.id);
35
+ mode: this.mode,
36
+ strategy: this.strategy,
37
+ switchThreshold: this.switchThreshold,
38
+ members: this.members.map((member) => {
39
+ const status = this.memberStatus(member);
39
40
  const credentialValid = member.credential.accessToken.length > 0 &&
40
41
  (member.credential.expiresAt === undefined ||
41
42
  member.credential.expiresAt > now ||
@@ -51,24 +52,23 @@ export class AccountSetStatusService {
51
52
  catalogReady: this.catalogReady(),
52
53
  models: [...member.models],
53
54
  now,
54
- isWindowRelevant: (key, limitName) => this.selector.windowRelevant(key, limitName, undefined)
55
+ isWindowRelevant: (key, limitName) => this.selector.windowRelevant(key, limitName, undefined),
56
+ isDiscoveredLimitRelevant: (limit) => this.selector.discoveredLimitRelevant(limit, undefined)
55
57
  });
56
58
  const auth = this.authHealth.snapshot(subscriptionAccountIdentity(this.mode, member.label), member.credentialFingerprint);
57
59
  const upstreamAuthState = auth.kind === "superseded" ? "unknown" : auth.kind;
58
- const readinessReasons = credentialValid
59
- ? [
60
- ...(auth.kind === "refreshing"
61
- ? [{ code: "provider_auth_refreshing" }]
62
- : auth.kind === "backoff" && auth.retryAt !== undefined
63
- ? [{ code: "provider_auth_backoff", until: auth.retryAt / 1000 }]
64
- : auth.kind === "rejected"
65
- ? [{ code: "provider_auth_rejected", status: auth.status ?? 401 }]
66
- : []),
67
- ...readiness.reasons
68
- ]
69
- : member.credential.expiresAt !== undefined && member.credential.expiresAt <= now
70
- ? readiness.reasons
71
- : [{ code: "credential_invalid" }, ...readiness.reasons];
60
+ const readinessReasons = [...readiness.reasons];
61
+ if (credentialValid) {
62
+ if (auth.kind === "refreshing")
63
+ readinessReasons.unshift({ code: "provider_auth_refreshing" });
64
+ else if (auth.kind === "backoff" && auth.retryAt !== undefined)
65
+ readinessReasons.unshift({ code: "provider_auth_backoff", until: auth.retryAt / 1000 });
66
+ else if (auth.kind === "rejected")
67
+ readinessReasons.unshift({ code: "provider_auth_rejected", status: auth.status ?? 401 });
68
+ }
69
+ else if (member.credential.expiresAt === undefined || member.credential.expiresAt > now) {
70
+ readinessReasons.unshift({ code: "credential_invalid" });
71
+ }
72
72
  const poolEligible = readiness.eligible &&
73
73
  (auth.kind === "unknown" ||
74
74
  auth.kind === "accepted" ||
@@ -5,7 +5,10 @@ import type { Effect } from "effect";
5
5
  import type { SubscriptionAccountSource } from "../account-source.js";
6
6
  import type { AccountActivityCoordinator, AccountActivityService } from "../activity.js";
7
7
  import type { AccountAuthCoordinator, AccountAuthService } from "../auth-health.js";
8
+ import type { CredentialStore } from "../credential-store.js";
9
+ import type { PoolStateStore } from "../pool-state-store.js";
8
10
  import type { ConsumeResetCreditResult } from "../provider.js";
11
+ import type { RefreshLock } from "../refresh-lock.js";
9
12
  import type { SubscriptionExecutionObserver } from "../subscription-request-executor.js";
10
13
  import type { SubscriptionAccountSetSnapshot, SubscriptionCredential, SubscriptionSelectionStrategy } from "../types.js";
11
14
  export type CoordinatorResource<T> = {
@@ -16,11 +19,24 @@ export type SubscriptionAccountSetOptions = {
16
19
  activity?: CoordinatorResource<AccountActivityCoordinator | AccountActivityService>;
17
20
  authHealth?: CoordinatorResource<AccountAuthCoordinator | AccountAuthService>;
18
21
  source?: SubscriptionAccountSource;
22
+ store?: CredentialStore;
23
+ refreshLock?: RefreshLock;
24
+ poolStateStore?: PoolStateStore;
19
25
  strategy?: SubscriptionSelectionStrategy;
20
26
  switchThreshold?: number;
21
27
  probeIntervalMs?: number;
22
28
  refreshSkewSeconds?: number;
23
29
  fallbackCooldownSeconds?: number;
30
+ /**
31
+ * Catalog inventory published by an external owner. When present, account
32
+ * selection is initialized from this inventory without live discovery.
33
+ */
34
+ publishedModels?: readonly string[];
35
+ /**
36
+ * Account-specific published model inventory keyed by credential-store ID.
37
+ * When present, selection only offers a model to members that advertised it.
38
+ */
39
+ publishedModelsByCredentialId?: Readonly<Record<string, readonly string[]>>;
24
40
  beforeAcquisitionRevalidation?: (member: {
25
41
  label: string;
26
42
  }) => Effect.Effect<void, Error, RouteKitPlatform>;
@@ -1,22 +1,35 @@
1
1
  import type { ModelCapabilityMetadata, ModelReasoningCapabilities, ModelSelectionSignals } from "@velum-labs/routekit-contracts";
2
2
  import type { SubscriptionMode } from "@velum-labs/routekit-registry";
3
3
  import { RouteKitFailure } from "@velum-labs/routekit-runtime/effect";
4
+ import { ResourceScope } from "@velum-labs/routekit-runtime/lifecycle";
4
5
  import { Effect } from "effect";
6
+ import { AccountActivityCoordinator, type AccountActivityService } from "./activity.js";
7
+ import { AccountAuthCoordinator, type AccountAuthService } from "./auth-health.js";
5
8
  import { type SubscriptionProvider } from "./provider.js";
6
9
  import { type SubscriptionExecutionObserver, type SubscriptionRequestOperation } from "./subscription-request-executor.js";
7
10
  export { SubscriptionAccountSetAuthError, SubscriptionAccountSetAuthRecoveryError, SubscriptionAccountSetExhaustedError } from "./subscription-pool-selection.js";
8
11
  export type { SubscriptionExecutionObserver, SubscriptionRequestOperation } from "./subscription-request-executor.js";
9
- export { SUBSCRIPTION_SSE_BUFFER_CAP_BYTES } from "./subscription-stream.js";
12
+ export { SUBSCRIPTION_BUFFERED_RESPONSE_CAP_BYTES, SUBSCRIPTION_SSE_BUFFER_CAP_BYTES } from "./subscription-stream.js";
10
13
  import type { SubscriptionAccountSetSnapshot } from "./types.js";
11
14
  export type { CoordinatorResource, RedeemResetCreditInput, RedeemResetCreditResult, SubscriptionAccountSetOptions } from "./account-set/types.js";
12
15
  import type { RedeemResetCreditInput, SubscriptionAccountSetOptions } from "./account-set/types.js";
16
+ export declare function ownCoordinatorResource<T extends AccountActivityCoordinator | AccountActivityService | AccountAuthCoordinator | AccountAuthService>(resources: ResourceScope, resource: T): T;
13
17
  export declare class SubscriptionAccountSet<M extends SubscriptionMode = SubscriptionMode> {
14
18
  #private;
15
19
  private constructor();
16
20
  static open<M extends SubscriptionMode>(provider: SubscriptionProvider<M>, options: SubscriptionAccountSetOptions): Effect.Effect<SubscriptionAccountSet<M>, import("effect/PlatformError").PlatformError | Error, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
17
21
  get mode(): SubscriptionMode;
22
+ /** Provider behavior injected by the daemon/provider package composition root. */
23
+ get provider(): SubscriptionProvider<M>;
18
24
  get size(): number;
19
25
  get enrolledSize(): number;
26
+ /**
27
+ * Authentication-only fingerprints for credentials owned by this set.
28
+ *
29
+ * This keeps daemon auth-health coordination independent of whether the
30
+ * CredentialStore is backed by files, PostgreSQL, or another durable store.
31
+ */
32
+ credentialFingerprints(): ReadonlyMap<string, string>;
20
33
  snapshot(): SubscriptionAccountSetSnapshot;
21
34
  statusSnapshot(): SubscriptionAccountSetSnapshot;
22
35
  discoverModels(signal?: AbortSignal): Effect.Effect<readonly string[], Error, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
@@ -26,9 +39,9 @@ export declare class SubscriptionAccountSet<M extends SubscriptionMode = Subscri
26
39
  modelSelectionSignals(model: string): ModelSelectionSignals | undefined;
27
40
  close(): Effect.Effect<void, RouteKitFailure, never>;
28
41
  [Symbol.asyncDispose](): Promise<void>;
29
- probe(signal?: AbortSignal): Effect.Effect<void, never, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/unstable/http/HttpClient").HttpClient>;
42
+ probe(signal?: AbortSignal): Effect.Effect<void, never, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
30
43
  /** List banked rate-limit reset credits for one enrolled member. */
31
- listResetCredits(label: string, signal?: AbortSignal): Effect.Effect<import("./types.js").ResetCreditSnapshot, import("effect/PlatformError").PlatformError | Error, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/unstable/http/HttpClient").HttpClient>;
44
+ listResetCredits(label: string, signal?: AbortSignal): Effect.Effect<import("./types.js").ResetCreditSnapshot, import("effect/PlatformError").PlatformError | Error, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
32
45
  /** Redeem one banked rate-limit reset for an enrolled member. */
33
46
  redeemResetCredit(input: RedeemResetCreditInput, signal?: AbortSignal): Effect.Effect<{
34
47
  creditId?: string;
@@ -43,6 +56,6 @@ export declare class SubscriptionAccountSet<M extends SubscriptionMode = Subscri
43
56
  * Refresh stale or missing usage without allowing rapid callers to hammer
44
57
  * provider quota endpoints. Failed attempts are throttled as well.
45
58
  */
46
- refreshUsage(maxAgeMs?: number, signal?: AbortSignal): Effect.Effect<void, Error, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/unstable/http/HttpClient").HttpClient>;
59
+ refreshUsage(maxAgeMs?: number, signal?: AbortSignal): Effect.Effect<void, Error, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
47
60
  execute(model: string | undefined, operation: SubscriptionRequestOperation, signal?: AbortSignal, observer?: SubscriptionExecutionObserver): Effect.Effect<Response, import("effect/PlatformError").PlatformError | Error, import("effect/unstable/http/HttpClient").HttpClient | import("@effect/platform-node/NodeServices").NodeServices>;
48
61
  }
@@ -1,36 +1,34 @@
1
1
  import { join } from "node:path";
2
- import { ResourceScope } from "@velum-labs/routekit-runtime/lifecycle";
3
2
  import { RouteKitFailure, routeKitError, runRouteKitEffect, toRouteKitFailure, withAbortSignal } from "@velum-labs/routekit-runtime/effect";
3
+ import { ResourceScope } from "@velum-labs/routekit-runtime/lifecycle";
4
4
  import { Cause, Deferred, Effect, Exit, Fiber } from "effect";
5
5
  import { AccountCatalogService } from "./account-set/catalog-service.js";
6
6
  import { ResetCreditService } from "./account-set/reset-credits.js";
7
7
  import { AccountSetStatusService } from "./account-set/status-service.js";
8
- import { resolveSubscriptionAccounts } from "./account-source.js";
9
- import { AccountActivityCoordinator, accountActivityService, isAccountActivityService, subscriptionAccountIdentity } from "./activity.js";
8
+ import { AccountActivityCoordinator, accountActivityService, subscriptionAccountIdentity } from "./activity.js";
10
9
  import { quotaAdmissionReasons } from "./admission.js";
11
- import { AccountAuthCoordinator, accountAuthService, isAccountAuthService } from "./auth-health.js";
12
- import { subscriptionCredentialFingerprint, subscriptionCredentialLabel } from "./credentials.js";
10
+ import { AccountAuthCoordinator, accountAuthService } from "./auth-health.js";
11
+ import { openDirectoryCredentialStore } from "./directory-credential-store.js";
13
12
  import { SubscriptionProviderRequestError, SubscriptionRefreshError } from "./provider.js";
14
13
  import { RateLimitTracker } from "./rate-limit-tracker.js";
14
+ import { makeInProcessRefreshLock } from "./refresh-lock.js";
15
15
  import { SubscriptionPoolSelector } from "./subscription-pool-selection.js";
16
16
  import { SubscriptionRequestExecutor } from "./subscription-request-executor.js";
17
17
  export { SubscriptionAccountSetAuthError, SubscriptionAccountSetAuthRecoveryError, SubscriptionAccountSetExhaustedError } from "./subscription-pool-selection.js";
18
- export { SUBSCRIPTION_SSE_BUFFER_CAP_BYTES } from "./subscription-stream.js";
18
+ export { SUBSCRIPTION_BUFFERED_RESPONSE_CAP_BYTES, SUBSCRIPTION_SSE_BUFFER_CAP_BYTES } from "./subscription-stream.js";
19
19
  const DEFAULT_SWITCH_THRESHOLD = 0.9;
20
20
  const DEFAULT_REFRESH_SKEW_SECONDS = 300;
21
21
  const DEFAULT_FALLBACK_COOLDOWN_SECONDS = 300;
22
22
  const RAMP_WINDOW_MS = 30_000;
23
23
  const RAMP_STEP_MS = 250;
24
- function ownActivityResource(resources, resource) {
25
- if (!isAccountActivityService(resource))
26
- return resources.own(resource);
27
- resources.defer(() => runRouteKitEffect(resource.close));
28
- return resource;
29
- }
30
- function ownAuthResource(resources, resource) {
31
- if (!isAccountAuthService(resource))
32
- return resources.own(resource);
33
- resources.defer(() => runRouteKitEffect(resource.close));
24
+ export function ownCoordinatorResource(resources, resource) {
25
+ const close = resource.close;
26
+ if (Effect.isEffect(close)) {
27
+ resources.defer(() => runRouteKitEffect(close));
28
+ }
29
+ else {
30
+ resources.own(resource);
31
+ }
34
32
  return resource;
35
33
  }
36
34
  function credentialLoadFailure(mode, label, sourcePath, cause) {
@@ -61,6 +59,8 @@ export class SubscriptionAccountSet {
61
59
  #selector;
62
60
  #executor;
63
61
  #tracker;
62
+ #store;
63
+ #refreshLock;
64
64
  #activity;
65
65
  #metadata = new Map();
66
66
  #selectionSignals = new Map();
@@ -76,7 +76,7 @@ export class SubscriptionAccountSet {
76
76
  #credentialLoadFailuresReported = false;
77
77
  #probeFiber;
78
78
  #closed = false;
79
- constructor(provider, options, members, tracker, enrolledSize, credentialLoadFailures) {
79
+ constructor(provider, options, members, tracker, store, refreshLock, enrolledSize, credentialLoadFailures) {
80
80
  this.#provider = provider;
81
81
  this.#enrolledSize = enrolledSize;
82
82
  this.#credentialLoadFailures = credentialLoadFailures;
@@ -88,7 +88,21 @@ export class SubscriptionAccountSet {
88
88
  fallbackCooldownSeconds: options.fallbackCooldownSeconds ?? DEFAULT_FALLBACK_COOLDOWN_SECONDS
89
89
  };
90
90
  this.#members = members;
91
+ if (options.publishedModelsByCredentialId !== undefined) {
92
+ for (const member of this.#members) {
93
+ member.models = new Set(options.publishedModelsByCredentialId[member.credentialId] ?? []);
94
+ }
95
+ this.#catalogReady = true;
96
+ }
97
+ else if (options.publishedModels !== undefined) {
98
+ const publishedModels = new Set(options.publishedModels);
99
+ for (const member of this.#members)
100
+ member.models = new Set(publishedModels);
101
+ this.#catalogReady = true;
102
+ }
91
103
  this.#tracker = tracker;
104
+ this.#store = store;
105
+ this.#refreshLock = refreshLock;
92
106
  this.#resetCredits = new ResetCreditService(provider, tracker);
93
107
  this.#catalog = new AccountCatalogService(provider.mode, members, this.#metadata, this.#selectionSignals, this.#reasoning, (member, signal) => Effect.asVoid(this.#ensureFresh(member, signal)), (member, signal) => this.#discoverMemberModels(member, signal), () => {
94
108
  this.#catalogReady = true;
@@ -128,37 +142,69 @@ export class SubscriptionAccountSet {
128
142
  return Effect.suspend(() => {
129
143
  const resources = new ResourceScope();
130
144
  return Effect.gen(function* () {
131
- const source = options.source ?? { kind: "auto" };
132
- const accounts = yield* resolveSubscriptionAccounts(provider.mode, source);
133
- const tracker = yield* RateLimitTracker.open(join(accounts.stateDirectory, ".state.json"), provider.mode);
145
+ if (options.source !== undefined && options.store !== undefined) {
146
+ return yield* new RouteKitFailure({
147
+ message: "subscription account set accepts either source or store, not both"
148
+ });
149
+ }
150
+ const directoryStore = options.store === undefined
151
+ ? yield* openDirectoryCredentialStore(provider.mode, options.source ?? { kind: "auto" }, {
152
+ load: (id) => provider.loadCredential(id),
153
+ ...(provider.persistCredential === undefined
154
+ ? {}
155
+ : {
156
+ persist: (previous, next) => provider.persistCredential(previous, next)
157
+ })
158
+ })
159
+ : undefined;
160
+ const store = options.store ?? directoryStore;
161
+ const refreshLock = options.refreshLock ??
162
+ (directoryStore === undefined ? undefined : makeInProcessRefreshLock());
163
+ if (refreshLock === undefined) {
164
+ return yield* new RouteKitFailure({
165
+ message: "store-backed subscription account sets require refreshLock"
166
+ });
167
+ }
168
+ const tracker = options.poolStateStore ??
169
+ (directoryStore === undefined
170
+ ? undefined
171
+ : yield* RateLimitTracker.open(join(directoryStore.stateDirectory, ".state.json"), provider.mode));
172
+ if (tracker === undefined) {
173
+ return yield* new RouteKitFailure({
174
+ message: "store-backed subscription account sets require poolStateStore"
175
+ });
176
+ }
134
177
  const activityResource = options.activity === undefined
135
178
  ? resources.own(yield* AccountActivityCoordinator.open())
136
179
  : options.activity.ownership === "owned"
137
- ? ownActivityResource(resources, options.activity.resource)
180
+ ? ownCoordinatorResource(resources, options.activity.resource)
138
181
  : resources.borrow(options.activity.resource);
139
182
  const activity = accountActivityService(activityResource);
140
183
  const authHealthResource = options.authHealth === undefined
141
184
  ? resources.own(yield* AccountAuthCoordinator.open())
142
185
  : options.authHealth.ownership === "owned"
143
- ? ownAuthResource(resources, options.authHealth.resource)
186
+ ? ownCoordinatorResource(resources, options.authHealth.resource)
144
187
  : resources.borrow(options.authHealth.resource);
145
188
  const authHealth = accountAuthService(authHealthResource);
146
189
  const members = [];
147
190
  const credentialLoadFailures = [];
148
- for (const sourcePath of accounts.paths) {
149
- const id = subscriptionCredentialLabel(sourcePath);
150
- const loaded = yield* Effect.exit(provider.loadCredential(sourcePath));
191
+ const grants = yield* store.list(provider.mode);
192
+ for (const grant of grants) {
193
+ const id = grant.label;
194
+ const loaded = yield* Effect.exit(store.get(grant.id));
151
195
  if (Exit.isFailure(loaded)) {
152
- credentialLoadFailures.push(credentialLoadFailure(provider.mode, id, sourcePath, Cause.squash(loaded.cause)));
196
+ credentialLoadFailures.push(credentialLoadFailure(provider.mode, grant.label, grant.id, Cause.squash(loaded.cause)));
153
197
  continue;
154
198
  }
155
- const credential = loaded.value;
156
- const credentialFingerprint = subscriptionCredentialFingerprint(sourcePath);
157
- authHealth.register(subscriptionAccountIdentity(provider.mode, id), credentialFingerprint);
199
+ const stored = loaded.value;
200
+ const credential = stored.credential;
201
+ const credentialFingerprint = stored.fingerprint;
202
+ authHealth.register(subscriptionAccountIdentity(provider.mode, grant.label), credentialFingerprint);
158
203
  members.push({
159
204
  id,
160
- label: id,
161
- sourcePath,
205
+ credentialId: grant.id,
206
+ label: grant.label,
207
+ sourcePath: credential.sourcePath,
162
208
  credential,
163
209
  models: new Set(),
164
210
  ...(tracker.coolingUntil(id) !== undefined
@@ -168,17 +214,20 @@ export class SubscriptionAccountSet {
168
214
  lastUsed: 0,
169
215
  inFlight: 0,
170
216
  switchedAt: 0,
171
- credentialFingerprint
217
+ credentialFingerprint,
218
+ credentialRevision: stored.revision
172
219
  });
173
220
  }
174
221
  const accountSet = new SubscriptionAccountSet(provider, {
175
222
  ...options,
176
223
  activity: { resource: activity, ownership: "borrowed" },
177
224
  authHealth: { resource: authHealth, ownership: "borrowed" }
178
- }, members, tracker, accounts.paths.length, credentialLoadFailures);
179
- resources.transferTo(accountSet.#resources);
180
- yield* accountSet.#startProbe();
181
- return accountSet;
225
+ }, members, tracker, store, refreshLock, grants.length, credentialLoadFailures);
226
+ return yield* Effect.uninterruptible(Effect.gen(function* () {
227
+ yield* accountSet.#startProbe();
228
+ resources.transferTo(accountSet.#resources);
229
+ return accountSet;
230
+ }));
182
231
  }).pipe(Effect.ensuring(Effect.tryPromise({
183
232
  try: () => resources.dispose(),
184
233
  catch: toRouteKitFailure
@@ -188,12 +237,25 @@ export class SubscriptionAccountSet {
188
237
  get mode() {
189
238
  return this.#provider.mode;
190
239
  }
240
+ /** Provider behavior injected by the daemon/provider package composition root. */
241
+ get provider() {
242
+ return this.#provider;
243
+ }
191
244
  get size() {
192
245
  return this.#members.length;
193
246
  }
194
247
  get enrolledSize() {
195
248
  return this.#enrolledSize;
196
249
  }
250
+ /**
251
+ * Authentication-only fingerprints for credentials owned by this set.
252
+ *
253
+ * This keeps daemon auth-health coordination independent of whether the
254
+ * CredentialStore is backed by files, PostgreSQL, or another durable store.
255
+ */
256
+ credentialFingerprints() {
257
+ return new Map(this.#members.map((member) => [member.label, member.credentialFingerprint]));
258
+ }
197
259
  snapshot() {
198
260
  return this.#status.snapshot();
199
261
  }
@@ -355,14 +417,19 @@ export class SubscriptionAccountSet {
355
417
  const snapshot = self.#authHealth.snapshot(identity, member.credentialFingerprint);
356
418
  if (snapshot.kind !== "superseded")
357
419
  return;
358
- const fingerprint = subscriptionCredentialFingerprint(member.sourcePath);
359
- if (fingerprint !== snapshot.currentFingerprint)
420
+ const stored = yield* self.#store.get(member.credentialId);
421
+ if (stored.fingerprint !== snapshot.currentFingerprint)
360
422
  return;
361
- member.credential = yield* self.#provider.loadCredential(member.sourcePath);
362
- member.credentialFingerprint = fingerprint;
363
- self.#authHealth.register(identity, fingerprint);
423
+ self.#adoptStoredCredential(member, stored);
424
+ yield* self.#authHealth.activateFingerprint(identity, stored.fingerprint);
364
425
  });
365
426
  }
427
+ #adoptStoredCredential(member, stored) {
428
+ member.credential = stored.credential;
429
+ member.sourcePath = stored.credential.sourcePath;
430
+ member.credentialRevision = stored.revision;
431
+ member.credentialFingerprint = stored.fingerprint;
432
+ }
366
433
  #recoverAuthentication(member, fingerprint, model, excluded, signal) {
367
434
  const self = this;
368
435
  return Effect.gen(function* () {
@@ -390,29 +457,9 @@ export class SubscriptionAccountSet {
390
457
  excluded.add(member.id);
391
458
  return undefined;
392
459
  }
393
- const expectedCooldownRevision = self.#tracker.cooldownRevision(member.id);
394
460
  const refreshSignal = AbortSignal.any([self.#authHealth.signal, AbortSignal.timeout(30_000)]);
395
- return yield* self.#provider.refresh(member.credential, refreshSignal).pipe(Effect.flatMap((credential) => Effect.gen(function* () {
396
- const refreshedFingerprint = subscriptionCredentialFingerprint(member.sourcePath);
397
- if (!(yield* self.#authHealth.markRefreshed(recovery.claim, refreshedFingerprint))) {
398
- yield* self.#synchronizeCredential(member);
399
- return undefined;
400
- }
401
- member.credential = credential;
402
- member.credentialFingerprint = refreshedFingerprint;
403
- if (yield* self.#tracker.resetAfterRefresh(member.id, expectedCooldownRevision)) {
404
- delete member.coolingUntil;
405
- }
406
- else {
407
- member.coolingUntil = self.#tracker.coolingUntil(member.id);
408
- }
409
- member.cooldownRevision = self.#tracker.cooldownRevision(member.id);
410
- if (signal?.aborted === true) {
411
- yield* self.#authHealth.finishProbation(recovery.claim, { kind: "inconclusive" });
412
- signal.throwIfAborted();
413
- }
414
- return recovery.claim;
415
- })), Effect.catch((error) => Effect.gen(function* () {
461
+ let pendingCommit;
462
+ const failRefresh = (error) => Effect.gen(function* () {
416
463
  if (error instanceof SubscriptionRefreshError) {
417
464
  yield* self.#authHealth.failRefresh(recovery.claim, error.failure);
418
465
  }
@@ -426,7 +473,51 @@ export class SubscriptionAccountSet {
426
473
  signal.throwIfAborted();
427
474
  excluded.add(member.id);
428
475
  return undefined;
429
- })));
476
+ });
477
+ const locked = yield* Effect.uninterruptible(self.#refreshLock.withExclusive(member.credentialId, Effect.gen(function* () {
478
+ const stored = yield* self.#store.get(member.credentialId);
479
+ if (stored.revision !== member.credentialRevision ||
480
+ stored.fingerprint !== member.credentialFingerprint) {
481
+ return { kind: "reloaded", stored };
482
+ }
483
+ const expectedCooldownRevision = self.#tracker.cooldownRevision(member.id);
484
+ const refreshed = yield* self.#provider.refresh(stored.credential, refreshSignal);
485
+ const committed = yield* self.#store.put(member.credentialId, refreshed, stored.revision);
486
+ pendingCommit = {
487
+ kind: "committed",
488
+ stored: committed,
489
+ expectedCooldownRevision
490
+ };
491
+ return pendingCommit;
492
+ }))).pipe(Effect.catch((error) => pendingCommit === undefined
493
+ ? failRefresh(error)
494
+ : self.#store.get(member.credentialId).pipe(Effect.flatMap((durable) => durable.revision === pendingCommit.stored.revision &&
495
+ durable.fingerprint === pendingCommit.stored.fingerprint
496
+ ? Effect.succeed({ ...pendingCommit, stored: durable })
497
+ : failRefresh(error)), Effect.catch(() => failRefresh(error)))));
498
+ if (locked === undefined)
499
+ return undefined;
500
+ self.#adoptStoredCredential(member, locked.stored);
501
+ if (locked.kind === "reloaded") {
502
+ yield* self.#authHealth.activateFingerprint(identity, locked.stored.fingerprint);
503
+ return undefined;
504
+ }
505
+ if (!(yield* self.#authHealth.markRefreshed(recovery.claim, locked.stored.fingerprint))) {
506
+ yield* self.#synchronizeCredential(member);
507
+ return undefined;
508
+ }
509
+ if (yield* self.#tracker.resetAfterRefresh(member.id, locked.expectedCooldownRevision)) {
510
+ delete member.coolingUntil;
511
+ }
512
+ else {
513
+ member.coolingUntil = self.#tracker.coolingUntil(member.id);
514
+ }
515
+ member.cooldownRevision = self.#tracker.cooldownRevision(member.id);
516
+ if (signal?.aborted === true) {
517
+ yield* self.#authHealth.finishProbation(recovery.claim, { kind: "inconclusive" });
518
+ signal.throwIfAborted();
519
+ }
520
+ return recovery.claim;
430
521
  });
431
522
  }
432
523
  #finishProbationForFailure(claim, failure) {
@@ -481,7 +572,9 @@ export class SubscriptionAccountSet {
481
572
  return (quotaAdmissionReasons({
482
573
  limits,
483
574
  switchThreshold: this.#options.switchThreshold,
484
- isWindowRelevant: relevant
575
+ isWindowRelevant: relevant,
576
+ isDiscoveredLimitRelevant: (limit) => context?.model === undefined ||
577
+ this.#selector.discoveredLimitRelevant(limit, context.model)
485
578
  }).length === 0);
486
579
  }
487
580
  /**
@@ -490,39 +583,14 @@ export class SubscriptionAccountSet {
490
583
  * every account dark until discovery succeeds again.
491
584
  */
492
585
  #discoverMemberModels(member, signal) {
493
- const self = this;
494
- return self.#provider.discoverModels(member.credential, signal).pipe(Effect.tap(() => Effect.sync(() => {
495
- self.#authHealth.markAccepted(subscriptionAccountIdentity(self.mode, member.label), member.credentialFingerprint);
496
- })), Effect.catch((error) => {
497
- if (!(error instanceof SubscriptionProviderRequestError) ||
498
- error.failure.scope !== "credential") {
499
- return Effect.fail(error);
500
- }
501
- return Effect.gen(function* () {
502
- const claim = yield* self.#recoverAuthentication(member, member.credentialFingerprint, undefined, new Set(), signal);
503
- if (claim === undefined)
504
- return yield* Effect.fail(error);
505
- return yield* self.#provider.discoverModels(member.credential, signal).pipe(Effect.tap(() => self.#authHealth.finishProbation(claim, { kind: "accepted" })), Effect.catch((retryError) => Effect.gen(function* () {
506
- if (retryError instanceof SubscriptionProviderRequestError &&
507
- retryError.failure.scope === "credential" &&
508
- (retryError.failure.status === 401 || retryError.failure.status === 403)) {
509
- yield* self.#authHealth.finishProbation(claim, {
510
- kind: "rejected",
511
- status: retryError.failure.status,
512
- reasonCode: self.#authReasonCode(retryError.failure)
513
- });
514
- }
515
- else {
516
- yield* self.#authHealth.finishProbation(claim, { kind: "inconclusive" });
517
- }
518
- return yield* Effect.fail(retryError);
519
- })));
520
- });
521
- }));
586
+ return this.#withProviderAuthRecovery(member, (credential) => this.#provider.discoverModels(credential, signal), signal);
522
587
  }
523
588
  #fetchUsageWithAuthRecovery(member, signal) {
589
+ return this.#withProviderAuthRecovery(member, (credential) => this.#provider.fetchUsage(credential, signal), signal);
590
+ }
591
+ #withProviderAuthRecovery(member, request, signal) {
524
592
  const self = this;
525
- return self.#provider.fetchUsage(member.credential, signal).pipe(Effect.tap(() => Effect.sync(() => {
593
+ return request(member.credential).pipe(Effect.tap(() => Effect.sync(() => {
526
594
  self.#authHealth.markAccepted(subscriptionAccountIdentity(self.mode, member.label), member.credentialFingerprint);
527
595
  })), Effect.catch((error) => {
528
596
  if (!(error instanceof SubscriptionProviderRequestError) ||
@@ -533,7 +601,7 @@ export class SubscriptionAccountSet {
533
601
  const claim = yield* self.#recoverAuthentication(member, member.credentialFingerprint, undefined, new Set(), signal);
534
602
  if (claim === undefined)
535
603
  return yield* Effect.fail(error);
536
- return yield* self.#provider.fetchUsage(member.credential, signal).pipe(Effect.tap(() => self.#authHealth.finishProbation(claim, { kind: "accepted" })), Effect.catch((retryError) => Effect.gen(function* () {
604
+ return yield* request(member.credential).pipe(Effect.tap(() => self.#authHealth.finishProbation(claim, { kind: "accepted" })), Effect.catch((retryError) => Effect.gen(function* () {
537
605
  if (retryError instanceof SubscriptionProviderRequestError &&
538
606
  retryError.failure.scope === "credential" &&
539
607
  (retryError.failure.status === 401 || retryError.failure.status === 403)) {
@@ -38,8 +38,8 @@ export function resolveSubscriptionAccounts(mode, source = { kind: "auto" }) {
38
38
  }
39
39
  const canonical = resolve(source.canonicalPath ?? defaultSubscriptionCredentialPath(mode));
40
40
  return {
41
- // The credential loader owns file/keychain fallback and will report a
42
- // precise error when neither source exists.
41
+ // The credential loader reports a precise error when the canonical
42
+ // file does not exist.
43
43
  paths: [canonical],
44
44
  stateDirectory: directory
45
45
  };