Add .denoignore file and update implementation plan with additional tasks for type safety, dependency management, testing, documentation, and build configuration.
This commit is contained in:
parent
00b1d15cfd
commit
4394c0773d
10 changed files with 1815 additions and 1096 deletions
|
@ -1,9 +1,15 @@
|
|||
import { checkLockfile, createLockfile, deleteLockfile, getConfigFilePath, type LockfileData } from './mcp-auth-config.ts'
|
||||
import type { EventEmitter } from 'node:events'
|
||||
import type { Server } from 'node:http'
|
||||
import express from 'npm:express'
|
||||
import type { AddressInfo } from 'node:net'
|
||||
import { log, setupOAuthCallbackServerWithLongPoll } from './utils.ts'
|
||||
import {
|
||||
checkLockfile,
|
||||
createLockfile,
|
||||
deleteLockfile,
|
||||
getConfigFilePath,
|
||||
type LockfileData,
|
||||
} from "./mcp-auth-config.ts";
|
||||
import type { EventEmitter } from "node:events";
|
||||
import type { Server } from "node:http";
|
||||
import express from "npm:express";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { log, setupOAuthCallbackServerWithLongPoll } from "./utils.ts";
|
||||
|
||||
/**
|
||||
* Checks if a process with the given PID is running
|
||||
|
@ -14,13 +20,13 @@ export async function isPidRunning(pid: number): Promise<boolean> {
|
|||
try {
|
||||
// Deno doesn't have a direct equivalent to process.kill(pid, 0)
|
||||
// On non-Windows platforms, we can try to use kill system call to check
|
||||
if (Deno.build.os !== 'windows') {
|
||||
if (Deno.build.os !== "windows") {
|
||||
try {
|
||||
// Using Deno.run to check if process exists
|
||||
const command = new Deno.Command('kill', {
|
||||
args: ['-0', pid.toString()],
|
||||
stdout: 'null',
|
||||
stderr: 'null',
|
||||
const command = new Deno.Command("kill", {
|
||||
args: ["-0", pid.toString()],
|
||||
stdout: "null",
|
||||
stderr: "null",
|
||||
});
|
||||
const { success } = await command.output();
|
||||
return success;
|
||||
|
@ -30,9 +36,9 @@ export async function isPidRunning(pid: number): Promise<boolean> {
|
|||
} else {
|
||||
// On Windows, use tasklist to check if process exists
|
||||
try {
|
||||
const command = new Deno.Command('tasklist', {
|
||||
args: ['/FI', `PID eq ${pid}`, '/NH'],
|
||||
stdout: 'piped',
|
||||
const command = new Deno.Command("tasklist", {
|
||||
args: ["/FI", `PID eq ${pid}`, "/NH"],
|
||||
stdout: "piped",
|
||||
});
|
||||
const { stdout } = await command.output();
|
||||
const output = new TextDecoder().decode(stdout);
|
||||
|
@ -53,32 +59,35 @@ export async function isPidRunning(pid: number): Promise<boolean> {
|
|||
*/
|
||||
export async function isLockValid(lockData: LockfileData): Promise<boolean> {
|
||||
// Check if the lockfile is too old (over 30 minutes)
|
||||
const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes
|
||||
const MAX_LOCK_AGE = 30 * 60 * 1000; // 30 minutes
|
||||
if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) {
|
||||
log('Lockfile is too old')
|
||||
return false
|
||||
log("Lockfile is too old");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the process is still running
|
||||
if (!(await isPidRunning(lockData.pid))) {
|
||||
log('Process from lockfile is not running')
|
||||
return false
|
||||
log("Process from lockfile is not running");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the endpoint is accessible
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 1000)
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1000);
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${lockData.port}/wait-for-auth?poll=false`, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${lockData.port}/wait-for-auth?poll=false`,
|
||||
{
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
|
||||
clearTimeout(timeout)
|
||||
return response.status === 200 || response.status === 202
|
||||
clearTimeout(timeout);
|
||||
return response.status === 200 || response.status === 202;
|
||||
} catch (error) {
|
||||
log(`Error connecting to auth server: ${(error as Error).message}`)
|
||||
return false
|
||||
log(`Error connecting to auth server: ${(error as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,31 +97,31 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
|
|||
* @returns True if authentication completed successfully, false otherwise
|
||||
*/
|
||||
export async function waitForAuthentication(port: number): Promise<boolean> {
|
||||
log(`Waiting for authentication from the server on port ${port}...`)
|
||||
log(`Waiting for authentication from the server on port ${port}...`);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const url = `http://127.0.0.1:${port}/wait-for-auth`
|
||||
log(`Querying: ${url}`)
|
||||
const response = await fetch(url)
|
||||
const url = `http://127.0.0.1:${port}/wait-for-auth`;
|
||||
log(`Querying: ${url}`);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.status === 200) {
|
||||
// Auth completed, but we don't return the code anymore
|
||||
log('Authentication completed by other instance')
|
||||
return true
|
||||
log("Authentication completed by other instance");
|
||||
return true;
|
||||
}
|
||||
if (response.status === 202) {
|
||||
// Continue polling
|
||||
log('Authentication still in progress')
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
log("Authentication still in progress");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
log(`Unexpected response status: ${response.status}`)
|
||||
return false
|
||||
log(`Unexpected response status: ${response.status}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Error waiting for authentication: ${(error as Error).message}`)
|
||||
return false
|
||||
log(`Error waiting for authentication: ${(error as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,72 +136,85 @@ export async function coordinateAuth(
|
|||
serverUrlHash: string,
|
||||
callbackPort: number,
|
||||
events: EventEmitter,
|
||||
): Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean }> {
|
||||
): Promise<
|
||||
{
|
||||
server: Server;
|
||||
waitForAuthCode: () => Promise<string>;
|
||||
skipBrowserAuth: boolean;
|
||||
}
|
||||
> {
|
||||
// Check for a lockfile (disabled on Windows for the time being)
|
||||
const lockData = Deno.build.os === 'windows' ? null : await checkLockfile(serverUrlHash)
|
||||
const lockData = Deno.build.os === "windows"
|
||||
? null
|
||||
: await checkLockfile(serverUrlHash);
|
||||
|
||||
// If there's a valid lockfile, try to use the existing auth process
|
||||
if (lockData && (await isLockValid(lockData))) {
|
||||
log(`Another instance is handling authentication on port ${lockData.port}`)
|
||||
log(`Another instance is handling authentication on port ${lockData.port}`);
|
||||
|
||||
try {
|
||||
// Try to wait for the authentication to complete
|
||||
const authCompleted = await waitForAuthentication(lockData.port)
|
||||
const authCompleted = await waitForAuthentication(lockData.port);
|
||||
if (authCompleted) {
|
||||
log('Authentication completed by another instance')
|
||||
log("Authentication completed by another instance");
|
||||
|
||||
// Setup a dummy server - the client will use tokens directly from disk
|
||||
const dummyServer = express().listen(0) // Listen on any available port
|
||||
const dummyServer = express().listen(0); // Listen on any available port
|
||||
|
||||
// This shouldn't actually be called in normal operation, but provide it for API compatibility
|
||||
const dummyWaitForAuthCode = () => {
|
||||
log('WARNING: waitForAuthCode called in secondary instance - this is unexpected')
|
||||
log(
|
||||
"WARNING: waitForAuthCode called in secondary instance - this is unexpected",
|
||||
);
|
||||
// Return a promise that never resolves - the client should use the tokens from disk instead
|
||||
return new Promise<string>(() => {})
|
||||
}
|
||||
return new Promise<string>(() => {});
|
||||
};
|
||||
|
||||
return {
|
||||
server: dummyServer,
|
||||
waitForAuthCode: dummyWaitForAuthCode,
|
||||
skipBrowserAuth: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
log('Taking over authentication process...')
|
||||
log("Taking over authentication process...");
|
||||
} catch (error) {
|
||||
log(`Error waiting for authentication: ${error}`)
|
||||
log(`Error waiting for authentication: ${error}`);
|
||||
}
|
||||
|
||||
// If we get here, the other process didn't complete auth successfully
|
||||
await deleteLockfile(serverUrlHash)
|
||||
await deleteLockfile(serverUrlHash);
|
||||
} else if (lockData) {
|
||||
// Invalid lockfile, delete its
|
||||
log('Found invalid lockfile, deleting it')
|
||||
await deleteLockfile(serverUrlHash)
|
||||
log("Found invalid lockfile, deleting it");
|
||||
await deleteLockfile(serverUrlHash);
|
||||
}
|
||||
|
||||
// Create our own lockfile
|
||||
const { server, waitForAuthCode, authCompletedPromise: _ } = setupOAuthCallbackServerWithLongPoll({
|
||||
port: callbackPort,
|
||||
path: '/oauth/callback',
|
||||
events,
|
||||
})
|
||||
const { server, waitForAuthCode, authCompletedPromise: _ } =
|
||||
setupOAuthCallbackServerWithLongPoll({
|
||||
port: callbackPort,
|
||||
path: "/oauth/callback",
|
||||
events,
|
||||
});
|
||||
|
||||
// Get the actual port the server is running on
|
||||
const address = server.address() as AddressInfo
|
||||
const actualPort = address.port
|
||||
const address = server.address() as AddressInfo;
|
||||
const actualPort = address.port;
|
||||
|
||||
log(`Creating lockfile for server ${serverUrlHash} with process ${Deno.pid} on port ${actualPort}`)
|
||||
await createLockfile(serverUrlHash, Deno.pid, actualPort)
|
||||
log(
|
||||
`Creating lockfile for server ${serverUrlHash} with process ${Deno.pid} on port ${actualPort}`,
|
||||
);
|
||||
await createLockfile(serverUrlHash, Deno.pid, actualPort);
|
||||
|
||||
// Make sure lockfile is deleted on process exit
|
||||
const cleanupHandler = async () => {
|
||||
try {
|
||||
log(`Cleaning up lockfile for server ${serverUrlHash}`)
|
||||
await deleteLockfile(serverUrlHash)
|
||||
log(`Cleaning up lockfile for server ${serverUrlHash}`);
|
||||
await deleteLockfile(serverUrlHash);
|
||||
} catch (error) {
|
||||
log(`Error cleaning up lockfile: ${error}`)
|
||||
log(`Error cleaning up lockfile: ${error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Setup exit handlers for Deno
|
||||
// Note: Deno doesn't have process.once but we can use addEventListener
|
||||
|
@ -200,7 +222,7 @@ export async function coordinateAuth(
|
|||
addEventListener("unload", () => {
|
||||
try {
|
||||
// Synchronous cleanup
|
||||
const configPath = getConfigFilePath(serverUrlHash, 'lock.json')
|
||||
const configPath = getConfigFilePath(serverUrlHash, "lock.json");
|
||||
// Use Deno's synchronous file API
|
||||
try {
|
||||
Deno.removeSync(configPath);
|
||||
|
@ -222,5 +244,5 @@ export async function coordinateAuth(
|
|||
server,
|
||||
waitForAuthCode,
|
||||
skipBrowserAuth: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue