tiny-http-mcp-server 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composition.json +1 -1
- package/node_modules/auth-store/README.md +4 -0
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +3 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +8 -0
- package/node_modules/mcp-oauth/README.md +17 -3
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +17 -2
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +18 -1
- package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +3 -1
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +7 -3
- package/node_modules/mcp-oauth/dist/client/types.d.ts +2 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +4 -0
- package/node_modules/tiny-mcp-client/dist/index.js +45 -7
- package/package.json +1 -1
package/dist/composition.json
CHANGED
|
@@ -56,6 +56,10 @@ directory or filesystem adapter. Injected encrypted-file adapters need
|
|
|
56
56
|
- Configurable salt, directory, and file name
|
|
57
57
|
- File permissions: `0600`
|
|
58
58
|
- Random IV per write
|
|
59
|
+
- `fileStore.throwOnInvalidDocument: true` makes malformed or unauthenticated
|
|
60
|
+
existing documents fail with a safe diagnostic; missing files still return
|
|
61
|
+
`null`. The default treats invalid documents as absent. Strict reads preserve
|
|
62
|
+
the existing file until an explicit reset or replacement.
|
|
59
63
|
|
|
60
64
|
### macOS Keychain
|
|
61
65
|
|
|
@@ -31,6 +31,8 @@ export interface EncryptedFileStoreInput {
|
|
|
31
31
|
getMachineIdentity?: () => MachineIdentity | Promise<MachineIdentity>;
|
|
32
32
|
getHomeDirectory?: () => string;
|
|
33
33
|
getRandomBytes?: (size: number) => Buffer;
|
|
34
|
+
/** Fail closed instead of treating malformed or unauthenticated documents as absent. */
|
|
35
|
+
throwOnInvalidDocument?: boolean;
|
|
34
36
|
}
|
|
35
37
|
export declare class EncryptedFileStore implements SecretStore {
|
|
36
38
|
private readonly fs;
|
|
@@ -40,6 +42,7 @@ export declare class EncryptedFileStore implements SecretStore {
|
|
|
40
42
|
private readonly getMachineIdentity;
|
|
41
43
|
private readonly getRandomBytes;
|
|
42
44
|
private keyPromise;
|
|
45
|
+
private readonly throwOnInvalidDocument;
|
|
43
46
|
constructor(input: EncryptedFileStoreInput);
|
|
44
47
|
get(): Promise<string | null>;
|
|
45
48
|
withLock<T>(operation: () => Promise<T>, options?: SecretStoreLockOptions): Promise<T>;
|
|
@@ -19,6 +19,7 @@ export class EncryptedFileStore {
|
|
|
19
19
|
getMachineIdentity;
|
|
20
20
|
getRandomBytes;
|
|
21
21
|
keyPromise = null;
|
|
22
|
+
throwOnInvalidDocument;
|
|
22
23
|
constructor(input) {
|
|
23
24
|
this.fs = input.fs ?? fs;
|
|
24
25
|
this.salt = input.salt;
|
|
@@ -37,6 +38,7 @@ export class EncryptedFileStore {
|
|
|
37
38
|
}
|
|
38
39
|
this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
|
|
39
40
|
this.getRandomBytes = input.getRandomBytes ?? randomBytes;
|
|
41
|
+
this.throwOnInvalidDocument = input.throwOnInvalidDocument ?? false;
|
|
40
42
|
}
|
|
41
43
|
async get() {
|
|
42
44
|
await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
|
|
@@ -52,6 +54,8 @@ export class EncryptedFileStore {
|
|
|
52
54
|
}
|
|
53
55
|
const document = parseEncryptedDocument(rawDocument);
|
|
54
56
|
if (!document) {
|
|
57
|
+
if (this.throwOnInvalidDocument)
|
|
58
|
+
throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
|
|
55
59
|
return null;
|
|
56
60
|
}
|
|
57
61
|
const key = await this.getEncryptionKey();
|
|
@@ -61,6 +65,8 @@ export class EncryptedFileStore {
|
|
|
61
65
|
const ciphertext = Buffer.from(document.ciphertext, "base64");
|
|
62
66
|
if (iv.byteLength !== ENCRYPTION_IV_BYTES ||
|
|
63
67
|
authTag.byteLength !== ENCRYPTION_AUTH_TAG_BYTES) {
|
|
68
|
+
if (this.throwOnInvalidDocument)
|
|
69
|
+
throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
|
|
64
70
|
return null;
|
|
65
71
|
}
|
|
66
72
|
const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);
|
|
@@ -69,6 +75,8 @@ export class EncryptedFileStore {
|
|
|
69
75
|
return plaintext.toString("utf8");
|
|
70
76
|
}
|
|
71
77
|
catch {
|
|
78
|
+
if (this.throwOnInvalidDocument)
|
|
79
|
+
throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
|
|
72
80
|
return null;
|
|
73
81
|
}
|
|
74
82
|
}
|
|
@@ -118,9 +118,23 @@ acquisition, while token and browser operations retain their own deadlines.
|
|
|
118
118
|
The native `auth-store` session adapter implements this hook for both encrypted
|
|
119
119
|
files and Keychain identities, including across independent processes. Locks
|
|
120
120
|
cover the complete read, refresh/authorization and persisted winner. Dead-owner
|
|
121
|
-
claims are recovered without stealing a live transaction.
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
claims are recovered without stealing a live transaction.
|
|
122
|
+
|
|
123
|
+
Before sending a refresh request, the provider persists a tokenless session with
|
|
124
|
+
`refreshState: "pending"`, retaining the original client and discovery binding.
|
|
125
|
+
A successful response replaces it with the rotated grant. A crash, cancellation,
|
|
126
|
+
network disconnect or incomplete response leaves the marker, so another process
|
|
127
|
+
cannot replay a possibly consumed refresh token or revive the initial import.
|
|
128
|
+
Such a session requires fresh authorization. Interactive unauthorized handling
|
|
129
|
+
can recover it; headless requests fail with an explicit unknown-outcome error.
|
|
130
|
+
Only complete OAuth error responses establish a rejected request and allow a
|
|
131
|
+
transient retry or restoration of the original grant. Gateway error pages do not.
|
|
132
|
+
|
|
133
|
+
Native persisted OAuth reads always reject corrupt encrypted documents and
|
|
134
|
+
invalid stored JSON, with diagnostics that omit decrypted contents. They retain
|
|
135
|
+
the existing record for explicit reset rather than interpreting corruption as
|
|
136
|
+
an absent session and reviving an initial grant. Caller file-backend settings
|
|
137
|
+
cannot disable this policy.
|
|
124
138
|
|
|
125
139
|
## Environment Variables
|
|
126
140
|
|
|
@@ -23,7 +23,13 @@ export function createAuthStoreSessionStore(options = {}) {
|
|
|
23
23
|
if (value === null) {
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(value);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error("Stored OAuth session must be valid JSON; reset the store explicitly to recover");
|
|
32
|
+
}
|
|
27
33
|
if (isStoredOAuthSession(parsed)) {
|
|
28
34
|
return parsed;
|
|
29
35
|
}
|
|
@@ -47,7 +53,13 @@ export function createAuthStoreClientStore(options) {
|
|
|
47
53
|
if (value === null) {
|
|
48
54
|
return null;
|
|
49
55
|
}
|
|
50
|
-
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(value);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
|
|
62
|
+
}
|
|
51
63
|
const clientId = isObjectRecord(parsed) ? getOwnString(parsed, "clientId") : undefined;
|
|
52
64
|
if (clientId !== undefined) {
|
|
53
65
|
const client = { clientId };
|
|
@@ -75,6 +87,7 @@ function createNamedSecretStore(key, options, defaults) {
|
|
|
75
87
|
const parsedFilePath = configuredFilePath === undefined ? null : path.parse(configuredFilePath);
|
|
76
88
|
const fileStore = {
|
|
77
89
|
...options.fileStore,
|
|
90
|
+
throwOnInvalidDocument: true,
|
|
78
91
|
filePath: parsedFilePath === null
|
|
79
92
|
? undefined
|
|
80
93
|
: path.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
|
|
@@ -125,6 +138,8 @@ function isStoredOAuthSession(value) {
|
|
|
125
138
|
isNonBlankOwnString(value, "authorizationServer") &&
|
|
126
139
|
isStoredOAuthClient(getOwnEntry(value, "client")) &&
|
|
127
140
|
isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
|
|
141
|
+
(getOwnEntry(value, "refreshState") === undefined ||
|
|
142
|
+
(getOwnEntry(value, "refreshState") === "pending" && getOwnEntry(value, "tokens") === undefined)) &&
|
|
128
143
|
isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
|
|
129
144
|
}
|
|
130
145
|
function isStoredOAuthClient(value) {
|
|
@@ -140,6 +140,11 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
140
140
|
if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
141
141
|
forceRefresh = false;
|
|
142
142
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
143
|
+
if (session?.refreshState === "pending") {
|
|
144
|
+
if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
|
|
145
|
+
throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
|
|
146
|
+
return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch, signal);
|
|
147
|
+
}
|
|
143
148
|
if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
144
149
|
return session;
|
|
145
150
|
}
|
|
@@ -169,6 +174,9 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
169
174
|
if (session.tokens?.refreshToken === undefined) {
|
|
170
175
|
return session;
|
|
171
176
|
}
|
|
177
|
+
const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
|
|
178
|
+
await saveSession(resource, pendingSession);
|
|
179
|
+
signal?.throwIfAborted();
|
|
172
180
|
let refreshAttempted = false;
|
|
173
181
|
let refreshedTokens;
|
|
174
182
|
while (true) {
|
|
@@ -186,7 +194,11 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
186
194
|
}
|
|
187
195
|
catch (error) {
|
|
188
196
|
signal?.throwIfAborted();
|
|
189
|
-
|
|
197
|
+
// Network errors, lost/malformed bodies and gateway failures cannot
|
|
198
|
+
// establish whether a rotating refresh token was already consumed.
|
|
199
|
+
if (!(error instanceof OAuthError) || !error.outcomeKnown)
|
|
200
|
+
throw error;
|
|
201
|
+
if (error.error === "invalid_grant") {
|
|
190
202
|
const clearedSession = clearSessionTokens(session);
|
|
191
203
|
await saveSession(resource, clearedSession);
|
|
192
204
|
return clearedSession;
|
|
@@ -200,6 +212,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
200
212
|
refreshAttempted = true;
|
|
201
213
|
continue;
|
|
202
214
|
}
|
|
215
|
+
await saveSession(resource, session);
|
|
203
216
|
throw error;
|
|
204
217
|
}
|
|
205
218
|
}
|
|
@@ -457,6 +470,7 @@ function sameTokenGrant(left, right) {
|
|
|
457
470
|
function clearSessionTokens(session) {
|
|
458
471
|
const nextSession = { ...session };
|
|
459
472
|
delete nextSession.tokens;
|
|
473
|
+
delete nextSession.refreshState;
|
|
460
474
|
return nextSession;
|
|
461
475
|
}
|
|
462
476
|
function hasCachedAccessToken(session) {
|
|
@@ -466,6 +480,9 @@ function normalizeLoadedSession(session) {
|
|
|
466
480
|
if (session === null) {
|
|
467
481
|
return null;
|
|
468
482
|
}
|
|
483
|
+
const refreshState = getOwnEntry(session, "refreshState");
|
|
484
|
+
if (refreshState !== undefined && (refreshState !== "pending" || getOwnEntry(session, "tokens") !== undefined))
|
|
485
|
+
throw new Error("Stored OAuth refresh state is invalid");
|
|
469
486
|
const client = normalizeStoredClient(getOwnEntry(session, "client"));
|
|
470
487
|
if (client === null) {
|
|
471
488
|
return { ...session, client: { clientId: "" }, tokens: undefined };
|
|
@@ -13,7 +13,9 @@ export declare class OAuthError extends Error {
|
|
|
13
13
|
readonly status: number;
|
|
14
14
|
readonly retryable: boolean;
|
|
15
15
|
readonly terminal: boolean;
|
|
16
|
-
|
|
16
|
+
/** True only when a complete OAuth error response establishes rejection. */
|
|
17
|
+
readonly outcomeKnown: boolean;
|
|
18
|
+
constructor(shape: OAuthErrorShape, status: number, outcomeKnown?: boolean);
|
|
17
19
|
}
|
|
18
20
|
export declare function isRetryableOAuthError(error: unknown): error is OAuthError;
|
|
19
21
|
export declare function exchangeAuthorizationCode(input: {
|
|
@@ -11,7 +11,9 @@ export class OAuthError extends Error {
|
|
|
11
11
|
status;
|
|
12
12
|
retryable;
|
|
13
13
|
terminal;
|
|
14
|
-
|
|
14
|
+
/** True only when a complete OAuth error response establishes rejection. */
|
|
15
|
+
outcomeKnown;
|
|
16
|
+
constructor(shape, status, outcomeKnown = true) {
|
|
15
17
|
super(shape.error_description ?? shape.error);
|
|
16
18
|
this.name = "OAuthError";
|
|
17
19
|
this.error = shape.error;
|
|
@@ -22,6 +24,7 @@ export class OAuthError extends Error {
|
|
|
22
24
|
this.status = status;
|
|
23
25
|
this.retryable = isRetryableOAuthError(this);
|
|
24
26
|
this.terminal = !this.retryable;
|
|
27
|
+
this.outcomeKnown = outcomeKnown;
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
export function isRetryableOAuthError(error) {
|
|
@@ -147,7 +150,8 @@ export async function readOAuthJsonObjectResponse(response, signal) {
|
|
|
147
150
|
}
|
|
148
151
|
const record = payload;
|
|
149
152
|
if (!response.ok) {
|
|
150
|
-
|
|
153
|
+
const error = getOwnEntry(record, "error");
|
|
154
|
+
throw new OAuthError(readOAuthError(record, fallbackError.error), response.status, typeof error === "string" && error.trim().length > 0);
|
|
151
155
|
}
|
|
152
156
|
return record;
|
|
153
157
|
}
|
|
@@ -166,7 +170,7 @@ function getOwnEntry(record, key) {
|
|
|
166
170
|
}
|
|
167
171
|
function createFallbackOAuthError(status) {
|
|
168
172
|
const error = status === 503 ? "temporarily_unavailable" : "server_error";
|
|
169
|
-
return new OAuthError({ error }, status);
|
|
173
|
+
return new OAuthError({ error }, status, false);
|
|
170
174
|
}
|
|
171
175
|
function normalizeBearerTokenType(value) {
|
|
172
176
|
if (typeof value !== "string") {
|
|
@@ -78,6 +78,8 @@ export interface StoredOAuthSession {
|
|
|
78
78
|
clientSecret?: string;
|
|
79
79
|
};
|
|
80
80
|
tokens?: StoredOAuthTokens;
|
|
81
|
+
/** A refresh was begun; its winning response may not have been persisted. */
|
|
82
|
+
refreshState?: "pending";
|
|
81
83
|
discovery: {
|
|
82
84
|
resourceMetadataUrl: string;
|
|
83
85
|
resourceMetadata: Record<string, unknown>;
|
|
@@ -70,6 +70,8 @@ interface EncryptedFileStoreInput {
|
|
|
70
70
|
getMachineIdentity?: () => MachineIdentity | Promise<MachineIdentity>;
|
|
71
71
|
getHomeDirectory?: () => string;
|
|
72
72
|
getRandomBytes?: (size: number) => Buffer;
|
|
73
|
+
/** Fail closed instead of treating malformed or unauthenticated documents as absent. */
|
|
74
|
+
throwOnInvalidDocument?: boolean;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
interface KeychainCommandResult {
|
|
@@ -179,6 +181,8 @@ interface StoredOAuthSession {
|
|
|
179
181
|
clientSecret?: string;
|
|
180
182
|
};
|
|
181
183
|
tokens?: StoredOAuthTokens;
|
|
184
|
+
/** A refresh was begun; its winning response may not have been persisted. */
|
|
185
|
+
refreshState?: "pending";
|
|
182
186
|
discovery: {
|
|
183
187
|
resourceMetadataUrl: string;
|
|
184
188
|
resourceMetadata: Record<string, unknown>;
|
|
@@ -3575,6 +3575,7 @@ var EncryptedFileStore = class {
|
|
|
3575
3575
|
getMachineIdentity;
|
|
3576
3576
|
getRandomBytes;
|
|
3577
3577
|
keyPromise = null;
|
|
3578
|
+
throwOnInvalidDocument;
|
|
3578
3579
|
constructor(input) {
|
|
3579
3580
|
this.fs = input.fs ?? fs;
|
|
3580
3581
|
this.salt = input.salt;
|
|
@@ -3592,6 +3593,7 @@ var EncryptedFileStore = class {
|
|
|
3592
3593
|
}
|
|
3593
3594
|
this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
|
|
3594
3595
|
this.getRandomBytes = input.getRandomBytes ?? randomBytes;
|
|
3596
|
+
this.throwOnInvalidDocument = input.throwOnInvalidDocument ?? false;
|
|
3595
3597
|
}
|
|
3596
3598
|
async get() {
|
|
3597
3599
|
await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
|
|
@@ -3606,6 +3608,8 @@ var EncryptedFileStore = class {
|
|
|
3606
3608
|
}
|
|
3607
3609
|
const document = parseEncryptedDocument(rawDocument);
|
|
3608
3610
|
if (!document) {
|
|
3611
|
+
if (this.throwOnInvalidDocument)
|
|
3612
|
+
throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
|
|
3609
3613
|
return null;
|
|
3610
3614
|
}
|
|
3611
3615
|
const key2 = await this.getEncryptionKey();
|
|
@@ -3614,6 +3618,8 @@ var EncryptedFileStore = class {
|
|
|
3614
3618
|
const authTag = Buffer.from(document.authTag, "base64");
|
|
3615
3619
|
const ciphertext = Buffer.from(document.ciphertext, "base64");
|
|
3616
3620
|
if (iv.byteLength !== ENCRYPTION_IV_BYTES || authTag.byteLength !== ENCRYPTION_AUTH_TAG_BYTES) {
|
|
3621
|
+
if (this.throwOnInvalidDocument)
|
|
3622
|
+
throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
|
|
3617
3623
|
return null;
|
|
3618
3624
|
}
|
|
3619
3625
|
const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key2, iv);
|
|
@@ -3621,6 +3627,8 @@ var EncryptedFileStore = class {
|
|
|
3621
3627
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
3622
3628
|
return plaintext.toString("utf8");
|
|
3623
3629
|
} catch {
|
|
3630
|
+
if (this.throwOnInvalidDocument)
|
|
3631
|
+
throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
|
|
3624
3632
|
return null;
|
|
3625
3633
|
}
|
|
3626
3634
|
}
|
|
@@ -4083,7 +4091,12 @@ function createAuthStoreSessionStore(options = {}) {
|
|
|
4083
4091
|
if (value === null) {
|
|
4084
4092
|
return null;
|
|
4085
4093
|
}
|
|
4086
|
-
|
|
4094
|
+
let parsed;
|
|
4095
|
+
try {
|
|
4096
|
+
parsed = JSON.parse(value);
|
|
4097
|
+
} catch {
|
|
4098
|
+
throw new Error("Stored OAuth session must be valid JSON; reset the store explicitly to recover");
|
|
4099
|
+
}
|
|
4087
4100
|
if (isStoredOAuthSession(parsed)) {
|
|
4088
4101
|
return parsed;
|
|
4089
4102
|
}
|
|
@@ -4107,7 +4120,12 @@ function createAuthStoreClientStore(options) {
|
|
|
4107
4120
|
if (value === null) {
|
|
4108
4121
|
return null;
|
|
4109
4122
|
}
|
|
4110
|
-
|
|
4123
|
+
let parsed;
|
|
4124
|
+
try {
|
|
4125
|
+
parsed = JSON.parse(value);
|
|
4126
|
+
} catch {
|
|
4127
|
+
throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
|
|
4128
|
+
}
|
|
4111
4129
|
const clientId = isObjectRecord(parsed) ? getOwnString(parsed, "clientId") : void 0;
|
|
4112
4130
|
if (clientId !== void 0) {
|
|
4113
4131
|
const client = { clientId };
|
|
@@ -4134,6 +4152,7 @@ function createNamedSecretStore(key2, options, defaults) {
|
|
|
4134
4152
|
const parsedFilePath = configuredFilePath === void 0 ? null : path4.parse(configuredFilePath);
|
|
4135
4153
|
const fileStore = {
|
|
4136
4154
|
...options.fileStore,
|
|
4155
|
+
throwOnInvalidDocument: true,
|
|
4137
4156
|
filePath: parsedFilePath === null ? void 0 : path4.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
|
|
4138
4157
|
salt: options.fileStore?.salt ?? defaults.salt,
|
|
4139
4158
|
defaultDirectory: options.fileStore?.defaultDirectory || defaults.directory,
|
|
@@ -4176,7 +4195,7 @@ function isStoredOAuthSession(value) {
|
|
|
4176
4195
|
if (!isObjectRecord(value)) {
|
|
4177
4196
|
return false;
|
|
4178
4197
|
}
|
|
4179
|
-
return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && isStoredOAuthClient(getOwnEntry3(value, "client")) && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
|
|
4198
|
+
return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && isStoredOAuthClient(getOwnEntry3(value, "client")) && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && (getOwnEntry3(value, "refreshState") === void 0 || getOwnEntry3(value, "refreshState") === "pending" && getOwnEntry3(value, "tokens") === void 0) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
|
|
4180
4199
|
}
|
|
4181
4200
|
function isStoredOAuthClient(value) {
|
|
4182
4201
|
if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
|
|
@@ -4605,7 +4624,9 @@ var OAuthError = class extends Error {
|
|
|
4605
4624
|
status;
|
|
4606
4625
|
retryable;
|
|
4607
4626
|
terminal;
|
|
4608
|
-
|
|
4627
|
+
/** True only when a complete OAuth error response establishes rejection. */
|
|
4628
|
+
outcomeKnown;
|
|
4629
|
+
constructor(shape, status, outcomeKnown = true) {
|
|
4609
4630
|
super(shape.error_description ?? shape.error);
|
|
4610
4631
|
this.name = "OAuthError";
|
|
4611
4632
|
this.error = shape.error;
|
|
@@ -4616,6 +4637,7 @@ var OAuthError = class extends Error {
|
|
|
4616
4637
|
this.status = status;
|
|
4617
4638
|
this.retryable = isRetryableOAuthError(this);
|
|
4618
4639
|
this.terminal = !this.retryable;
|
|
4640
|
+
this.outcomeKnown = outcomeKnown;
|
|
4619
4641
|
}
|
|
4620
4642
|
};
|
|
4621
4643
|
function isRetryableOAuthError(error) {
|
|
@@ -4730,7 +4752,8 @@ async function readOAuthJsonObjectResponse(response, signal) {
|
|
|
4730
4752
|
}
|
|
4731
4753
|
const record2 = payload;
|
|
4732
4754
|
if (!response.ok) {
|
|
4733
|
-
|
|
4755
|
+
const error = getOwnEntry5(record2, "error");
|
|
4756
|
+
throw new OAuthError(readOAuthError(record2, fallbackError.error), response.status, typeof error === "string" && error.trim().length > 0);
|
|
4734
4757
|
}
|
|
4735
4758
|
return record2;
|
|
4736
4759
|
}
|
|
@@ -4749,7 +4772,7 @@ function getOwnEntry5(record2, key2) {
|
|
|
4749
4772
|
}
|
|
4750
4773
|
function createFallbackOAuthError(status) {
|
|
4751
4774
|
const error = status === 503 ? "temporarily_unavailable" : "server_error";
|
|
4752
|
-
return new OAuthError({ error }, status);
|
|
4775
|
+
return new OAuthError({ error }, status, false);
|
|
4753
4776
|
}
|
|
4754
4777
|
function normalizeBearerTokenType(value) {
|
|
4755
4778
|
if (typeof value !== "string") {
|
|
@@ -4935,6 +4958,11 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4935
4958
|
if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
4936
4959
|
forceRefresh = false;
|
|
4937
4960
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
4961
|
+
if (session?.refreshState === "pending") {
|
|
4962
|
+
if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
|
|
4963
|
+
throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
|
|
4964
|
+
return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch2, signal);
|
|
4965
|
+
}
|
|
4938
4966
|
if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
4939
4967
|
return session;
|
|
4940
4968
|
}
|
|
@@ -4962,6 +4990,9 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4962
4990
|
if (session.tokens?.refreshToken === void 0) {
|
|
4963
4991
|
return session;
|
|
4964
4992
|
}
|
|
4993
|
+
const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
|
|
4994
|
+
await saveSession(resource, pendingSession);
|
|
4995
|
+
signal?.throwIfAborted();
|
|
4965
4996
|
let refreshAttempted = false;
|
|
4966
4997
|
let refreshedTokens;
|
|
4967
4998
|
while (true) {
|
|
@@ -4979,7 +5010,9 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4979
5010
|
break;
|
|
4980
5011
|
} catch (error) {
|
|
4981
5012
|
signal?.throwIfAborted();
|
|
4982
|
-
if (error instanceof OAuthError
|
|
5013
|
+
if (!(error instanceof OAuthError) || !error.outcomeKnown)
|
|
5014
|
+
throw error;
|
|
5015
|
+
if (error.error === "invalid_grant") {
|
|
4983
5016
|
const clearedSession = clearSessionTokens(session);
|
|
4984
5017
|
await saveSession(resource, clearedSession);
|
|
4985
5018
|
return clearedSession;
|
|
@@ -4993,6 +5026,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4993
5026
|
refreshAttempted = true;
|
|
4994
5027
|
continue;
|
|
4995
5028
|
}
|
|
5029
|
+
await saveSession(resource, session);
|
|
4996
5030
|
throw error;
|
|
4997
5031
|
}
|
|
4998
5032
|
}
|
|
@@ -5239,6 +5273,7 @@ function sameTokenGrant(left, right) {
|
|
|
5239
5273
|
function clearSessionTokens(session) {
|
|
5240
5274
|
const nextSession = { ...session };
|
|
5241
5275
|
delete nextSession.tokens;
|
|
5276
|
+
delete nextSession.refreshState;
|
|
5242
5277
|
return nextSession;
|
|
5243
5278
|
}
|
|
5244
5279
|
function hasCachedAccessToken(session) {
|
|
@@ -5248,6 +5283,9 @@ function normalizeLoadedSession(session) {
|
|
|
5248
5283
|
if (session === null) {
|
|
5249
5284
|
return null;
|
|
5250
5285
|
}
|
|
5286
|
+
const refreshState = getOwnEntry6(session, "refreshState");
|
|
5287
|
+
if (refreshState !== void 0 && (refreshState !== "pending" || getOwnEntry6(session, "tokens") !== void 0))
|
|
5288
|
+
throw new Error("Stored OAuth refresh state is invalid");
|
|
5251
5289
|
const client = normalizeStoredClient(getOwnEntry6(session, "client"));
|
|
5252
5290
|
if (client === null) {
|
|
5253
5291
|
return { ...session, client: { clientId: "" }, tokens: void 0 };
|