@crabeye-ai/crabeye-mcp-bridge 1.2.1 → 1.3.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 CHANGED
@@ -84,7 +84,7 @@ Alternatively, you can add the bridge alongside your existing `mcpServers` entri
84
84
 
85
85
  - **Discovery + search.** Two meta-tools (`search_tools`, `run_tool`) instead of N×M tool definitions in context. See [docs/how-it-works.md](docs/how-it-works.md).
86
86
  - **Configuration.** STDIO, HTTP, and SSE upstreams; categories; multi-source config keys. See [docs/configuration.md](docs/configuration.md).
87
- - **Authentication.** Encrypted credential store, OS-keychain-backed master key, `${credential:key}` templates. See [docs/auth.md](docs/auth.md).
87
+ - **Authentication.** Encrypted credential store, OS-keychain-backed master key, `${credential:key}` templates, and a one-shot `auth <server>` OAuth flow for HTTP upstreams. See [docs/auth.md](docs/auth.md).
88
88
  - **Policies.** Per-tool / per-server / global tool policies (`always` / `prompt` / `never`), rate limiting, discovery modes. See [docs/policies.md](docs/policies.md).
89
89
  - **STDIO manager.** STDIO upstreams routed through a per-user manager process so multiple bridges share a single subprocess per upstream. See [docs/stdio-manager.md](docs/stdio-manager.md).
90
90
  - **CLI.** `init`, `restore`, `credential`, `daemon`, `--validate`. See [docs/cli.md](docs/cli.md).
