/**
* Local HTTP callback server for OAuth flows
* Listens on port 19876 for OAuth redirects
*/
import type { Server } from "bun";
export interface CallbackData {
code: string;
state: string;
error?: string;
error_description?: string;
}
interface PendingCallback {
resolve: (data: CallbackData) => void;
reject: (error: Error) => void;
timeout: Timer;
}
/**
* HTML response for successful OAuth callback
*/
function successHtml(): string {
return `
Authorization Successful
✓
Authorization Successful!
You can close this window and return to Letta Code.
`;
}
/**
* HTML response for OAuth error
*/
function errorHtml(error: string, description?: string): string {
return `
Authorization Failed
✗
Authorization Failed
${description || error}
Error: ${error}
`;
}
export class OAuthCallbackServer {
private server: Server | null = null;
private pendingCallbacks: Map = new Map();
private static instance: OAuthCallbackServer | null = null;
static getInstance(): OAuthCallbackServer {
if (!OAuthCallbackServer.instance) {
OAuthCallbackServer.instance = new OAuthCallbackServer();
}
return OAuthCallbackServer.instance;
}
async ensureRunning(): Promise {
if (this.server) return;
const self = this;
this.server = Bun.serve({
port: 19876,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/callback") {
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const error = url.searchParams.get("error");
const errorDescription = url.searchParams.get("error_description");
// Handle error from OAuth provider
if (error) {
const callbackData: CallbackData = {
code: "",
state: state || "",
error,
error_description: errorDescription || undefined,
};
// Resolve pending callback with error
if (state) {
const pending = self.pendingCallbacks.get(state);
if (pending) {
clearTimeout(pending.timeout);
pending.resolve(callbackData);
self.pendingCallbacks.delete(state);
}
}
return new Response(
errorHtml(error, errorDescription || undefined),
{
headers: { "Content-Type": "text/html" },
},
);
}
// Validate required params
if (!code || !state) {
return new Response(
errorHtml("invalid_request", "Missing code or state parameter"),
{
status: 400,
headers: { "Content-Type": "text/html" },
},
);
}
const callbackData: CallbackData = {
code,
state,
};
// Resolve pending callback
const pending = self.pendingCallbacks.get(state);
if (pending) {
clearTimeout(pending.timeout);
pending.resolve(callbackData);
self.pendingCallbacks.delete(state);
}
return new Response(successHtml(), {
headers: { "Content-Type": "text/html" },
});
}
// Health check endpoint
if (url.pathname === "/health") {
return new Response("OK", { status: 200 });
}
return new Response("Not Found", { status: 404 });
},
});
}
/**
* Wait for OAuth callback with matching state
* @param state The state parameter to match
* @param timeout Timeout in milliseconds (default: 5 minutes)
*/
async waitForCallback(
state: string,
timeout: number = 300000,
): Promise {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.pendingCallbacks.delete(state);
reject(
new Error(`OAuth callback timeout after ${timeout / 1000} seconds`),
);
}, timeout);
this.pendingCallbacks.set(state, {
resolve,
reject,
timeout: timeoutId,
});
});
}
/**
* Cancel a pending callback
*/
cancelPending(state: string): void {
const pending = this.pendingCallbacks.get(state);
if (pending) {
clearTimeout(pending.timeout);
pending.reject(new Error("Callback cancelled"));
this.pendingCallbacks.delete(state);
}
}
/**
* Stop the callback server
*/
stop(): void {
// Reject all pending callbacks
for (const [state, pending] of this.pendingCallbacks) {
clearTimeout(pending.timeout);
pending.reject(new Error("Server stopped"));
this.pendingCallbacks.delete(state);
}
this.server?.stop();
this.server = null;
}
/**
* Check if server is running
*/
isRunning(): boolean {
return this.server !== null;
}
}
// Export singleton instance
export const oauthCallbackServer = OAuthCallbackServer.getInstance();