UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

455 lines (454 loc) 14.5 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import express from "express"; import { URL } from "url"; import { logger } from "../../core/monitoring/logger.js"; import { LinearAuthManager } from "./auth.js"; import chalk from "chalk"; import { IntegrationError, ErrorCode } from "../../core/errors/index.js"; function getEnv(key, defaultValue) { const value = process.env[key]; if (value === void 0) { if (defaultValue !== void 0) return defaultValue; throw new IntegrationError( `Environment variable ${key} is required`, ErrorCode.LINEAR_AUTH_FAILED ); } return value; } function getOptionalEnv(key) { return process.env[key]; } class LinearOAuthServer { app; server = null; authManager; config; pendingCodeVerifiers = /* @__PURE__ */ new Map(); authCompleteCallbacks = /* @__PURE__ */ new Map(); constructor(projectRoot, config) { this.app = express(); this.authManager = new LinearAuthManager(projectRoot); this.config = { port: config?.port || 3456, host: config?.host || "localhost", redirectPath: config?.redirectPath || "/auth/linear/callback", autoShutdown: config?.autoShutdown !== false, shutdownDelay: config?.shutdownDelay || 5e3 }; this.setupRoutes(); } setupRoutes() { this.app.get("/health", (req, res) => { res.json({ status: "healthy", service: "linear-oauth", timestamp: (/* @__PURE__ */ new Date()).toISOString() }); }); this.app.get(this.config.redirectPath, async (req, res) => { const { code, state, error, error_description } = req.query; if (error) { logger.error(`OAuth error: ${error} - ${error_description}`); res.send( this.generateErrorPage( "Authorization Failed", `${error}: ${error_description || "An error occurred during authorization"}` ) ); if (state && this.authCompleteCallbacks.has(state)) { this.authCompleteCallbacks.get(state)(false); this.authCompleteCallbacks.delete(state); } this.scheduleShutdown(); return; } if (!code) { res.send( this.generateErrorPage( "Missing Authorization Code", "No authorization code was provided in the callback" ) ); this.scheduleShutdown(); return; } try { const codeVerifier = state ? this.pendingCodeVerifiers.get(state) : process.env["_LINEAR_CODE_VERIFIER"]; if (!codeVerifier) { throw new IntegrationError( "Code verifier not found. Please restart the authorization process.", ErrorCode.LINEAR_AUTH_FAILED ); } logger.info("Exchanging authorization code for tokens..."); await this.authManager.exchangeCodeForToken( code, codeVerifier ); if (state) { this.pendingCodeVerifiers.delete(state); } delete process.env["_LINEAR_CODE_VERIFIER"]; const testSuccess = await this.testConnection(); if (testSuccess) { res.send(this.generateSuccessPage()); logger.info("Linear OAuth authentication completed successfully!"); } else { throw new IntegrationError( "Failed to verify Linear connection", ErrorCode.LINEAR_AUTH_FAILED ); } if (state && this.authCompleteCallbacks.has(state)) { this.authCompleteCallbacks.get(state)(true); this.authCompleteCallbacks.delete(state); } this.scheduleShutdown(); } catch (error2) { logger.error("Failed to complete OAuth flow:", error2); res.send( this.generateErrorPage( "Authentication Failed", error2.message ) ); if (state && this.authCompleteCallbacks.has(state)) { this.authCompleteCallbacks.get(state)(false); this.authCompleteCallbacks.delete(state); } this.scheduleShutdown(); } }); this.app.get("/auth/linear/start", (req, res) => { try { const config = this.authManager.loadConfig(); if (!config) { res.status(400).send( this.generateErrorPage( "Configuration Missing", "Linear OAuth configuration not found. Please configure your client ID and secret." ) ); return; } const state = this.generateState(); const { url, codeVerifier } = this.authManager.generateAuthUrl(state); this.pendingCodeVerifiers.set(state, codeVerifier); res.redirect(url); } catch (error) { logger.error("Failed to start OAuth flow:", error); res.status(500).send( this.generateErrorPage( "OAuth Start Failed", error.message ) ); } }); this.app.use((req, res) => { res.status(404).json({ error: "Not found" }); }); } generateState() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } generateSuccessPage() { return ` <!DOCTYPE html> <html> <head> <title>Linear Authorization Successful</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .container { background: white; padding: 3rem; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); text-align: center; max-width: 400px; } h1 { color: #2d3748; margin-bottom: 1rem; } .success-icon { font-size: 4rem; margin-bottom: 1rem; } p { color: #4a5568; line-height: 1.6; margin: 1rem 0; } .close-note { color: #718096; font-size: 0.875rem; margin-top: 2rem; } code { background: #f7fafc; padding: 0.25rem 0.5rem; border-radius: 4px; font-family: 'Courier New', monospace; } </style> </head> <body> <div class="container"> <div class="success-icon">\u2705</div> <h1>Authorization Successful!</h1> <p>Your Linear account has been successfully connected to StackMemory.</p> <p>You can now use Linear integration features:</p> <p><code>stackmemory linear sync</code></p> <p><code>stackmemory linear create</code></p> <p class="close-note">You can safely close this window and return to your terminal.</p> </div> </body> </html> `; } generateErrorPage(title, message) { return ` <!DOCTYPE html> <html> <head> <title>${title}</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); } .container { background: white; padding: 3rem; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); text-align: center; max-width: 400px; } h1 { color: #e53e3e; margin-bottom: 1rem; } .error-icon { font-size: 4rem; margin-bottom: 1rem; } p { color: #4a5568; line-height: 1.6; margin: 1rem 0; } .error-message { background: #fff5f5; border: 1px solid #fed7d7; color: #742a2a; padding: 1rem; border-radius: 6px; margin-top: 1rem; font-size: 0.875rem; } .retry-note { color: #718096; font-size: 0.875rem; margin-top: 2rem; } code { background: #f7fafc; padding: 0.25rem 0.5rem; border-radius: 4px; font-family: 'Courier New', monospace; } </style> </head> <body> <div class="container"> <div class="error-icon">\u274C</div> <h1>${title}</h1> <p>Unable to complete Linear authorization.</p> <div class="error-message">${message}</div> <p class="retry-note"> Please try again with:<br> <code>stackmemory linear auth</code> </p> </div> </body> </html> `; } async testConnection() { try { const token = await this.authManager.getValidToken(); const response = await fetch("https://api.linear.app/graphql", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ query: "query { viewer { id name email } }" }) }); if (response.ok) { const result = await response.json(); if (result.data?.viewer) { logger.info( `Connected to Linear as: ${result.data.viewer.name} (${result.data.viewer.email})` ); return true; } } return false; } catch (error) { logger.error("Linear connection test failed:", error); return false; } } scheduleShutdown() { if (this.config.autoShutdown && this.server) { setTimeout(() => { logger.info("Auto-shutting down OAuth server..."); this.stop(); }, this.config.shutdownDelay); } } async start() { return new Promise((resolve, reject) => { try { const config = this.authManager.loadConfig(); if (!config) { const setupUrl = `http://${this.config.host}:${this.config.port}/auth/linear/start`; this.server = this.app.listen( this.config.port, this.config.host, () => { console.log( chalk.green("\u2713") + chalk.bold(" Linear OAuth Server Started") ); console.log(chalk.cyan(" Authorization URL: ") + setupUrl); console.log( chalk.cyan(" Callback URL: ") + `http://${this.config.host}:${this.config.port}${this.config.redirectPath}` ); console.log(""); console.log(chalk.yellow(" \u26A0 Configuration Required:")); console.log( " 1. Create a Linear OAuth app at: https://linear.app/settings/api" ); console.log( ` 2. Set redirect URI to: http://${this.config.host}:${this.config.port}${this.config.redirectPath}` ); console.log(" 3. Set environment variables:"); console.log(' export LINEAR_CLIENT_ID="your_client_id"'); console.log( ' export LINEAR_CLIENT_SECRET="your_client_secret"' ); console.log(" 4. Restart the auth process"); resolve({ url: setupUrl }); } ); return; } const state = this.generateState(); const { url, codeVerifier } = this.authManager.generateAuthUrl(state); this.pendingCodeVerifiers.set(state, codeVerifier); this.server = this.app.listen( this.config.port, this.config.host, () => { console.log( chalk.green("\u2713") + chalk.bold(" Linear OAuth Server Started") ); console.log(chalk.cyan(" Open this URL in your browser:")); console.log(" " + chalk.underline(url)); console.log(""); console.log( chalk.gray( " The server will automatically shut down after authorization completes." ) ); resolve({ url, codeVerifier }); } ); this.authCompleteCallbacks.set(state, (success) => { if (success) { console.log( chalk.green("\n\u2713 Linear authorization completed successfully!") ); } else { console.log(chalk.red("\n\u2717 Linear authorization failed")); } }); } catch (error) { reject(error); } }); } async stop() { return new Promise((resolve) => { if (this.server) { this.server.close(() => { logger.info("OAuth server stopped"); this.server = null; resolve(); }); } else { resolve(); } }); } async waitForAuth(state, timeout = 3e5) { return new Promise((resolve) => { const timeoutId = setTimeout(() => { this.authCompleteCallbacks.delete(state); resolve(false); }, timeout); this.authCompleteCallbacks.set(state, (success) => { clearTimeout(timeoutId); resolve(success); }); }); } } if (process.argv[1] === new URL(import.meta.url).pathname) { const projectRoot = process.cwd(); const server = new LinearOAuthServer(projectRoot, { autoShutdown: true, shutdownDelay: 5e3 }); server.start().then(({ url }) => { if (url) { console.log(chalk.cyan("\nWaiting for authorization...")); console.log(chalk.gray("Press Ctrl+C to cancel\n")); } }).catch((error) => { console.error(chalk.red("Failed to start OAuth server:"), error); process.exit(1); }); process.on("SIGINT", async () => { console.log(chalk.yellow("\n\nShutting down OAuth server...")); await server.stop(); process.exit(0); }); } export { LinearOAuthServer }; //# sourceMappingURL=oauth-server.js.map