@oh-my-pi/pi-ai 18.8.1 → 18.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/types/auth/cascade.d.ts +12 -3
- package/dist/types/auth/oauth.d.ts +5 -3
- package/dist/types/auth/select.d.ts +2 -0
- package/dist/types/auth/types.d.ts +17 -4
- package/dist/types/error/auth.d.ts +10 -0
- package/package.json +6 -6
- package/src/auth/cascade.ts +36 -6
- package/src/auth/oauth.ts +5 -3
- package/src/auth/select.ts +30 -10
- package/src/auth/types.ts +17 -4
- package/src/error/auth.ts +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.8.3] - 2026-10-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `OAuthRefreshUnavailableError`, a retryable error that `keys.getWithCredential` and `oauth.access` reject with when every usable OAuth credential failed to refresh transiently; `keys.get` resolves `undefined` instead so availability probes move on to their next candidate ([#14843](https://github.com/can1357/oh-my-pi/pull/14843) by [@H4vC](https://github.com/H4vC))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed a single transient OAuth token-refresh failure (network blip, timeout, 5xx) ending a running session, including subagents restricted to an account pool, with a non-retryable "No API key for provider" error while the stored credential was still valid; the refresh error now surfaces and the request is retried ([#14843](https://github.com/can1357/oh-my-pi/pull/14843) by [@H4vC](https://github.com/H4vC))
|
|
14
|
+
|
|
5
15
|
## [18.8.1] - 2026-10-07
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -2,7 +2,7 @@ import { type ApiKeyResolver, type ResolvedApiKey } from "../auth-retry.js";
|
|
|
2
2
|
import type { SessionAffinity } from "./affinity.js";
|
|
3
3
|
import type { CredentialPool } from "./pool.js";
|
|
4
4
|
import type { CredentialSelector } from "./select.js";
|
|
5
|
-
import type { AuthApiKeyOptions, AuthCredential, AuthSource, AuthSourceOptions, KeysApi, LimitsApi
|
|
5
|
+
import type { AuthApiKeyOptions, AuthCredential, AuthSource, AuthSourceOptions, KeysApi, LimitsApi } from "./types.js";
|
|
6
6
|
/** Runtime (--api-key) and config (models.yml) key overrides plus the config-value resolver. */
|
|
7
7
|
export declare class KeyOverrides {
|
|
8
8
|
#private;
|
|
@@ -117,7 +117,13 @@ export declare class KeyCascade implements KeysApi {
|
|
|
117
117
|
* credential to reach the correct host.
|
|
118
118
|
*/
|
|
119
119
|
peek(provider: string): Promise<string | undefined>;
|
|
120
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a bearer together with the stored row that supplied it, for a request.
|
|
122
|
+
* Same precedence as {@link KeyCascade.get}, but rejects with
|
|
123
|
+
* {@link AIError.OAuthRefreshUnavailableError} (transient, retryable) when OAuth
|
|
124
|
+
* refresh failed with a retryable error and no other permitted source could serve
|
|
125
|
+
* the request, so request paths retry instead of reporting a missing key.
|
|
126
|
+
*/
|
|
121
127
|
getWithCredential(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<ResolvedApiKey | undefined>;
|
|
122
128
|
/**
|
|
123
129
|
* Get API key for a provider.
|
|
@@ -127,8 +133,11 @@ export declare class KeyCascade implements KeysApi {
|
|
|
127
133
|
* OAuth accounts and throws {@link AIError.MissingApiKeyError} when none
|
|
128
134
|
* can serve or a config key (a models.yml `apiKey`, often for a proxy
|
|
129
135
|
* `baseUrl`) owns the provider, rather than using any other credential.
|
|
136
|
+
* A transient OAuth refresh failure resolves `undefined`, so availability
|
|
137
|
+
* probes move on to their next candidate; request paths use
|
|
138
|
+
* {@link KeyCascade.getWithCredential}, which surfaces it.
|
|
130
139
|
*/
|
|
131
|
-
get(provider: string, sessionId?: string, options?: AuthApiKeyOptions
|
|
140
|
+
get(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
|
|
132
141
|
/**
|
|
133
142
|
* Build an {@link ApiKeyResolver} backed by this storage, implementing the
|
|
134
143
|
* central a/b/c auth-retry policy:
|
|
@@ -37,9 +37,11 @@ export declare class OAuthAccounts implements OAuthApi {
|
|
|
37
37
|
* `enterpriseUrl`). For pure "give me the bytes for `Authorization`"
|
|
38
38
|
* scenarios, prefer API-key resolution.
|
|
39
39
|
*
|
|
40
|
-
* Returns `undefined` when no OAuth credential is available
|
|
41
|
-
*
|
|
42
|
-
* OAuth with an explicit API key.
|
|
40
|
+
* Returns `undefined` when no usable OAuth credential is available
|
|
41
|
+
* (none stored, or every one definitively failed to refresh) or
|
|
42
|
+
* runtime/config overrides have replaced OAuth with an explicit API key.
|
|
43
|
+
* Rejects with {@link AIError.OAuthRefreshUnavailableError} (transient,
|
|
44
|
+
* retryable) when a retryable refresh failure left no usable credential.
|
|
43
45
|
*/
|
|
44
46
|
access(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<OAuthAccess | undefined>;
|
|
45
47
|
/**
|
|
@@ -32,6 +32,8 @@ export type TryOAuthOptions = {
|
|
|
32
32
|
blockScopes?: readonly string[];
|
|
33
33
|
/** When false, a definitive failure of THIS credential returns undefined instead of falling back to the ranked/round-robin selector (target-only resolution). */
|
|
34
34
|
allowFallback?: boolean;
|
|
35
|
+
/** Receives a non-definitive refresh failure that left this credential unusable; the caller filters for retryable ones. */
|
|
36
|
+
onTransientRefreshFailure?: (error: unknown) => void;
|
|
35
37
|
};
|
|
36
38
|
/** Services consulted by CredentialSelector for policy, usage, blocks, refresh, and session affinity. */
|
|
37
39
|
export interface CredentialSelectorDeps {
|
|
@@ -812,9 +812,20 @@ export interface KeysApi {
|
|
|
812
812
|
* 4. API key persisted by a successful `/login`
|
|
813
813
|
* 5. Environment variable
|
|
814
814
|
* 6. Stored API key (e.g. a broker-migrated copy) — last resort, so an explicit env var wins
|
|
815
|
+
*
|
|
816
|
+
* Resolves `undefined` when no permitted credential can serve, including when
|
|
817
|
+
* OAuth refresh failed transiently (network, timeout, 5xx), so availability
|
|
818
|
+
* probes move on to their next candidate. Request paths use
|
|
819
|
+
* {@link KeysApi.getWithCredential}, which surfaces that failure.
|
|
815
820
|
*/
|
|
816
821
|
get(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
|
|
817
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* Resolve a bearer together with its durable stored credential row id, when known.
|
|
824
|
+
* Same precedence as {@link KeysApi.get}, but rejects with
|
|
825
|
+
* `OAuthRefreshUnavailableError` (transient, retryable) when OAuth refresh failed
|
|
826
|
+
* with a retryable error and no other permitted source could serve the request;
|
|
827
|
+
* the stored credential is kept.
|
|
828
|
+
*/
|
|
818
829
|
getWithCredential(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<ResolvedApiKey | undefined>;
|
|
819
830
|
/**
|
|
820
831
|
* Peek at API key for a provider without refreshing OAuth tokens.
|
|
@@ -935,9 +946,11 @@ export interface OAuthApi {
|
|
|
935
946
|
* `enterpriseUrl`). For pure "give me the bytes for `Authorization`"
|
|
936
947
|
* scenarios, prefer {@link AuthStorage.keys.get}.
|
|
937
948
|
*
|
|
938
|
-
* Returns `undefined` when no OAuth credential is available
|
|
939
|
-
*
|
|
940
|
-
* OAuth with an explicit API key.
|
|
949
|
+
* Returns `undefined` when no usable OAuth credential is available (none
|
|
950
|
+
* stored, or every one definitively failed to refresh) or runtime/config
|
|
951
|
+
* overrides have replaced OAuth with an explicit API key. Rejects with
|
|
952
|
+
* `OAuthRefreshUnavailableError` (transient, retryable) when a retryable
|
|
953
|
+
* refresh failure left no usable credential.
|
|
941
954
|
*/
|
|
942
955
|
access(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<OAuthAccess | undefined>;
|
|
943
956
|
/**
|
|
@@ -10,6 +10,16 @@ export declare class MissingApiKeyError extends Error {
|
|
|
10
10
|
readonly provider: string | undefined;
|
|
11
11
|
constructor(provider?: string, message?: string);
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Every usable OAuth credential for a provider failed to refresh with a
|
|
15
|
+
* retryable error (network blip, timeout, 5xx). The stored credentials are
|
|
16
|
+
* intact, so this is classified transient: retrying the request can succeed,
|
|
17
|
+
* unlike {@link MissingApiKeyError}. The refresh failure is kept as `cause`.
|
|
18
|
+
*/
|
|
19
|
+
export declare class OAuthRefreshUnavailableError extends Error {
|
|
20
|
+
readonly provider: string;
|
|
21
|
+
constructor(provider: string, cause: unknown);
|
|
22
|
+
}
|
|
13
23
|
/** A user-facing login flow required an `onPrompt` callback that was not supplied. */
|
|
14
24
|
export declare class OnPromptRequiredError extends Error {
|
|
15
25
|
constructor(providerLabel: string);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.8.
|
|
3
|
+
"version": "18.8.3",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.8.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.8.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.8.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.8.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.8.
|
|
158
|
+
"@oh-my-pi/omptype": "18.8.3",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.8.3",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.8.3",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.8.3",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.8.3"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/auth/cascade.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { authPolicyFor } from "@oh-my-pi/pi-catalog/compat/auth";
|
|
2
|
-
import { $env, $envExact } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $env, $envExact, logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type ApiKeyResolver, markAfterSiblingWait, type ResolvedApiKey } from "../auth-retry";
|
|
4
4
|
import { getEnvApiKey, getEnvApiKeyName } from "../env-api-key";
|
|
5
5
|
import * as AIError from "../error";
|
|
@@ -7,7 +7,7 @@ import { isUsageLimitOutcome } from "../error/rate-limit";
|
|
|
7
7
|
import { AUTHENTICATED_SENTINEL } from "../registry/types";
|
|
8
8
|
import type { SessionAffinity } from "./affinity";
|
|
9
9
|
import type { CredentialPool } from "./pool";
|
|
10
|
-
import type { CredentialSelector } from "./select";
|
|
10
|
+
import type { CredentialSelector, OAuthResolutionResult } from "./select";
|
|
11
11
|
import type {
|
|
12
12
|
AuthApiKeyOptions,
|
|
13
13
|
AuthCredential,
|
|
@@ -308,7 +308,13 @@ export class KeyCascade implements KeysApi {
|
|
|
308
308
|
return undefined;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
/**
|
|
311
|
+
/**
|
|
312
|
+
* Resolve a bearer together with the stored row that supplied it, for a request.
|
|
313
|
+
* Same precedence as {@link KeyCascade.get}, but rejects with
|
|
314
|
+
* {@link AIError.OAuthRefreshUnavailableError} (transient, retryable) when OAuth
|
|
315
|
+
* refresh failed with a retryable error and no other permitted source could serve
|
|
316
|
+
* the request, so request paths retry instead of reporting a missing key.
|
|
317
|
+
*/
|
|
312
318
|
async getWithCredential(
|
|
313
319
|
provider: string,
|
|
314
320
|
sessionId?: string,
|
|
@@ -316,7 +322,7 @@ export class KeyCascade implements KeysApi {
|
|
|
316
322
|
): Promise<ResolvedApiKey | undefined> {
|
|
317
323
|
let credentialId: number | undefined;
|
|
318
324
|
let oauthIdentity: OAuthRequestIdentity | undefined;
|
|
319
|
-
const apiKey = await this
|
|
325
|
+
const apiKey = await this.#resolve(provider, sessionId, options, (id, identity) => {
|
|
320
326
|
credentialId = id;
|
|
321
327
|
oauthIdentity = identity;
|
|
322
328
|
});
|
|
@@ -331,8 +337,21 @@ export class KeyCascade implements KeysApi {
|
|
|
331
337
|
* OAuth accounts and throws {@link AIError.MissingApiKeyError} when none
|
|
332
338
|
* can serve or a config key (a models.yml `apiKey`, often for a proxy
|
|
333
339
|
* `baseUrl`) owns the provider, rather than using any other credential.
|
|
340
|
+
* A transient OAuth refresh failure resolves `undefined`, so availability
|
|
341
|
+
* probes move on to their next candidate; request paths use
|
|
342
|
+
* {@link KeyCascade.getWithCredential}, which surfaces it.
|
|
334
343
|
*/
|
|
335
|
-
async get(
|
|
344
|
+
async get(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
|
|
345
|
+
try {
|
|
346
|
+
return await this.#resolve(provider, sessionId, options);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (!(error instanceof AIError.OAuthRefreshUnavailableError)) throw error;
|
|
349
|
+
logger.debug("API key probe skipped transient OAuth refresh failure", { provider, error: error.message });
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async #resolve(
|
|
336
355
|
provider: string,
|
|
337
356
|
sessionId?: string,
|
|
338
357
|
options?: AuthApiKeyOptions,
|
|
@@ -363,7 +382,16 @@ export class KeyCascade implements KeysApi {
|
|
|
363
382
|
|
|
364
383
|
// Precedence: a deliberate OAuth/login credential wins, then an explicit env var,
|
|
365
384
|
// then a stored static api_key (which may be a stale broker-migrated copy) as a last resort.
|
|
366
|
-
|
|
385
|
+
// A transient OAuth refresh failure still lets later sources serve the request; it is
|
|
386
|
+
// rethrown only when none can, so callers retry instead of reporting a missing key.
|
|
387
|
+
let oauthResolved: OAuthResolutionResult | undefined;
|
|
388
|
+
let oauthRefreshFailure: AIError.OAuthRefreshUnavailableError | undefined;
|
|
389
|
+
try {
|
|
390
|
+
oauthResolved = await this.#deps.selector.resolveOAuth(provider, sessionId, options);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
if (!(error instanceof AIError.OAuthRefreshUnavailableError)) throw error;
|
|
393
|
+
oauthRefreshFailure = error;
|
|
394
|
+
}
|
|
367
395
|
if (oauthResolved) {
|
|
368
396
|
if (onCredentialId && oauthResolved.credentialId !== undefined) {
|
|
369
397
|
const { orgId, region, inferenceRegion } = oauthResolved.credential;
|
|
@@ -372,6 +400,7 @@ export class KeyCascade implements KeysApi {
|
|
|
372
400
|
return oauthResolved.apiKey;
|
|
373
401
|
}
|
|
374
402
|
if (restricted) {
|
|
403
|
+
if (oauthRefreshFailure) throw oauthRefreshFailure;
|
|
375
404
|
throw new AIError.MissingApiKeyError(
|
|
376
405
|
provider,
|
|
377
406
|
`No API key for provider: ${provider} (session ${sessionId} is restricted to its OAuth account pool and none of those accounts is available)`,
|
|
@@ -420,6 +449,7 @@ export class KeyCascade implements KeysApi {
|
|
|
420
449
|
if (apiKey !== undefined && credentialId !== undefined) onCredentialId?.(credentialId);
|
|
421
450
|
return apiKey;
|
|
422
451
|
}
|
|
452
|
+
if (oauthRefreshFailure) throw oauthRefreshFailure;
|
|
423
453
|
return undefined;
|
|
424
454
|
}
|
|
425
455
|
|
package/src/auth/oauth.ts
CHANGED
|
@@ -127,9 +127,11 @@ export class OAuthAccounts implements OAuthApi {
|
|
|
127
127
|
* `enterpriseUrl`). For pure "give me the bytes for `Authorization`"
|
|
128
128
|
* scenarios, prefer API-key resolution.
|
|
129
129
|
*
|
|
130
|
-
* Returns `undefined` when no OAuth credential is available
|
|
131
|
-
*
|
|
132
|
-
* OAuth with an explicit API key.
|
|
130
|
+
* Returns `undefined` when no usable OAuth credential is available
|
|
131
|
+
* (none stored, or every one definitively failed to refresh) or
|
|
132
|
+
* runtime/config overrides have replaced OAuth with an explicit API key.
|
|
133
|
+
* Rejects with {@link AIError.OAuthRefreshUnavailableError} (transient,
|
|
134
|
+
* retryable) when a retryable refresh failure left no usable credential.
|
|
133
135
|
*/
|
|
134
136
|
async access(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<OAuthAccess | undefined> {
|
|
135
137
|
// Runtime / config overrides intentionally short-circuit OAuth: when the
|
package/src/auth/select.ts
CHANGED
|
@@ -65,6 +65,8 @@ export type TryOAuthOptions = {
|
|
|
65
65
|
blockScopes?: readonly string[];
|
|
66
66
|
/** When false, a definitive failure of THIS credential returns undefined instead of falling back to the ranked/round-robin selector (target-only resolution). */
|
|
67
67
|
allowFallback?: boolean;
|
|
68
|
+
/** Receives a non-definitive refresh failure that left this credential unusable; the caller filters for retryable ones. */
|
|
69
|
+
onTransientRefreshFailure?: (error: unknown) => void;
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
/** Services consulted by CredentialSelector for policy, usage, blocks, refresh, and session affinity. */
|
|
@@ -653,6 +655,14 @@ export class CredentialSelector {
|
|
|
653
655
|
: { selection, usage: null, usageChecked: false },
|
|
654
656
|
);
|
|
655
657
|
const preflightFailures = new Set<OAuthCandidate>();
|
|
658
|
+
// The last retryable refresh error (network, timeout, 5xx) that removed a candidate.
|
|
659
|
+
// When no candidate resolves, it is rethrown so callers retry instead of reporting
|
|
660
|
+
// a missing key. Refresher outcomes after a dead grant (row disabled, CAS lost) are
|
|
661
|
+
// classified auth failures and keep resolving to undefined.
|
|
662
|
+
let transientRefreshFailure: unknown;
|
|
663
|
+
const recordTransientRefreshFailure = (error: unknown): void => {
|
|
664
|
+
if (AIError.retriable(AIError.classify(error))) transientRefreshFailure = error;
|
|
665
|
+
};
|
|
656
666
|
|
|
657
667
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
658
668
|
candidate =>
|
|
@@ -799,16 +809,21 @@ export class CredentialSelector {
|
|
|
799
809
|
// to a sibling account with the wrong prefetched usage/plan.
|
|
800
810
|
return;
|
|
801
811
|
}
|
|
802
|
-
} else
|
|
803
|
-
|
|
804
|
-
if (
|
|
805
|
-
this.#deps.
|
|
806
|
-
provider
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
+
} else {
|
|
813
|
+
recordTransientRefreshFailure(error);
|
|
814
|
+
if (credentialId !== undefined) {
|
|
815
|
+
const latestIndex = this.#deps.pool
|
|
816
|
+
.entries(provider)
|
|
817
|
+
.findIndex(entry => entry.id === credentialId);
|
|
818
|
+
if (latestIndex !== -1) {
|
|
819
|
+
this.#deps.blocks.mark(
|
|
820
|
+
provider,
|
|
821
|
+
providerKey,
|
|
822
|
+
latestIndex,
|
|
823
|
+
Date.now() + OAUTH_REFRESH_FAILURE_BACKOFF_MS,
|
|
824
|
+
AUTH_BLOCK_SCOPE,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
812
827
|
}
|
|
813
828
|
}
|
|
814
829
|
preflightFailures.add(candidate);
|
|
@@ -882,11 +897,15 @@ export class CredentialSelector {
|
|
|
882
897
|
rankingContext,
|
|
883
898
|
blockScope,
|
|
884
899
|
blockScopes,
|
|
900
|
+
onTransientRefreshFailure: recordTransientRefreshFailure,
|
|
885
901
|
});
|
|
886
902
|
if (resolved) return resolved;
|
|
887
903
|
}
|
|
888
904
|
}
|
|
889
905
|
|
|
906
|
+
if (transientRefreshFailure !== undefined) {
|
|
907
|
+
throw new AIError.OAuthRefreshUnavailableError(provider, transientRefreshFailure);
|
|
908
|
+
}
|
|
890
909
|
return undefined;
|
|
891
910
|
}
|
|
892
911
|
|
|
@@ -1117,6 +1136,7 @@ export class CredentialSelector {
|
|
|
1117
1136
|
if (allowFallback) return this.resolveOAuth(provider, sessionId, options);
|
|
1118
1137
|
}
|
|
1119
1138
|
} else {
|
|
1139
|
+
usageOptions.onTransientRefreshFailure?.(error);
|
|
1120
1140
|
// Block temporarily for transient failures (5 minutes)
|
|
1121
1141
|
this.#deps.blocks.mark(
|
|
1122
1142
|
provider,
|
package/src/auth/types.ts
CHANGED
|
@@ -894,9 +894,20 @@ export interface KeysApi {
|
|
|
894
894
|
* 4. API key persisted by a successful `/login`
|
|
895
895
|
* 5. Environment variable
|
|
896
896
|
* 6. Stored API key (e.g. a broker-migrated copy) — last resort, so an explicit env var wins
|
|
897
|
+
*
|
|
898
|
+
* Resolves `undefined` when no permitted credential can serve, including when
|
|
899
|
+
* OAuth refresh failed transiently (network, timeout, 5xx), so availability
|
|
900
|
+
* probes move on to their next candidate. Request paths use
|
|
901
|
+
* {@link KeysApi.getWithCredential}, which surfaces that failure.
|
|
897
902
|
*/
|
|
898
903
|
get(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
|
|
899
|
-
/**
|
|
904
|
+
/**
|
|
905
|
+
* Resolve a bearer together with its durable stored credential row id, when known.
|
|
906
|
+
* Same precedence as {@link KeysApi.get}, but rejects with
|
|
907
|
+
* `OAuthRefreshUnavailableError` (transient, retryable) when OAuth refresh failed
|
|
908
|
+
* with a retryable error and no other permitted source could serve the request;
|
|
909
|
+
* the stored credential is kept.
|
|
910
|
+
*/
|
|
900
911
|
getWithCredential(
|
|
901
912
|
provider: string,
|
|
902
913
|
sessionId?: string,
|
|
@@ -1017,9 +1028,11 @@ export interface OAuthApi {
|
|
|
1017
1028
|
* `enterpriseUrl`). For pure "give me the bytes for `Authorization`"
|
|
1018
1029
|
* scenarios, prefer {@link AuthStorage.keys.get}.
|
|
1019
1030
|
*
|
|
1020
|
-
* Returns `undefined` when no OAuth credential is available
|
|
1021
|
-
*
|
|
1022
|
-
* OAuth with an explicit API key.
|
|
1031
|
+
* Returns `undefined` when no usable OAuth credential is available (none
|
|
1032
|
+
* stored, or every one definitively failed to refresh) or runtime/config
|
|
1033
|
+
* overrides have replaced OAuth with an explicit API key. Rejects with
|
|
1034
|
+
* `OAuthRefreshUnavailableError` (transient, retryable) when a retryable
|
|
1035
|
+
* refresh failure left no usable credential.
|
|
1023
1036
|
*/
|
|
1024
1037
|
access(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<OAuthAccess | undefined>;
|
|
1025
1038
|
/**
|
package/src/error/auth.ts
CHANGED
|
@@ -19,6 +19,26 @@ export class MissingApiKeyError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Every usable OAuth credential for a provider failed to refresh with a
|
|
24
|
+
* retryable error (network blip, timeout, 5xx). The stored credentials are
|
|
25
|
+
* intact, so this is classified transient: retrying the request can succeed,
|
|
26
|
+
* unlike {@link MissingApiKeyError}. The refresh failure is kept as `cause`.
|
|
27
|
+
*/
|
|
28
|
+
export class OAuthRefreshUnavailableError extends Error {
|
|
29
|
+
readonly provider: string;
|
|
30
|
+
|
|
31
|
+
constructor(provider: string, cause: unknown) {
|
|
32
|
+
super(
|
|
33
|
+
`OAuth token refresh failed for provider ${provider}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
34
|
+
{ cause },
|
|
35
|
+
);
|
|
36
|
+
this.name = "OAuthRefreshUnavailableError";
|
|
37
|
+
this.provider = provider;
|
|
38
|
+
attach(this, create(Flag.Transient));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
/** A user-facing login flow required an `onPrompt` callback that was not supplied. */
|
|
23
43
|
export class OnPromptRequiredError extends Error {
|
|
24
44
|
constructor(providerLabel: string) {
|