@@ -0,0 +1,610 @@
1
+ import {
2
+ BridgeOAuthClientProvider,
3
+ ConfigError,
4
+ CredentialError,
5
+ CredentialStore,
6
+ OAuthError,
7
+ clientInfoKey,
8
+ clientSecretKey,
9
+ createKeychainAdapter,
10
+ findInlineClientSecrets,
11
+ loadConfig,
12
+ loadMergedConfig,
13
+ makeOriginPinningFetch,
14
+ oauthCredentialKey,
15
+ resolveClientSecret,
16
+ resolveConfigPath
17
+ } from "./chunk-GDMDS6AA.js";
18
+ import "./chunk-4P5BLKL7.js";
19
+ import "./chunk-GZ6RB4AV.js";
20
+ import {
21
+ APP_NAME,
22
+ isHttpServer,
23
+ resolveUpstreams
24
+ } from "./chunk-S7FBI3BM.js";
25
+
26
+ // src/commands/auth.ts
27
+ import { timingSafeEqual } from "crypto";
28
+ import { auth, discoverOAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
29
+
30
+ // src/oauth/callback-server.ts
31
+ import { createServer } from "http";
32
+ var SECURITY_HEADERS = Object.freeze({
33
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
34
+ "Cross-Origin-Resource-Policy": "same-origin",
35
+ "Cross-Origin-Opener-Policy": "same-origin",
36
+ "X-Content-Type-Options": "nosniff",
37
+ "Referrer-Policy": "no-referrer",
38
+ "Cache-Control": "no-store"
39
+ });
40
+ function applySecurityHeaders(res) {
41
+ for (const [k, v] of Object.entries(SECURITY_HEADERS)) res.setHeader(k, v);
42
+ }
43
+ var DEFAULT_TIMEOUT_MS = 5 * 6e4;
44
+ var MAX_CALLBACK_PARAM_LENGTH = 4096;
45
+ function htmlEscape(s) {
46
+ return s.replace(
47
+ /[&<>"']/g,
48
+ (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === '"' ? "&quot;" : "&#39;"
49
+ );
50
+ }
51
+ function truncate(value) {
52
+ if (value.length <= MAX_CALLBACK_PARAM_LENGTH) return value;
53
+ return value.slice(0, MAX_CALLBACK_PARAM_LENGTH) + "\u2026(truncated)";
54
+ }
55
+ function successPage(serverName) {
56
+ const who = serverName ? ` for <strong>${htmlEscape(serverName)}</strong>` : "";
57
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Authorized</title>
58
+ <style>body{font:14px system-ui;margin:48px auto;max-width:480px;text-align:center;color:#222}</style>
59
+ </head><body><h2>Authorization complete${who}</h2>
60
+ <p>You can close this window and return to your terminal.</p></body></html>`;
61
+ }
62
+ function errorPage(error, description) {
63
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Authorization failed</title>
64
+ <style>body{font:14px system-ui;margin:48px auto;max-width:480px;color:#222}h2{color:#c0392b}</style>
65
+ </head><body><h2>Authorization failed</h2>
66
+ <p><strong>${htmlEscape(error)}</strong></p>
67
+ ${description ? `<p>${htmlEscape(description)}</p>` : ""}
68
+ <p>Return to your terminal for details.</p></body></html>`;
69
+ }
70
+ async function startCallbackServer(options = {}) {
71
+ const path = options.path ?? "/callback";
72
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
73
+ let resolveResult;
74
+ let rejectResult;
75
+ const resultPromise = new Promise((resolve, reject) => {
76
+ resolveResult = resolve;
77
+ rejectResult = reject;
78
+ });
79
+ let settled = false;
80
+ let server;
81
+ let timeoutTimer;
82
+ let abortHandler;
83
+ let cleanupPromise;
84
+ const cleanup = () => {
85
+ if (cleanupPromise) return cleanupPromise;
86
+ cleanupPromise = (async () => {
87
+ if (timeoutTimer) {
88
+ clearTimeout(timeoutTimer);
89
+ timeoutTimer = void 0;
90
+ }
91
+ if (abortHandler && options.signal) {
92
+ options.signal.removeEventListener("abort", abortHandler);
93
+ abortHandler = void 0;
94
+ }
95
+ if (server) {
96
+ const s = server;
97
+ server = void 0;
98
+ await new Promise((resolve) => {
99
+ s.close(() => resolve());
100
+ });
101
+ }
102
+ })();
103
+ return cleanupPromise;
104
+ };
105
+ const settle = (err, value) => {
106
+ if (settled) return;
107
+ settled = true;
108
+ if (err) rejectResult(err);
109
+ else resolveResult(value);
110
+ void cleanup();
111
+ };
112
+ server = createServer((req, res) => {
113
+ applySecurityHeaders(res);
114
+ const expectedHost = `127.0.0.1:${req.socket.localPort}`;
115
+ if (req.headers.host !== expectedHost) {
116
+ res.statusCode = 421;
117
+ res.end();
118
+ return;
119
+ }
120
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
121
+ if (url.pathname !== path) {
122
+ res.statusCode = 404;
123
+ res.end("Not Found");
124
+ return;
125
+ }
126
+ if (req.method !== "GET") {
127
+ res.statusCode = 405;
128
+ res.setHeader("Allow", "GET");
129
+ res.end();
130
+ return;
131
+ }
132
+ const error = url.searchParams.get("error");
133
+ const errorDescription = url.searchParams.get("error_description") ?? void 0;
134
+ const code = url.searchParams.get("code");
135
+ const state = url.searchParams.get("state");
136
+ const sendError = (page, settleErr) => {
137
+ res.statusCode = 400;
138
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
139
+ res.end(errorPage(page.error, page.description), () => settle(settleErr));
140
+ };
141
+ if (error) {
142
+ const safeError = truncate(error);
143
+ const safeDescription = errorDescription !== void 0 ? truncate(errorDescription) : void 0;
144
+ sendError(
145
+ { error: safeError, description: safeDescription },
146
+ new OAuthError(
147
+ "authorization_failed",
148
+ `Authorization provider returned error: ${safeError}${safeDescription ? ` \u2014 ${safeDescription}` : ""}`
149
+ )
150
+ );
151
+ return;
152
+ }
153
+ if (!code || !state) {
154
+ sendError(
155
+ { error: "invalid_response", description: "Missing code or state" },
156
+ new OAuthError(
157
+ "invalid_callback",
158
+ "Callback missing required parameters (code, state)"
159
+ )
160
+ );
161
+ return;
162
+ }
163
+ if (code.length > MAX_CALLBACK_PARAM_LENGTH || state.length > MAX_CALLBACK_PARAM_LENGTH) {
164
+ sendError(
165
+ { error: "invalid_response", description: "Callback parameters too long" },
166
+ new OAuthError(
167
+ "invalid_callback",
168
+ `Callback parameter exceeded ${MAX_CALLBACK_PARAM_LENGTH} bytes`
169
+ )
170
+ );
171
+ return;
172
+ }
173
+ res.statusCode = 200;
174
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
175
+ res.end(successPage(), () => settle(null, { code, state }));
176
+ });
177
+ await new Promise((resolve, reject) => {
178
+ server.once("error", reject);
179
+ server.listen(
180
+ { port: options.port ?? 0, host: "127.0.0.1", exclusive: true },
181
+ () => {
182
+ server.removeListener("error", reject);
183
+ resolve();
184
+ }
185
+ );
186
+ });
187
+ const addr = server.address();
188
+ const port = addr.port;
189
+ const redirectUri = `http://127.0.0.1:${port}${path}`;
190
+ if (timeoutMs > 0) {
191
+ timeoutTimer = setTimeout(() => {
192
+ settle(
193
+ new OAuthError(
194
+ "timeout",
195
+ `Authorization timed out after ${Math.round(timeoutMs / 1e3)}s \u2014 no callback received`
196
+ )
197
+ );
198
+ }, timeoutMs);
199
+ timeoutTimer.unref?.();
200
+ }
201
+ if (options.signal) {
202
+ if (options.signal.aborted) {
203
+ settle(new OAuthError("aborted", "Authorization aborted"));
204
+ } else {
205
+ abortHandler = () => {
206
+ settle(new OAuthError("aborted", "Authorization aborted"));
207
+ };
208
+ options.signal.addEventListener("abort", abortHandler, { once: true });
209
+ }
210
+ }
211
+ return {
212
+ redirectUri,
213
+ port,
214
+ result: resultPromise,
215
+ close: cleanup
216
+ };
217
+ }
218
+
219
+ // src/oauth/browser.ts
220
+ import { spawn } from "child_process";
221
+ import { platform } from "os";
222
+ function isSafeLaunchUrl(url) {
223
+ try {
224
+ const u = new URL(url);
225
+ if (u.protocol !== "https:" && u.protocol !== "http:") return false;
226
+ if (u.username !== "" || u.password !== "") return false;
227
+ return true;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+ async function openBrowser(url, options = {}) {
233
+ if (!isSafeLaunchUrl(url)) return false;
234
+ const os = options._platform ?? platform();
235
+ let command;
236
+ let args;
237
+ switch (os) {
238
+ case "darwin":
239
+ command = "open";
240
+ args = [url];
241
+ break;
242
+ case "win32":
243
+ command = "powershell";
244
+ args = ["-NoProfile", "-Command", "Start-Process", url];
245
+ break;
246
+ default:
247
+ command = "xdg-open";
248
+ args = [url];
249
+ break;
250
+ }
251
+ return new Promise((resolve) => {
252
+ let settled = false;
253
+ const finish = (value) => {
254
+ if (settled) return;
255
+ settled = true;
256
+ resolve(value);
257
+ };
258
+ try {
259
+ const child = spawn(command, args, {
260
+ stdio: "ignore",
261
+ detached: true
262
+ });
263
+ child.once("error", () => finish(false));
264
+ child.once("spawn", () => {
265
+ child.unref?.();
266
+ finish(true);
267
+ });
268
+ } catch {
269
+ finish(false);
270
+ }
271
+ });
272
+ }
273
+
274
+ // src/commands/auth.ts
275
+ var USAGE = `Usage:
276
+ ${APP_NAME} auth <server> run OAuth flow for a server
277
+ ${APP_NAME} auth --list show auth status for all servers (default)
278
+ ${APP_NAME} auth --remove <server> delete stored credentials for a server
279
+ ${APP_NAME} auth help show this help
280
+
281
+ Options:
282
+ --list show auth status for all servers (default for bare invocation)
283
+ --remove <server> delete the stored \`oauth:<server>\` credential (local only)
284
+ -h, --help show this help
285
+ `;
286
+ var DISCOVERY_PROBE_TIMEOUT_MS = 3e3;
287
+ function defaultPrint(line) {
288
+ process.stdout.write(line + "\n");
289
+ }
290
+ function defaultErrPrint(line) {
291
+ process.stderr.write(line + "\n");
292
+ }
293
+ async function loadConfigForAuth(explicitPath) {
294
+ if (explicitPath) {
295
+ const configPath = resolveConfigPath({ configPath: explicitPath });
296
+ return loadConfig({ configPath });
297
+ }
298
+ const envPath = process.env.MCP_BRIDGE_CONFIG;
299
+ if (envPath) return loadConfig({ configPath: envPath });
300
+ const merged = await loadMergedConfig();
301
+ return merged.config;
302
+ }
303
+ function formatExpiry(expiresAt) {
304
+ if (expiresAt === void 0) return "\u2014";
305
+ return new Date(expiresAt * 1e3).toLocaleString();
306
+ }
307
+ function isExpired(expiresAt) {
308
+ if (expiresAt === void 0) return false;
309
+ return expiresAt * 1e3 <= Date.now();
310
+ }
311
+ async function buildStatusRows(config, store, discover) {
312
+ const upstreams = resolveUpstreams(config);
313
+ const rowPromises = Object.entries(upstreams).map(
314
+ async ([name, server]) => {
315
+ const oauthCfg = server._bridge?.auth;
316
+ if (oauthCfg && oauthCfg.type === "oauth2") {
317
+ return rowFromConfig(name, oauthCfg.scopes ?? [], store);
318
+ }
319
+ if (!isHttpServer(server)) return void 0;
320
+ const [advertises, stored] = await Promise.all([
321
+ probeOAuthAdvertised(server, discover),
322
+ store.get(oauthCredentialKey(name))
323
+ ]);
324
+ if (stored && stored.type === "oauth2") {
325
+ return rowFromStoredCredential(name, stored, "discovery");
326
+ }
327
+ if (!advertises) return void 0;
328
+ return {
329
+ name,
330
+ status: "advertises-oauth",
331
+ expiresAt: void 0,
332
+ scopes: [],
333
+ source: "discovery"
334
+ };
335
+ }
336
+ );
337
+ const rows = (await Promise.all(rowPromises)).filter(
338
+ (r) => r !== void 0
339
+ );
340
+ return rows.sort((a, b) => a.name.localeCompare(b.name));
341
+ }
342
+ async function probeOAuthAdvertised(server, discover) {
343
+ const signal = AbortSignal.timeout(DISCOVERY_PROBE_TIMEOUT_MS);
344
+ const fetchWithTimeout = (input, init) => fetch(input, { ...init, signal });
345
+ try {
346
+ const meta = await discover(server.url, void 0, fetchWithTimeout);
347
+ return meta !== void 0;
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+ async function rowFromConfig(name, scopes, store) {
353
+ const cred = await store.get(oauthCredentialKey(name));
354
+ if (!cred || cred.type !== "oauth2") {
355
+ return {
356
+ name,
357
+ status: "auth-required",
358
+ expiresAt: void 0,
359
+ scopes,
360
+ source: "config"
361
+ };
362
+ }
363
+ return rowFromStoredCredential(name, cred, "config", scopes);
364
+ }
365
+ function rowFromStoredCredential(name, cred, source, configuredScopes = []) {
366
+ const hasRefresh = typeof cred.refresh_token === "string" && cred.refresh_token.length > 0;
367
+ const status = isExpired(cred.expires_at) && !hasRefresh ? "auth-required" : "authenticated";
368
+ return {
369
+ name,
370
+ status,
371
+ expiresAt: cred.expires_at,
372
+ scopes: configuredScopes,
373
+ source
374
+ };
375
+ }
376
+ function renderRows(rows) {
377
+ if (rows.length === 0) {
378
+ return "No servers with OAuth configuration or advertised OAuth metadata.";
379
+ }
380
+ const headers = ["SERVER", "STATUS", "EXPIRES", "SCOPES", "SOURCE"];
381
+ const data = rows.map((r) => [
382
+ r.name,
383
+ r.status,
384
+ formatExpiry(r.expiresAt),
385
+ r.scopes.length > 0 ? r.scopes.join(" ") : "\u2014",
386
+ r.source
387
+ ]);
388
+ const widths = headers.map(
389
+ (h, i) => Math.max(h.length, ...data.map((row) => row[i].length))
390
+ );
391
+ const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
392
+ return [fmt(headers), ...data.map(fmt)].join("\n");
393
+ }
394
+ async function runAuthList(opts, deps = {}) {
395
+ const print = deps.print ?? defaultPrint;
396
+ const errPrint = deps.errPrint ?? defaultErrPrint;
397
+ try {
398
+ const config = deps.loadConfig ? await deps.loadConfig() : await loadConfigForAuth(opts.configPath);
399
+ warnInlineClientSecrets(config, errPrint);
400
+ const store = deps.store ?? new CredentialStore({ keychain: createKeychainAdapter() });
401
+ const discover = deps.discoverProtectedResource ?? discoverOAuthProtectedResourceMetadata;
402
+ const rows = await buildStatusRows(config, store, discover);
403
+ print(renderRows(rows));
404
+ return 0;
405
+ } catch (err) {
406
+ errPrint(`Error: ${formatErr(err)}`);
407
+ return 1;
408
+ }
409
+ }
410
+ function warnInlineClientSecrets(config, errPrint) {
411
+ const offenders = findInlineClientSecrets(config);
412
+ for (const name of offenders) {
413
+ errPrint(
414
+ `warning: clientSecret for "${name}" is an inline string in config. Prefer the credential store (\`credential set ${clientSecretKey(name)} <value>\`) or a \${ENV_VAR} reference so secrets stay out of shared config files.`
415
+ );
416
+ }
417
+ }
418
+ async function runAuthRemove(serverName, opts = {}, deps = {}) {
419
+ const print = deps.print ?? defaultPrint;
420
+ const errPrint = deps.errPrint ?? defaultErrPrint;
421
+ try {
422
+ const store = deps.store ?? new CredentialStore({ keychain: createKeychainAdapter() });
423
+ let canonical = serverName;
424
+ try {
425
+ const config = deps.loadConfig ? await deps.loadConfig() : await loadConfigForAuth(opts.configPath);
426
+ const resolved = findUpstreamName(resolveUpstreams(config), serverName);
427
+ if (resolved) canonical = resolved;
428
+ } catch {
429
+ }
430
+ const tokenKey = oauthCredentialKey(canonical);
431
+ const secretKey = clientSecretKey(canonical);
432
+ const clientKey = clientInfoKey(canonical);
433
+ const removed = await store.deleteMany([tokenKey, secretKey, clientKey]);
434
+ if (removed.length === 0) {
435
+ errPrint(`No stored credentials for "${serverName}"`);
436
+ return 1;
437
+ }
438
+ const parts = [];
439
+ if (removed.includes(tokenKey)) parts.push("token");
440
+ if (removed.includes(secretKey)) parts.push("client secret");
441
+ if (removed.includes(clientKey)) parts.push("registered client");
442
+ print(`Removed local ${parts.join(" + ")} for "${canonical}"`);
443
+ return 0;
444
+ } catch (err) {
445
+ errPrint(`Error: ${formatErr(err)}`);
446
+ return 1;
447
+ }
448
+ }
449
+ function findUpstreamName(upstreams, typed) {
450
+ if (Object.hasOwn(upstreams, typed)) return typed;
451
+ const lower = typed.toLowerCase();
452
+ for (const name of Object.keys(upstreams)) {
453
+ if (name.toLowerCase() === lower) return name;
454
+ }
455
+ return void 0;
456
+ }
457
+ function getUpstream(config, name) {
458
+ const upstreams = resolveUpstreams(config);
459
+ const canonicalName = findUpstreamName(upstreams, name);
460
+ if (canonicalName === void 0) return void 0;
461
+ const server = upstreams[canonicalName];
462
+ if (!isHttpServer(server)) return void 0;
463
+ return { canonicalName, server, authConfig: server._bridge?.auth };
464
+ }
465
+ function assertSafeAuthorizationUrl(authUrl) {
466
+ if (authUrl.username !== "" || authUrl.password !== "") {
467
+ throw new OAuthError(
468
+ "insecure_authorization_url",
469
+ `Authorization URL must not embed userinfo (got ${authUrl.protocol}//${authUrl.host}).`
470
+ );
471
+ }
472
+ if (authUrl.protocol === "https:") return;
473
+ const loopback = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
474
+ if (loopback.has(authUrl.hostname)) return;
475
+ throw new OAuthError(
476
+ "insecure_authorization_url",
477
+ `Authorization endpoint must use https (got ${authUrl.protocol}//${authUrl.host}). Refusing to open the browser at an insecure URL.`
478
+ );
479
+ }
480
+ async function runAuthLogin(opts, deps = {}) {
481
+ const print = deps.print ?? defaultPrint;
482
+ const errPrint = deps.errPrint ?? defaultErrPrint;
483
+ let handle;
484
+ try {
485
+ const config = deps.loadConfig ? await deps.loadConfig() : await loadConfigForAuth(opts.configPath);
486
+ warnInlineClientSecrets(config, errPrint);
487
+ const upstream = getUpstream(config, opts.serverName);
488
+ if (!upstream) {
489
+ errPrint(
490
+ `Error: server "${opts.serverName}" is not configured or is not an HTTP upstream.
491
+ OAuth via \`auth <server>\` requires an HTTP/streamable-http server.`
492
+ );
493
+ return 1;
494
+ }
495
+ const store = deps.store ?? new CredentialStore({ keychain: createKeychainAdapter() });
496
+ const authConfig = upstream.authConfig;
497
+ const serverName = upstream.canonicalName;
498
+ const clientSecret = await resolveClientSecret(
499
+ serverName,
500
+ authConfig?.clientSecret,
501
+ store
502
+ );
503
+ const startFn = deps.startCallbackServer ?? startCallbackServer;
504
+ const openFn = deps.openBrowser ?? openBrowser;
505
+ const authFn = deps.auth ?? auth;
506
+ handle = await startFn({
507
+ port: authConfig?.redirectPort,
508
+ signal: deps.signal
509
+ });
510
+ const provider = new BridgeOAuthClientProvider({
511
+ serverName,
512
+ store,
513
+ redirectUrl: handle.redirectUri,
514
+ clientId: authConfig?.clientId,
515
+ clientSecret,
516
+ scopes: authConfig?.scopes,
517
+ onRedirect: async (url) => {
518
+ assertSafeAuthorizationUrl(url);
519
+ errPrint(`Opening authorization URL for "${serverName}":`);
520
+ errPrint(` ${url}`);
521
+ errPrint(`Listening on ${handle.redirectUri} (Ctrl-C to cancel)`);
522
+ void openFn(String(url)).catch(() => void 0);
523
+ }
524
+ });
525
+ const pinningFetch = makeOriginPinningFetch();
526
+ const first = await authFn(provider, {
527
+ serverUrl: upstream.server.url,
528
+ fetchFn: pinningFetch
529
+ });
530
+ if (first === "AUTHORIZED") {
531
+ print(`Already authenticated as "${serverName}"`);
532
+ return 0;
533
+ }
534
+ const callback = await handle.result;
535
+ const expected = provider.expectedState();
536
+ if (expected === void 0) {
537
+ errPrint(
538
+ `Error: OAuth flow did not initialize state \u2014 internal error.
539
+ Re-run \`${APP_NAME} auth ${serverName}\`.`
540
+ );
541
+ return 1;
542
+ }
543
+ if (!constantTimeEquals(expected, callback.state)) {
544
+ errPrint(
545
+ `Error: OAuth state mismatch \u2014 refusing to exchange the callback code.
546
+ This usually means the authorization flow was interrupted or a stale
547
+ callback arrived. Re-run \`${APP_NAME} auth ${serverName}\`.`
548
+ );
549
+ return 1;
550
+ }
551
+ const result = await authFn(provider, {
552
+ serverUrl: upstream.server.url,
553
+ authorizationCode: callback.code,
554
+ fetchFn: pinningFetch
555
+ });
556
+ if (result !== "AUTHORIZED") {
557
+ errPrint("Error: authorization did not complete");
558
+ return 1;
559
+ }
560
+ const stored = await provider.tokens();
561
+ print(`Authenticated "${serverName}"`);
562
+ if (stored?.scope) print(` Scopes: ${stored.scope}`);
563
+ if (stored?.expires_in !== void 0) {
564
+ const expiresAt = Math.floor(Date.now() / 1e3) + stored.expires_in;
565
+ print(` Expires: ${formatExpiry(expiresAt)}`);
566
+ }
567
+ return 0;
568
+ } catch (err) {
569
+ errPrint(`Error: ${formatErr(err)}`);
570
+ return 1;
571
+ } finally {
572
+ if (handle) {
573
+ try {
574
+ await handle.close();
575
+ } catch (closeErr) {
576
+ const message = closeErr instanceof Error ? closeErr.message : String(closeErr);
577
+ errPrint(`warning: failed to close callback listener: ${message}`);
578
+ }
579
+ }
580
+ }
581
+ }
582
+ function constantTimeEquals(a, b) {
583
+ const bufA = Buffer.from(a, "utf8");
584
+ const bufB = Buffer.from(b, "utf8");
585
+ if (bufA.length !== bufB.length) return false;
586
+ return timingSafeEqual(bufA, bufB);
587
+ }
588
+ function formatErr(err) {
589
+ if (err instanceof ConfigError) {
590
+ let msg = err.message;
591
+ for (const issue of err.issues) msg += `
592
+ ${issue.path}: ${issue.message}`;
593
+ return msg;
594
+ }
595
+ if (err instanceof CredentialError || err instanceof OAuthError) return err.message;
596
+ if (err instanceof Error) {
597
+ const errorCode = err.errorCode;
598
+ if (typeof errorCode === "string") {
599
+ return `[${errorCode}] ${err.message}`;
600
+ }
601
+ }
602
+ return err instanceof Error ? err.message : String(err);
603
+ }
604
+ export {
605
+ USAGE as authUsage,
606
+ runAuthList,
607
+ runAuthLogin,
608
+ runAuthRemove
609
+ };
610
+ //# sourceMappingURL=auth-EI4HPCPE.js.map