@stackmemoryai/stackmemory
Version:
Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a
454 lines (453 loc) • 14.5 kB
JavaScript
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
};