@leanmcp/auth 0.3.1 → 0.4.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 +148 -661
- package/dist/{auth0-54GZT2EI.mjs → auth0-UTD4QBG6.mjs} +4 -2
- package/dist/chunk-LPEX4YW6.mjs +13 -0
- package/dist/{chunk-EVD2TRPR.mjs → chunk-P4HFKA5R.mjs} +50 -21
- package/dist/chunk-RGCCBQWG.mjs +113 -0
- package/dist/chunk-ZOPKMOPV.mjs +53 -0
- package/dist/{clerk-FR7ITM33.mjs → clerk-3SDKGD6C.mjs} +4 -2
- package/dist/client/index.d.mts +499 -0
- package/dist/client/index.d.ts +499 -0
- package/dist/client/index.js +1039 -0
- package/dist/client/index.mjs +869 -0
- package/dist/{cognito-I6V5YNYM.mjs → cognito-QQT7LK2Y.mjs} +4 -2
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +186 -15
- package/dist/index.mjs +2 -1
- package/dist/leanmcp-Y7TXNSTD.mjs +140 -0
- package/dist/proxy/index.d.mts +376 -0
- package/dist/proxy/index.d.ts +376 -0
- package/dist/proxy/index.js +536 -0
- package/dist/proxy/index.mjs +480 -0
- package/dist/server/index.d.mts +496 -0
- package/dist/server/index.d.ts +496 -0
- package/dist/server/index.js +882 -0
- package/dist/server/index.mjs +847 -0
- package/dist/storage/index.d.mts +181 -0
- package/dist/storage/index.d.ts +181 -0
- package/dist/storage/index.js +499 -0
- package/dist/storage/index.mjs +372 -0
- package/dist/types-DMpGN530.d.mts +122 -0
- package/dist/types-DMpGN530.d.ts +122 -0
- package/package.json +45 -8
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/client/index.ts
|
|
22
|
+
var client_exports = {};
|
|
23
|
+
__export(client_exports, {
|
|
24
|
+
OAuthClient: () => OAuthClient,
|
|
25
|
+
RefreshManager: () => RefreshManager,
|
|
26
|
+
clearMetadataCache: () => clearMetadataCache,
|
|
27
|
+
createRefreshManager: () => createRefreshManager,
|
|
28
|
+
discoverOAuthMetadata: () => discoverOAuthMetadata,
|
|
29
|
+
findAvailablePort: () => findAvailablePort,
|
|
30
|
+
generateCodeChallenge: () => generateCodeChallenge,
|
|
31
|
+
generateCodeVerifier: () => generateCodeVerifier,
|
|
32
|
+
generatePKCE: () => generatePKCE,
|
|
33
|
+
isValidCodeVerifier: () => isValidCodeVerifier,
|
|
34
|
+
serverSupports: () => serverSupports,
|
|
35
|
+
startCallbackServer: () => startCallbackServer,
|
|
36
|
+
validateMetadata: () => validateMetadata,
|
|
37
|
+
verifyPKCE: () => verifyPKCE
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(client_exports);
|
|
40
|
+
|
|
41
|
+
// src/client/pkce.ts
|
|
42
|
+
var import_crypto = require("crypto");
|
|
43
|
+
function generateCodeVerifier(length = 64) {
|
|
44
|
+
if (length < 43 || length > 128) {
|
|
45
|
+
throw new Error("PKCE code verifier must be between 43-128 characters");
|
|
46
|
+
}
|
|
47
|
+
const bytesNeeded = Math.ceil(length * 0.75);
|
|
48
|
+
const randomBuffer = (0, import_crypto.randomBytes)(bytesNeeded);
|
|
49
|
+
return randomBuffer.toString("base64url").slice(0, length);
|
|
50
|
+
}
|
|
51
|
+
__name(generateCodeVerifier, "generateCodeVerifier");
|
|
52
|
+
function generateCodeChallenge(verifier) {
|
|
53
|
+
return (0, import_crypto.createHash)("sha256").update(verifier, "utf8").digest("base64url");
|
|
54
|
+
}
|
|
55
|
+
__name(generateCodeChallenge, "generateCodeChallenge");
|
|
56
|
+
function generatePKCE(verifierLength = 64) {
|
|
57
|
+
const verifier = generateCodeVerifier(verifierLength);
|
|
58
|
+
const challenge = generateCodeChallenge(verifier);
|
|
59
|
+
return {
|
|
60
|
+
verifier,
|
|
61
|
+
challenge,
|
|
62
|
+
method: "S256"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
__name(generatePKCE, "generatePKCE");
|
|
66
|
+
function verifyPKCE(verifier, challenge, method = "S256") {
|
|
67
|
+
if (method === "plain") {
|
|
68
|
+
return verifier === challenge;
|
|
69
|
+
}
|
|
70
|
+
const expectedChallenge = generateCodeChallenge(verifier);
|
|
71
|
+
return expectedChallenge === challenge;
|
|
72
|
+
}
|
|
73
|
+
__name(verifyPKCE, "verifyPKCE");
|
|
74
|
+
function isValidCodeVerifier(verifier) {
|
|
75
|
+
if (verifier.length < 43 || verifier.length > 128) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const validPattern = /^[A-Za-z0-9\-._~]+$/;
|
|
79
|
+
return validPattern.test(verifier);
|
|
80
|
+
}
|
|
81
|
+
__name(isValidCodeVerifier, "isValidCodeVerifier");
|
|
82
|
+
|
|
83
|
+
// src/client/callback-server.ts
|
|
84
|
+
var import_http = require("http");
|
|
85
|
+
var import_url = require("url");
|
|
86
|
+
var DEFAULT_SUCCESS_HTML = `
|
|
87
|
+
<!DOCTYPE html>
|
|
88
|
+
<html>
|
|
89
|
+
<head>
|
|
90
|
+
<title>Authentication Successful</title>
|
|
91
|
+
<style>
|
|
92
|
+
body {
|
|
93
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
94
|
+
display: flex;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
align-items: center;
|
|
97
|
+
height: 100vh;
|
|
98
|
+
margin: 0;
|
|
99
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
100
|
+
color: white;
|
|
101
|
+
}
|
|
102
|
+
.container {
|
|
103
|
+
text-align: center;
|
|
104
|
+
padding: 40px;
|
|
105
|
+
background: rgba(255,255,255,0.1);
|
|
106
|
+
border-radius: 16px;
|
|
107
|
+
backdrop-filter: blur(10px);
|
|
108
|
+
}
|
|
109
|
+
.success-icon { font-size: 64px; margin-bottom: 20px; }
|
|
110
|
+
h1 { margin: 0 0 10px; font-size: 24px; }
|
|
111
|
+
p { margin: 0; opacity: 0.9; }
|
|
112
|
+
</style>
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<div class="container">
|
|
116
|
+
<div class="success-icon">\u2713</div>
|
|
117
|
+
<h1>Authentication Successful</h1>
|
|
118
|
+
<p>You can close this window and return to your application.</p>
|
|
119
|
+
</div>
|
|
120
|
+
</body>
|
|
121
|
+
</html>
|
|
122
|
+
`;
|
|
123
|
+
var DEFAULT_ERROR_HTML = `
|
|
124
|
+
<!DOCTYPE html>
|
|
125
|
+
<html>
|
|
126
|
+
<head>
|
|
127
|
+
<title>Authentication Failed</title>
|
|
128
|
+
<style>
|
|
129
|
+
body {
|
|
130
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
131
|
+
display: flex;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
align-items: center;
|
|
134
|
+
height: 100vh;
|
|
135
|
+
margin: 0;
|
|
136
|
+
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
|
137
|
+
color: white;
|
|
138
|
+
}
|
|
139
|
+
.container {
|
|
140
|
+
text-align: center;
|
|
141
|
+
padding: 40px;
|
|
142
|
+
background: rgba(0,0,0,0.2);
|
|
143
|
+
border-radius: 16px;
|
|
144
|
+
}
|
|
145
|
+
.error-icon { font-size: 64px; margin-bottom: 20px; }
|
|
146
|
+
h1 { margin: 0 0 10px; font-size: 24px; }
|
|
147
|
+
p { margin: 0; opacity: 0.9; }
|
|
148
|
+
.details { margin-top: 20px; font-size: 14px; opacity: 0.7; }
|
|
149
|
+
</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<div class="container">
|
|
153
|
+
<div class="error-icon">\u2717</div>
|
|
154
|
+
<h1>Authentication Failed</h1>
|
|
155
|
+
<p>{{ERROR_MESSAGE}}</p>
|
|
156
|
+
<p class="details">{{ERROR_DESCRIPTION}}</p>
|
|
157
|
+
</div>
|
|
158
|
+
</body>
|
|
159
|
+
</html>
|
|
160
|
+
`;
|
|
161
|
+
async function startCallbackServer(options = {}) {
|
|
162
|
+
const { port = 0, host = "127.0.0.1", path = "/callback", timeout = 5 * 60 * 1e3, successHtml = DEFAULT_SUCCESS_HTML, errorHtml = DEFAULT_ERROR_HTML } = options;
|
|
163
|
+
let resolveCallback;
|
|
164
|
+
let rejectCallback;
|
|
165
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
166
|
+
resolveCallback = resolve;
|
|
167
|
+
rejectCallback = reject;
|
|
168
|
+
});
|
|
169
|
+
const server = (0, import_http.createServer)((req, res) => {
|
|
170
|
+
const url = new import_url.URL(req.url || "/", `http://${host}:${port}`);
|
|
171
|
+
if (url.pathname !== path) {
|
|
172
|
+
res.writeHead(404, {
|
|
173
|
+
"Content-Type": "text/plain"
|
|
174
|
+
});
|
|
175
|
+
res.end("Not Found");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const code = url.searchParams.get("code");
|
|
179
|
+
const state = url.searchParams.get("state");
|
|
180
|
+
const error = url.searchParams.get("error");
|
|
181
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
182
|
+
if (error) {
|
|
183
|
+
const html = errorHtml.replace("{{ERROR_MESSAGE}}", error).replace("{{ERROR_DESCRIPTION}}", errorDescription || "");
|
|
184
|
+
res.writeHead(400, {
|
|
185
|
+
"Content-Type": "text/html"
|
|
186
|
+
});
|
|
187
|
+
res.end(html);
|
|
188
|
+
rejectCallback(new Error(`OAuth error: ${error} - ${errorDescription || "No description"}`));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!code) {
|
|
192
|
+
const html = errorHtml.replace("{{ERROR_MESSAGE}}", "Missing authorization code").replace("{{ERROR_DESCRIPTION}}", "The OAuth server did not return an authorization code.");
|
|
193
|
+
res.writeHead(400, {
|
|
194
|
+
"Content-Type": "text/html"
|
|
195
|
+
});
|
|
196
|
+
res.end(html);
|
|
197
|
+
rejectCallback(new Error("No authorization code received"));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!state) {
|
|
201
|
+
const html = errorHtml.replace("{{ERROR_MESSAGE}}", "Missing state parameter").replace("{{ERROR_DESCRIPTION}}", "The OAuth server did not return the state parameter.");
|
|
202
|
+
res.writeHead(400, {
|
|
203
|
+
"Content-Type": "text/html"
|
|
204
|
+
});
|
|
205
|
+
res.end(html);
|
|
206
|
+
rejectCallback(new Error("No state parameter received"));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
res.writeHead(200, {
|
|
210
|
+
"Content-Type": "text/html"
|
|
211
|
+
});
|
|
212
|
+
res.end(successHtml);
|
|
213
|
+
resolveCallback({
|
|
214
|
+
code,
|
|
215
|
+
state
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
await new Promise((resolve, reject) => {
|
|
219
|
+
server.once("error", reject);
|
|
220
|
+
server.listen(port, host, () => {
|
|
221
|
+
server.removeListener("error", reject);
|
|
222
|
+
resolve();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
const actualPort = server.address().port;
|
|
226
|
+
const redirectUri = `http://${host}:${actualPort}${path}`;
|
|
227
|
+
const timeoutId = setTimeout(() => {
|
|
228
|
+
rejectCallback(new Error(`OAuth callback timed out after ${timeout / 1e3} seconds`));
|
|
229
|
+
}, timeout);
|
|
230
|
+
const shutdown = /* @__PURE__ */ __name(async () => {
|
|
231
|
+
clearTimeout(timeoutId);
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
server.close((err) => {
|
|
234
|
+
if (err) reject(err);
|
|
235
|
+
else resolve();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}, "shutdown");
|
|
239
|
+
return {
|
|
240
|
+
redirectUri,
|
|
241
|
+
port: actualPort,
|
|
242
|
+
waitForCallback: /* @__PURE__ */ __name(async () => {
|
|
243
|
+
try {
|
|
244
|
+
return await callbackPromise;
|
|
245
|
+
} finally {
|
|
246
|
+
await shutdown().catch(() => {
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}, "waitForCallback"),
|
|
250
|
+
shutdown
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
__name(startCallbackServer, "startCallbackServer");
|
|
254
|
+
async function findAvailablePort(preferredPort) {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
const server = (0, import_http.createServer)();
|
|
257
|
+
server.once("error", reject);
|
|
258
|
+
server.listen(preferredPort || 0, "127.0.0.1", () => {
|
|
259
|
+
const port = server.address().port;
|
|
260
|
+
server.close(() => resolve(port));
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
__name(findAvailablePort, "findAvailablePort");
|
|
265
|
+
|
|
266
|
+
// src/storage/types.ts
|
|
267
|
+
function isTokenExpired(tokens, bufferSeconds = 60) {
|
|
268
|
+
if (!tokens.expires_at && !tokens.expires_in) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const expiresAt = tokens.expires_at ?? Date.now() / 1e3 + (tokens.expires_in ?? 0);
|
|
272
|
+
const now = Date.now() / 1e3;
|
|
273
|
+
return expiresAt <= now + bufferSeconds;
|
|
274
|
+
}
|
|
275
|
+
__name(isTokenExpired, "isTokenExpired");
|
|
276
|
+
function withExpiresAt(tokens) {
|
|
277
|
+
if (tokens.expires_at || !tokens.expires_in) {
|
|
278
|
+
return tokens;
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
...tokens,
|
|
282
|
+
expires_at: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
__name(withExpiresAt, "withExpiresAt");
|
|
286
|
+
|
|
287
|
+
// src/storage/memory.ts
|
|
288
|
+
var MemoryStorage = class {
|
|
289
|
+
static {
|
|
290
|
+
__name(this, "MemoryStorage");
|
|
291
|
+
}
|
|
292
|
+
tokens = /* @__PURE__ */ new Map();
|
|
293
|
+
clients = /* @__PURE__ */ new Map();
|
|
294
|
+
/**
|
|
295
|
+
* Normalize server URL for consistent key lookup
|
|
296
|
+
*/
|
|
297
|
+
normalizeUrl(serverUrl) {
|
|
298
|
+
return serverUrl.replace(/\/+$/, "").toLowerCase();
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check if an entry is expired
|
|
302
|
+
*/
|
|
303
|
+
isExpired(entry) {
|
|
304
|
+
if (!entry) return true;
|
|
305
|
+
if (!entry.expiresAt) return false;
|
|
306
|
+
return Date.now() / 1e3 >= entry.expiresAt;
|
|
307
|
+
}
|
|
308
|
+
async getTokens(serverUrl) {
|
|
309
|
+
const key = this.normalizeUrl(serverUrl);
|
|
310
|
+
const entry = this.tokens.get(key);
|
|
311
|
+
if (this.isExpired(entry)) {
|
|
312
|
+
this.tokens.delete(key);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return entry?.value ?? null;
|
|
316
|
+
}
|
|
317
|
+
async setTokens(serverUrl, tokens) {
|
|
318
|
+
const key = this.normalizeUrl(serverUrl);
|
|
319
|
+
const enrichedTokens = withExpiresAt(tokens);
|
|
320
|
+
this.tokens.set(key, {
|
|
321
|
+
value: enrichedTokens,
|
|
322
|
+
expiresAt: enrichedTokens.expires_at
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async clearTokens(serverUrl) {
|
|
326
|
+
const key = this.normalizeUrl(serverUrl);
|
|
327
|
+
this.tokens.delete(key);
|
|
328
|
+
}
|
|
329
|
+
async getClientInfo(serverUrl) {
|
|
330
|
+
const key = this.normalizeUrl(serverUrl);
|
|
331
|
+
const entry = this.clients.get(key);
|
|
332
|
+
if (this.isExpired(entry)) {
|
|
333
|
+
this.clients.delete(key);
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return entry?.value ?? null;
|
|
337
|
+
}
|
|
338
|
+
async setClientInfo(serverUrl, info) {
|
|
339
|
+
const key = this.normalizeUrl(serverUrl);
|
|
340
|
+
this.clients.set(key, {
|
|
341
|
+
value: info,
|
|
342
|
+
expiresAt: info.client_secret_expires_at
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async clearClientInfo(serverUrl) {
|
|
346
|
+
const key = this.normalizeUrl(serverUrl);
|
|
347
|
+
this.clients.delete(key);
|
|
348
|
+
}
|
|
349
|
+
async clearAll() {
|
|
350
|
+
this.tokens.clear();
|
|
351
|
+
this.clients.clear();
|
|
352
|
+
}
|
|
353
|
+
async getAllSessions() {
|
|
354
|
+
const sessions = [];
|
|
355
|
+
for (const [url, entry] of this.tokens.entries()) {
|
|
356
|
+
if (!this.isExpired(entry)) {
|
|
357
|
+
sessions.push({
|
|
358
|
+
serverUrl: url,
|
|
359
|
+
tokens: entry.value,
|
|
360
|
+
clientInfo: this.clients.get(url)?.value,
|
|
361
|
+
createdAt: Date.now(),
|
|
362
|
+
updatedAt: Date.now()
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return sessions;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/client/oauth-client.ts
|
|
371
|
+
var OAuthClient = class {
|
|
372
|
+
static {
|
|
373
|
+
__name(this, "OAuthClient");
|
|
374
|
+
}
|
|
375
|
+
serverUrl;
|
|
376
|
+
scopes;
|
|
377
|
+
clientName;
|
|
378
|
+
storage;
|
|
379
|
+
pkceEnabled;
|
|
380
|
+
autoRefresh;
|
|
381
|
+
refreshBuffer;
|
|
382
|
+
callbackPort;
|
|
383
|
+
timeout;
|
|
384
|
+
// OAuth endpoints
|
|
385
|
+
authorizationEndpoint;
|
|
386
|
+
tokenEndpoint;
|
|
387
|
+
registrationEndpoint;
|
|
388
|
+
// Pre-configured credentials
|
|
389
|
+
preConfiguredClientId;
|
|
390
|
+
preConfiguredClientSecret;
|
|
391
|
+
// Runtime state
|
|
392
|
+
pendingRefresh;
|
|
393
|
+
metadata;
|
|
394
|
+
constructor(options) {
|
|
395
|
+
this.serverUrl = options.serverUrl.replace(/\/+$/, "");
|
|
396
|
+
this.scopes = options.scopes ?? [];
|
|
397
|
+
this.clientName = options.clientName ?? "LeanMCP Client";
|
|
398
|
+
this.storage = options.storage ?? new MemoryStorage();
|
|
399
|
+
this.pkceEnabled = options.pkceEnabled ?? true;
|
|
400
|
+
this.autoRefresh = options.autoRefresh ?? true;
|
|
401
|
+
this.refreshBuffer = options.refreshBuffer ?? 60;
|
|
402
|
+
this.callbackPort = options.callbackPort;
|
|
403
|
+
this.timeout = options.timeout ?? 5 * 60 * 1e3;
|
|
404
|
+
this.authorizationEndpoint = options.authorizationEndpoint;
|
|
405
|
+
this.tokenEndpoint = options.tokenEndpoint;
|
|
406
|
+
this.registrationEndpoint = options.registrationEndpoint;
|
|
407
|
+
this.preConfiguredClientId = options.clientId;
|
|
408
|
+
this.preConfiguredClientSecret = options.clientSecret;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Discover OAuth metadata from .well-known endpoint
|
|
412
|
+
*/
|
|
413
|
+
async discoverMetadata() {
|
|
414
|
+
if (this.metadata) return this.metadata;
|
|
415
|
+
const wellKnownUrl = `${this.serverUrl}/.well-known/oauth-authorization-server`;
|
|
416
|
+
try {
|
|
417
|
+
const response = await fetch(wellKnownUrl);
|
|
418
|
+
if (response.ok) {
|
|
419
|
+
this.metadata = await response.json();
|
|
420
|
+
return this.metadata;
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const oidcUrl = `${this.serverUrl}/.well-known/openid-configuration`;
|
|
426
|
+
const response = await fetch(oidcUrl);
|
|
427
|
+
if (response.ok) {
|
|
428
|
+
this.metadata = await response.json();
|
|
429
|
+
return this.metadata;
|
|
430
|
+
}
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
this.metadata = {
|
|
434
|
+
issuer: this.serverUrl,
|
|
435
|
+
authorization_endpoint: this.authorizationEndpoint || `${this.serverUrl}/authorize`,
|
|
436
|
+
token_endpoint: this.tokenEndpoint || `${this.serverUrl}/token`,
|
|
437
|
+
registration_endpoint: this.registrationEndpoint
|
|
438
|
+
};
|
|
439
|
+
return this.metadata;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get or register OAuth client credentials
|
|
443
|
+
*/
|
|
444
|
+
async getClientCredentials(redirectUri) {
|
|
445
|
+
if (this.preConfiguredClientId) {
|
|
446
|
+
return {
|
|
447
|
+
client_id: this.preConfiguredClientId,
|
|
448
|
+
client_secret: this.preConfiguredClientSecret
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const stored = await this.storage.getClientInfo(this.serverUrl);
|
|
452
|
+
if (stored && (!stored.client_secret_expires_at || stored.client_secret_expires_at > Date.now() / 1e3)) {
|
|
453
|
+
return stored;
|
|
454
|
+
}
|
|
455
|
+
const metadata = await this.discoverMetadata();
|
|
456
|
+
if (!metadata.registration_endpoint) {
|
|
457
|
+
throw new Error("No client credentials configured and server does not support dynamic registration. Please provide clientId in OAuthClientOptions.");
|
|
458
|
+
}
|
|
459
|
+
const registrationPayload = {
|
|
460
|
+
client_name: this.clientName,
|
|
461
|
+
redirect_uris: [
|
|
462
|
+
redirectUri
|
|
463
|
+
],
|
|
464
|
+
grant_types: [
|
|
465
|
+
"authorization_code",
|
|
466
|
+
"refresh_token"
|
|
467
|
+
],
|
|
468
|
+
response_types: [
|
|
469
|
+
"code"
|
|
470
|
+
],
|
|
471
|
+
scope: this.scopes.join(" ")
|
|
472
|
+
};
|
|
473
|
+
const response = await fetch(metadata.registration_endpoint, {
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers: {
|
|
476
|
+
"Content-Type": "application/json"
|
|
477
|
+
},
|
|
478
|
+
body: JSON.stringify(registrationPayload)
|
|
479
|
+
});
|
|
480
|
+
if (!response.ok) {
|
|
481
|
+
const error = await response.text();
|
|
482
|
+
throw new Error(`Client registration failed: ${error}`);
|
|
483
|
+
}
|
|
484
|
+
const registration = await response.json();
|
|
485
|
+
await this.storage.setClientInfo(this.serverUrl, registration);
|
|
486
|
+
return registration;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Start the browser-based OAuth flow
|
|
490
|
+
*
|
|
491
|
+
* Opens the user's browser to the authorization URL and waits for the callback.
|
|
492
|
+
*
|
|
493
|
+
* @returns OAuth tokens
|
|
494
|
+
*/
|
|
495
|
+
async authenticate() {
|
|
496
|
+
const existing = await this.storage.getTokens(this.serverUrl);
|
|
497
|
+
if (existing && !isTokenExpired(existing, this.refreshBuffer)) {
|
|
498
|
+
return existing;
|
|
499
|
+
}
|
|
500
|
+
if (existing?.refresh_token) {
|
|
501
|
+
try {
|
|
502
|
+
return await this.refreshTokens();
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const callbackServer = await startCallbackServer({
|
|
507
|
+
port: this.callbackPort,
|
|
508
|
+
timeout: this.timeout
|
|
509
|
+
});
|
|
510
|
+
try {
|
|
511
|
+
const metadata = await this.discoverMetadata();
|
|
512
|
+
const clientCredentials = await this.getClientCredentials(callbackServer.redirectUri);
|
|
513
|
+
let pkce;
|
|
514
|
+
if (this.pkceEnabled) {
|
|
515
|
+
pkce = generatePKCE();
|
|
516
|
+
}
|
|
517
|
+
const state = crypto.randomUUID();
|
|
518
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
519
|
+
authUrl.searchParams.set("response_type", "code");
|
|
520
|
+
authUrl.searchParams.set("client_id", clientCredentials.client_id);
|
|
521
|
+
authUrl.searchParams.set("redirect_uri", callbackServer.redirectUri);
|
|
522
|
+
authUrl.searchParams.set("state", state);
|
|
523
|
+
if (this.scopes.length > 0) {
|
|
524
|
+
authUrl.searchParams.set("scope", this.scopes.join(" "));
|
|
525
|
+
}
|
|
526
|
+
if (pkce) {
|
|
527
|
+
authUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
528
|
+
authUrl.searchParams.set("code_challenge_method", pkce.method);
|
|
529
|
+
}
|
|
530
|
+
const openBrowser = await this.openBrowser(authUrl.toString());
|
|
531
|
+
if (!openBrowser) {
|
|
532
|
+
console.log(`
|
|
533
|
+
Please open this URL in your browser:
|
|
534
|
+
${authUrl.toString()}
|
|
535
|
+
`);
|
|
536
|
+
}
|
|
537
|
+
const result = await callbackServer.waitForCallback();
|
|
538
|
+
if (result.state !== state) {
|
|
539
|
+
throw new Error("State mismatch - possible CSRF attack");
|
|
540
|
+
}
|
|
541
|
+
const tokens = await this.exchangeCodeForTokens(result.code, callbackServer.redirectUri, clientCredentials, pkce?.verifier);
|
|
542
|
+
const enrichedTokens = withExpiresAt(tokens);
|
|
543
|
+
await this.storage.setTokens(this.serverUrl, enrichedTokens);
|
|
544
|
+
return enrichedTokens;
|
|
545
|
+
} finally {
|
|
546
|
+
await callbackServer.shutdown().catch(() => {
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Open URL in browser
|
|
552
|
+
*/
|
|
553
|
+
async openBrowser(url) {
|
|
554
|
+
try {
|
|
555
|
+
const open = require("open");
|
|
556
|
+
await open(url);
|
|
557
|
+
return true;
|
|
558
|
+
} catch {
|
|
559
|
+
try {
|
|
560
|
+
const { exec } = require("child_process");
|
|
561
|
+
const platform = process.platform;
|
|
562
|
+
if (platform === "darwin") {
|
|
563
|
+
exec(`open "${url}"`);
|
|
564
|
+
} else if (platform === "win32") {
|
|
565
|
+
exec(`start "" "${url}"`);
|
|
566
|
+
} else {
|
|
567
|
+
exec(`xdg-open "${url}"`);
|
|
568
|
+
}
|
|
569
|
+
return true;
|
|
570
|
+
} catch {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Exchange authorization code for tokens
|
|
577
|
+
*/
|
|
578
|
+
async exchangeCodeForTokens(code, redirectUri, credentials, codeVerifier) {
|
|
579
|
+
const metadata = await this.discoverMetadata();
|
|
580
|
+
const tokenPayload = {
|
|
581
|
+
grant_type: "authorization_code",
|
|
582
|
+
code,
|
|
583
|
+
redirect_uri: redirectUri,
|
|
584
|
+
client_id: credentials.client_id
|
|
585
|
+
};
|
|
586
|
+
if (credentials.client_secret) {
|
|
587
|
+
tokenPayload.client_secret = credentials.client_secret;
|
|
588
|
+
}
|
|
589
|
+
if (codeVerifier) {
|
|
590
|
+
tokenPayload.code_verifier = codeVerifier;
|
|
591
|
+
}
|
|
592
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers: {
|
|
595
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
596
|
+
},
|
|
597
|
+
body: new URLSearchParams(tokenPayload)
|
|
598
|
+
});
|
|
599
|
+
if (!response.ok) {
|
|
600
|
+
const error = await response.text();
|
|
601
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
602
|
+
}
|
|
603
|
+
return response.json();
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Refresh the access token using the refresh token
|
|
607
|
+
*/
|
|
608
|
+
async refreshTokens() {
|
|
609
|
+
if (this.pendingRefresh) {
|
|
610
|
+
return this.pendingRefresh;
|
|
611
|
+
}
|
|
612
|
+
this.pendingRefresh = this.doRefreshTokens();
|
|
613
|
+
try {
|
|
614
|
+
return await this.pendingRefresh;
|
|
615
|
+
} finally {
|
|
616
|
+
this.pendingRefresh = void 0;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async doRefreshTokens() {
|
|
620
|
+
const existing = await this.storage.getTokens(this.serverUrl);
|
|
621
|
+
if (!existing?.refresh_token) {
|
|
622
|
+
throw new Error("No refresh token available");
|
|
623
|
+
}
|
|
624
|
+
const metadata = await this.discoverMetadata();
|
|
625
|
+
const credentials = await this.getClientCredentials("");
|
|
626
|
+
const tokenPayload = {
|
|
627
|
+
grant_type: "refresh_token",
|
|
628
|
+
refresh_token: existing.refresh_token,
|
|
629
|
+
client_id: credentials.client_id
|
|
630
|
+
};
|
|
631
|
+
if (credentials.client_secret) {
|
|
632
|
+
tokenPayload.client_secret = credentials.client_secret;
|
|
633
|
+
}
|
|
634
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
635
|
+
method: "POST",
|
|
636
|
+
headers: {
|
|
637
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
638
|
+
},
|
|
639
|
+
body: new URLSearchParams(tokenPayload)
|
|
640
|
+
});
|
|
641
|
+
if (!response.ok) {
|
|
642
|
+
await this.storage.clearTokens(this.serverUrl);
|
|
643
|
+
throw new Error("Token refresh failed");
|
|
644
|
+
}
|
|
645
|
+
const tokens = await response.json();
|
|
646
|
+
if (!tokens.refresh_token && existing.refresh_token) {
|
|
647
|
+
tokens.refresh_token = existing.refresh_token;
|
|
648
|
+
}
|
|
649
|
+
const enrichedTokens = withExpiresAt(tokens);
|
|
650
|
+
await this.storage.setTokens(this.serverUrl, enrichedTokens);
|
|
651
|
+
return enrichedTokens;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Get a valid access token, refreshing if necessary
|
|
655
|
+
*/
|
|
656
|
+
async getValidToken() {
|
|
657
|
+
let tokens = await this.storage.getTokens(this.serverUrl);
|
|
658
|
+
if (!tokens) {
|
|
659
|
+
throw new Error("Not authenticated. Call authenticate() first.");
|
|
660
|
+
}
|
|
661
|
+
if (this.autoRefresh && isTokenExpired(tokens, this.refreshBuffer)) {
|
|
662
|
+
if (tokens.refresh_token) {
|
|
663
|
+
tokens = await this.refreshTokens();
|
|
664
|
+
} else {
|
|
665
|
+
throw new Error("Token expired and no refresh token available. Re-authenticate required.");
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return tokens.access_token;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Get current tokens (may be expired)
|
|
672
|
+
*/
|
|
673
|
+
async getTokens() {
|
|
674
|
+
return this.storage.getTokens(this.serverUrl);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Check if we have valid (non-expired) tokens
|
|
678
|
+
*/
|
|
679
|
+
async isAuthenticated() {
|
|
680
|
+
const tokens = await this.storage.getTokens(this.serverUrl);
|
|
681
|
+
if (!tokens) return false;
|
|
682
|
+
return !isTokenExpired(tokens, this.refreshBuffer) || !!tokens.refresh_token;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Clear stored tokens and log out
|
|
686
|
+
*/
|
|
687
|
+
async logout() {
|
|
688
|
+
await this.storage.clearTokens(this.serverUrl);
|
|
689
|
+
await this.storage.clearClientInfo(this.serverUrl);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Create an auth handler for HTTP requests
|
|
693
|
+
*
|
|
694
|
+
* @example
|
|
695
|
+
* ```typescript
|
|
696
|
+
* const authHandler = client.asAuthHandler();
|
|
697
|
+
* const authedRequest = await authHandler(request);
|
|
698
|
+
* ```
|
|
699
|
+
*/
|
|
700
|
+
asAuthHandler() {
|
|
701
|
+
return async (request) => {
|
|
702
|
+
const token = await this.getValidToken();
|
|
703
|
+
const headers = new Headers(request.headers);
|
|
704
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
705
|
+
return new Request(request.url, {
|
|
706
|
+
method: request.method,
|
|
707
|
+
headers,
|
|
708
|
+
body: request.body
|
|
709
|
+
});
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/client/discovery.ts
|
|
715
|
+
var metadataCache = /* @__PURE__ */ new Map();
|
|
716
|
+
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
717
|
+
async function discoverOAuthMetadata(serverUrl, options = {}) {
|
|
718
|
+
const { timeout = 1e4, cache = true, fetch: customFetch = fetch } = options;
|
|
719
|
+
const normalizedUrl = serverUrl.replace(/\/+$/, "");
|
|
720
|
+
if (cache) {
|
|
721
|
+
const cached = metadataCache.get(normalizedUrl);
|
|
722
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
723
|
+
return cached.metadata;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const controller = new AbortController();
|
|
727
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
728
|
+
try {
|
|
729
|
+
const oauthUrl = `${normalizedUrl}/.well-known/oauth-authorization-server`;
|
|
730
|
+
try {
|
|
731
|
+
const response = await customFetch(oauthUrl, {
|
|
732
|
+
signal: controller.signal,
|
|
733
|
+
headers: {
|
|
734
|
+
"Accept": "application/json"
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
if (response.ok) {
|
|
738
|
+
const metadata = await response.json();
|
|
739
|
+
cacheMetadata(normalizedUrl, metadata, cache);
|
|
740
|
+
return metadata;
|
|
741
|
+
}
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
const oidcUrl = `${normalizedUrl}/.well-known/openid-configuration`;
|
|
745
|
+
try {
|
|
746
|
+
const response = await customFetch(oidcUrl, {
|
|
747
|
+
signal: controller.signal,
|
|
748
|
+
headers: {
|
|
749
|
+
"Accept": "application/json"
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
if (response.ok) {
|
|
753
|
+
const metadata = await response.json();
|
|
754
|
+
cacheMetadata(normalizedUrl, metadata, cache);
|
|
755
|
+
return metadata;
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
const fallbackMetadata = {
|
|
760
|
+
issuer: normalizedUrl,
|
|
761
|
+
authorization_endpoint: `${normalizedUrl}/authorize`,
|
|
762
|
+
token_endpoint: `${normalizedUrl}/token`
|
|
763
|
+
};
|
|
764
|
+
if (cache) {
|
|
765
|
+
metadataCache.set(normalizedUrl, {
|
|
766
|
+
metadata: fallbackMetadata,
|
|
767
|
+
expiresAt: Date.now() + 5 * 60 * 1e3
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
return fallbackMetadata;
|
|
771
|
+
} finally {
|
|
772
|
+
clearTimeout(timeoutId);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
__name(discoverOAuthMetadata, "discoverOAuthMetadata");
|
|
776
|
+
function cacheMetadata(url, metadata, shouldCache) {
|
|
777
|
+
if (shouldCache) {
|
|
778
|
+
metadataCache.set(url, {
|
|
779
|
+
metadata,
|
|
780
|
+
expiresAt: Date.now() + CACHE_TTL_MS
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
__name(cacheMetadata, "cacheMetadata");
|
|
785
|
+
function clearMetadataCache(serverUrl) {
|
|
786
|
+
if (serverUrl) {
|
|
787
|
+
const normalizedUrl = serverUrl.replace(/\/+$/, "");
|
|
788
|
+
metadataCache.delete(normalizedUrl);
|
|
789
|
+
} else {
|
|
790
|
+
metadataCache.clear();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
__name(clearMetadataCache, "clearMetadataCache");
|
|
794
|
+
function serverSupports(metadata, feature) {
|
|
795
|
+
switch (feature) {
|
|
796
|
+
case "pkce":
|
|
797
|
+
return metadata.code_challenge_methods_supported?.includes("S256") ?? false;
|
|
798
|
+
case "refresh_token":
|
|
799
|
+
return metadata.grant_types_supported?.includes("refresh_token") ?? true;
|
|
800
|
+
// Assume supported if not specified
|
|
801
|
+
case "dynamic_registration":
|
|
802
|
+
return !!metadata.registration_endpoint;
|
|
803
|
+
case "revocation":
|
|
804
|
+
return !!metadata.revocation_endpoint;
|
|
805
|
+
default:
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
__name(serverSupports, "serverSupports");
|
|
810
|
+
function validateMetadata(metadata) {
|
|
811
|
+
const errors = [];
|
|
812
|
+
if (!metadata.issuer) {
|
|
813
|
+
errors.push("Missing required field: issuer");
|
|
814
|
+
}
|
|
815
|
+
if (!metadata.authorization_endpoint) {
|
|
816
|
+
errors.push("Missing required field: authorization_endpoint");
|
|
817
|
+
}
|
|
818
|
+
if (!metadata.token_endpoint) {
|
|
819
|
+
errors.push("Missing required field: token_endpoint");
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
valid: errors.length === 0,
|
|
823
|
+
errors
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
__name(validateMetadata, "validateMetadata");
|
|
827
|
+
|
|
828
|
+
// src/client/refresh-manager.ts
|
|
829
|
+
var RefreshManager = class {
|
|
830
|
+
static {
|
|
831
|
+
__name(this, "RefreshManager");
|
|
832
|
+
}
|
|
833
|
+
storage;
|
|
834
|
+
refreshFn;
|
|
835
|
+
serverUrl;
|
|
836
|
+
refreshBuffer;
|
|
837
|
+
checkInterval;
|
|
838
|
+
maxRetries;
|
|
839
|
+
retryDelay;
|
|
840
|
+
intervalId;
|
|
841
|
+
pendingRefresh;
|
|
842
|
+
retryCount = 0;
|
|
843
|
+
listeners = [];
|
|
844
|
+
isRunning = false;
|
|
845
|
+
constructor(options) {
|
|
846
|
+
this.storage = options.storage;
|
|
847
|
+
this.refreshFn = options.refreshFn;
|
|
848
|
+
this.serverUrl = options.serverUrl;
|
|
849
|
+
this.refreshBuffer = options.refreshBuffer ?? 300;
|
|
850
|
+
this.checkInterval = options.checkInterval ?? 6e4;
|
|
851
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
852
|
+
this.retryDelay = options.retryDelay ?? 5e3;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Start background refresh monitoring
|
|
856
|
+
*/
|
|
857
|
+
start() {
|
|
858
|
+
if (this.isRunning) return;
|
|
859
|
+
this.isRunning = true;
|
|
860
|
+
this.checkAndRefresh();
|
|
861
|
+
this.intervalId = setInterval(() => {
|
|
862
|
+
this.checkAndRefresh();
|
|
863
|
+
}, this.checkInterval);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Stop background refresh monitoring
|
|
867
|
+
*/
|
|
868
|
+
stop() {
|
|
869
|
+
this.isRunning = false;
|
|
870
|
+
if (this.intervalId) {
|
|
871
|
+
clearInterval(this.intervalId);
|
|
872
|
+
this.intervalId = void 0;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Register event listener
|
|
877
|
+
*/
|
|
878
|
+
on(listener) {
|
|
879
|
+
this.listeners.push(listener);
|
|
880
|
+
return () => {
|
|
881
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Emit event to all listeners
|
|
886
|
+
*/
|
|
887
|
+
emit(event) {
|
|
888
|
+
for (const listener of this.listeners) {
|
|
889
|
+
try {
|
|
890
|
+
listener(event);
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Check token and refresh if needed
|
|
897
|
+
*/
|
|
898
|
+
async checkAndRefresh() {
|
|
899
|
+
if (this.pendingRefresh) return;
|
|
900
|
+
try {
|
|
901
|
+
const tokens = await this.storage.getTokens(this.serverUrl);
|
|
902
|
+
if (!tokens) return;
|
|
903
|
+
if (!isTokenExpired(tokens, this.refreshBuffer)) {
|
|
904
|
+
this.retryCount = 0;
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (!tokens.refresh_token) {
|
|
908
|
+
this.emit({
|
|
909
|
+
type: "token_expired",
|
|
910
|
+
serverUrl: this.serverUrl
|
|
911
|
+
});
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
await this.performRefresh(tokens.refresh_token);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
console.error("[RefreshManager] Error checking tokens:", error);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Perform token refresh with retry logic
|
|
921
|
+
*/
|
|
922
|
+
async performRefresh(refreshToken) {
|
|
923
|
+
this.emit({
|
|
924
|
+
type: "refresh_started",
|
|
925
|
+
serverUrl: this.serverUrl
|
|
926
|
+
});
|
|
927
|
+
this.pendingRefresh = this.doRefresh(refreshToken);
|
|
928
|
+
try {
|
|
929
|
+
const tokens = await this.pendingRefresh;
|
|
930
|
+
this.retryCount = 0;
|
|
931
|
+
this.emit({
|
|
932
|
+
type: "refresh_success",
|
|
933
|
+
serverUrl: this.serverUrl,
|
|
934
|
+
tokens
|
|
935
|
+
});
|
|
936
|
+
} catch (error) {
|
|
937
|
+
this.emit({
|
|
938
|
+
type: "refresh_failed",
|
|
939
|
+
serverUrl: this.serverUrl,
|
|
940
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
941
|
+
});
|
|
942
|
+
if (this.retryCount < this.maxRetries) {
|
|
943
|
+
this.retryCount++;
|
|
944
|
+
setTimeout(() => {
|
|
945
|
+
this.checkAndRefresh();
|
|
946
|
+
}, this.retryDelay * this.retryCount);
|
|
947
|
+
}
|
|
948
|
+
} finally {
|
|
949
|
+
this.pendingRefresh = void 0;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Actually perform the refresh
|
|
954
|
+
*/
|
|
955
|
+
async doRefresh(refreshToken) {
|
|
956
|
+
const newTokens = await this.refreshFn(refreshToken);
|
|
957
|
+
const enrichedTokens = withExpiresAt(newTokens);
|
|
958
|
+
if (!enrichedTokens.refresh_token) {
|
|
959
|
+
enrichedTokens.refresh_token = refreshToken;
|
|
960
|
+
}
|
|
961
|
+
await this.storage.setTokens(this.serverUrl, enrichedTokens);
|
|
962
|
+
return enrichedTokens;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Force an immediate refresh
|
|
966
|
+
*/
|
|
967
|
+
async forceRefresh() {
|
|
968
|
+
const tokens = await this.storage.getTokens(this.serverUrl);
|
|
969
|
+
if (!tokens?.refresh_token) {
|
|
970
|
+
throw new Error("No refresh token available");
|
|
971
|
+
}
|
|
972
|
+
if (this.pendingRefresh) {
|
|
973
|
+
return this.pendingRefresh;
|
|
974
|
+
}
|
|
975
|
+
this.pendingRefresh = this.doRefresh(tokens.refresh_token);
|
|
976
|
+
try {
|
|
977
|
+
const newTokens = await this.pendingRefresh;
|
|
978
|
+
this.emit({
|
|
979
|
+
type: "refresh_success",
|
|
980
|
+
serverUrl: this.serverUrl,
|
|
981
|
+
tokens: newTokens
|
|
982
|
+
});
|
|
983
|
+
return newTokens;
|
|
984
|
+
} finally {
|
|
985
|
+
this.pendingRefresh = void 0;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Get current running state
|
|
990
|
+
*/
|
|
991
|
+
get running() {
|
|
992
|
+
return this.isRunning;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
function createRefreshManager(storage, serverUrl, tokenEndpoint, clientId, clientSecret) {
|
|
996
|
+
return new RefreshManager({
|
|
997
|
+
storage,
|
|
998
|
+
serverUrl,
|
|
999
|
+
refreshFn: /* @__PURE__ */ __name(async (refreshToken) => {
|
|
1000
|
+
const payload = {
|
|
1001
|
+
grant_type: "refresh_token",
|
|
1002
|
+
refresh_token: refreshToken,
|
|
1003
|
+
client_id: clientId
|
|
1004
|
+
};
|
|
1005
|
+
if (clientSecret) {
|
|
1006
|
+
payload.client_secret = clientSecret;
|
|
1007
|
+
}
|
|
1008
|
+
const response = await fetch(tokenEndpoint, {
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
headers: {
|
|
1011
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1012
|
+
},
|
|
1013
|
+
body: new URLSearchParams(payload)
|
|
1014
|
+
});
|
|
1015
|
+
if (!response.ok) {
|
|
1016
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
1017
|
+
}
|
|
1018
|
+
return response.json();
|
|
1019
|
+
}, "refreshFn")
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
__name(createRefreshManager, "createRefreshManager");
|
|
1023
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1024
|
+
0 && (module.exports = {
|
|
1025
|
+
OAuthClient,
|
|
1026
|
+
RefreshManager,
|
|
1027
|
+
clearMetadataCache,
|
|
1028
|
+
createRefreshManager,
|
|
1029
|
+
discoverOAuthMetadata,
|
|
1030
|
+
findAvailablePort,
|
|
1031
|
+
generateCodeChallenge,
|
|
1032
|
+
generateCodeVerifier,
|
|
1033
|
+
generatePKCE,
|
|
1034
|
+
isValidCodeVerifier,
|
|
1035
|
+
serverSupports,
|
|
1036
|
+
startCallbackServer,
|
|
1037
|
+
validateMetadata,
|
|
1038
|
+
verifyPKCE
|
|
1039
|
+
});
|