tiny-http-mcp-server 0.1.35 → 0.1.37

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.
@@ -3395,1296 +3395,1444 @@ var SubscriptionManager = class {
3395
3395
  }
3396
3396
  };
3397
3397
 
3398
- // ../mcp-oauth/dist/client/scope.js
3399
- function normalizeOAuthScope(scope) {
3400
- if (scope === void 0)
3401
- return void 0;
3402
- if (typeof scope !== "string" || [...scope].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
3403
- throw new Error("Invalid OAuth scope syntax");
3404
- const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
3405
- return normalized || void 0;
3406
- }
3398
+ // ../mcp-oauth/dist/client/loopback-authorization.js
3399
+ import http from "node:http";
3407
3400
 
3408
- // ../mcp-oauth/dist/client/token-auth-method.js
3409
- function normalizeOAuthTokenEndpointAuthMethod(value) {
3410
- if (value === void 0 || value === null)
3411
- return void 0;
3412
- if (value !== "none" && value !== "client_secret_post" && value !== "client_secret_basic")
3413
- throw new Error("Unsupported OAuth token endpoint authentication method");
3414
- return value;
3401
+ // ../mcp-oauth/dist/client/authorization-state.js
3402
+ import crypto from "node:crypto";
3403
+ function createAuthorizationState(input) {
3404
+ const payload = {
3405
+ v: 1,
3406
+ n: crypto.randomBytes(16).toString("base64url"),
3407
+ i: input.issuer,
3408
+ r: input.requireIssuer
3409
+ };
3410
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
3415
3411
  }
3416
-
3417
- // ../mcp-oauth/dist/client/client-registration.js
3418
- function parseOAuthClientRegistration(value) {
3419
- const invalid = () => new Error("Invalid OAuth client registration metadata");
3420
- let nodes = 0;
3421
- function copy(input, depth) {
3422
- if (++nodes > 2e4 || depth > 64)
3423
- throw invalid();
3424
- if (input === null || typeof input === "boolean" || typeof input === "string")
3425
- return input;
3426
- if (typeof input === "number" && Number.isFinite(input))
3427
- return input;
3428
- if (typeof input !== "object" || input === null)
3429
- throw invalid();
3430
- const descriptors = Object.getOwnPropertyDescriptors(input);
3431
- if (Array.isArray(input)) {
3432
- const length = descriptors.length?.value;
3433
- if (length > 2e4)
3434
- throw invalid();
3435
- const result2 = [];
3436
- for (let index = 0; index < length; index++) {
3437
- const descriptor = descriptors[String(index)];
3438
- if (descriptor === void 0 || !Object.hasOwn(descriptor, "value"))
3439
- throw invalid();
3440
- result2.push(copy(descriptor.value, depth + 1));
3441
- }
3442
- return result2;
3443
- }
3444
- if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
3445
- throw invalid();
3446
- return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key2, descriptor]) => {
3447
- if (!Object.hasOwn(descriptor, "value"))
3448
- throw invalid();
3449
- return [key2, copy(descriptor.value, depth + 1)];
3450
- }));
3451
- }
3452
- let result;
3453
- try {
3454
- result = copy(value, 0);
3455
- } catch {
3456
- throw invalid();
3457
- }
3458
- if (typeof result !== "object" || result === null || Array.isArray(result))
3459
- throw invalid();
3460
- const record2 = result;
3461
- if (!Object.hasOwn(record2, "client_id") || typeof record2.client_id !== "string" || record2.client_id.trim() === "")
3462
- throw new Error("OAuth client registration response missing client_id");
3463
- for (const key2 of [
3464
- "client_id",
3465
- "client_secret",
3466
- "token_endpoint_auth_method",
3467
- "application_type",
3468
- "client_name",
3469
- "client_uri",
3470
- "logo_uri",
3471
- "scope",
3472
- "tos_uri",
3473
- "policy_uri",
3474
- "jwks_uri",
3475
- "software_id",
3476
- "software_version",
3477
- "software_statement",
3478
- "registration_access_token",
3479
- "registration_client_uri",
3480
- "issuer"
3481
- ]) {
3482
- if (Object.hasOwn(record2, key2) && record2[key2] !== null && typeof record2[key2] !== "string")
3483
- throw invalid();
3484
- }
3485
- if (typeof record2.client_secret === "string" && record2.client_secret.trim() === "")
3486
- throw invalid();
3487
- for (const key2 of ["redirect_uris", "grant_types", "response_types", "contacts"]) {
3488
- const entry = record2[key2];
3489
- if (Object.hasOwn(record2, key2) && entry !== null && (!Array.isArray(entry) || entry.some((item) => typeof item !== "string")))
3490
- throw invalid();
3491
- }
3492
- for (const key2 of ["client_id_issued_at", "client_secret_expires_at"]) {
3493
- const entry = record2[key2];
3494
- if (Object.hasOwn(record2, key2) && entry !== null && (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 0))
3495
- throw invalid();
3412
+ function parseAuthorizationState(value) {
3413
+ if (value === null || value.length === 0) {
3414
+ return null;
3496
3415
  }
3497
3416
  try {
3498
- normalizeOAuthScope(Object.hasOwn(record2, "scope") && record2.scope !== null ? record2.scope : void 0);
3417
+ const decoded = Buffer.from(value, "base64url").toString("utf8");
3418
+ const parsed = JSON.parse(decoded);
3419
+ if (!isObjectRecord(parsed)) {
3420
+ return null;
3421
+ }
3422
+ const version = getOwnEntry(parsed, "v");
3423
+ const nonce = getOwnEntry(parsed, "n");
3424
+ const issuer = getOwnEntry(parsed, "i");
3425
+ const requireIssuer = getOwnEntry(parsed, "r");
3426
+ if (version !== 1 || typeof nonce !== "string" || nonce.length === 0 || typeof issuer !== "string" || issuer.length === 0 || typeof requireIssuer !== "boolean") {
3427
+ return null;
3428
+ }
3429
+ return {
3430
+ issuer,
3431
+ requireIssuer
3432
+ };
3499
3433
  } catch {
3500
- throw invalid();
3501
- }
3502
- if (Buffer.byteLength(JSON.stringify(record2), "utf8") > 64 * 1024)
3503
- throw invalid();
3504
- return record2;
3505
- }
3506
- function normalizeStoredOAuthClient(value) {
3507
- if (typeof value !== "object" || value === null || Array.isArray(value))
3508
- return null;
3509
- const record2 = value;
3510
- const clientId = Object.hasOwn(record2, "clientId") ? record2.clientId : void 0;
3511
- const clientSecret = Object.hasOwn(record2, "clientSecret") ? record2.clientSecret : void 0;
3512
- if (typeof clientId !== "string" || clientId.trim() === "" || clientSecret !== void 0 && (typeof clientSecret !== "string" || clientSecret.trim() === ""))
3513
3434
  return null;
3514
- const client = { clientId: clientId.trim(), ...clientSecret === void 0 ? {} : { clientSecret: clientSecret.trim() } };
3515
- const method = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(record2, "tokenEndpointAuthMethod") ? record2.tokenEndpointAuthMethod : void 0);
3516
- if (Object.hasOwn(record2, "registration") && record2.registration !== void 0) {
3517
- const registration = parseOAuthClientRegistration(record2.registration);
3518
- const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : void 0;
3519
- if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
3520
- throw new Error("OAuth client registration does not match the client identity");
3521
- client.registration = registration;
3522
- const registrationMethod = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(registration, "token_endpoint_auth_method") ? registration.token_endpoint_auth_method : void 0);
3523
- if (method !== void 0 && registrationMethod !== void 0 && method !== registrationMethod)
3524
- throw new Error("OAuth token endpoint authentication conflicts with the client registration");
3525
- if (registrationMethod !== void 0)
3526
- client.tokenEndpointAuthMethod = registrationMethod;
3527
3435
  }
3528
- if (method !== void 0)
3529
- client.tokenEndpointAuthMethod = method;
3530
- return client;
3531
3436
  }
