@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.
- package/README.md +4 -5
- package/dist/account-set/reset-credits.d.ts +4 -4
- package/dist/account-set/status-service.d.ts +2 -2
- package/dist/account-set/status-service.js +19 -19
- package/dist/account-set/types.d.ts +16 -0
- package/dist/account-set.d.ts +17 -4
- package/dist/account-set.js +164 -96
- package/dist/account-source.js +2 -2
- package/dist/activity.d.ts +2 -4
- package/dist/activity.js +12 -6
- package/dist/admission.d.ts +5 -2
- package/dist/admission.js +49 -5
- package/dist/auth-health.d.ts +8 -4
- package/dist/auth-health.js +31 -8
- package/dist/backend.d.ts +5 -3
- package/dist/backend.js +30 -13
- package/dist/codex-originator.d.ts +9 -0
- package/dist/codex-originator.js +29 -0
- package/dist/connector.js +3 -2
- package/dist/credential-store.d.ts +48 -0
- package/dist/credential-store.js +23 -0
- package/dist/credentials.d.ts +3 -1
- package/dist/credentials.js +28 -44
- package/dist/directory-credential-store.d.ts +14 -0
- package/dist/directory-credential-store.js +127 -0
- package/dist/effect/stream.d.ts +1 -1
- package/dist/effect/stream.js +5 -3
- package/dist/gateway-port.d.ts +10 -0
- package/dist/gateway.d.ts +20 -109
- package/dist/gateway.js +74 -77
- package/dist/index.d.ts +25 -16
- package/dist/index.js +17 -13
- package/dist/managed-login.d.ts +70 -26
- package/dist/managed-login.js +625 -167
- package/dist/pool-api.d.ts +9 -0
- package/dist/pool-api.js +4 -0
- package/dist/pool-state-store.d.ts +28 -0
- package/dist/provider/index.d.ts +0 -4
- package/dist/provider/index.js +0 -12
- package/dist/provider/shared.d.ts +26 -2
- package/dist/provider/shared.js +187 -8
- package/dist/provider-api.d.ts +5 -0
- package/dist/provider-api.js +3 -0
- package/dist/provider-port.d.ts +15 -1
- package/dist/proxy.d.ts +6 -2
- package/dist/proxy.js +2 -0
- package/dist/rate-limit-tracker-codec.d.ts +1 -0
- package/dist/rate-limit-tracker-codec.js +195 -0
- package/dist/rate-limit-tracker.d.ts +5 -0
- package/dist/rate-limit-tracker.js +12 -36
- package/dist/refresh-lock.d.ts +8 -0
- package/dist/refresh-lock.js +15 -0
- package/dist/relay-api.d.ts +4 -4
- package/dist/relay-api.js +1 -1
- package/dist/relay.d.ts +2 -19
- package/dist/relay.js +0 -100
- package/dist/subscription-pool-selection.d.ts +9 -6
- package/dist/subscription-pool-selection.js +136 -25
- package/dist/subscription-request-executor.d.ts +10 -3
- package/dist/subscription-request-executor.js +145 -49
- package/dist/subscription-stream.d.ts +5 -3
- package/dist/subscription-stream.js +20 -18
- package/dist/test/account-auth-health.test.js +22 -0
- package/dist/test/admission.test.js +77 -1
- package/dist/test/cliproxy.test.js +46 -22
- package/dist/test/codex-originator.test.js +16 -0
- package/dist/test/credential-store.test.d.ts +1 -0
- package/dist/test/credential-store.test.js +232 -0
- package/dist/test/credentials.test.js +103 -3
- package/dist/test/effect-lifecycle.test.js +52 -4
- package/dist/test/fixtures/anthropic-usage.d.ts +76 -0
- package/dist/test/fixtures/anthropic-usage.js +80 -0
- package/dist/test/foundry-provider-exposure.test.d.ts +1 -0
- package/dist/test/foundry-provider-exposure.test.js +305 -0
- package/dist/test/http-submission.test.d.ts +1 -0
- package/dist/test/http-submission.test.js +506 -0
- package/dist/test/managed-login.test.js +583 -97
- package/dist/{provider → test/provider-fixtures}/anthropic.d.ts +1 -1
- package/dist/{provider → test/provider-fixtures}/anthropic.js +14 -5
- package/dist/{provider → test/provider-fixtures}/codex.d.ts +1 -1
- package/dist/{provider → test/provider-fixtures}/codex.js +17 -5
- package/dist/test/provider-fixtures/index.d.ts +13 -0
- package/dist/test/provider-fixtures/index.js +28 -0
- package/dist/test/subscription-account-source.test.js +4 -3
- package/dist/test/subscription-backend.test.js +5 -4
- package/dist/test/subscription-pool-discovery.test.js +67 -0
- package/dist/test/subscription-pool-execution-streaming.test.js +186 -13
- package/dist/test/subscription-pool-fixtures.d.ts +2 -2
- package/dist/test/subscription-pool-fixtures.js +14 -2
- package/dist/test/subscription-pool-usage.test.js +374 -2
- package/dist/test/subscription-provider.test.js +188 -20
- package/dist/test/subscription-retry-safety.test.d.ts +1 -0
- package/dist/test/subscription-retry-safety.test.js +162 -0
- package/dist/test/subscription-sdk.test.js +147 -3
- package/dist/types.d.ts +45 -0
- package/dist/usage.d.ts +74 -5
- package/dist/usage.js +2 -2
- package/dist/wire.d.ts +44 -0
- package/dist/wire.js +35 -0
- package/package.json +11 -5
- package/dist/codex-relay.d.ts +0 -124
- package/dist/codex-relay.js +0 -248
- package/dist/test/codex-relay.test.js +0 -493
- package/dist/test/subscription-relay.test.js +0 -297
- /package/dist/{test/codex-relay.test.d.ts → pool-state-store.js} +0 -0
- /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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 {
|
|
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:
|
|
13
|
-
list(member: SubscriptionPoolMember, signal?: AbortSignal): Effect.Effect<import("../types.js").ResetCreditSnapshot,
|
|
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
|
-
},
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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>;
|
package/dist/account-set.d.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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
|
}
|
package/dist/account-set.js
CHANGED
|
@@ -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 {
|
|
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
|
|
12
|
-
import {
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
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,
|
|
196
|
+
credentialLoadFailures.push(credentialLoadFailure(provider.mode, grant.label, grant.id, Cause.squash(loaded.cause)));
|
|
153
197
|
continue;
|
|
154
198
|
}
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
359
|
-
if (fingerprint !== snapshot.currentFingerprint)
|
|
420
|
+
const stored = yield* self.#store.get(member.credentialId);
|
|
421
|
+
if (stored.fingerprint !== snapshot.currentFingerprint)
|
|
360
422
|
return;
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
|
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*
|
|
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)) {
|
package/dist/account-source.js
CHANGED
|
@@ -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
|
|
42
|
-
//
|
|
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
|
};
|