diff --git a/README.md b/README.md index beff48d..ce233e5 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ const authProvider = browserAuth({ port: 3000, scope: "read write", + launch: open, // Opens browser for OAuth consent store: inMemoryStore(), // Or fileStore() for persistence }); @@ -155,17 +156,20 @@ import { browserAuth, inMemoryStore, fileStore } from "oauth-callback/mcp"; // Ephemeral storage (tokens lost on restart) const ephemeralAuth = browserAuth({ + launch: open, store: inMemoryStore(), }); // Persistent file storage (default: ~/.mcp/tokens.json) const persistentAuth = browserAuth({ + launch: open, store: fileStore(), storeKey: "my-app-tokens", // Namespace for multiple apps }); // Custom file location const customAuth = browserAuth({ + launch: open, store: fileStore("/path/to/tokens.json"), }); ``` @@ -179,6 +183,7 @@ const authProvider = browserAuth({ clientId: "your-client-id", clientSecret: "your-client-secret", scope: "read write", + launch: open, // Opens browser for OAuth consent store: fileStore(), // Persist tokens across sessions }); ``` @@ -283,6 +288,7 @@ Available from `oauth-callback/mcp`. Creates an MCP SDK-compatible OAuth provide - `clientSecret` (string): Pre-registered client secret (optional) - `store` (TokenStore): Token storage implementation (default: inMemoryStore()) - `storeKey` (string): Storage key for tokens (default: "mcp-tokens") + - `launch` (function): Callback to launch auth URL (e.g., `open`) - `authTimeout` (number): Authorization timeout in ms (default: 300000) - `successHtml` (string): Custom success page HTML - `errorHtml` (string): Custom error page HTML diff --git a/docs/api/browser-auth.md b/docs/api/browser-auth.md index 5eb7e33..064e5c2 100644 --- a/docs/api/browser-auth.md +++ b/docs/api/browser-auth.md @@ -96,9 +96,11 @@ await client.connect(transport); Store tokens across sessions: ```typescript +import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ + launch: open, store: fileStore(), // Persists to ~/.mcp/tokens.json scope: "read write", }); @@ -111,10 +113,13 @@ const authProvider = browserAuth({ If you have pre-registered OAuth credentials: ```typescript +import open from "open"; + const authProvider = browserAuth({ clientId: process.env.OAUTH_CLIENT_ID, clientSecret: process.env.OAUTH_CLIENT_SECRET, scope: "read write admin", + launch: open, store: fileStore(), }); ``` @@ -124,9 +129,11 @@ const authProvider = browserAuth({ Store tokens in a specific location: ```typescript +import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ + launch: open, store: fileStore("/path/to/my-tokens.json"), storeKey: "my-app-production", // Namespace for multiple environments }); @@ -137,10 +144,13 @@ const authProvider = browserAuth({ Configure the callback server: ```typescript +import open from "open"; + const authProvider = browserAuth({ port: 8080, hostname: "127.0.0.1", callbackPath: "/oauth/callback", + launch: open, store: fileStore(), }); ``` @@ -157,7 +167,10 @@ Ensure your OAuth app's redirect URI matches your configuration: Provide branded callback pages: ```typescript +import open from "open"; + const authProvider = browserAuth({ + launch: open, successHtml: ` @@ -208,7 +221,10 @@ const authProvider = browserAuth({ Monitor OAuth flow for debugging: ```typescript +import open from "open"; + const authProvider = browserAuth({ + launch: open, onRequest: (req) => { const url = new URL(req.url); console.log(`[OAuth] ${req.method} ${url.pathname}`); @@ -263,9 +279,12 @@ sequenceDiagram No pre-registration needed: ```typescript +import open from "open"; + // No clientId or clientSecret required! const authProvider = browserAuth({ scope: "read write", + launch: open, store: fileStore(), // Persist dynamically registered client }); @@ -317,9 +336,11 @@ interface OAuthStore extends TokenStore { Ephemeral storage (tokens lost on restart): ```typescript +import open from "open"; import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ + launch: open, store: inMemoryStore(), }); ``` @@ -335,15 +356,18 @@ const authProvider = browserAuth({ Persistent storage to JSON file: ```typescript +import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; // Default location: ~/.mcp/tokens.json const authProvider = browserAuth({ + launch: open, store: fileStore(), }); // Custom location const customAuth = browserAuth({ + launch: open, store: fileStore("/path/to/tokens.json"), }); ``` @@ -388,6 +412,7 @@ class RedisStore implements TokenStore { // Use custom store const authProvider = browserAuth({ + launch: open, store: new RedisStore(redisClient), }); ``` @@ -409,8 +434,10 @@ PKCE prevents authorization code interception attacks by: The provider automatically generates secure state parameters: ```typescript +import open from "open"; + // State is automatically generated and validated -const authProvider = browserAuth(); +const authProvider = browserAuth({ launch: open }); // No manual state handling needed! ``` @@ -430,8 +457,11 @@ Tokens are automatically managed with expiry tracking: File storage uses restrictive permissions: ```typescript +import open from "open"; + // Files are created with mode 0600 (owner read/write only) const authProvider = browserAuth({ + launch: open, store: fileStore(), // Secure file permissions }); ``` @@ -472,7 +502,10 @@ The provider includes automatic retry for transient failures: Configure timeout for different scenarios: ```typescript +import open from "open"; + const authProvider = browserAuth({ + launch: open, authTimeout: 600000, // 10 minutes for first-time setup }); ``` @@ -484,6 +517,7 @@ const authProvider = browserAuth({ Full example with Dynamic Client Registration: ```typescript +import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; @@ -491,6 +525,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ async function connectToNotion() { // No client credentials needed - uses DCR! const authProvider = browserAuth({ + launch: open, // Opens browser for OAuth consent store: fileStore(), // Persist tokens and client registration scope: "read write", onRequest: (req) => { @@ -536,24 +571,28 @@ connectToNotion(); Support development, staging, and production: ```typescript -import { browserAuth, fileStore } from "oauth-callback/mcp"; +import open from "open"; +import { browserAuth, fileStore, inMemoryStore } from "oauth-callback/mcp"; function createAuthProvider(environment: "dev" | "staging" | "prod") { const configs = { dev: { port: 3000, + launch: open, store: inMemoryStore(), // No persistence in dev authTimeout: 60000, - onRequest: (req) => console.log("[DEV]", req.url), + onRequest: (req: Request) => console.log("[DEV]", req.url), }, staging: { port: 3001, + launch: open, store: fileStore("~/.mcp/staging-tokens.json"), storeKey: "staging", authTimeout: 120000, }, prod: { port: 3002, + launch: open, store: fileStore("~/.mcp/prod-tokens.json"), storeKey: "production", authTimeout: 300000, @@ -576,9 +615,11 @@ const authProvider = createAuthProvider( While automatic refresh is pending full implementation, you can handle expired tokens: ```typescript +import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ + launch: open, store: fileStore(), scope: "offline_access", // Request refresh token }); @@ -701,8 +742,11 @@ describe("OAuth Flow Integration", () => { ::: details Port Already in Use ```typescript +import open from "open"; + // Use a different port const authProvider = browserAuth({ + launch: open, port: 8080, // Try alternative port }); ``` @@ -712,8 +756,11 @@ const authProvider = browserAuth({ ::: details Tokens Not Persisting ```typescript +import open from "open"; + // Ensure you're using file store, not in-memory const authProvider = browserAuth({ + launch: open, store: fileStore(), // ✅ Persistent // store: inMemoryStore() // ❌ Lost on restart }); @@ -725,8 +772,11 @@ const authProvider = browserAuth({ Some servers may not support Dynamic Client Registration: ```typescript +import open from "open"; + // Fallback to pre-registered credentials const authProvider = browserAuth({ + launch: open, clientId: "your-client-id", clientSecret: "your-client-secret", }); diff --git a/docs/api/storage-providers.md b/docs/api/storage-providers.md index 564b9a2..1db5bcd 100644 --- a/docs/api/storage-providers.md +++ b/docs/api/storage-providers.md @@ -81,9 +81,11 @@ function inMemoryStore(): TokenStore; #### Usage ```typescript +import open from "open"; import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ + launch: open, store: inMemoryStore(), }); ``` @@ -125,20 +127,24 @@ function fileStore(filepath?: string): TokenStore; #### Usage ```typescript +import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; // Use default location (~/.mcp/tokens.json) const authProvider = browserAuth({ + launch: open, store: fileStore(), }); // Use custom location const customAuth = browserAuth({ + launch: open, store: fileStore("/path/to/my-tokens.json"), }); // Environment-specific storage const envAuth = browserAuth({ + launch: open, store: fileStore(`~/.myapp/${process.env.NODE_ENV}-tokens.json`), }); ``` @@ -183,7 +189,10 @@ Storage keys namespace tokens for different applications or environments: ### Single Application ```typescript +import open from "open"; + const authProvider = browserAuth({ + launch: open, store: fileStore(), storeKey: "my-app", // Default: "mcp-tokens" }); @@ -192,14 +201,18 @@ const authProvider = browserAuth({ ### Multiple Applications ```typescript +import open from "open"; + // App 1 const app1Auth = browserAuth({ + launch: open, store: fileStore(), storeKey: "app1-tokens", }); // App 2 (same file, different key) const app2Auth = browserAuth({ + launch: open, store: fileStore(), storeKey: "app2-tokens", }); @@ -208,7 +221,10 @@ const app2Auth = browserAuth({ ### Environment Separation ```typescript +import open from "open"; + const authProvider = browserAuth({ + launch: open, store: fileStore(), storeKey: `${process.env.APP_NAME}-${process.env.NODE_ENV}`, }); @@ -266,8 +282,10 @@ class RedisTokenStore implements TokenStore { } // Usage +import open from "open"; const redis = new Redis(); const authProvider = browserAuth({ + launch: open, store: new RedisTokenStore(redis), }); ``` @@ -340,7 +358,9 @@ class SQLiteTokenStore implements TokenStore { } // Usage +import open from "open"; const authProvider = browserAuth({ + launch: open, store: new SQLiteTokenStore("./oauth-tokens.db"), }); ``` @@ -444,11 +464,13 @@ class MongoOAuthStore implements OAuthStore { } // Usage +import open from "open"; const client = new MongoClient("mongodb://localhost:27017"); await client.connect(); const db = client.db("oauth"); const authProvider = browserAuth({ + launch: open, store: new MongoOAuthStore(db), }); ``` @@ -538,6 +560,7 @@ class EncryptedTokenStore implements TokenStore { } // Usage +import open from "open"; const encryptedStore = new EncryptedTokenStore( fileStore(), process.env.ENCRYPTION_PASSWORD!, @@ -545,6 +568,7 @@ const encryptedStore = new EncryptedTokenStore( await encryptedStore.init(process.env.ENCRYPTION_PASSWORD!); const authProvider = browserAuth({ + launch: open, store: encryptedStore, }); ``` @@ -606,7 +630,9 @@ class TenantAwareStore implements TokenStore { } // Usage +import open from "open"; const authProvider = browserAuth({ + launch: open, store: new TenantAwareStore(), storeKey: `${tenantId}:${appName}`, }); @@ -667,7 +693,9 @@ class CachedTokenStore implements TokenStore { } // Usage +import open from "open"; const authProvider = browserAuth({ + launch: open, store: new CachedTokenStore(fileStore(), 600), // 10 min cache }); ``` @@ -728,6 +756,7 @@ describe("OAuth Flow", () => { it("should use stored tokens", async () => { const authProvider = browserAuth({ + launch: () => {}, // Noop for tests store: mockStore, storeKey: "test-key", }); @@ -863,7 +892,9 @@ class ResilientTokenStore implements TokenStore { } // Usage: Redis with file fallback +import open from "open"; const authProvider = browserAuth({ + launch: open, store: new ResilientTokenStore(new RedisTokenStore(redis), fileStore()), }); ``` diff --git a/docs/examples/notion.md b/docs/examples/notion.md index 3755928..455c8d4 100644 --- a/docs/examples/notion.md +++ b/docs/examples/notion.md @@ -345,7 +345,10 @@ const authProvider = browserAuth({ Debug OAuth flow with detailed logging: ```typescript +import open from "open"; + const authProvider = browserAuth({ + launch: open, onRequest(req) { const url = new URL(req.url); const timestamp = new Date().toISOString(); @@ -367,8 +370,11 @@ const authProvider = browserAuth({ Support multiple Notion accounts: ```typescript +import open from "open"; + function createNotionAuth(accountName: string) { return browserAuth({ + launch: open, store: fileStore(`~/.mcp/notion-${accountName}.json`), storeKey: `notion-${accountName}`, port: 3000 + Math.floor(Math.random() * 1000), // Random port @@ -417,9 +423,11 @@ const authProvider = browserAuth({ Ensure you're using file storage, not in-memory: ```typescript +import open from "open"; import { fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ + launch: open, store: fileStore(), // ✅ Persistent storage // store: inMemoryStore() // ❌ Lost on restart }); @@ -449,8 +457,11 @@ await authProvider.invalidateCredentials("client"); 1. **Use Ephemeral Storage for Sensitive Data** ```typescript + import open from "open"; + // Tokens are never written to disk const authProvider = browserAuth({ + launch: open, store: inMemoryStore(), }); ``` @@ -486,6 +497,7 @@ tokens.json For a production-ready implementation with full error handling: ```typescript +import open from "open"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { browserAuth, fileStore } from "oauth-callback/mcp"; @@ -496,6 +508,7 @@ class NotionMCPClient { constructor() { this.authProvider = browserAuth({ + launch: open, port: 3000, scope: "read write", store: fileStore("~/.mcp/notion.json"), diff --git a/docs/getting-started.md b/docs/getting-started.md index 86322d8..d33028a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -255,24 +255,29 @@ Choose between ephemeral and persistent token storage: ::: code-group ```typescript [Ephemeral (Memory)] +import open from "open"; import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; // Tokens are lost when the process exits const authProvider = browserAuth({ + launch: open, store: inMemoryStore(), }); ``` ```typescript [Persistent (File)] +import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; // Tokens persist across sessions const authProvider = browserAuth({ + launch: open, store: fileStore(), // Saves to ~/.mcp/tokens.json }); // Or specify custom location const customAuth = browserAuth({ + launch: open, store: fileStore("/path/to/tokens.json"), }); ``` @@ -284,10 +289,13 @@ const customAuth = browserAuth({ If you have pre-registered OAuth credentials: ```typescript +import open from "open"; + const authProvider = browserAuth({ clientId: "your-client-id", clientSecret: "your-client-secret", scope: "read write", + launch: open, store: fileStore(), storeKey: "my-app", // Namespace for multiple apps }); @@ -542,7 +550,10 @@ On first run, your OS firewall may show a warning. Allow connections for: For MCP apps with token refresh issues: ```typescript +import open from "open"; + const authProvider = browserAuth({ + launch: open, store: fileStore(), // Use persistent storage authTimeout: 300000, // Increase timeout to 5 minutes }); diff --git a/examples/notion.ts b/examples/notion.ts index a5992d7..546080a 100644 --- a/examples/notion.ts +++ b/examples/notion.ts @@ -14,6 +14,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import open from "open"; import { browserAuth, inMemoryStore } from "../src/mcp"; async function main() { @@ -30,6 +31,7 @@ async function main() { port: 3000, scope: "read write", store: inMemoryStore(), // Ephemeral storage - tokens lost on restart + launch: open, // Opens browser for OAuth consent onRequest(req) { const url = new URL(req.url); console.log(`📨 Received ${req.method} request to ${url.pathname}`); diff --git a/package.json b/package.json index 568aae2..e9b6da6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oauth-callback", - "version": "2.0.0", + "version": "2.1.0", "description": "Lightweight OAuth 2.0 callback handler for Node.js, Deno, and Bun with built-in browser flow and MCP SDK integration", "keywords": [ "oauth", diff --git a/src/auth/browser-auth.ts b/src/auth/browser-auth.ts index 3926859..8cf30a1 100644 --- a/src/auth/browser-auth.ts +++ b/src/auth/browser-auth.ts @@ -260,17 +260,26 @@ class BrowserOAuthProvider implements OAuthClientProvider { } private async _doAuthorization(authorizationUrl: URL): Promise { - const result = await getAuthCode({ - authorizationUrl: authorizationUrl.href, + // Use managed mode (with launch) or headless mode based on _launch presence + const baseOptions = { port: this._port, hostname: this._hostname, callbackPath: this._callbackPath, timeout: this._authTimeout, - launch: this._launch, successHtml: this._successHtml, errorHtml: this._errorHtml, onRequest: this._onRequest, - }); + }; + + const result = await getAuthCode( + this._launch + ? { + ...baseOptions, + authorizationUrl: authorizationUrl.href, + launch: this._launch, + } + : baseOptions, + ); /** Cache auth code for SDK's separate token exchange call. */ this._pendingAuthCode = result.code; diff --git a/src/index.ts b/src/index.ts index aa6bb2e..c7acf56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,11 +22,51 @@ export { fileStore } from "./storage/file"; import * as mcp from "./mcp"; export { mcp }; +/** + * Builds the redirect URI for OAuth configuration. + * Use this to construct the redirect_uri parameter for your authorization URL. + * + * @example + * ```typescript + * const redirectUri = getRedirectUrl({ port: 3000 }); + * // => "http://localhost:3000/callback" + * + * const authUrl = `https://oauth.example.com/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`; + * console.log('Open:', authUrl); + * await getAuthCode({ port: 3000 }); + * ``` + */ +export function getRedirectUrl( + options: { + port?: number; + hostname?: string; + callbackPath?: string; + } = {}, +): string { + const { + port = 3000, + hostname = "localhost", + callbackPath = "/callback", + } = options; + return `http://${hostname}:${port}${callbackPath}`; +} + +async function authorizationUrlToOptions( + input: string, +): Promise { + const open = await import("open"); + return { authorizationUrl: input, launch: open.default }; +} + /** * Captures OAuth authorization code via localhost callback. * Starts a temporary server, optionally launches auth URL, waits for redirect. * - * @param input - Auth URL string or GetAuthCodeOptions with config + * Two modes: + * - **Managed**: Pass both `authorizationUrl` and `launch` — library opens browser + * - **Headless**: Pass neither — caller handles URL display (CI/SSH/custom UI) + * + * @param input - Auth URL string (auto-launches browser) or GetAuthCodeOptions * @returns Promise with code and params * @throws {OAuthError} Provider errors (access_denied, invalid_scope) * @throws {Error} Timeout, network failures, port conflicts @@ -35,26 +75,25 @@ export { mcp }; * ```typescript * import open from "open"; * - * // With browser launch + * // Managed mode: library launches browser * const result = await getAuthCode({ * authorizationUrl: 'https://oauth.example.com/authorize?...', * launch: open, * }); * - * // Headless (print URL, let user open manually) - * const url = 'https://oauth.example.com/authorize?...'; - * console.log('Open:', url); - * const result = await getAuthCode({ authorizationUrl: url }); + * // Headless mode: caller handles URL display + * const authUrl = 'https://oauth.example.com/authorize?...'; + * console.log('Open this URL:', authUrl); + * const result = await getAuthCode({ port: 3000, timeout: 60000 }); * ``` */ export async function getAuthCode( input: GetAuthCodeOptions | string, ): Promise { const options: GetAuthCodeOptions = - typeof input === "string" ? { authorizationUrl: input } : input; + typeof input === "string" ? await authorizationUrlToOptions(input) : input; const { - authorizationUrl, port = 3000, hostname = "localhost", timeout = 30000, @@ -63,7 +102,6 @@ export async function getAuthCode( errorHtml, signal, onRequest, - launch, } = options; const server = createCallbackServer(); @@ -78,8 +116,14 @@ export async function getAuthCode( onRequest, }); - // Best-effort launch: fire-and-forget, swallow errors - if (launch) void Promise.resolve(launch(authorizationUrl)).catch(() => {}); + // Best-effort launch: fire-and-forget, swallow errors (managed mode only) + if ( + "authorizationUrl" in options && + typeof (options as any).launch === "function" + ) { + const { authorizationUrl, launch } = options as any; + void Promise.resolve(launch(authorizationUrl)).catch(() => {}); + } const result = await server.waitForCallback(callbackPath, timeout); diff --git a/src/types.ts b/src/types.ts index de81060..12810d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,13 +4,7 @@ /** * Configuration options for OAuth authorization code flow */ -export interface GetAuthCodeOptions { - /** - * OAuth authorization URL that the user will be redirected to. - * Should include all necessary query parameters like client_id, redirect_uri, etc. - */ - authorizationUrl: string; - +interface GetAuthCodeOptionsBase { /** * Port for the local callback server. Make sure this matches the * redirect_uri registered with your OAuth provider. @@ -39,22 +33,6 @@ export interface GetAuthCodeOptions { */ timeout?: number; - /** - * Optional callback to launch the authorization URL. - * Called after the callback server starts, best-effort (errors are swallowed). - * If omitted, the library does nothing — caller is responsible for opening the URL. - * - * Returns `unknown` (not `void`) to accept any launcher without casting—e.g., - * the `open` package returns `Promise`. Return value is ignored. - * - * @example - * ```typescript - * import open from "open"; - * await getAuthCode({ authorizationUrl: url, launch: open }); - * ``` - */ - launch?: (url: string) => unknown; - /** * Custom HTML content to display when authorization is successful. * If not provided, a default success page with auto-close functionality is used. @@ -81,3 +59,43 @@ export interface GetAuthCodeOptions { */ onRequest?: (req: Request) => void; } + +/** + * Headless mode: caller handles URL display, library just runs callback server. + * Use when you want to print the URL yourself or in CI/SSH environments. + */ +type GetAuthCodeOptionsHeadless = GetAuthCodeOptionsBase & { + authorizationUrl?: never; + launch?: never; +}; + +/** + * Managed mode: library launches the authorization URL automatically. + * Both authorizationUrl and launch are required together. + */ +type GetAuthCodeOptionsManaged = GetAuthCodeOptionsBase & { + /** + * OAuth authorization URL that the user will be redirected to. + * Should include all necessary query parameters like client_id, redirect_uri, etc. + */ + authorizationUrl: string; + + /** + * Callback to launch the authorization URL. + * Called after the callback server starts, best-effort (errors are swallowed). + * + * Returns `unknown` (not `void`) to accept any launcher without casting—e.g., + * the `open` package returns `Promise`. Return value is ignored. + * + * @example + * ```typescript + * import open from "open"; + * await getAuthCode({ authorizationUrl: url, launch: open }); + * ``` + */ + launch: (url: string) => unknown; +}; + +export type GetAuthCodeOptions = + | GetAuthCodeOptionsHeadless + | GetAuthCodeOptionsManaged; diff --git a/test/e2e.test.ts b/test/e2e.test.ts index 1fc8074..debecad 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -185,13 +185,10 @@ test("successful authorization with string input", async () => { }); test("timeout throws TimeoutError", async () => { - // Use a URL that doesn't exist to ensure no callback is made - const authUrl = "http://localhost:9999/nonexistent"; - + // Headless mode: no authorizationUrl, no launch — just wait for callback let errorThrown = false; try { await getAuthCode({ - authorizationUrl: authUrl, port: 3004, timeout: 100, }); @@ -206,16 +203,14 @@ test("timeout throws TimeoutError", async () => { test("abort signal handling", async () => { const controller = new AbortController(); - // Use a non-existent URL to prevent immediate callback - const authUrl = "http://localhost:9999/nonexistent"; // Abort after 50ms setTimeout(() => controller.abort(), 50); let errorThrown = false; try { + // Headless mode: no authorizationUrl, no launch — just wait for callback await getAuthCode({ - authorizationUrl: authUrl, port: 3005, signal: controller.signal, }); @@ -278,17 +273,14 @@ test("onRequest callback is called", async () => { }); test("server cleanup on early stop", async () => { - // Use a non-existent URL to prevent immediate callback - const authUrl = "http://localhost:9999/nonexistent"; - // This should not throw even if we never receive a callback let errorThrown = false; try { const controller = new AbortController(); setTimeout(() => controller.abort(), 10); + // Headless mode: no authorizationUrl, no launch — just wait for callback await getAuthCode({ - authorizationUrl: authUrl, port: 3008, signal: controller.signal, });