3532
-
3533
- // ../mcp-oauth/dist/client/auth-store-session-store.js
3534
- import crypto from "node:crypto";
3535
- import path4 from "node:path";
3536
-
3537
- // ../auth-store/dist/encrypted-file-store.js
3538
- import { createCipheriv, createDecipheriv, randomBytes, randomUUID as randomUUID2, scrypt } from "node:crypto";
3539
- import { promises as fs } from "node:fs";
3540
- import { homedir, hostname, userInfo } from "node:os";
3541
- import path2 from "node:path";
3542
-
3543
- // ../auth-store/dist/error-codes.js
3544
- function hasOwnErrorCode(error, code) {
3545
- return error instanceof Error && Object.prototype.hasOwnProperty.call(error, "code") && error.code === code;
3437
+ function isObjectRecord(value) {
3438
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3439
+ }
3440
+ function getOwnEntry(record2, key2) {
3441
+ return Object.prototype.hasOwnProperty.call(record2, key2) ? record2[key2] : void 0;
3546
3442
  }
3547
3443
 
3548
- // ../auth-store/dist/transaction-lock.js
3549
- import { randomUUID } from "node:crypto";
3550
- import path from "node:path";
3551
- async function withSecretStoreFileLock(fs2, lockDirectory, operation, options = {}) {
3552
- const timeoutMs = options.timeoutMs ?? 3e4;
3553
- if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 2147483647)
3554
- throw new Error("Invalid secret-store transaction lock timeout");
3444
+ // ../mcp-oauth/dist/client/loopback-authorization.js
3445
+ async function createLoopbackAuthorizationSession(options = {}) {
3555
3446
  options.signal?.throwIfAborted();
3556
- const deadline = performance.now() + timeoutMs;
3557
- const directory = path.resolve(lockDirectory);
3558
- await assertLockDirectoryPath(fs2, directory);
3559
- await fs2.mkdir(directory, { recursive: true, mode: 448 });
3560
- await assertLockDirectoryPath(fs2, directory);
3561
- const name = `${process.pid}-${randomUUID()}.claim`;
3562
- const claimPath = path.join(directory, name);
3563
- const temporaryPath = `${claimPath}.tmp`;
3564
- let claimed = true;
3565
- let temporaryCreated = false;
3566
- let outcome;
3447
+ const timeoutMs = options.timeoutMs ?? 12e4;
3448
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
3449
+ throw new Error("OAuth authorization timeoutMs must be a positive supported timer interval");
3450
+ const target = loopbackTarget(options);
3451
+ const server = options.createServer ? options.createServer() : http.createServer();
3452
+ const controller = new AbortController();
3453
+ let closed = false;
3454
+ let used = false;
3455
+ const callerAbort = () => controller.abort(options.signal?.reason);
3456
+ const teardown = () => {
3457
+ if (closed)
3458
+ return;
3459
+ closed = true;
3460
+ clearTimeout(timer);
3461
+ options.signal?.removeEventListener("abort", callerAbort);
3462
+ server.closeAllConnections?.();
3463
+ server.close();
3464
+ };
3465
+ const timer = setTimeout(() => controller.abort(new Error("OAuth authorization timed out")), timeoutMs);
3466
+ timer.unref?.();
3467
+ controller.signal.addEventListener("abort", teardown, { once: true });
3468
+ options.signal?.addEventListener("abort", callerAbort, { once: true });
3469
+ if (options.signal?.aborted)
3470
+ callerAbort();
3471
+ let port;
3567
3472
  try {
3568
- try {
3569
- await fs2.writeFile(claimPath, JSON.stringify({ ticket: null }), { encoding: "utf8", flag: "wx", mode: 384 });
3570
- } catch (error) {
3571
- if (hasOwnErrorCode(error, "EEXIST"))
3572
- claimed = false;
3573
- throw error;
3473
+ port = await startServer(server, target.port, target.host, controller.signal);
3474
+ } catch (error) {
3475
+ controller.abort(error);
3476
+ throw error;
3477
+ }
3478
+ const redirectUri = options.redirectUri ?? `http://127.0.0.1:${port}${target.callbackPath}`;
3479
+ return {
3480
+ redirectUri,
3481
+ async waitForCode(authorizationUrl) {
3482
+ controller.signal.throwIfAborted();
3483
+ if (used)
3484
+ throw new Error("OAuth authorization session has already been used");
3485
+ used = true;
3486
+ try {
3487
+ return await waitForAuthorizationCode(server, authorizationUrl, options, target.callbackPath, controller.signal);
3488
+ } finally {
3489
+ clearTimeout(timer);
3490
+ }
3491
+ },
3492
+ close() {
3493
+ controller.abort(new Error("OAuth authorization session closed"));
3574
3494
  }
3575
- const existing = await readClaims(fs2, directory, name);
3576
- const ticket = existing.reduce((max, claim) => Math.max(max, claim.ticket ?? 0), 0) + 1;
3577
- if (!Number.isSafeInteger(ticket))
3578
- throw new Error("Secret-store transaction lock ticket overflow");
3579
- temporaryCreated = true;
3495
+ };
3496
+ }
3497
+ function loopbackTarget(options) {
3498
+ if (options.redirectUri !== void 0) {
3499
+ let url;
3580
3500
  try {
3581
- await fs2.writeFile(temporaryPath, JSON.stringify({ ticket }), { encoding: "utf8", flag: "wx", mode: 384 });
3582
- } catch (error) {
3583
- if (hasOwnErrorCode(error, "EEXIST"))
3584
- temporaryCreated = false;
3585
- throw error;
3586
- }
3587
- await fs2.rename(temporaryPath, claimPath);
3588
- temporaryCreated = false;
3589
- for (; ; ) {
3590
- options.signal?.throwIfAborted();
3591
- const peers = await readClaims(fs2, directory, name);
3592
- if (!peers.some((peer) => peer.ticket === null || peer.ticket < ticket || peer.ticket === ticket && peer.name < name))
3593
- break;
3594
- const remaining = deadline - performance.now();
3595
- if (remaining <= 0)
3596
- throw new Error("Timed out waiting for secret-store transaction lock");
3597
- await new Promise((resolve, reject) => {
3598
- const abort = () => {
3599
- clearTimeout(timer);
3600
- options.signal?.removeEventListener("abort", abort);
3601
- reject(options.signal?.reason);
3602
- };
3603
- const timer = setTimeout(() => {
3604
- options.signal?.removeEventListener("abort", abort);
3605
- resolve();
3606
- }, Math.min(10, remaining));
3607
- options.signal?.addEventListener("abort", abort, { once: true });
3608
- if (options.signal?.aborted)
3609
- abort();
3610
- });
3501
+ url = new URL(options.redirectUri);
3502
+ } catch (cause) {
3503
+ throw new Error("Invalid OAuth loopback redirect URI", { cause });
3611
3504
  }
3612
- options.signal?.throwIfAborted();
3613
- outcome = { result: await operation() };
3614
- } catch (error) {
3615
- outcome = { error };
3505
+ const forbiddenQuery = ["code", "state", "error", "error_description", "iss"];
3506
+ if (url.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password || url.hash || url.port === "0" || forbiddenQuery.some((name) => url.searchParams.has(name)) || [...options.redirectUri].some((char) => char.codePointAt(0) <= 32) || options.callbackPath !== void 0 && options.callbackPath !== url.pathname)
3507
+ throw new Error("Invalid OAuth loopback redirect URI");
3508
+ return { port: url.port ? Number(url.port) : 80, host: url.hostname === "[::1]" ? "::1" : url.hostname, callbackPath: url.pathname };
3616
3509
  }
3617
- const cleanup = [];
3618
- for (const target of [...temporaryCreated ? [temporaryPath] : [], ...claimed ? [claimPath] : []]) {
3510
+ const callbackPath = options.callbackPath ?? "/callback";
3511
+ const parsed = new URL(callbackPath, "http://127.0.0.1");
3512
+ if (!callbackPath.startsWith("/") || parsed.origin !== "http://127.0.0.1" || parsed.pathname !== callbackPath || parsed.search || parsed.hash)
3513
+ throw new Error("Invalid OAuth loopback callback path");
3514
+ return { port: 0, host: "127.0.0.1", callbackPath };
3515
+ }
3516
+ async function startServer(server, port, host, signal) {
3517
+ signal.throwIfAborted();
3518
+ return new Promise((resolve, reject) => {
3519
+ const cleanup = () => {
3520
+ server.off("error", handleError);
3521
+ signal.removeEventListener("abort", aborted);
3522
+ };
3523
+ const handleError = (error) => {
3524
+ cleanup();
3525
+ reject(error);
3526
+ };
3527
+ const aborted = () => {
3528
+ cleanup();
3529
+ reject(signal.reason);
3530
+ };
3531
+ server.once("error", handleError);
3532
+ signal.addEventListener("abort", aborted, { once: true });
3619
3533
  try {
3620
- await fs2.unlink(target);
3534
+ server.listen(port, host, () => {
3535
+ cleanup();
3536
+ if (signal.aborted) {
3537
+ server.close();
3538
+ reject(signal.reason);
3539
+ return;
3540
+ }
3541
+ const address = server.address();
3542
+ if (address === null || typeof address === "string") {
3543
+ reject(new Error("OAuth listener has no TCP address"));
3544
+ return;
3545
+ }
3546
+ resolve(address.port);
3547
+ });
3621
3548
  } catch (error) {
3622
- if (!hasOwnErrorCode(error, "ENOENT"))
3623
- cleanup.push(error);
3549
+ cleanup();
3550
+ reject(error);
3624
3551
  }
3625
- }
3626
- if (cleanup.length)
3627
- throw new AggregateError([..."error" in outcome ? [outcome.error] : [], ...cleanup], "Secret-store transaction lock cleanup failed");
3628
- if ("error" in outcome)
3629
- throw outcome.error;
3630
- return outcome.result;
3552
+ });
3631
3553
  }
3632
- async function assertNoSymbolicLink(fs2, target) {
3554
+ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPath, signal) {
3555
+ signal.throwIfAborted();
3556
+ const expectedAuthorization = readExpectedAuthorizationCallback(authorizationUrl);
3557
+ return new Promise((resolve, reject) => {
3558
+ let settled = false;
3559
+ const settle = (fn) => {
3560
+ if (settled)
3561
+ return;
3562
+ settled = true;
3563
+ server.off("request", request);
3564
+ signal.removeEventListener("abort", aborted);
3565
+ fn();
3566
+ };
3567
+ const aborted = () => settle(() => reject(signal.reason));
3568
+ const request = (req, res) => {
3569
+ let url;
3570
+ try {
3571
+ url = new URL(req.url ?? "/", "http://127.0.0.1");
3572
+ } catch {
3573
+ res.writeHead(400);
3574
+ res.end("Invalid callback URL");
3575
+ return;
3576
+ }
3577
+ if (url.pathname !== callbackPath) {
3578
+ res.writeHead(404);
3579
+ res.end("Not found");
3580
+ return;
3581
+ }
3582
+ const callbackParameters = {
3583
+ code: url.searchParams.get("code"),
3584
+ error: url.searchParams.get("error"),
3585
+ errorDescription: url.searchParams.get("error_description"),
3586
+ state: url.searchParams.get("state"),
3587
+ iss: url.searchParams.get("iss")
3588
+ };
3589
+ try {
3590
+ validateAuthorizationCallbackBinding(callbackParameters, expectedAuthorization);
3591
+ if (callbackParameters.error !== null)
3592
+ throw createAuthorizationError(callbackParameters.error, callbackParameters.errorDescription ?? callbackParameters.error);
3593
+ const code = validateAuthorizationCallbackParameters(callbackParameters, expectedAuthorization);
3594
+ res.writeHead(200, { "Content-Type": "text/html" });
3595
+ res.end(buildSuccessPage(options.landingPage));
3596
+ settle(() => resolve(code));
3597
+ } catch (error) {
3598
+ res.writeHead(400);
3599
+ res.end(error instanceof Error ? error.message : "Invalid OAuth callback");
3600
+ settle(() => reject(error));
3601
+ }
3602
+ };
3603
+ server.on("request", request);
3604
+ signal.addEventListener("abort", aborted, { once: true });
3605
+ if (options.readLine !== void 0) {
3606
+ void Promise.resolve().then(() => settled ? void 0 : options.readLine()).then((input) => {
3607
+ if (settled)
3608
+ return;
3609
+ const callbackParameters = extractCallbackParametersFromInput(input);
3610
+ if (callbackParameters === null)
3611
+ throw new Error("OAuth callback missing authorization code");
3612
+ validateAuthorizationCallbackBinding(callbackParameters, expectedAuthorization);
3613
+ if (callbackParameters.error !== null)
3614
+ throw createAuthorizationError(callbackParameters.error, callbackParameters.errorDescription ?? callbackParameters.error);
3615
+ const code = validateAuthorizationCallbackParameters(callbackParameters, expectedAuthorization);
3616
+ settle(() => resolve(code));
3617
+ }).catch((error) => settle(() => reject(error)));
3618
+ }
3619
+ if (options.openBrowser !== void 0) {
3620
+ void Promise.resolve().then(() => settled ? void 0 : options.openBrowser(authorizationUrl)).catch((error) => settle(() => reject(error)));
3621
+ }
3622
+ });
3623
+ }
3624
+ function extractCallbackParametersFromInput(input) {
3625
+ const trimmed = input.replaceAll("\r", "").replaceAll("\n", "").trim();
3626
+ if (trimmed.length === 0) {
3627
+ return null;
3628
+ }
3633
3629
  try {
3634
- if ((await fs2.lstat(target)).isSymbolicLink())
3635
- throw new Error("Refusing secret-store transaction lock through symbolic link");
3636
- } catch (error) {
3637
- if (!hasOwnErrorCode(error, "ENOENT"))
3638
- throw error;
3630
+ const url = new URL(trimmed);
3631
+ return {
3632
+ code: url.searchParams.get("code"),
3633
+ error: url.searchParams.get("error"),
3634
+ errorDescription: url.searchParams.get("error_description"),
3635
+ state: url.searchParams.get("state"),
3636
+ iss: url.searchParams.get("iss")
3637
+ };
3638
+ } catch {
3639
+ return {
3640
+ code: trimmed,
3641
+ error: null,
3642
+ errorDescription: null,
3643
+ state: null,
3644
+ iss: null
3645
+ };
3639
3646
  }
3640
3647
  }
3641
- async function assertLockDirectoryPath(fs2, directory) {
3642
- const root = path.parse(directory).root;
3643
- const segments = directory.slice(root.length).split(path.sep).filter(Boolean);
3644
- let current = root;
3645
- for (const [index, segment] of segments.entries()) {
3646
- current = path.join(current, segment);
3647
- if (index > 0 || segments.length === 1)
3648
- await assertNoSymbolicLink(fs2, current);
3648
+ function readExpectedAuthorizationCallback(authorizationUrl) {
3649
+ const url = new URL(authorizationUrl);
3650
+ const state = url.searchParams.get("state");
3651
+ const parsedState = parseAuthorizationState(state);
3652
+ return {
3653
+ state,
3654
+ issuer: parsedState?.issuer ?? null,
3655
+ requireIssuer: parsedState?.requireIssuer ?? false
3656
+ };
3657
+ }
3658
+ function validateAuthorizationCallbackParameters(callback, expected) {
3659
+ validateAuthorizationCallbackBinding(callback, expected);
3660
+ if (callback.code === null || callback.code.length === 0) {
3661
+ throw new Error("OAuth callback missing authorization code");
3649
3662
  }
3663
+ return callback.code;
3650
3664
  }
3651
- async function readClaims(fs2, directory, ownName) {
3652
- const claims = [];
3653
- for (const name of await fs2.readdir(directory)) {
3654
- if (name === ownName || !name.endsWith(".claim"))
3655
- continue;
3656
- const pidText = name.slice(0, name.indexOf("-"));
3657
- const pid = Number(pidText);
3658
- if (!Number.isSafeInteger(pid) || pid < 1 || String(pid) !== pidText)
3659
- throw new Error("Malformed secret-store transaction lock owner");
3660
- const target = path.join(directory, name);
3661
- await assertNoSymbolicLink(fs2, target);
3662
- let alive = true;
3663
- try {
3664
- process.kill(pid, 0);
3665
- } catch (error) {
3666
- alive = !hasOwnErrorCode(error, "ESRCH");
3667
- }
3668
- if (!alive) {
3669
- try {
3670
- await fs2.unlink(target);
3671
- } catch (error) {
3672
- if (!hasOwnErrorCode(error, "ENOENT"))
3673
- throw error;
3674
- }
3675
- continue;
3676
- }
3677
- let ticket = null;
3678
- let raw;
3679
- try {
3680
- raw = await fs2.readFile(target, "utf8");
3681
- } catch (error) {
3682
- if (hasOwnErrorCode(error, "ENOENT"))
3683
- continue;
3684
- throw error;
3665
+ function validateAuthorizationCallbackBinding(callback, expected) {
3666
+ if (expected.state !== null) {
3667
+ if (callback.state === null || callback.state.length === 0) {
3668
+ throw new Error("OAuth callback missing state");
3685
3669
  }
3686
- try {
3687
- const value = JSON.parse(raw);
3688
- if (value !== null && typeof value === "object" && "ticket" in value && typeof value.ticket === "number" && Number.isSafeInteger(value.ticket) && value.ticket > 0)
3689
- ticket = value.ticket;
3690
- } catch {
3670
+ if (callback.state !== expected.state) {
3671
+ throw new Error("OAuth callback state mismatch");
3691
3672
  }
3692
- claims.push({ name, ticket });
3693
3673
  }
3694
- return claims;
3695
- }
3696
-
3697
- // ../auth-store/dist/encrypted-file-store.js
3698
- var derivedKeyCache = /* @__PURE__ */ new Map();
3699
- var ENCRYPTION_ALGORITHM = "aes-256-gcm";
3700
- var ENCRYPTION_VERSION = 1;
3701
- var ENCRYPTION_KEY_BYTES = 32;
3702
- var ENCRYPTION_IV_BYTES = 12;
3703
- var ENCRYPTION_AUTH_TAG_BYTES = 16;
3704
- var ENCRYPTION_FILE_MODE = 384;
3705
- var EncryptedFileStore = class {
3706
- fs;
3707
- filePath;
3708
- symbolicLinkCheckStartPath;
3709
- salt;
3710
- getMachineIdentity;
3711
- getRandomBytes;
3712
- keyPromise = null;
3713
- throwOnInvalidDocument;
3714
- constructor(input) {
3715
- this.fs = input.fs ?? fs;
3716
- this.salt = input.salt;
3717
- if (input.filePath === void 0) {
3718
- const homeDirectory = (input.getHomeDirectory ?? homedir)();
3719
- const defaultDirectory = input.defaultDirectory ?? ".auth-store";
3720
- const defaultFileName = input.defaultFileName ?? "credentials.enc";
3721
- assertSafeDefaultDirectory(defaultDirectory);
3722
- assertSafeDefaultFileName(defaultFileName);
3723
- this.filePath = path2.join(homeDirectory, defaultDirectory, defaultFileName);
3724
- this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
3725
- } else {
3726
- this.filePath = input.filePath;
3727
- this.symbolicLinkCheckStartPath = null;
3728
- }
3729
- this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
3730
- this.getRandomBytes = input.getRandomBytes ?? randomBytes;
3731
- this.throwOnInvalidDocument = input.throwOnInvalidDocument ?? false;
3732
- }
3733
- async get() {
3734
- await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
3735
- let rawDocument;
3736
- try {
3737
- rawDocument = await this.fs.readFile(this.filePath, "utf8");
3738
- } catch (error) {
3739
- if (isNotFoundError(error)) {
3740
- return null;
3741
- }
3742
- throw error;
3743
- }
3744
- const document = parseEncryptedDocument(rawDocument);
3745
- if (!document) {
3746
- if (this.throwOnInvalidDocument)
3747
- throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
3748
- return null;
3749
- }
3750
- const key2 = await this.getEncryptionKey();
3751
- try {
3752
- const iv = Buffer.from(document.iv, "base64");
3753
- const authTag = Buffer.from(document.authTag, "base64");
3754
- const ciphertext = Buffer.from(document.ciphertext, "base64");
3755
- if (iv.byteLength !== ENCRYPTION_IV_BYTES || authTag.byteLength !== ENCRYPTION_AUTH_TAG_BYTES) {
3756
- if (this.throwOnInvalidDocument)
3757
- throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
3758
- return null;
3759
- }
3760
- const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key2, iv);
3761
- decipher.setAuthTag(authTag);
3762
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
3763
- return plaintext.toString("utf8");
3764
- } catch {
3765
- if (this.throwOnInvalidDocument)
3766
- throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
3767
- return null;
3768
- }
3769
- }
3770
- async withLock(operation, options = {}) {
3771
- await this.assertCredentialPathHasNoSymbolicLinks(`${this.filePath}.lock`);
3772
- if (this.fs.readdir === void 0)
3773
- throw new Error("Secret-store transaction locks require filesystem readdir support");
3774
- return withSecretStoreFileLock(this.fs, `${this.filePath}.lock`, operation, options);
3775
- }
3776
- async set(value) {
3777
- await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
3778
- const key2 = await this.getEncryptionKey();
3779
- const iv = this.getRandomBytes(ENCRYPTION_IV_BYTES);
3780
- const cipher = createCipheriv(ENCRYPTION_ALGORITHM, key2, iv);
3781
- const ciphertext = Buffer.concat([
3782
- cipher.update(value, "utf8"),
3783
- cipher.final()
3784
- ]);
3785
- const authTag = cipher.getAuthTag();
3786
- const document = {
3787
- version: ENCRYPTION_VERSION,
3788
- iv: iv.toString("base64"),
3789
- authTag: authTag.toString("base64"),
3790
- ciphertext: ciphertext.toString("base64")
3791
- };
3792
- await this.fs.mkdir(path2.dirname(this.filePath), { recursive: true });
3793
- await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
3794
- const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID2()}.tmp`;
3795
- let temporaryCreated = false;
3796
- try {
3797
- await this.assertCredentialPathHasNoSymbolicLinks(temporaryPath);
3798
- await this.fs.writeFile(temporaryPath, JSON.stringify(document), {
3799
- encoding: "utf8",
3800
- flag: "wx",
3801
- mode: ENCRYPTION_FILE_MODE
3802
- });
3803
- temporaryCreated = true;
3804
- await this.fs.chmod(temporaryPath, ENCRYPTION_FILE_MODE);
3805
- await this.fs.rename(temporaryPath, this.filePath);
3806
- } catch (error) {
3807
- if (temporaryCreated || !isAlreadyExistsError(error)) {
3808
- await removeIfPresent(this.fs, temporaryPath).catch(() => void 0);
3809
- }
3810
- throw error;
3811
- }
3812
- }
3813
- async delete() {
3814
- await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
3815
- try {
3816
- await this.fs.unlink(this.filePath);
3817
- } catch (error) {
3818
- if (!isNotFoundError(error)) {
3819
- throw error;
3820
- }
3821
- }
3822
- }
3823
- async assertCredentialPathHasNoSymbolicLinks(targetPath) {
3824
- const resolvedPath = path2.resolve(targetPath);
3825
- const protectedPaths = getProtectedCredentialPaths(resolvedPath, this.symbolicLinkCheckStartPath);
3826
- for (const currentPath of protectedPaths) {
3827
- try {
3828
- const stats = await this.fs.lstat(currentPath);
3829
- if (stats.isSymbolicLink()) {
3830
- throw new Error(`Refusing to use encrypted credential path through symbolic link: ${currentPath}`);
3831
- }
3832
- } catch (error) {
3833
- if (isNotFoundError(error)) {
3834
- return;
3835
- }
3836
- throw error;
3837
- }
3838
- }
3839
- }
3840
- getEncryptionKey() {
3841
- if (!this.keyPromise) {
3842
- const retryableKeyPromise = deriveEncryptionKey(this.getMachineIdentity, this.salt).catch((error) => {
3843
- if (this.keyPromise === retryableKeyPromise) {
3844
- this.keyPromise = null;
3845
- }
3846
- throw error;
3847
- });
3848
- this.keyPromise = retryableKeyPromise;
3849
- }
3850
- return this.keyPromise;
3851
- }
3852
- };
3853
- function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
3854
- const [firstSegment] = defaultDirectory.split(/[\\/]+/).filter(Boolean);
3855
- return path2.resolve(homeDirectory, firstSegment ?? ".");
3856
- }
3857
- function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
3858
- if (symbolicLinkCheckStartPath === null) {
3859
- return getExplicitProtectedCredentialPaths(resolvedPath);
3860
- }
3861
- const resolvedStartPath = path2.resolve(symbolicLinkCheckStartPath);
3862
- if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
3863
- return [path2.dirname(resolvedPath), resolvedPath];
3864
- }
3865
- const protectedPaths = [resolvedStartPath];
3866
- let currentPath = resolvedStartPath;
3867
- for (const segment of path2.relative(resolvedStartPath, resolvedPath).split(path2.sep).filter(Boolean)) {
3868
- currentPath = path2.join(currentPath, segment);
3869
- protectedPaths.push(currentPath);
3870
- }
3871
- return protectedPaths;
3872
- }
3873
- function assertSafeDefaultDirectory(defaultDirectory) {
3874
- if (path2.isAbsolute(defaultDirectory) || path2.win32.isAbsolute(defaultDirectory)) {
3875
- throw new Error("defaultDirectory must be a relative path inside the home directory");
3876
- }
3877
- for (const segment of splitPathSegments(defaultDirectory)) {
3878
- if (segment === "..") {
3879
- throw new Error("defaultDirectory must be a relative path inside the home directory");
3880
- }
3881
- }
3882
- }
3883
- function assertSafeDefaultFileName(defaultFileName) {
3884
- if (defaultFileName.trim().length === 0 || defaultFileName === "." || defaultFileName === ".." || splitPathSegments(defaultFileName).length !== 1) {
3885
- throw new Error("defaultFileName must be a file name without path separators");
3886
- }
3887
- }
3888
- function splitPathSegments(value) {
3889
- return value.split("/").flatMap((segment) => segment.split("\\")).filter((segment) => segment.length > 0);
3890
- }
3891
- function getExplicitProtectedCredentialPaths(resolvedPath) {
3892
- const parsed = path2.parse(resolvedPath);
3893
- const segments = resolvedPath.slice(parsed.root.length).split(path2.sep).filter((segment) => segment.length > 0);
3894
- if (segments.length <= 1) {
3895
- return [resolvedPath];
3896
- }
3897
- const protectedPaths = [];
3898
- let currentPath = parsed.root;
3899
- for (const [index, segment] of segments.entries()) {
3900
- currentPath = path2.join(currentPath, segment);
3901
- if (index === 0) {
3902
- continue;
3903
- }
3904
- protectedPaths.push(currentPath);
3905
- }
3906
- return protectedPaths;
3907
- }
3908
- function isPathInsideOrEqual(childPath, parentPath) {
3909
- const relativePath = path2.relative(parentPath, childPath);
3910
- return relativePath === "" || !relativePath.startsWith("..") && !path2.isAbsolute(relativePath);
3911
- }
3912
- async function removeIfPresent(fileSystem, filePath) {
3913
- try {
3914
- await fileSystem.unlink(filePath);
3915
- } catch (error) {
3916
- if (!isNotFoundError(error)) {
3917
- throw error;
3918
- }
3919
- }
3920
- }
3921
- function defaultMachineIdentity() {
3922
- return {
3923
- hostname: hostname(),
3924
- username: userInfo().username
3925
- };
3926
- }
3927
- async function deriveEncryptionKey(getMachineIdentity, salt) {
3928
- const machineIdentity = await getMachineIdentity();
3929
- const secret = `${machineIdentity.hostname}:${machineIdentity.username}`;
3930
- const cacheKey = JSON.stringify([machineIdentity.hostname, machineIdentity.username, salt]);
3931
- const cached = derivedKeyCache.get(cacheKey);
3932
- if (cached) {
3933
- return cached;
3934
- }
3935
- const keyPromise = new Promise((resolve, reject) => {
3936
- scrypt(secret, salt, ENCRYPTION_KEY_BYTES, (error, derivedKey) => {
3937
- if (error) {
3938
- reject(error);
3939
- return;
3940
- }
3941
- resolve(Buffer.from(derivedKey));
3942
- });
3943
- });
3944
- derivedKeyCache.set(cacheKey, keyPromise);
3945
- return keyPromise.catch((error) => {
3946
- if (derivedKeyCache.get(cacheKey) === keyPromise) {
3947
- derivedKeyCache.delete(cacheKey);
3948
- }
3949
- throw error;
3950
- });
3951
- }
3952
- function parseEncryptedDocument(raw) {
3953
- try {
3954
- const parsed = JSON.parse(raw);
3955
- if (!isRecord2(parsed)) {
3956
- return null;
3957
- }
3958
- const version = getOwnEntry(parsed, "version");
3959
- const iv = getOwnEntry(parsed, "iv");
3960
- const authTag = getOwnEntry(parsed, "authTag");
3961
- const ciphertext = getOwnEntry(parsed, "ciphertext");
3962
- if (version !== ENCRYPTION_VERSION) {
3963
- return null;
3964
- }
3965
- if (typeof iv !== "string" || typeof authTag !== "string" || typeof ciphertext !== "string") {
3966
- return null;
3674
+ if (expected.requireIssuer) {
3675
+ if (callback.iss === null || callback.iss.length === 0) {
3676
+ throw new Error("OAuth callback missing issuer");
3967
3677
  }
3968
- return {
3969
- version,
3970
- iv,
3971
- authTag,
3972
- ciphertext
3973
- };
3974
- } catch {
3975
- return null;
3678
+ }
3679
+ if (callback.iss !== null && callback.iss.length > 0 && expected.issuer !== null && callback.iss !== expected.issuer) {
3680
+ throw new Error("OAuth callback issuer mismatch");
3976
3681
  }
3977
3682
  }
3978
- function isRecord2(value) {
3979
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
3683
+ function createAuthorizationError(error, description) {
3684
+ return new Error(`OAuth authorization failed: ${error} \u2014 ${description}`);
3980
3685
  }
3981
- function getOwnEntry(record2, key2) {
3982
- return Object.prototype.hasOwnProperty.call(record2, key2) ? record2[key2] : void 0;
3686
+ function escapeHtml(text) {
3687
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
3983
3688
  }
3984
- function isNotFoundError(error) {
3985
- return hasOwnErrorCode(error, "ENOENT");
3689
+ function buildSuccessPage(landingPage) {
3690
+ const title = landingPage?.title ?? "Connected";
3691
+ const body = landingPage?.body ?? "You can close this tab and return to your terminal.";
3692
+ return [
3693
+ "<!DOCTYPE html>",
3694
+ `<html><head><meta charset=utf-8><title>${escapeHtml(title)}</title></head>`,
3695
+ '<body style="font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">',
3696
+ '<div style="text-align:center">',
3697
+ `<h1>${escapeHtml(title)}</h1>`,
3698
+ `<p style="color:#666">${escapeHtml(body)}</p>`,
3699
+ "</div></body></html>"
3700
+ ].join("");
3986
3701
  }
3987
- function isAlreadyExistsError(error) {
3988
- return hasOwnErrorCode(error, "EEXIST");
3702
+
3703
+ // ../mcp-oauth/dist/client/scope.js
3704
+ function normalizeOAuthScope(scope) {
3705
+ if (scope === void 0)
3706
+ return void 0;
3707
+ if (typeof scope !== "string" || [...scope].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
3708
+ throw new Error("Invalid OAuth scope syntax");
3709
+ const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
3710
+ return normalized || void 0;
3989
3711
  }
3990
3712
 
3991
- // ../auth-store/dist/keychain-store.js
3992
- import { spawn } from "node:child_process";
3993
- import { createHash } from "node:crypto";
3994
- import { promises as nodeFs } from "node:fs";
3995
- import { homedir as homedir2 } from "node:os";
3996
- import path3 from "node:path";
3997
- var SECURITY_CLI = "security";
3998
- var KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE = 44;
3999
- var KeychainStore = class {
4000
- runCommand;
4001
- service;
4002
- account;
4003
- lockFs;
4004
- lockDirectory;
4005
- constructor(input) {
4006
- this.runCommand = input.runCommand ?? runSecurityCommand;
4007
- this.service = input.service.trim();
4008
- this.account = input.account.trim();
4009
- this.lockFs = input.lock?.fs ?? nodeFs;
4010
- this.lockDirectory = input.lock?.directory ?? path3.join(homedir2(), ".auth-store", "keychain-locks");
4011
- if (this.service.length === 0) {
4012
- throw new Error("Keychain service must not be empty");
4013
- }
4014
- if (this.account.length === 0) {
4015
- throw new Error("Keychain account must not be empty");
4016
- }
4017
- }
4018
- async get() {
4019
- const result = await this.executeSecurityCommand(["find-generic-password", "-s", this.service, "-a", this.account, "-w"], "read secret from macOS Keychain");
4020
- if (getCommandExitCode(result) === 0) {
4021
- return stripTrailingLineBreak(getCommandOutput(result, "stdout"));
4022
- }
4023
- if (isKeychainEntryNotFound(result)) {
4024
- return null;
3713
+ // ../mcp-oauth/dist/client/token-auth-method.js
3714
+ function normalizeOAuthTokenEndpointAuthMethod(value) {
3715
+ if (value === void 0 || value === null)
3716
+ return void 0;
3717
+ if (value !== "none" && value !== "client_secret_post" && value !== "client_secret_basic")
3718
+ throw new Error("Unsupported OAuth token endpoint authentication method");
3719
+ return value;
3720
+ }
3721
+
3722
+ // ../mcp-oauth/dist/client/client-registration.js
3723
+ function parseOAuthClientRegistration(value) {
3724
+ const invalid = () => new Error("Invalid OAuth client registration metadata");
3725
+ let nodes = 0;
3726
+ function copy(input, depth) {
3727
+ if (++nodes > 2e4 || depth > 64)
3728
+ throw invalid();
3729
+ if (input === null || typeof input === "boolean" || typeof input === "string")
3730
+ return input;
3731
+ if (typeof input === "number" && Number.isFinite(input))
3732
+ return input;
3733
+ if (typeof input !== "object" || input === null)
3734
+ throw invalid();
3735
+ const descriptors = Object.getOwnPropertyDescriptors(input);
3736
+ if (Array.isArray(input)) {
3737
+ const length = descriptors.length?.value;
3738
+ if (length > 2e4)
3739
+ throw invalid();
3740
+ const result2 = [];
3741
+ for (let index = 0; index < length; index++) {
3742
+ const descriptor = descriptors[String(index)];
3743
+ if (descriptor === void 0 || !Object.hasOwn(descriptor, "value"))
3744
+ throw invalid();
3745
+ result2.push(copy(descriptor.value, depth + 1));
3746
+ }
3747
+ return result2;
4025
3748
  }
4026
- throw createSecurityCliFailure("read secret from macOS Keychain", result);
4027
- }
4028
- async withLock(operation, options = {}) {
4029
- const identity = createHash("sha256").update(JSON.stringify([this.service, this.account])).digest("hex");
4030
- return withSecretStoreFileLock(this.lockFs, path3.join(this.lockDirectory, identity), operation, options);
3749
+ if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
3750
+ throw invalid();
3751
+ return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key2, descriptor]) => {
3752
+ if (!Object.hasOwn(descriptor, "value"))
3753
+ throw invalid();
3754
+ return [key2, copy(descriptor.value, depth + 1)];
3755
+ }));
4031
3756
  }
4032
- async set(value) {
4033
- if (value.includes("\n") || value.includes("\r")) {
4034
- throw new Error("Keychain secrets cannot contain line breaks");
4035
- }
4036
- const result = await this.executeSecurityCommand([
4037
- "add-generic-password",
4038
- "-s",
4039
- this.service,
4040
- "-a",
4041
- this.account,
4042
- "-U",
4043
- "-w",
4044
- value
4045
- ], "store secret in macOS Keychain");
4046
- if (getCommandExitCode(result) !== 0) {
4047
- throw createSecurityCliFailure("store secret in macOS Keychain", result);
4048
- }
3757
+ let result;
3758
+ try {
3759
+ result = copy(value, 0);
3760
+ } catch {
3761
+ throw invalid();
4049
3762
  }
4050
- async delete() {
4051
- const result = await this.executeSecurityCommand(["delete-generic-password", "-s", this.service, "-a", this.account], "delete secret from macOS Keychain");
4052
- if (getCommandExitCode(result) === 0 || isKeychainEntryNotFound(result)) {
4053
- return;
4054
- }
4055
- throw createSecurityCliFailure("delete secret from macOS Keychain", result);
3763
+ if (typeof result !== "object" || result === null || Array.isArray(result))
3764
+ throw invalid();
3765
+ const record2 = result;
3766
+ if (!Object.hasOwn(record2, "client_id") || typeof record2.client_id !== "string" || record2.client_id.trim() === "")
3767
+ throw new Error("OAuth client registration response missing client_id");
3768
+ for (const key2 of [
3769
+ "client_id",
3770
+ "client_secret",
3771
+ "token_endpoint_auth_method",
3772
+ "application_type",
3773
+ "client_name",
3774
+ "client_uri",
3775
+ "logo_uri",
3776
+ "scope",
3777
+ "tos_uri",
3778
+ "policy_uri",
3779
+ "jwks_uri",
3780
+ "software_id",
3781
+ "software_version",
3782
+ "software_statement",
3783
+ "registration_access_token",
3784
+ "registration_client_uri",
3785
+ "issuer"
3786
+ ]) {
3787
+ if (Object.hasOwn(record2, key2) && record2[key2] !== null && typeof record2[key2] !== "string")
3788
+ throw invalid();
4056
3789
  }
4057
- async executeSecurityCommand(args, operation, options) {
4058
- try {
4059
- if (options === void 0) {
4060
- return await this.runCommand(SECURITY_CLI, args);
4061
- }
4062
- return await this.runCommand(SECURITY_CLI, args, options);
4063
- } catch (error) {
4064
- const message = error instanceof Error ? error.message : String(error);
4065
- throw new Error(`Failed to ${operation}: ${message}`);
4066
- }
3790
+ if (typeof record2.client_secret === "string" && record2.client_secret.trim() === "")
3791
+ throw invalid();
3792
+ for (const key2 of ["redirect_uris", "grant_types", "response_types", "contacts"]) {
3793
+ const entry = record2[key2];
3794
+ if (Object.hasOwn(record2, key2) && entry !== null && (!Array.isArray(entry) || entry.some((item) => typeof item !== "string")))
3795
+ throw invalid();
4067
3796
  }
4068
- };
4069
- function runSecurityCommand(command, args, options) {
4070
- return new Promise((resolve) => {
4071
- const child = spawn(command, args, {
4072
- stdio: [options?.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
4073
- });
4074
- let stdout = "";
4075
- let stderr = "";
4076
- let stdinErrorMessage;
4077
- const appendStderr = (message) => {
4078
- stderr = stderr.length === 0 ? message : `${stderr}${stderr.endsWith("\n") ? "" : "\n"}${message}`;
4079
- };
4080
- const appendStdinError = () => {
4081
- if (stdinErrorMessage === void 0) {
4082
- return;
4083
- }
4084
- appendStderr(stdinErrorMessage);
4085
- stdinErrorMessage = void 0;
4086
- };
4087
- child.stdout?.setEncoding("utf8");
4088
- child.stdout?.on("data", (chunk) => {
4089
- stdout += chunk.toString();
4090
- });
4091
- child.stderr?.setEncoding("utf8");
4092
- child.stderr?.on("data", (chunk) => {
4093
- stderr += chunk.toString();
4094
- });
4095
- if (options?.stdin !== void 0) {
4096
- child.stdin?.once("error", (error) => {
4097
- stdinErrorMessage = error instanceof Error ? error.message : String(error);
4098
- });
4099
- child.stdin?.end(options.stdin);
4100
- }
4101
- child.on("error", (error) => {
4102
- const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
4103
- appendStdinError();
4104
- appendStderr(message);
4105
- resolve({
4106
- stdout,
4107
- stderr,
4108
- exitCode: 127
4109
- });
4110
- });
4111
- child.on("close", (code) => {
4112
- appendStdinError();
4113
- resolve({
4114
- stdout,
4115
- stderr,
4116
- exitCode: code ?? 1
4117
- });
4118
- });
4119
- });
4120
- }
4121
- function stripTrailingLineBreak(value) {
4122
- if (value.endsWith("\r\n")) {
4123
- return value.slice(0, -2);
3797
+ for (const key2 of ["client_id_issued_at", "client_secret_expires_at"]) {
3798
+ const entry = record2[key2];
3799
+ if (Object.hasOwn(record2, key2) && entry !== null && (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 0))
3800
+ throw invalid();
4124
3801
  }
4125
- if (value.endsWith("\n") || value.endsWith("\r")) {
4126
- return value.slice(0, -1);
3802
+ try {
3803
+ normalizeOAuthScope(Object.hasOwn(record2, "scope") && record2.scope !== null ? record2.scope : void 0);
3804
+ } catch {
3805
+ throw invalid();
4127
3806
  }
4128
- return value;
4129
- }
4130
- function isKeychainEntryNotFound(result) {
4131
- return getCommandExitCode(result) === KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE;
3807
+ if (Buffer.byteLength(JSON.stringify(record2), "utf8") > 64 * 1024)
3808
+ throw invalid();
3809
+ return record2;
4132
3810
  }
4133
- function createSecurityCliFailure(operation, result) {
4134
- const exitCode = getCommandExitCode(result);
4135
- const details = getCommandOutput(result, "stderr").trim() || getCommandOutput(result, "stdout").trim();
4136
- if (details) {
4137
- return new Error(`Failed to ${operation}: security exited with code ${exitCode}: ${details}`);
3811
+ function normalizeStoredOAuthClient(value) {
3812
+ if (typeof value !== "object" || value === null || Array.isArray(value))
3813
+ return null;
3814
+ const record2 = value;
3815
+ const clientId = Object.hasOwn(record2, "clientId") ? record2.clientId : void 0;
3816
+ const clientSecret = Object.hasOwn(record2, "clientSecret") ? record2.clientSecret : void 0;
3817
+ if (typeof clientId !== "string" || clientId.trim() === "" || clientSecret !== void 0 && (typeof clientSecret !== "string" || clientSecret.trim() === ""))
3818
+ return null;
3819
+ const client = { clientId: clientId.trim(), ...clientSecret === void 0 ? {} : { clientSecret: clientSecret.trim() } };
3820
+ const requestedRedirectUri = Object.hasOwn(record2, "requestedRedirectUri") ? record2.requestedRedirectUri : void 0;
3821
+ if (requestedRedirectUri !== void 0) {
3822
+ try {
3823
+ if (typeof requestedRedirectUri !== "string")
3824
+ throw new Error("Invalid redirect identity");
3825
+ loopbackTarget({ redirectUri: requestedRedirectUri });
3826
+ } catch {
3827
+ throw new Error("Invalid stored OAuth registration redirect identity");
3828
+ }
3829
+ client.requestedRedirectUri = requestedRedirectUri;
4138
3830
  }
4139
- return new Error(`Failed to ${operation}: security exited with code ${exitCode}`);
4140
- }
4141
- function getCommandExitCode(result) {
4142
- const value = getOwnEntry2(result, "exitCode");
4143
- return typeof value === "number" && Number.isInteger(value) ? value : 1;
3831
+ const method = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(record2, "tokenEndpointAuthMethod") ? record2.tokenEndpointAuthMethod : void 0);
3832
+ if (Object.hasOwn(record2, "registration") && record2.registration !== void 0) {
3833
+ const registration = parseOAuthClientRegistration(record2.registration);
3834
+ const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : void 0;
3835
+ if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
3836
+ throw new Error("OAuth client registration does not match the client identity");
3837
+ client.registration = registration;
3838
+ const registrationMethod = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(registration, "token_endpoint_auth_method") ? registration.token_endpoint_auth_method : void 0);
3839
+ if (method !== void 0 && registrationMethod !== void 0 && method !== registrationMethod)
3840
+ throw new Error("OAuth token endpoint authentication conflicts with the client registration");
3841
+ if (registrationMethod !== void 0)
3842
+ client.tokenEndpointAuthMethod = registrationMethod;
3843
+ }
3844
+ if (method !== void 0)
3845
+ client.tokenEndpointAuthMethod = method;
3846
+ return client;
4144
3847
  }
4145
- function getCommandOutput(result, key2) {
4146
- const value = getOwnEntry2(result, key2);
4147
- return typeof value === "string" ? value : "";
3848
+ function registrationMatchesRedirect(client, requestedUri, fresh = false) {
3849
+ if (client.requestedRedirectUri !== void 0)
3850
+ return client.requestedRedirectUri === requestedUri;
3851
+ const redirects = client.registration?.redirect_uris;
3852
+ if (redirects === void 0 || redirects === null || redirects.length === 0)
3853
+ return true;
3854
+ return redirects.some((returnedUri) => {
3855
+ if (returnedUri === requestedUri)
3856
+ return true;
3857
+ if (!fresh)
3858
+ return false;
3859
+ let returned, requested;
3860
+ try {
3861
+ returned = new URL(returnedUri);
3862
+ requested = new URL(requestedUri);
3863
+ } catch {
3864
+ return false;
3865
+ }
3866
+ if (requested.protocol !== "http:" || returned.protocol !== "http:" || !["127.0.0.1", "[::1]", "localhost"].includes(requested.hostname))
3867
+ return false;
3868
+ const sameHost = returned.hostname === requested.hostname;
3869
+ const normalizedIpv4 = requested.hostname === "127.0.0.1" && returned.hostname === "localhost" && returned.port === "";
3870
+ if (!sameHost && !normalizedIpv4)
3871
+ return false;
3872
+ returned.hostname = requested.hostname;
3873
+ returned.port = requested.port;
3874
+ return returned.href === requested.href;
3875
+ });
4148
3876
  }
4149
- function getOwnEntry2(record2, key2) {
4150
- return Object.prototype.hasOwnProperty.call(record2, key2) ? record2[key2] : void 0;
3877
+
3878
+ // ../mcp-oauth/dist/client/auth-store-session-store.js
3879
+ import crypto2 from "node:crypto";
3880
+ import path4 from "node:path";
3881
+
3882
+ // ../auth-store/dist/encrypted-file-store.js
3883
+ import { createCipheriv, createDecipheriv, randomBytes, randomUUID as randomUUID2, scrypt } from "node:crypto";
3884
+ import { promises as fs } from "node:fs";
3885
+ import { homedir, hostname, userInfo } from "node:os";
3886
+ import path2 from "node:path";
3887
+
3888
+ // ../auth-store/dist/error-codes.js
3889
+ function hasOwnErrorCode(error, code) {
3890
+ return error instanceof Error && Object.prototype.hasOwnProperty.call(error, "code") && error.code === code;
4151
3891
  }
4152
3892
 
4153
- // ../auth-store/dist/create-secret-store.js
4154
- var DEFAULT_BACKEND_ENV_VAR = "AUTH_BACKEND";
4155
- var MACOS_PLATFORM = "darwin";
4156
- var storeFactories = {
4157
- file: (input) => {
4158
- if (!input.fileStore) {
4159
- throw new Error("fileStore configuration is required for file backend");
3893
+ // ../auth-store/dist/transaction-lock.js
3894
+ import { randomUUID } from "node:crypto";
3895
+ import path from "node:path";
3896
+ async function withSecretStoreFileLock(fs2, lockDirectory, operation, options = {}) {
3897
+ const timeoutMs = options.timeoutMs ?? 3e4;
3898
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 2147483647)
3899
+ throw new Error("Invalid secret-store transaction lock timeout");
3900
+ options.signal?.throwIfAborted();
3901
+ const deadline = performance.now() + timeoutMs;
3902
+ const directory = path.resolve(lockDirectory);
3903
+ await assertLockDirectoryPath(fs2, directory);
3904
+ await fs2.mkdir(directory, { recursive: true, mode: 448 });
3905
+ await assertLockDirectoryPath(fs2, directory);
3906
+ const name = `${process.pid}-${randomUUID()}.claim`;
3907
+ const claimPath = path.join(directory, name);
3908
+ const temporaryPath = `${claimPath}.tmp`;
3909
+ let claimed = true;
3910
+ let temporaryCreated = false;
3911
+ let outcome;
3912
+ try {
3913
+ try {
3914
+ await fs2.writeFile(claimPath, JSON.stringify({ ticket: null }), { encoding: "utf8", flag: "wx", mode: 384 });
3915
+ } catch (error) {
3916
+ if (hasOwnErrorCode(error, "EEXIST"))
3917
+ claimed = false;
3918
+ throw error;
4160
3919
  }
4161
- return new EncryptedFileStore(input.fileStore);
4162
- },
4163
- keychain: (input) => {
4164
- if (!input.keychainStore) {
4165
- throw new Error("keychainStore configuration is required for keychain backend");
3920
+ const existing = await readClaims(fs2, directory, name);
3921
+ const ticket = existing.reduce((max, claim) => Math.max(max, claim.ticket ?? 0), 0) + 1;
3922
+ if (!Number.isSafeInteger(ticket))
3923
+ throw new Error("Secret-store transaction lock ticket overflow");
3924
+ temporaryCreated = true;
3925
+ try {
3926
+ await fs2.writeFile(temporaryPath, JSON.stringify({ ticket }), { encoding: "utf8", flag: "wx", mode: 384 });
3927
+ } catch (error) {
3928
+ if (hasOwnErrorCode(error, "EEXIST"))
3929
+ temporaryCreated = false;
3930
+ throw error;
4166
3931
  }
4167
- return new KeychainStore(input.keychainStore);
3932
+ await fs2.rename(temporaryPath, claimPath);
3933
+ temporaryCreated = false;
3934
+ for (; ; ) {
3935
+ options.signal?.throwIfAborted();
3936
+ const peers = await readClaims(fs2, directory, name);
3937
+ if (!peers.some((peer) => peer.ticket === null || peer.ticket < ticket || peer.ticket === ticket && peer.name < name))
3938
+ break;
3939
+ const remaining = deadline - performance.now();
3940
+ if (remaining <= 0)
3941
+ throw new Error("Timed out waiting for secret-store transaction lock");
3942
+ await new Promise((resolve, reject) => {
3943
+ const abort = () => {
3944
+ clearTimeout(timer);
3945
+ options.signal?.removeEventListener("abort", abort);
3946
+ reject(options.signal?.reason);
3947
+ };
3948
+ const timer = setTimeout(() => {
3949
+ options.signal?.removeEventListener("abort", abort);
3950
+ resolve();
3951
+ }, Math.min(10, remaining));
3952
+ options.signal?.addEventListener("abort", abort, { once: true });
3953
+ if (options.signal?.aborted)
3954
+ abort();
3955
+ });
3956
+ }
3957
+ options.signal?.throwIfAborted();
3958
+ outcome = { result: await operation() };
3959
+ } catch (error) {
3960
+ outcome = { error };
4168
3961
  }
4169
- };
4170
- function createSecretStore(input) {
4171
- const backend = resolveBackend(input);
4172
- const platform = input.platform ?? process.platform;
4173
- if (backend === "keychain" && platform !== MACOS_PLATFORM) {
4174
- throw new Error(`Keychain backend is only supported on macOS. Current platform: ${platform}`);
3962
+ const cleanup = [];
3963
+ for (const target of [...temporaryCreated ? [temporaryPath] : [], ...claimed ? [claimPath] : []]) {
3964
+ try {
3965
+ await fs2.unlink(target);
3966
+ } catch (error) {
3967
+ if (!hasOwnErrorCode(error, "ENOENT"))
3968
+ cleanup.push(error);
3969
+ }
4175
3970
  }
4176
- const store = storeFactories[backend](input);
4177
- return { backend, store };
3971
+ if (cleanup.length)
3972
+ throw new AggregateError([..."error" in outcome ? [outcome.error] : [], ...cleanup], "Secret-store transaction lock cleanup failed");
3973
+ if ("error" in outcome)
3974
+ throw outcome.error;
3975
+ return outcome.result;
4178
3976
  }
4179
- function resolveBackend(input) {
4180
- const envVar = input.backendEnvVar ?? DEFAULT_BACKEND_ENV_VAR;
4181
- const configuredBackend = input.backend ?? getOwnEnvValue(input.env, envVar) ?? getOwnEnvValue(process.env, envVar);
4182
- const backend = configuredBackend?.trim();
4183
- if (backend === "keychain") {
4184
- return "keychain";
4185
- }
4186
- if (backend === void 0 || backend === "file") {
4187
- return "file";
3977
+ async function assertNoSymbolicLink(fs2, target) {
3978
+ try {
3979
+ if ((await fs2.lstat(target)).isSymbolicLink())
3980
+ throw new Error("Refusing secret-store transaction lock through symbolic link");
3981
+ } catch (error) {
3982
+ if (!hasOwnErrorCode(error, "ENOENT"))
3983
+ throw error;
4188
3984
  }
4189
- throw new Error(`Unsupported auth store backend: ${backend}`);
4190
3985
  }
4191
- function getOwnEnvValue(env, key2) {
4192
- return env !== void 0 && Object.prototype.hasOwnProperty.call(env, key2) ? env[key2] : void 0;
3986
+ async function assertLockDirectoryPath(fs2, directory) {
3987
+ const root = path.parse(directory).root;
3988
+ const segments = directory.slice(root.length).split(path.sep).filter(Boolean);
3989
+ let current = root;
3990
+ for (const [index, segment] of segments.entries()) {
3991
+ current = path.join(current, segment);
3992
+ if (index > 0 || segments.length === 1)
3993
+ await assertNoSymbolicLink(fs2, current);
3994
+ }
4193
3995
  }
4194
-
4195
- // ../mcp-oauth/dist/resource-indicator.js
4196
- function canonicalizeResourceIndicator(value) {
4197
- let url;
4198
- try {
4199
- url = value instanceof URL ? new URL(value.toString()) : new URL(value);
4200
- } catch {
4201
- throw new Error("Resource indicator must be an absolute URL");
3996
+ async function readClaims(fs2, directory, ownName) {
3997
+ const claims = [];
3998
+ for (const name of await fs2.readdir(directory)) {
3999
+ if (name === ownName || !name.endsWith(".claim"))
4000
+ continue;
4001
+ const pidText = name.slice(0, name.indexOf("-"));
4002
+ const pid = Number(pidText);
4003
+ if (!Number.isSafeInteger(pid) || pid < 1 || String(pid) !== pidText)
4004
+ throw new Error("Malformed secret-store transaction lock owner");
4005
+ const target = path.join(directory, name);
4006
+ await assertNoSymbolicLink(fs2, target);
4007
+ let alive = true;
4008
+ try {
4009
+ process.kill(pid, 0);
4010
+ } catch (error) {
4011
+ alive = !hasOwnErrorCode(error, "ESRCH");
4012
+ }
4013
+ if (!alive) {
4014
+ try {
4015
+ await fs2.unlink(target);
4016
+ } catch (error) {
4017
+ if (!hasOwnErrorCode(error, "ENOENT"))
4018
+ throw error;
4019
+ }
4020
+ continue;
4021
+ }
4022
+ let ticket = null;
4023
+ let raw;
4024
+ try {
4025
+ raw = await fs2.readFile(target, "utf8");
4026
+ } catch (error) {
4027
+ if (hasOwnErrorCode(error, "ENOENT"))
4028
+ continue;
4029
+ throw error;
4030
+ }
4031
+ try {
4032
+ const value = JSON.parse(raw);
4033
+ if (value !== null && typeof value === "object" && "ticket" in value && typeof value.ticket === "number" && Number.isSafeInteger(value.ticket) && value.ticket > 0)
4034
+ ticket = value.ticket;
4035
+ } catch {
4036
+ }
4037
+ claims.push({ name, ticket });
4202
4038
  }
4203
- url.hash = "";
4204
- return url.toString();
4039
+ return claims;
4205
4040
  }
4206
4041
 
4207
- // ../mcp-oauth/dist/client/auth-store-session-store.js
4208
- var DEFAULT_FILE_SALT = "poe-code:mcp-oauth:v1";
4209
- var DEFAULT_FILE_DIRECTORY = ".poe-code/mcp-oauth";
4210
- var DEFAULT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth";
4211
- var DEFAULT_CLIENT_FILE_SALT = "poe-code:mcp-oauth:clients:v1";
4212
- var DEFAULT_CLIENT_FILE_DIRECTORY = ".poe-code/mcp-oauth/clients";
4213
- var DEFAULT_CLIENT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth-clients";
4214
- var MAX_JS_DATE_MS = 864e13;
4215
- function createAuthStoreSessionStore(options = {}, namespace) {
4216
- assertPersistenceNamespace(namespace);
4217
- return {
4218
- async withLock(resource, operation, lockOptions) {
4219
- const store = createResourceSecretStore(resource, options, namespace);
4220
- if (store.withLock === void 0)
4221
- throw new Error("OAuth secret-store backend does not support transaction locks");
4222
- return store.withLock(operation, lockOptions);
4223
- },
4224
- async load(resource) {
4225
- const store = createResourceSecretStore(resource, options, namespace);
4226
- const value = await store.get();
4227
- if (value === null) {
4042
+ // ../auth-store/dist/encrypted-file-store.js
4043
+ var derivedKeyCache = /* @__PURE__ */ new Map();
4044
+ var ENCRYPTION_ALGORITHM = "aes-256-gcm";
4045
+ var ENCRYPTION_VERSION = 1;
4046
+ var ENCRYPTION_KEY_BYTES = 32;
4047
+ var ENCRYPTION_IV_BYTES = 12;
4048
+ var ENCRYPTION_AUTH_TAG_BYTES = 16;
4049
+ var ENCRYPTION_FILE_MODE = 384;
4050
+ var EncryptedFileStore = class {
4051
+ fs;
4052
+ filePath;
4053
+ symbolicLinkCheckStartPath;
4054
+ salt;
4055
+ getMachineIdentity;
4056
+ getRandomBytes;
4057
+ keyPromise = null;
4058
+ throwOnInvalidDocument;
4059
+ constructor(input) {
4060
+ this.fs = input.fs ?? fs;
4061
+ this.salt = input.salt;
4062
+ if (input.filePath === void 0) {
4063
+ const homeDirectory = (input.getHomeDirectory ?? homedir)();
4064
+ const defaultDirectory = input.defaultDirectory ?? ".auth-store";
4065
+ const defaultFileName = input.defaultFileName ?? "credentials.enc";
4066
+ assertSafeDefaultDirectory(defaultDirectory);
4067
+ assertSafeDefaultFileName(defaultFileName);
4068
+ this.filePath = path2.join(homeDirectory, defaultDirectory, defaultFileName);
4069
+ this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
4070
+ } else {
4071
+ this.filePath = input.filePath;
4072
+ this.symbolicLinkCheckStartPath = null;
4073
+ }
4074
+ this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
4075
+ this.getRandomBytes = input.getRandomBytes ?? randomBytes;
4076
+ this.throwOnInvalidDocument = input.throwOnInvalidDocument ?? false;
4077
+ }
4078
+ async get() {
4079
+ await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
4080
+ let rawDocument;
4081
+ try {
4082
+ rawDocument = await this.fs.readFile(this.filePath, "utf8");
4083
+ } catch (error) {
4084
+ if (isNotFoundError(error)) {
4228
4085
  return null;
4229
4086
  }
4230
- let parsed;
4231
- try {
4232
- parsed = JSON.parse(value);
4233
- } catch {
4234
- throw new Error("Stored OAuth session must be valid JSON; reset the store explicitly to recover");
4087
+ throw error;
4088
+ }
4089
+ const document = parseEncryptedDocument(rawDocument);
4090
+ if (!document) {
4091
+ if (this.throwOnInvalidDocument)
4092
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
4093
+ return null;
4094
+ }
4095
+ const key2 = await this.getEncryptionKey();
4096
+ try {
4097
+ const iv = Buffer.from(document.iv, "base64");
4098
+ const authTag = Buffer.from(document.authTag, "base64");
4099
+ const ciphertext = Buffer.from(document.ciphertext, "base64");
4100
+ if (iv.byteLength !== ENCRYPTION_IV_BYTES || authTag.byteLength !== ENCRYPTION_AUTH_TAG_BYTES) {
4101
+ if (this.throwOnInvalidDocument)
4102
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
4103
+ return null;
4235
4104
  }
4236
- if (isStoredOAuthSession(parsed)) {
4237
- return parsed;
4105
+ const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key2, iv);
4106
+ decipher.setAuthTag(authTag);
4107
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
4108
+ return plaintext.toString("utf8");
4109
+ } catch {
4110
+ if (this.throwOnInvalidDocument)
4111
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
4112
+ return null;
4113
+ }
4114
+ }
4115
+ async withLock(operation, options = {}) {
4116
+ await this.assertCredentialPathHasNoSymbolicLinks(`${this.filePath}.lock`);
4117
+ if (this.fs.readdir === void 0)
4118
+ throw new Error("Secret-store transaction locks require filesystem readdir support");
4119
+ return withSecretStoreFileLock(this.fs, `${this.filePath}.lock`, operation, options);
4120
+ }
4121
+ async set(value) {
4122
+ await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
4123
+ const key2 = await this.getEncryptionKey();
4124
+ const iv = this.getRandomBytes(ENCRYPTION_IV_BYTES);
4125
+ const cipher = createCipheriv(ENCRYPTION_ALGORITHM, key2, iv);
4126
+ const ciphertext = Buffer.concat([
4127
+ cipher.update(value, "utf8"),
4128
+ cipher.final()
4129
+ ]);
4130
+ const authTag = cipher.getAuthTag();
4131
+ const document = {
4132
+ version: ENCRYPTION_VERSION,
4133
+ iv: iv.toString("base64"),
4134
+ authTag: authTag.toString("base64"),
4135
+ ciphertext: ciphertext.toString("base64")
4136
+ };
4137
+ await this.fs.mkdir(path2.dirname(this.filePath), { recursive: true });
4138
+ await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
4139
+ const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID2()}.tmp`;
4140
+ let temporaryCreated = false;
4141
+ try {
4142
+ await this.assertCredentialPathHasNoSymbolicLinks(temporaryPath);
4143
+ await this.fs.writeFile(temporaryPath, JSON.stringify(document), {
4144
+ encoding: "utf8",
4145
+ flag: "wx",
4146
+ mode: ENCRYPTION_FILE_MODE
4147
+ });
4148
+ temporaryCreated = true;
4149
+ await this.fs.chmod(temporaryPath, ENCRYPTION_FILE_MODE);
4150
+ await this.fs.rename(temporaryPath, this.filePath);
4151
+ } catch (error) {
4152
+ if (temporaryCreated || !isAlreadyExistsError(error)) {
4153
+ await removeIfPresent(this.fs, temporaryPath).catch(() => void 0);
4238
4154
  }
4239
- throw new Error("Stored OAuth session must match the expected shape");
4240
- },
4241
- async save(resource, session) {
4242
- const store = createResourceSecretStore(resource, options, namespace);
4243
- await store.set(JSON.stringify(session));
4244
- },
4245
- async clear(resource) {
4246
- const store = createResourceSecretStore(resource, options, namespace);
4247
- await store.delete();
4155
+ throw error;
4248
4156
  }
4249
- };
4250
- }
4251
- function createAuthStoreClientStore(options, namespace) {
4252
- assertPersistenceNamespace(namespace);
4253
- return {
4254
- async load(issuer) {
4255
- const store = createIssuerSecretStore(issuer, options, namespace);
4256
- const value = await store.get();
4257
- if (value === null) {
4258
- return null;
4157
+ }
4158
+ async delete() {
4159
+ await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
4160
+ try {
4161
+ await this.fs.unlink(this.filePath);
4162
+ } catch (error) {
4163
+ if (!isNotFoundError(error)) {
4164
+ throw error;
4259
4165
  }
4260
- let parsed;
4166
+ }
4167
+ }
4168
+ async assertCredentialPathHasNoSymbolicLinks(targetPath) {
4169
+ const resolvedPath = path2.resolve(targetPath);
4170
+ const protectedPaths = getProtectedCredentialPaths(resolvedPath, this.symbolicLinkCheckStartPath);
4171
+ for (const currentPath of protectedPaths) {
4261
4172
  try {
4262
- parsed = JSON.parse(value);
4263
- } catch {
4264
- throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
4173
+ const stats = await this.fs.lstat(currentPath);
4174
+ if (stats.isSymbolicLink()) {
4175
+ throw new Error(`Refusing to use encrypted credential path through symbolic link: ${currentPath}`);
4176
+ }
4177
+ } catch (error) {
4178
+ if (isNotFoundError(error)) {
4179
+ return;
4180
+ }
4181
+ throw error;
4265
4182
  }
4266
- if (isObjectRecord(parsed) && typeof getOwnEntry3(parsed, "clientId") === "string")
4267
- return parsed;
4268
- throw new Error("Stored OAuth client must be a JSON object with clientId");
4269
- },
4270
- async save(issuer, client) {
4271
- const store = createIssuerSecretStore(issuer, options, namespace);
4272
- await store.set(JSON.stringify(client));
4273
- },
4274
- async clear(issuer) {
4275
- const store = createIssuerSecretStore(issuer, options, namespace);
4276
- await store.delete();
4277
4183
  }
4278
- };
4279
- }
4280
- function createNamedSecretStore(key2, options, defaults, namespace) {
4281
- const hash = crypto.createHash("sha256").update(namespace === void 0 ? key2 : JSON.stringify([namespace, key2])).digest("hex");
4282
- const configuredFilePath = options.fileStore?.filePath;
4283
- const parsedFilePath = configuredFilePath === void 0 ? null : path4.parse(configuredFilePath);
4284
- const fileStore = {
4285
- ...options.fileStore,
4286
- throwOnInvalidDocument: true,
4287
- filePath: parsedFilePath === null ? void 0 : path4.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
4288
- salt: options.fileStore?.salt ?? defaults.salt,
4289
- defaultDirectory: options.fileStore?.defaultDirectory || defaults.directory,
4290
- defaultFileName: parsedFilePath === null ? `${hash}.enc` : `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`
4291
- };
4292
- const keychainStore = {
4293
- ...options.keychainStore,
4294
- service: options.keychainStore?.service ?? defaults.service,
4295
- account: `${options.keychainStore?.account ?? defaults.accountPrefix}:${hash}`
4296
- };
4297
- return createSecretStore({ ...options, fileStore, keychainStore }).store;
4298
- }
4299
- function createResourceSecretStore(resource, options, namespace) {
4300
- return createNamedSecretStore(canonicalizeResourceIndicator(resource), options, {
4301
- salt: DEFAULT_FILE_SALT,
4302
- directory: DEFAULT_FILE_DIRECTORY,
4303
- service: DEFAULT_KEYCHAIN_SERVICE,
4304
- accountPrefix: "provider"
4305
- }, namespace);
4306
- }
4307
- function createIssuerSecretStore(issuer, options, namespace) {
4308
- return createNamedSecretStore(issuer, options, {
4309
- salt: DEFAULT_CLIENT_FILE_SALT,
4310
- directory: DEFAULT_CLIENT_FILE_DIRECTORY,
4311
- service: DEFAULT_CLIENT_KEYCHAIN_SERVICE,
4312
- accountPrefix: "issuer"
4313
- }, namespace);
4314
- }
4315
- function assertPersistenceNamespace(namespace) {
4316
- if (namespace !== void 0 && (typeof namespace !== "string" || namespace.trim() === "" || Buffer.byteLength(namespace, "utf8") > 1024))
4317
- throw new Error("OAuth persistence namespace must be a nonempty string within 1024 bytes");
4318
- }
4319
- function isObjectRecord(value) {
4320
- return typeof value === "object" && value !== null && !Array.isArray(value);
4321
- }
4322
- function getOwnEntry3(record2, key2) {
4323
- return Object.prototype.hasOwnProperty.call(record2, key2) ? record2[key2] : void 0;
4324
- }
4325
- function getOwnString(record2, key2) {
4326
- const value = getOwnEntry3(record2, key2);
4327
- return typeof value === "string" ? value : void 0;
4328
- }
4329
- function isStoredOAuthSession(value) {
4330
- if (!isObjectRecord(value)) {
4331
- return false;
4332
4184
  }
4333
- return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && normalizeStoredOAuthClient(getOwnEntry3(value, "client")) !== null && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && (getOwnEntry3(value, "requestedScope") === void 0 || isNonBlankOwnString(value, "requestedScope")) && (getOwnEntry3(value, "refreshState") === void 0 || getOwnEntry3(value, "refreshState") === "pending" && getOwnEntry3(value, "tokens") === void 0) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
4185
+ getEncryptionKey() {
4186
+ if (!this.keyPromise) {
4187
+ const retryableKeyPromise = deriveEncryptionKey(this.getMachineIdentity, this.salt).catch((error) => {
4188
+ if (this.keyPromise === retryableKeyPromise) {
4189
+ this.keyPromise = null;
4190
+ }
4191
+ throw error;
4192
+ });
4193
+ this.keyPromise = retryableKeyPromise;
4194
+ }
4195
+ return this.keyPromise;
4196
+ }
4197
+ };
4198
+ function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
4199
+ const [firstSegment] = defaultDirectory.split(/[\\/]+/).filter(Boolean);
4200
+ return path2.resolve(homeDirectory, firstSegment ?? ".");
4334
4201
  }
4335
- function isStoredOAuthDiscovery(value) {
4336
- if (!isObjectRecord(value)) {
4337
- return false;
4202
+ function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
4203
+ if (symbolicLinkCheckStartPath === null) {
4204
+ return getExplicitProtectedCredentialPaths(resolvedPath);
4205
+ }
4206
+ const resolvedStartPath = path2.resolve(symbolicLinkCheckStartPath);
4207
+ if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
4208
+ return [path2.dirname(resolvedPath), resolvedPath];
4209
+ }
4210
+ const protectedPaths = [resolvedStartPath];
4211
+ let currentPath = resolvedStartPath;
4212
+ for (const segment of path2.relative(resolvedStartPath, resolvedPath).split(path2.sep).filter(Boolean)) {
4213
+ currentPath = path2.join(currentPath, segment);
4214
+ protectedPaths.push(currentPath);
4338
4215
  }
4339
- return isNonBlankOwnString(value, "resourceMetadataUrl") && isObjectRecord(getOwnEntry3(value, "resourceMetadata")) && isObjectRecord(getOwnEntry3(value, "authorizationServerMetadata"));
4216
+ return protectedPaths;
4340
4217
  }
4341
- function isStoredOAuthTokensOrMissing(value) {
4342
- if (value === void 0) {
4343
- return true;
4218
+ function assertSafeDefaultDirectory(defaultDirectory) {
4219
+ if (path2.isAbsolute(defaultDirectory) || path2.win32.isAbsolute(defaultDirectory)) {
4220
+ throw new Error("defaultDirectory must be a relative path inside the home directory");
4344
4221
  }
4345
- if (!isObjectRecord(value)) {
4346
- return false;
4222
+ for (const segment of splitPathSegments(defaultDirectory)) {
4223
+ if (segment === "..") {
4224
+ throw new Error("defaultDirectory must be a relative path inside the home directory");
4225
+ }
4347
4226
  }
4348
- if (!isNonBlankOwnString(value, "accessToken") || getOwnString(value, "tokenType") !== "Bearer") {
4349
- return false;
4227
+ }
4228
+ function assertSafeDefaultFileName(defaultFileName) {
4229
+ if (defaultFileName.trim().length === 0 || defaultFileName === "." || defaultFileName === ".." || splitPathSegments(defaultFileName).length !== 1) {
4230
+ throw new Error("defaultFileName must be a file name without path separators");
4350
4231
  }
4351
- const expiresAt = getOwnEntry3(value, "expiresAt");
4352
- if (expiresAt !== null && (typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt) || expiresAt > MAX_JS_DATE_MS || !Number.isFinite(new Date(expiresAt).getTime()))) {
4353
- return false;
4232
+ }
4233
+ function splitPathSegments(value) {
4234
+ return value.split("/").flatMap((segment) => segment.split("\\")).filter((segment) => segment.length > 0);
4235
+ }
4236
+ function getExplicitProtectedCredentialPaths(resolvedPath) {
4237
+ const parsed = path2.parse(resolvedPath);
4238
+ const segments = resolvedPath.slice(parsed.root.length).split(path2.sep).filter((segment) => segment.length > 0);
4239
+ if (segments.length <= 1) {
4240
+ return [resolvedPath];
4354
4241
  }
4355
- const refreshToken = getOwnEntry3(value, "refreshToken");
4356
- if (refreshToken !== void 0 && (typeof refreshToken !== "string" || refreshToken.trim().length === 0)) {
4357
- return false;
4242
+ const protectedPaths = [];
4243
+ let currentPath = parsed.root;
4244
+ for (const [index, segment] of segments.entries()) {
4245
+ currentPath = path2.join(currentPath, segment);
4246
+ if (index === 0) {
4247
+ continue;
4248
+ }
4249
+ protectedPaths.push(currentPath);
4358
4250
  }
4359
- const scope = getOwnEntry3(value, "scope");
4360
- return scope === void 0 || typeof scope === "string" && scope.trim().length > 0;
4251
+ return protectedPaths;
4361
4252
  }
4362
- function isNonBlankOwnString(record2, key2) {
4363
- const value = getOwnString(record2, key2);
4364
- return value !== void 0 && value.trim().length > 0;
4253
+ function isPathInsideOrEqual(childPath, parentPath) {
4254
+ const relativePath = path2.relative(parentPath, childPath);
4255
+ return relativePath === "" || !relativePath.startsWith("..") && !path2.isAbsolute(relativePath);
4365
4256
  }
4366
-
4367
- // ../mcp-oauth/dist/client/default-oauth-client-provider.js
4368
- import { isIP } from "node:net";
4369
-
4370
- // ../mcp-oauth/dist/http-fetch.js
4371
- async function fetchMcpResponse(fetchImplementation, input, init = {}) {
4372
- const response = await fetchImplementation(input, { ...init, redirect: "error" });
4373
- if (response.redirected || response.type === "opaqueredirect") {
4374
- void response.body?.cancel().catch(() => void 0);
4375
- throw new Error("MCP HTTP redirects are not allowed");
4257
+ async function removeIfPresent(fileSystem, filePath) {
4258
+ try {
4259
+ await fileSystem.unlink(filePath);
4260
+ } catch (error) {
4261
+ if (!isNotFoundError(error)) {
4262
+ throw error;
4263
+ }
4376
4264
  }
4377
- return response;
4378
4265
  }
4379
-
4380
- // ../mcp-oauth/dist/client/default-oauth-client-provider.js
4381
- import { URL as URL2 } from "node:url";
4382
-
4383
- // ../mcp-oauth/dist/client/loopback-authorization.js
4384
- import http from "node:http";
4385
-
4386
- // ../mcp-oauth/dist/client/authorization-state.js
4387
- import crypto2 from "node:crypto";
4388
- function createAuthorizationState(input) {
4389
- const payload = {
4390
- v: 1,
4391
- n: crypto2.randomBytes(16).toString("base64url"),
4392
- i: input.issuer,
4393
- r: input.requireIssuer
4266
+ function defaultMachineIdentity() {
4267
+ return {
4268
+ hostname: hostname(),
4269
+ username: userInfo().username
4394
4270
  };
4395
- return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
4396
4271
  }
4397
- function parseAuthorizationState(value) {
4398
- if (value === null || value.length === 0) {
4399
- return null;
4272
+ async function deriveEncryptionKey(getMachineIdentity, salt) {
4273
+ const machineIdentity = await getMachineIdentity();
4274
+ const secret = `${machineIdentity.hostname}:${machineIdentity.username}`;
4275
+ const cacheKey = JSON.stringify([machineIdentity.hostname, machineIdentity.username, salt]);
4276
+ const cached = derivedKeyCache.get(cacheKey);
4277
+ if (cached) {
4278
+ return cached;
4400
4279
  }
4280
+ const keyPromise = new Promise((resolve, reject) => {
4281
+ scrypt(secret, salt, ENCRYPTION_KEY_BYTES, (error, derivedKey) => {
4282
+ if (error) {
4283
+ reject(error);
4284
+ return;
4285
+ }
4286
+ resolve(Buffer.from(derivedKey));
4287
+ });
4288
+ });
4289
+ derivedKeyCache.set(cacheKey, keyPromise);
4290
+ return keyPromise.catch((error) => {
4291
+ if (derivedKeyCache.get(cacheKey) === keyPromise) {
4292
+ derivedKeyCache.delete(cacheKey);
4293
+ }
4294
+ throw error;
4295
+ });
4296
+ }
4297
+ function parseEncryptedDocument(raw) {
4401
4298
  try {
4402
- const decoded = Buffer.from(value, "base64url").toString("utf8");
4403
- const parsed = JSON.parse(decoded);
4404
- if (!isObjectRecord2(parsed)) {
4299
+ const parsed = JSON.parse(raw);
4300
+ if (!isRecord2(parsed)) {
4405
4301
  return null;
4406
4302
  }
4407
- const version = getOwnEntry4(parsed, "v");
4408
- const nonce = getOwnEntry4(parsed, "n");
4409
- const issuer = getOwnEntry4(parsed, "i");
4410
- const requireIssuer = getOwnEntry4(parsed, "r");
4411
- if (version !== 1 || typeof nonce !== "string" || nonce.length === 0 || typeof issuer !== "string" || issuer.length === 0 || typeof requireIssuer !== "boolean") {
4303
+ const version = getOwnEntry2(parsed, "version");
4304
+ const iv = getOwnEntry2(parsed, "iv");
4305
+ const authTag = getOwnEntry2(parsed, "authTag");
4306
+ const ciphertext = getOwnEntry2(parsed, "ciphertext");
4307
+ if (version !== ENCRYPTION_VERSION) {
4308
+ return null;
4309
+ }
4310
+ if (typeof iv !== "string" || typeof authTag !== "string" || typeof ciphertext !== "string") {
4412
4311
  return null;
4413
4312
  }
4414
4313
  return {
4415
- issuer,
4416
- requireIssuer
4314
+ version,
4315
+ iv,
4316
+ authTag,
4317
+ ciphertext
4417
4318
  };
4418
4319
  } catch {
4419
4320
  return null;
4420
4321
  }
4421
4322
  }
4422
- function isObjectRecord2(value) {
4423
- return typeof value === "object" && value !== null && !Array.isArray(value);
4323
+ function isRecord2(value) {
4324
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
4424
4325
  }
4425
- function getOwnEntry4(record2, key2) {
4326
+ function getOwnEntry2(record2, key2) {
4426
4327
  return Object.prototype.hasOwnProperty.call(record2, key2) ? record2[key2] : void 0;
4427
4328
  }
4329
+ function isNotFoundError(error) {
4330
+ return hasOwnErrorCode(error, "ENOENT");
4331
+ }
4332
+ function isAlreadyExistsError(error) {
4333
+ return hasOwnErrorCode(error, "EEXIST");
4334
+ }
4428
4335
 
4429
- // ../mcp-oauth/dist/client/loopback-authorization.js
4430
- async function createLoopbackAuthorizationSession(options = {}) {
4431
- options.signal?.throwIfAborted();
4432
- const timeoutMs = options.timeoutMs ?? 12e4;
4433
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
4434
- throw new Error("OAuth authorization timeoutMs must be a positive supported timer interval");
4435
- const target = loopbackTarget(options);
4436
- const server = options.createServer ? options.createServer() : http.createServer();
4437
- const controller = new AbortController();
4438
- let closed = false;
4439
- let used = false;
4440
- const callerAbort = () => controller.abort(options.signal?.reason);
4441
- const teardown = () => {
4442
- if (closed)
4443
- return;
4444
- closed = true;
4445
- clearTimeout(timer);
4446
- options.signal?.removeEventListener("abort", callerAbort);
4447
- server.closeAllConnections?.();
4448
- server.close();
4449
- };
4450
- const timer = setTimeout(() => controller.abort(new Error("OAuth authorization timed out")), timeoutMs);
4451
- timer.unref?.();
4452
- controller.signal.addEventListener("abort", teardown, { once: true });
4453
- options.signal?.addEventListener("abort", callerAbort, { once: true });
4454
- if (options.signal?.aborted)
4455
- callerAbort();
4456
- let port;
4457
- try {
4458
- port = await startServer(server, target.port, target.host, controller.signal);
4459
- } catch (error) {
4460
- controller.abort(error);
4461
- throw error;
4336
+ // ../auth-store/dist/keychain-store.js
4337
+ import { spawn } from "node:child_process";
4338
+ import { createHash } from "node:crypto";
4339
+ import { promises as nodeFs } from "node:fs";
4340
+ import { homedir as homedir2 } from "node:os";
4341
+ import path3 from "node:path";
4342
+ var SECURITY_CLI = "security";
4343
+ var KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE = 44;
4344
+ var KeychainStore = class {
4345
+ runCommand;
4346
+ service;
4347
+ account;
4348
+ lockFs;
4349
+ lockDirectory;
4350
+ constructor(input) {
4351
+ this.runCommand = input.runCommand ?? runSecurityCommand;
4352
+ this.service = input.service.trim();
4353
+ this.account = input.account.trim();
4354
+ this.lockFs = input.lock?.fs ?? nodeFs;
4355
+ this.lockDirectory = input.lock?.directory ?? path3.join(homedir2(), ".auth-store", "keychain-locks");
4356
+ if (this.service.length === 0) {
4357
+ throw new Error("Keychain service must not be empty");
4358
+ }
4359
+ if (this.account.length === 0) {
4360
+ throw new Error("Keychain account must not be empty");
4361
+ }
4462
4362
  }
4463
- const redirectUri = options.redirectUri ?? `http://127.0.0.1:${port}${target.callbackPath}`;
4464
- return {
4465
- redirectUri,
4466
- async waitForCode(authorizationUrl) {
4467
- controller.signal.throwIfAborted();
4468
- if (used)
4469
- throw new Error("OAuth authorization session has already been used");
4470
- used = true;
4471
- try {
4472
- return await waitForAuthorizationCode(server, authorizationUrl, options, target.callbackPath, controller.signal);
4473
- } finally {
4474
- clearTimeout(timer);
4475
- }
4476
- },
4477
- close() {
4478
- controller.abort(new Error("OAuth authorization session closed"));
4363
+ async get() {
4364
+ const result = await this.executeSecurityCommand(["find-generic-password", "-s", this.service, "-a", this.account, "-w"], "read secret from macOS Keychain");
4365
+ if (getCommandExitCode(result) === 0) {
4366
+ return stripTrailingLineBreak(getCommandOutput(result, "stdout"));
4367
+ }
4368
+ if (isKeychainEntryNotFound(result)) {
4369
+ return null;
4370
+ }
4371
+ throw createSecurityCliFailure("read secret from macOS Keychain", result);
4372
+ }
4373
+ async withLock(operation, options = {}) {
4374
+ const identity = createHash("sha256").update(JSON.stringify([this.service, this.account])).digest("hex");
4375
+ return withSecretStoreFileLock(this.lockFs, path3.join(this.lockDirectory, identity), operation, options);
4376
+ }
4377
+ async set(value) {
4378
+ if (value.includes("\n") || value.includes("\r")) {
4379
+ throw new Error("Keychain secrets cannot contain line breaks");
4380
+ }
4381
+ const result = await this.executeSecurityCommand([
4382
+ "add-generic-password",
4383
+ "-s",
4384
+ this.service,
4385
+ "-a",
4386
+ this.account,
4387
+ "-U",
4388
+ "-w",
4389
+ value
4390
+ ], "store secret in macOS Keychain");
4391
+ if (getCommandExitCode(result) !== 0) {
4392
+ throw createSecurityCliFailure("store secret in macOS Keychain", result);
4479
4393
  }
4480
- };
4481
- }
4482
- function loopbackTarget(options) {
4483
- if (options.redirectUri !== void 0) {
4484
- let url;
4485
- try {
4486
- url = new URL(options.redirectUri);
4487
- } catch (cause) {
4488
- throw new Error("Invalid OAuth loopback redirect URI", { cause });
4394
+ }
4395
+ async delete() {
4396
+ const result = await this.executeSecurityCommand(["delete-generic-password", "-s", this.service, "-a", this.account], "delete secret from macOS Keychain");
4397
+ if (getCommandExitCode(result) === 0 || isKeychainEntryNotFound(result)) {
4398
+ return;
4489
4399
  }
4490
- const forbiddenQuery = ["code", "state", "error", "error_description", "iss"];
4491
- if (url.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password || url.hash || url.port === "0" || forbiddenQuery.some((name) => url.searchParams.has(name)) || [...options.redirectUri].some((char) => char.codePointAt(0) <= 32) || options.callbackPath !== void 0 && options.callbackPath !== url.pathname)
4492
- throw new Error("Invalid OAuth loopback redirect URI");
4493
- return { port: url.port ? Number(url.port) : 80, host: url.hostname === "[::1]" ? "::1" : url.hostname, callbackPath: url.pathname };
4400
+ throw createSecurityCliFailure("delete secret from macOS Keychain", result);
4494
4401
  }
4495
- const callbackPath = options.callbackPath ?? "/callback";
4496
- const parsed = new URL(callbackPath, "http://127.0.0.1");
4497
- if (!callbackPath.startsWith("/") || parsed.origin !== "http://127.0.0.1" || parsed.pathname !== callbackPath || parsed.search || parsed.hash)
4498
- throw new Error("Invalid OAuth loopback callback path");
4499
- return { port: 0, host: "127.0.0.1", callbackPath };
4500
- }
4501
- async function startServer(server, port, host, signal) {
4502
- signal.throwIfAborted();
4503
- return new Promise((resolve, reject) => {
4504
- const cleanup = () => {
4505
- server.off("error", handleError);
4506
- signal.removeEventListener("abort", aborted);
4507
- };
4508
- const handleError = (error) => {
4509
- cleanup();
4510
- reject(error);
4511
- };
4512
- const aborted = () => {
4513
- cleanup();
4514
- reject(signal.reason);
4515
- };
4516
- server.once("error", handleError);
4517
- signal.addEventListener("abort", aborted, { once: true });
4402
+ async executeSecurityCommand(args, operation, options) {
4518
4403
  try {
4519
- server.listen(port, host, () => {
4520
- cleanup();
4521
- if (signal.aborted) {
4522
- server.close();
4523
- reject(signal.reason);
4524
- return;
4525
- }
4526
- const address = server.address();
4527
- if (address === null || typeof address === "string") {
4528
- reject(new Error("OAuth listener has no TCP address"));
4529
- return;
4530
- }
4531
- resolve(address.port);
4532
- });
4404
+ if (options === void 0) {
4405
+ return await this.runCommand(SECURITY_CLI, args);
4406
+ }
4407
+ return await this.runCommand(SECURITY_CLI, args, options);
4533
4408
  } catch (error) {
4534
- cleanup();
4535
- reject(error);
4409
+ const message = error instanceof Error ? error.message : String(error);
4410
+ throw new Error(`Failed to ${operation}: ${message}`);
4536
4411
  }
4537
- });
4538
- }
4539
- function waitForAuthorizationCode(server, authorizationUrl, options, callbackPath, signal) {
4540
- signal.throwIfAborted();
4541
- const expectedAuthorization = readExpectedAuthorizationCallback(authorizationUrl);
4542
- return new Promise((resolve, reject) => {
4543
- let settled = false;
4544
- const settle = (fn) => {
4545
- if (settled)
4546
- return;
4547
- settled = true;
4548
- server.off("request", request);
4549
- signal.removeEventListener("abort", aborted);
4550
- fn();
4412
+ }
4413
+ };
4414
+ function runSecurityCommand(command, args, options) {
4415
+ return new Promise((resolve) => {
4416
+ const child = spawn(command, args, {
4417
+ stdio: [options?.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
4418
+ });
4419
+ let stdout = "";
4420
+ let stderr = "";
4421
+ let stdinErrorMessage;
4422
+ const appendStderr = (message) => {
4423
+ stderr = stderr.length === 0 ? message : `${stderr}${stderr.endsWith("\n") ? "" : "\n"}${message}`;
4551
4424
  };
4552
- const aborted = () => settle(() => reject(signal.reason));
4553
- const request = (req, res) => {
4554
- let url;
4555
- try {
4556
- url = new URL(req.url ?? "/", "http://127.0.0.1");
4557
- } catch {
4558
- res.writeHead(400);
4559
- res.end("Invalid callback URL");
4560
- return;
4561
- }
4562
- if (url.pathname !== callbackPath) {
4563
- res.writeHead(404);
4564
- res.end("Not found");
4425
+ const appendStdinError = () => {
4426
+ if (stdinErrorMessage === void 0) {
4565
4427
  return;
4566
4428
  }
4567
- const callbackParameters = {
4568
- code: url.searchParams.get("code"),
4569
- error: url.searchParams.get("error"),
4570
- errorDescription: url.searchParams.get("error_description"),
4571
- state: url.searchParams.get("state"),
4572
- iss: url.searchParams.get("iss")
4573
- };
4574
- try {
4575
- validateAuthorizationCallbackBinding(callbackParameters, expectedAuthorization);
4576
- if (callbackParameters.error !== null)
4577
- throw createAuthorizationError(callbackParameters.error, callbackParameters.errorDescription ?? callbackParameters.error);
4578
- const code = validateAuthorizationCallbackParameters(callbackParameters, expectedAuthorization);
4579
- res.writeHead(200, { "Content-Type": "text/html" });
4580
- res.end(buildSuccessPage(options.landingPage));
4581
- settle(() => resolve(code));
4582
- } catch (error) {
4583
- res.writeHead(400);
4584
- res.end(error instanceof Error ? error.message : "Invalid OAuth callback");
4585
- settle(() => reject(error));
4586
- }
4429
+ appendStderr(stdinErrorMessage);
4430
+ stdinErrorMessage = void 0;
4587
4431
  };
4588
- server.on("request", request);
4589
- signal.addEventListener("abort", aborted, { once: true });
4590
- if (options.readLine !== void 0) {
4591
- void Promise.resolve().then(() => settled ? void 0 : options.readLine()).then((input) => {
4592
- if (settled)
4593
- return;
4594
- const callbackParameters = extractCallbackParametersFromInput(input);
4595
- if (callbackParameters === null)
4596
- throw new Error("OAuth callback missing authorization code");
4597
- validateAuthorizationCallbackBinding(callbackParameters, expectedAuthorization);
4598
- if (callbackParameters.error !== null)
4599
- throw createAuthorizationError(callbackParameters.error, callbackParameters.errorDescription ?? callbackParameters.error);
4600
- const code = validateAuthorizationCallbackParameters(callbackParameters, expectedAuthorization);
4601
- settle(() => resolve(code));
4602
- }).catch((error) => settle(() => reject(error)));
4603
- }
4604
- if (options.openBrowser !== void 0) {
4605
- void Promise.resolve().then(() => settled ? void 0 : options.openBrowser(authorizationUrl)).catch((error) => settle(() => reject(error)));
4432
+ child.stdout?.setEncoding("utf8");
4433
+ child.stdout?.on("data", (chunk) => {
4434
+ stdout += chunk.toString();
4435
+ });
4436
+ child.stderr?.setEncoding("utf8");
4437
+ child.stderr?.on("data", (chunk) => {
4438
+ stderr += chunk.toString();
4439
+ });
4440
+ if (options?.stdin !== void 0) {
4441
+ child.stdin?.once("error", (error) => {
4442
+ stdinErrorMessage = error instanceof Error ? error.message : String(error);
4443
+ });
4444
+ child.stdin?.end(options.stdin);
4606
4445
  }
4446
+ child.on("error", (error) => {
4447
+ const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
4448
+ appendStdinError();
4449
+ appendStderr(message);
4450
+ resolve({
4451
+ stdout,
4452
+ stderr,
4453
+ exitCode: 127
4454
+ });
4455
+ });
4456
+ child.on("close", (code) => {
4457
+ appendStdinError();
4458
+ resolve({
4459
+ stdout,
4460
+ stderr,
4461
+ exitCode: code ?? 1
4462
+ });
4463
+ });
4607
4464
  });
4608
4465
  }
4609
- function extractCallbackParametersFromInput(input) {
4610
- const trimmed = input.replaceAll("\r", "").replaceAll("\n", "").trim();
4611
- if (trimmed.length === 0) {
4612
- return null;
4466
+ function stripTrailingLineBreak(value) {
4467
+ if (value.endsWith("\r\n")) {
4468
+ return value.slice(0, -2);
4469
+ }
4470
+ if (value.endsWith("\n") || value.endsWith("\r")) {
4471
+ return value.slice(0, -1);
4472
+ }
4473
+ return value;
4474
+ }
4475
+ function isKeychainEntryNotFound(result) {
4476
+ return getCommandExitCode(result) === KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE;
4477
+ }
4478
+ function createSecurityCliFailure(operation, result) {
4479
+ const exitCode = getCommandExitCode(result);
4480
+ const details = getCommandOutput(result, "stderr").trim() || getCommandOutput(result, "stdout").trim();
4481
+ if (details) {
4482
+ return new Error(`Failed to ${operation}: security exited with code ${exitCode}: ${details}`);
4483
+ }
4484
+ return new Error(`Failed to ${operation}: security exited with code ${exitCode}`);
4485
+ }
4486
+ function getCommandExitCode(result) {
4487
+ const value = getOwnEntry3(result, "exitCode");
4488
+ return typeof value === "number" && Number.isInteger(value) ? value : 1;
4489
+ }
4490
+ function getCommandOutput(result, key2) {
4491
+ const value = getOwnEntry3(result, key2);
4492
+ return typeof value === "string" ? value : "";
4493
+ }
4494
+ function getOwnEntry3(record2, key2) {
4495
+ return Object.prototype.hasOwnProperty.call(record2, key2) ? record2[key2] : void 0;
4496
+ }
4497
+
4498
+ // ../auth-store/dist/create-secret-store.js
4499
+ var DEFAULT_BACKEND_ENV_VAR = "AUTH_BACKEND";
4500
+ var MACOS_PLATFORM = "darwin";
4501
+ var storeFactories = {
4502
+ file: (input) => {
4503
+ if (!input.fileStore) {
4504
+ throw new Error("fileStore configuration is required for file backend");
4505
+ }
4506
+ return new EncryptedFileStore(input.fileStore);
4507
+ },
4508
+ keychain: (input) => {
4509
+ if (!input.keychainStore) {
4510
+ throw new Error("keychainStore configuration is required for keychain backend");
4511
+ }
4512
+ return new KeychainStore(input.keychainStore);
4513
+ }
4514
+ };
4515
+ function createSecretStore(input) {
4516
+ const backend = resolveBackend(input);
4517
+ const platform = input.platform ?? process.platform;
4518
+ if (backend === "keychain" && platform !== MACOS_PLATFORM) {
4519
+ throw new Error(`Keychain backend is only supported on macOS. Current platform: ${platform}`);
4520
+ }
4521
+ const store = storeFactories[backend](input);
4522
+ return { backend, store };
4523
+ }
4524
+ function resolveBackend(input) {
4525
+ const envVar = input.backendEnvVar ?? DEFAULT_BACKEND_ENV_VAR;
4526
+ const configuredBackend = input.backend ?? getOwnEnvValue(input.env, envVar) ?? getOwnEnvValue(process.env, envVar);
4527
+ const backend = configuredBackend?.trim();
4528
+ if (backend === "keychain") {
4529
+ return "keychain";
4530
+ }
4531
+ if (backend === void 0 || backend === "file") {
4532
+ return "file";
4613
4533
  }
4534
+ throw new Error(`Unsupported auth store backend: ${backend}`);
4535
+ }
4536
+ function getOwnEnvValue(env, key2) {
4537
+ return env !== void 0 && Object.prototype.hasOwnProperty.call(env, key2) ? env[key2] : void 0;
4538
+ }
4539
+
4540
+ // ../mcp-oauth/dist/resource-indicator.js
4541
+ function canonicalizeResourceIndicator(value) {
4542
+ let url;
4614
4543
  try {
4615
- const url = new URL(trimmed);
4616
- return {
4617
- code: url.searchParams.get("code"),
4618
- error: url.searchParams.get("error"),
4619
- errorDescription: url.searchParams.get("error_description"),
4620
- state: url.searchParams.get("state"),
4621
- iss: url.searchParams.get("iss")
4622
- };
4544
+ url = value instanceof URL ? new URL(value.toString()) : new URL(value);
4623
4545
  } catch {
4624
- return {
4625
- code: trimmed,
4626
- error: null,
4627
- errorDescription: null,
4628
- state: null,
4629
- iss: null
4630
- };
4546
+ throw new Error("Resource indicator must be an absolute URL");
4631
4547
  }
4548
+ url.hash = "";
4549
+ return url.toString();
4632
4550
  }
4633
- function readExpectedAuthorizationCallback(authorizationUrl) {
4634
- const url = new URL(authorizationUrl);
4635
- const state = url.searchParams.get("state");
4636
- const parsedState = parseAuthorizationState(state);
4551
+
4552
+ // ../mcp-oauth/dist/client/auth-store-session-store.js
4553
+ var DEFAULT_FILE_SALT = "poe-code:mcp-oauth:v1";
4554
+ var DEFAULT_FILE_DIRECTORY = ".poe-code/mcp-oauth";
4555
+ var DEFAULT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth";
4556
+ var DEFAULT_CLIENT_FILE_SALT = "poe-code:mcp-oauth:clients:v1";
4557
+ var DEFAULT_CLIENT_FILE_DIRECTORY = ".poe-code/mcp-oauth/clients";
4558
+ var DEFAULT_CLIENT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth-clients";
4559
+ var MAX_JS_DATE_MS = 864e13;
4560
+ function createAuthStoreSessionStore(options = {}, namespace) {
4561
+ assertPersistenceNamespace(namespace);
4637
4562
  return {
4638
- state,
4639
- issuer: parsedState?.issuer ?? null,
4640
- requireIssuer: parsedState?.requireIssuer ?? false
4563
+ async withLock(resource, operation, lockOptions) {
4564
+ const store = createResourceSecretStore(resource, options, namespace);
4565
+ if (store.withLock === void 0)
4566
+ throw new Error("OAuth secret-store backend does not support transaction locks");
4567
+ return store.withLock(operation, lockOptions);
4568
+ },
4569
+ async load(resource) {
4570
+ const store = createResourceSecretStore(resource, options, namespace);
4571
+ const value = await store.get();
4572
+ if (value === null) {
4573
+ return null;
4574
+ }
4575
+ let parsed;
4576
+ try {
4577
+ parsed = JSON.parse(value);
4578
+ } catch {
4579
+ throw new Error("Stored OAuth session must be valid JSON; reset the store explicitly to recover");
4580
+ }
4581
+ if (isStoredOAuthSession(parsed)) {
4582
+ return parsed;
4583
+ }
4584
+ throw new Error("Stored OAuth session must match the expected shape");
4585
+ },
4586
+ async save(resource, session) {
4587
+ const store = createResourceSecretStore(resource, options, namespace);
4588
+ await store.set(JSON.stringify(session));
4589
+ },
4590
+ async clear(resource) {
4591
+ const store = createResourceSecretStore(resource, options, namespace);
4592
+ await store.delete();
4593
+ }
4641
4594
  };
4642
4595
  }
4643
- function validateAuthorizationCallbackParameters(callback, expected) {
4644
- validateAuthorizationCallbackBinding(callback, expected);
4645
- if (callback.code === null || callback.code.length === 0) {
4646
- throw new Error("OAuth callback missing authorization code");
4596
+ function createAuthStoreClientStore(options, namespace) {
4597
+ assertPersistenceNamespace(namespace);
4598
+ return {
4599
+ async load(issuer) {
4600
+ const store = createIssuerSecretStore(issuer, options, namespace);
4601
+ const value = await store.get();
4602
+ if (value === null) {
4603
+ return null;
4604
+ }
4605
+ let parsed;
4606
+ try {
4607
+ parsed = JSON.parse(value);
4608
+ } catch {
4609
+ throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
4610
+ }
4611
+ if (isObjectRecord2(parsed) && typeof getOwnEntry4(parsed, "clientId") === "string")
4612
+ return parsed;
4613
+ throw new Error("Stored OAuth client must be a JSON object with clientId");
4614
+ },
4615
+ async save(issuer, client) {
4616
+ const store = createIssuerSecretStore(issuer, options, namespace);
4617
+ await store.set(JSON.stringify(client));
4618
+ },
4619
+ async clear(issuer) {
4620
+ const store = createIssuerSecretStore(issuer, options, namespace);
4621
+ await store.delete();
4622
+ }
4623
+ };
4624
+ }
4625
+ function createNamedSecretStore(key2, options, defaults, namespace) {
4626
+ const hash = crypto2.createHash("sha256").update(namespace === void 0 ? key2 : JSON.stringify([namespace, key2])).digest("hex");
4627
+ const configuredFilePath = options.fileStore?.filePath;
4628
+ const parsedFilePath = configuredFilePath === void 0 ? null : path4.parse(configuredFilePath);
4629
+ const fileStore = {
4630
+ ...options.fileStore,
4631
+ throwOnInvalidDocument: true,
4632
+ filePath: parsedFilePath === null ? void 0 : path4.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
4633
+ salt: options.fileStore?.salt ?? defaults.salt,
4634
+ defaultDirectory: options.fileStore?.defaultDirectory || defaults.directory,
4635
+ defaultFileName: parsedFilePath === null ? `${hash}.enc` : `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`
4636
+ };
4637
+ const keychainStore = {
4638
+ ...options.keychainStore,
4639
+ service: options.keychainStore?.service ?? defaults.service,
4640
+ account: `${options.keychainStore?.account ?? defaults.accountPrefix}:${hash}`
4641
+ };
4642
+ return createSecretStore({ ...options, fileStore, keychainStore }).store;
4643
+ }
4644
+ function createResourceSecretStore(resource, options, namespace) {
4645
+ return createNamedSecretStore(canonicalizeResourceIndicator(resource), options, {
4646
+ salt: DEFAULT_FILE_SALT,
4647
+ directory: DEFAULT_FILE_DIRECTORY,
4648
+ service: DEFAULT_KEYCHAIN_SERVICE,
4649
+ accountPrefix: "provider"
4650
+ }, namespace);
4651
+ }
4652
+ function createIssuerSecretStore(issuer, options, namespace) {
4653
+ return createNamedSecretStore(issuer, options, {
4654
+ salt: DEFAULT_CLIENT_FILE_SALT,
4655
+ directory: DEFAULT_CLIENT_FILE_DIRECTORY,
4656
+ service: DEFAULT_CLIENT_KEYCHAIN_SERVICE,
4657
+ accountPrefix: "issuer"
4658
+ }, namespace);
4659
+ }
4660
+ function assertPersistenceNamespace(namespace) {
4661
+ if (namespace !== void 0 && (typeof namespace !== "string" || namespace.trim() === "" || Buffer.byteLength(namespace, "utf8") > 1024))
4662
+ throw new Error("OAuth persistence namespace must be a nonempty string within 1024 bytes");
4663
+ }
4664
+ function isObjectRecord2(value) {
4665
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4666
+ }
4667
+ function getOwnEntry4(record2, key2) {
4668
+ return Object.prototype.hasOwnProperty.call(record2, key2) ? record2[key2] : void 0;
4669
+ }
4670
+ function getOwnString(record2, key2) {
4671
+ const value = getOwnEntry4(record2, key2);
4672
+ return typeof value === "string" ? value : void 0;
4673
+ }
4674
+ function isStoredOAuthSession(value) {
4675
+ if (!isObjectRecord2(value)) {
4676
+ return false;
4647
4677
  }
4648
- return callback.code;
4678
+ return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && normalizeStoredOAuthClient(getOwnEntry4(value, "client")) !== null && isStoredOAuthDiscovery(getOwnEntry4(value, "discovery")) && (getOwnEntry4(value, "requestedScope") === void 0 || isNonBlankOwnString(value, "requestedScope")) && (getOwnEntry4(value, "refreshState") === void 0 || getOwnEntry4(value, "refreshState") === "pending" && getOwnEntry4(value, "tokens") === void 0) && isStoredOAuthTokensOrMissing(getOwnEntry4(value, "tokens"));
4649
4679
  }
4650
- function validateAuthorizationCallbackBinding(callback, expected) {
4651
- if (expected.state !== null) {
4652
- if (callback.state === null || callback.state.length === 0) {
4653
- throw new Error("OAuth callback missing state");
4654
- }
4655
- if (callback.state !== expected.state) {
4656
- throw new Error("OAuth callback state mismatch");
4657
- }
4680
+ function isStoredOAuthDiscovery(value) {
4681
+ if (!isObjectRecord2(value)) {
4682
+ return false;
4658
4683
  }
4659
- if (expected.requireIssuer) {
4660
- if (callback.iss === null || callback.iss.length === 0) {
4661
- throw new Error("OAuth callback missing issuer");
4662
- }
4684
+ return isNonBlankOwnString(value, "resourceMetadataUrl") && isObjectRecord2(getOwnEntry4(value, "resourceMetadata")) && isObjectRecord2(getOwnEntry4(value, "authorizationServerMetadata"));
4685
+ }
4686
+ function isStoredOAuthTokensOrMissing(value) {
4687
+ if (value === void 0) {
4688
+ return true;
4663
4689
  }
4664
- if (callback.iss !== null && callback.iss.length > 0 && expected.issuer !== null && callback.iss !== expected.issuer) {
4665
- throw new Error("OAuth callback issuer mismatch");
4690
+ if (!isObjectRecord2(value)) {
4691
+ return false;
4692
+ }
4693
+ if (!isNonBlankOwnString(value, "accessToken") || getOwnString(value, "tokenType") !== "Bearer") {
4694
+ return false;
4695
+ }
4696
+ const expiresAt = getOwnEntry4(value, "expiresAt");
4697
+ if (expiresAt !== null && (typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt) || expiresAt > MAX_JS_DATE_MS || !Number.isFinite(new Date(expiresAt).getTime()))) {
4698
+ return false;
4666
4699
  }
4700
+ const refreshToken = getOwnEntry4(value, "refreshToken");
4701
+ if (refreshToken !== void 0 && (typeof refreshToken !== "string" || refreshToken.trim().length === 0)) {
4702
+ return false;
4703
+ }
4704
+ const scope = getOwnEntry4(value, "scope");
4705
+ return scope === void 0 || typeof scope === "string" && scope.trim().length > 0;
4667
4706
  }
4668
- function createAuthorizationError(error, description) {
4669
- return new Error(`OAuth authorization failed: ${error} \u2014 ${description}`);
4707
+ function isNonBlankOwnString(record2, key2) {
4708
+ const value = getOwnString(record2, key2);
4709
+ return value !== void 0 && value.trim().length > 0;
4670
4710
  }
4671
- function escapeHtml(text) {
4672
- return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4711
+
4712
+ // ../mcp-oauth/dist/client/resource-bound-store.js
4713
+ function createResourceBoundOAuthStores(options, namespace, identity) {
4714
+ assertPersistenceNamespace(identity);
4715
+ const store = createNamedSecretStore(identity, options, {
4716
+ salt: "poe-code:mcp-oauth:resources:v1",
4717
+ directory: ".poe-code/mcp-oauth/resources",
4718
+ service: "poe-code-mcp-oauth-resources",
4719
+ accountPrefix: "resource"
4720
+ }, namespace);
4721
+ const result = { initialGrantAllowed: true, sessionStore: {}, clientStore: {} };
4722
+ async function read() {
4723
+ const raw = await store.get();
4724
+ if (raw === null)
4725
+ return null;
4726
+ let value;
4727
+ try {
4728
+ value = JSON.parse(raw);
4729
+ } catch {
4730
+ throw new Error("Stored OAuth resource identity must be valid JSON; reset explicitly to recover");
4731
+ }
4732
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4733
+ throw new Error("Invalid stored OAuth resource identity");
4734
+ const record2 = value;
4735
+ if (!["version", "resource", "generation", "session", "clients"].every((key2) => Object.hasOwn(record2, key2)) || record2.version !== 1 || typeof record2.resource !== "string" || record2.resource !== canonicalizeResourceIndicator(record2.resource) || !Number.isSafeInteger(record2.generation) || record2.generation < 0 || record2.session !== null && (!isStoredOAuthSession(record2.session) || record2.session.resource !== record2.resource) || typeof record2.clients !== "object" || record2.clients === null || Array.isArray(record2.clients))
4736
+ throw new Error("Invalid stored OAuth resource identity; reset explicitly to recover");
4737
+ let resourceUrl;
4738
+ try {
4739
+ resourceUrl = new URL(record2.resource);
4740
+ } catch {
4741
+ throw new Error("Invalid stored OAuth resource identity URL");
4742
+ }
4743
+ if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.hash)
4744
+ throw new Error("Invalid stored OAuth resource identity URL");
4745
+ for (const [issuer, client] of Object.entries(record2.clients)) {
4746
+ const normalized = normalizeStoredOAuthClient(client);
4747
+ let url;
4748
+ try {
4749
+ url = new URL(issuer);
4750
+ } catch {
4751
+ throw new Error("Invalid stored OAuth resource client issuer");
4752
+ }
4753
+ if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
4754
+ throw new Error("Invalid stored OAuth resource client");
4755
+ Object.defineProperty(record2.clients, issuer, { value: normalized, enumerable: true, configurable: true, writable: true });
4756
+ }
4757
+ return record2;
4758
+ }
4759
+ async function reconcile(resource) {
4760
+ resource = canonicalizeResourceIndicator(resource);
4761
+ const existing = await read();
4762
+ if (existing?.resource === resource) {
4763
+ result.initialGrantAllowed = existing.generation === 0;
4764
+ return existing;
4765
+ }
4766
+ const record2 = { version: 1, resource, generation: existing === null ? 0 : existing.generation + 1, session: null, clients: {} };
4767
+ if (!Number.isSafeInteger(record2.generation))
4768
+ throw new Error("OAuth resource identity generation limit exceeded");
4769
+ await store.set(JSON.stringify(record2));
4770
+ result.initialGrantAllowed = record2.generation === 0;
4771
+ return record2;
4772
+ }
4773
+ result.sessionStore = {
4774
+ async withLock(_resource, operation, options2) {
4775
+ if (store.withLock === void 0)
4776
+ throw new Error("OAuth resource identity backend must support transaction locks");
4777
+ return store.withLock(operation, options2);
4778
+ },
4779
+ async load(resource) {
4780
+ return (await reconcile(resource)).session;
4781
+ },
4782
+ async save(resource, session) {
4783
+ const record2 = await reconcile(resource);
4784
+ if (canonicalizeResourceIndicator(session.resource) !== record2.resource)
4785
+ throw new Error("OAuth session does not match its resource identity");
4786
+ record2.session = session;
4787
+ await store.set(JSON.stringify(record2));
4788
+ },
4789
+ async clear(resource) {
4790
+ const record2 = await reconcile(resource);
4791
+ record2.session = null;
4792
+ record2.generation = Math.max(1, record2.generation);
4793
+ result.initialGrantAllowed = false;
4794
+ await store.set(JSON.stringify(record2));
4795
+ }
4796
+ };
4797
+ result.clientStore = {
4798
+ async load(issuer) {
4799
+ const record2 = await read();
4800
+ return record2 !== null && Object.hasOwn(record2.clients, issuer) ? record2.clients[issuer] : null;
4801
+ },
4802
+ async save(issuer, client) {
4803
+ const record2 = await read();
4804
+ if (record2 === null)
4805
+ throw new Error("OAuth resource identity must be bound before registering a client");
4806
+ Object.defineProperty(record2.clients, issuer, { value: client, enumerable: true, configurable: true, writable: true });
4807
+ await store.set(JSON.stringify(record2));
4808
+ },
4809
+ async clear(issuer) {
4810
+ const record2 = await read();
4811
+ if (record2 === null || !Object.hasOwn(record2.clients, issuer))
4812
+ return;
4813
+ delete record2.clients[issuer];
4814
+ await store.set(JSON.stringify(record2));
4815
+ }
4816
+ };
4817
+ return result;
4673
4818
  }
4674
- function buildSuccessPage(landingPage) {
4675
- const title = landingPage?.title ?? "Connected";
4676
- const body = landingPage?.body ?? "You can close this tab and return to your terminal.";
4677
- return [
4678
- "<!DOCTYPE html>",
4679
- `<html><head><meta charset=utf-8><title>${escapeHtml(title)}</title></head>`,
4680
- '<body style="font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">',
4681
- '<div style="text-align:center">',
4682
- `<h1>${escapeHtml(title)}</h1>`,
4683
- `<p style="color:#666">${escapeHtml(body)}</p>`,
4684
- "</div></body></html>"
4685
- ].join("");
4819
+
4820
+ // ../mcp-oauth/dist/client/default-oauth-client-provider.js
4821
+ import { isIP } from "node:net";
4822
+
4823
+ // ../mcp-oauth/dist/http-fetch.js
4824
+ async function fetchMcpResponse(fetchImplementation, input, init = {}) {
4825
+ const response = await fetchImplementation(input, { ...init, redirect: "error" });
4826
+ if (response.redirected || response.type === "opaqueredirect") {
4827
+ void response.body?.cancel().catch(() => void 0);
4828
+ throw new Error("MCP HTTP redirects are not allowed");
4829
+ }
4830
+ return response;
4686
4831
  }
4687
4832
 
4833
+ // ../mcp-oauth/dist/client/default-oauth-client-provider.js
4834
+ import { URL as URL2 } from "node:url";
4835
+
4688
4836
  // ../mcp-oauth/dist/client/pkce.js
4689
4837
  import crypto3 from "node:crypto";
4690
4838
  function generateCodeVerifier() {
@@ -4983,8 +5131,11 @@ function createDefaultOAuthClientProvider(options) {
4983
5131
  const configuredClient = normalizeConfiguredClient(options.client);
4984
5132
  const requestedTokenMethod = normalizeOAuthTokenEndpointAuthMethod(options.client.tokenEndpointAuthMethod);
4985
5133
  const configuredTokenMethod = requestedTokenMethod ?? configuredClient?.tokenEndpointAuthMethod;
4986
- const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
4987
- const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
5134
+ if (options.resourceIdentity !== void 0 && options.sessionStore !== void 0)
5135
+ throw new Error("OAuth resourceIdentity requires native-owned persistence; custom stores own their resource trust policy");
5136
+ const resourceStores = options.resourceIdentity === void 0 ? void 0 : createResourceBoundOAuthStores(options.authStore ?? {}, options.persistenceNamespace, options.resourceIdentity);
5137
+ const sessionStore = resourceStores?.sessionStore ?? options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
5138
+ const clientStore = resourceStores?.clientStore ?? (options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace));
4988
5139
  const now = options.now ?? Date.now;
4989
5140
  const registeredClients = /* @__PURE__ */ new Map();
4990
5141
  if (options.initialGrant !== void 0) {
@@ -5077,6 +5228,11 @@ function createDefaultOAuthClientProvider(options) {
5077
5228
  const canonicalResource = canonicalizeResourceIndicator(resource);
5078
5229
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
5079
5230
  let session = await loadSession(canonicalResource);
5231
+ if (resourceStores !== void 0) {
5232
+ registeredClients.clear();
5233
+ if (!resourceStores.initialGrantAllowed)
5234
+ initialGrantConsumed = true;
5235
+ }
5080
5236
  if (session !== null)
5081
5237
  assertRegistrationIssuer(session.client, session.authorizationServer);
5082
5238
  if (configuredClient !== null && discovery !== void 0)
@@ -5318,7 +5474,7 @@ function createDefaultOAuthClientProvider(options) {
5318
5474
  let storedClient = await loadRegisteredClient(discovery.authorizationServer);
5319
5475
  if (storedClient !== null) {
5320
5476
  assertRegistrationIssuer(storedClient, discovery.authorizationServer);
5321
- if (hasExpiredClientSecret(storedClient, now)) {
5477
+ if (hasExpiredClientSecret(storedClient, now) || !registrationMatchesRedirect(storedClient, redirectUri)) {
5322
5478
  await clearRegisteredClient(discovery.authorizationServer);
5323
5479
  storedClient = null;
5324
5480
  }
@@ -5331,7 +5487,7 @@ function createDefaultOAuthClientProvider(options) {
5331
5487
  };
5332
5488
  }
5333
5489
  if (registrationEndpoint === void 0) {
5334
- if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
5490
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now) && registrationMatchesRedirect(existingSession.client, redirectUri)) {
5335
5491
  return {
5336
5492
  kind: "dynamic",
5337
5493
  fromStoredRegistration: true,
@@ -5340,7 +5496,7 @@ function createDefaultOAuthClientProvider(options) {
5340
5496
  }
5341
5497
  throw new Error("Authorization server metadata is missing registration_endpoint");
5342
5498
  }
5343
- if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
5499
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now) && registrationMatchesRedirect(existingSession.client, redirectUri)) {
5344
5500
  const isConfiguredStaticFallback = configuredClient !== null && existingSession.client.clientId === configuredClient.clientId && existingSession.client.clientSecret === configuredClient.clientSecret;
5345
5501
  if (!isConfiguredStaticFallback) {
5346
5502
  await saveRegisteredClient(discovery.authorizationServer, existingSession.client);
@@ -5379,11 +5535,14 @@ function createDefaultOAuthClientProvider(options) {
5379
5535
  assertRegistrationIssuer(registeredClient, discovery.authorizationServer);
5380
5536
  if (hasExpiredClientSecret(registeredClient, now))
5381
5537
  throw new Error("OAuth client secret has expired in the registration response");
5382
- await saveRegisteredClient(discovery.authorizationServer, registeredClient);
5538
+ if (!registrationMatchesRedirect(registeredClient, redirectUri, true))
5539
+ throw new Error("OAuth registration response does not match the requested redirect URI");
5540
+ const clientWithRedirect = { ...registeredClient, requestedRedirectUri: redirectUri };
5541
+ await saveRegisteredClient(discovery.authorizationServer, clientWithRedirect);
5383
5542
  return {
5384
5543
  kind: "dynamic",
5385
5544
  fromStoredRegistration: false,
5386
- client: registeredClient
5545
+ client: clientWithRedirect
5387
5546
  };
5388
5547
  }
5389
5548
  async function loadSession(resource) {