@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.
529 lines (528 loc) • 16.1 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 jwt from "jsonwebtoken";
import jwksRsa from "jwks-rsa";
import { RateLimiterRedis } from "rate-limiter-flexible";
import Redis from "ioredis";
import BetterSqlite3 from "better-sqlite3";
import { logger } from "../../core/monitoring/logger.js";
import { metrics } from "../../core/monitoring/metrics.js";
import { getUserModel } from "../../models/user.model.js";
function getEnv(key, defaultValue) {
const value = process.env[key];
if (value === void 0) {
if (defaultValue !== void 0) return defaultValue;
throw new Error(`Environment variable ${key} is required`);
}
return value;
}
function getOptionalEnv(key) {
return process.env[key];
}
class AuthMiddleware {
constructor(config) {
this.config = config;
this.redis = new Redis(config.redisUrl);
const dbPath = config.dbPath || process.env["STACKMEMORY_DB"] || ".stackmemory/auth.db";
this.db = new BetterSqlite3(dbPath);
this.userModel = getUserModel(this.db);
this.jwksClient = jwksRsa({
jwksUri: `https://${config.auth0Domain}/.well-known/jwks.json`,
cache: true,
cacheMaxAge: 6e5,
// 10 minutes
rateLimit: true,
jwksRequestsPerMinute: 5
});
this.initializeRateLimiters();
this.setupTokenBlacklistSync();
}
jwksClient;
redis;
rateLimiters;
blacklistedTokens = /* @__PURE__ */ new Set();
userModel;
db;
mockUser;
mockUserInitializing = false;
initializeRateLimiters() {
this.rateLimiters = /* @__PURE__ */ new Map([
[
"free",
new RateLimiterRedis({
storeClient: this.redis,
keyPrefix: "rl:free",
points: 100,
// requests
duration: 900,
// per 15 minutes
blockDuration: 900
// block for 15 minutes
})
],
[
"pro",
new RateLimiterRedis({
storeClient: this.redis,
keyPrefix: "rl:pro",
points: 1e3,
duration: 900,
blockDuration: 300
})
],
[
"enterprise",
new RateLimiterRedis({
storeClient: this.redis,
keyPrefix: "rl:enterprise",
points: 1e4,
duration: 900,
blockDuration: 60
})
]
]);
this.rateLimiters.set(
"auth",
new RateLimiterRedis({
storeClient: this.redis,
keyPrefix: "rl:auth",
points: 10,
// Only 10 auth attempts
duration: 900,
blockDuration: 3600
// Block for 1 hour on excessive auth attempts
})
);
}
setupTokenBlacklistSync() {
const subscriber = new Redis(this.config.redisUrl);
subscriber.subscribe("token:revoked");
subscriber.on("message", (channel, token) => {
if (channel === "token:revoked") {
this.blacklistedTokens.add(token);
if (this.blacklistedTokens.size > 1e4) {
this.blacklistedTokens.clear();
}
}
});
}
async getSigningKey(kid) {
return new Promise((resolve, reject) => {
this.jwksClient.getSigningKey(kid, (err, key) => {
if (err) {
reject(err);
} else {
const signingKey = key?.getPublicKey();
if (!signingKey) {
reject(new Error("No signing key found"));
} else {
resolve(signingKey);
}
}
});
});
}
/**
* Main authentication middleware
*/
authenticate = async (req, res, next) => {
const startTime = Date.now();
try {
if (req.path === "/health" || req.path === "/metrics") {
return next();
}
if (this.config.bypassAuth && process.env["NODE_ENV"] === "development") {
req.user = this.getMockUser();
return next();
}
const token = this.extractToken(req);
const apiKey = this.extractApiKey(req);
if (!token && !apiKey) {
metrics.increment("auth.missing_credentials");
return res.status(401).json({
error: "Authentication required",
code: "MISSING_CREDENTIALS"
});
}
if (apiKey) {
const user2 = await this.userModel.validateApiKey(apiKey);
if (!user2) {
metrics.increment("auth.invalid_api_key");
return res.status(401).json({
error: "Invalid API key",
code: "INVALID_API_KEY"
});
}
req.user = {
id: user2.id,
sub: user2.sub,
email: user2.email,
name: user2.name,
picture: user2.avatar,
tier: user2.tier,
permissions: user2.permissions,
organizations: user2.organizations.map((org) => org.id),
metadata: { ...user2.metadata, authMethod: "api_key" }
};
metrics.increment("auth.api_key_success");
await metrics.timing("auth.api_key_duration", Date.now() - startTime);
return next();
}
if (token && this.blacklistedTokens.has(token)) {
metrics.increment("auth.blacklisted_token");
return res.status(401).json({
error: "Token has been revoked",
code: "TOKEN_REVOKED"
});
}
if (!token) {
return res.status(401).json({
error: "No token provided",
code: "NO_TOKEN"
});
}
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
metrics.increment("auth.invalid_token");
return res.status(401).json({
error: "Invalid token format",
code: "INVALID_TOKEN"
});
}
const signingKey = await this.getSigningKey(decoded.header.kid);
const verified = jwt.verify(token, signingKey, {
algorithms: ["RS256"],
audience: this.config.auth0Audience,
issuer: `https://${this.config.auth0Domain}/`
});
const user = await this.loadUser(verified.sub, verified);
if (!user) {
metrics.increment("auth.user_not_found");
return res.status(403).json({
error: "User not found",
code: "USER_NOT_FOUND"
});
}
if (user.metadata?.suspended) {
metrics.increment("auth.user_suspended");
return res.status(403).json({
error: "Account suspended",
code: "ACCOUNT_SUSPENDED"
});
}
const rateLimiter = this.rateLimiters.get(user.tier) || this.rateLimiters.get("free");
try {
const rateLimitRes = await rateLimiter.consume(user.id);
req.rateLimitInfo = rateLimitRes;
res.setHeader("X-RateLimit-Limit", rateLimiter.points.toString());
res.setHeader(
"X-RateLimit-Remaining",
rateLimitRes.remainingPoints.toString()
);
res.setHeader(
"X-RateLimit-Reset",
new Date(Date.now() + rateLimitRes.msBeforeNext).toISOString()
);
} catch (rateLimitError) {
metrics.increment("auth.rate_limited");
res.setHeader(
"Retry-After",
Math.round(rateLimitError.msBeforeNext / 1e3).toString()
);
return res.status(429).json({
error: "Too many requests",
code: "RATE_LIMITED",
retryAfter: rateLimitError.msBeforeNext
});
}
req.user = user;
metrics.increment("auth.success", { tier: user.tier });
metrics.timing("auth.duration", Date.now() - startTime);
logger.info("Authentication successful", {
userId: user.id,
tier: user.tier,
path: req.path
});
next();
} catch (error) {
metrics.increment("auth.error");
logger.error("Authentication error", error);
if (error.name === "TokenExpiredError") {
return res.status(401).json({
error: "Token expired",
code: "TOKEN_EXPIRED"
});
}
if (error.name === "JsonWebTokenError") {
return res.status(401).json({
error: "Invalid token",
code: "INVALID_TOKEN"
});
}
res.status(500).json({
error: "Authentication failed",
code: "AUTH_ERROR"
});
}
};
/**
* WebSocket authentication handler
*/
authenticateWebSocket = async (token) => {
try {
const decoded = jwt.decode(token, { complete: true });
if (!decoded || this.blacklistedTokens.has(token)) {
return null;
}
const signingKey = await this.getSigningKey(decoded.header.kid);
const verified = jwt.verify(token, signingKey, {
algorithms: ["RS256"],
audience: this.config.auth0Audience,
issuer: `https://${this.config.auth0Domain}/`
});
return await this.loadUser(verified.sub, verified);
} catch (error) {
logger.error(
"WebSocket authentication failed",
error instanceof Error ? error : void 0
);
return null;
}
};
/**
* Permission checking middleware
*/
requirePermission = (permission) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: "Authentication required",
code: "NOT_AUTHENTICATED"
});
}
if (!req.user.permissions.includes(permission)) {
metrics.increment("auth.permission_denied", { permission });
return res.status(403).json({
error: "Insufficient permissions",
code: "PERMISSION_DENIED",
required: permission
});
}
return next();
};
};
/**
* Organization access middleware
*/
requireOrganization = (req, res, next) => {
const orgId = req.params.orgId || req.query.orgId;
if (!req.user || !orgId) {
return res.status(401).json({
error: "Authentication required",
code: "NOT_AUTHENTICATED"
});
}
if (!req.user.organizations?.includes(orgId)) {
return res.status(403).json({
error: "Organization access denied",
code: "ORG_ACCESS_DENIED"
});
}
return next();
};
extractApiKey(req) {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith("Bearer sk-")) {
return authHeader.substring(7);
}
const apiKeyHeader = req.headers["x-api-key"];
if (apiKeyHeader?.startsWith("sk-")) {
return apiKeyHeader;
}
return null;
}
extractToken(req) {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith("Bearer ") && !authHeader.startsWith("Bearer sk-")) {
return authHeader.substring(7);
}
return req.cookies?.access_token || null;
}
async loadUser(sub, tokenPayload) {
const cached = await this.redis.get(`user:${sub}`);
if (cached) {
const cachedUser = JSON.parse(cached);
this.userModel.updateLastLogin(cachedUser.id).catch((err) => logger.error("Failed to update last login", err));
return cachedUser;
}
let dbUser = await this.userModel.findUserBySub(sub);
if (!dbUser && tokenPayload) {
dbUser = await this.userModel.createUser({
sub,
email: tokenPayload.email || `${sub}.local`,
name: tokenPayload.name,
avatar: tokenPayload.picture,
tier: this.determineTier(tokenPayload),
permissions: this.determinePermissions(tokenPayload),
organizations: this.extractOrganizations(tokenPayload),
metadata: {
auth0: tokenPayload,
signupSource: "auth0",
createdVia: "auth-middleware"
}
});
logger.info("Auto-created user from auth token", {
sub,
email: dbUser.email
});
}
if (!dbUser) {
return null;
}
await this.userModel.updateLastLogin(dbUser.id);
const user = {
id: dbUser.id,
sub: dbUser.sub,
email: dbUser.email,
name: dbUser.name,
picture: dbUser.avatar,
tier: dbUser.tier,
permissions: dbUser.permissions,
organizations: dbUser.organizations.map((org) => org.id),
metadata: dbUser.metadata
};
await this.redis.setex(`user:${sub}`, 300, JSON.stringify(user));
return user;
}
determineTier(tokenPayload) {
if (tokenPayload["https://stackmemory.ai/tier"]) {
return tokenPayload["https://stackmemory.ai/tier"];
}
if (tokenPayload.subscription?.plan) {
const plan = tokenPayload.subscription.plan.toLowerCase();
if (plan.includes("enterprise")) return "enterprise";
if (plan.includes("pro") || plan.includes("premium")) return "pro";
}
return "free";
}
determinePermissions(tokenPayload) {
const permissions = ["read", "write"];
if (tokenPayload["https://stackmemory.ai/permissions"]) {
return tokenPayload["https://stackmemory.ai/permissions"];
}
if (tokenPayload.permissions && Array.isArray(tokenPayload.permissions)) {
return tokenPayload.permissions;
}
if (tokenPayload.roles && Array.isArray(tokenPayload.roles)) {
if (tokenPayload.roles.includes("admin")) {
permissions.push("admin", "delete");
}
if (tokenPayload.roles.includes("moderator")) {
permissions.push("moderate");
}
}
return permissions;
}
extractOrganizations(tokenPayload) {
const orgs = [];
if (tokenPayload["https://stackmemory.ai/organizations"]) {
return tokenPayload["https://stackmemory.ai/organizations"];
}
if (tokenPayload.org_id) {
orgs.push({
id: tokenPayload.org_id,
name: tokenPayload.org_name || tokenPayload.org_id,
role: tokenPayload.org_role || "member"
});
}
return orgs;
}
async initializeMockUser() {
const mockSub = "dev-sub";
let dbUser = await this.userModel.findUserBySub(mockSub);
if (!dbUser) {
dbUser = await this.userModel.createUser({
sub: mockSub,
email: "dev@stackmemory.local",
name: "Development User",
tier: "enterprise",
permissions: ["read", "write", "admin", "delete"],
organizations: [
{
id: "dev-org",
name: "Development Organization",
role: "admin"
}
],
metadata: {
isDevelopmentUser: true,
createdAt: (/* @__PURE__ */ new Date()).toISOString()
}
});
logger.info("Created development mock user");
}
return {
id: dbUser.id,
sub: dbUser.sub,
email: dbUser.email,
name: dbUser.name,
picture: dbUser.avatar,
tier: dbUser.tier,
permissions: dbUser.permissions,
organizations: dbUser.organizations.map((org) => org.id),
metadata: dbUser.metadata
};
}
getMockUser() {
if (this.mockUser) {
return this.mockUser;
}
if (!this.mockUserInitializing) {
this.mockUserInitializing = true;
this.initializeMockUser().then((user) => {
this.mockUser = user;
this.mockUserInitializing = false;
logger.info("Mock user initialized and cached");
}).catch((err) => {
logger.error("Failed to initialize mock user", err);
this.mockUserInitializing = false;
});
}
return {
id: "temp-dev-user-id",
sub: "dev-sub",
email: "dev@stackmemory.local",
name: "Development User",
tier: "enterprise",
permissions: ["read", "write", "admin", "delete"],
organizations: ["dev-org"],
metadata: { temporary: true }
};
}
/**
* Revoke a token (add to blacklist)
*/
async revokeToken(token) {
this.blacklistedTokens.add(token);
await this.redis.publish("token:revoked", token);
const decoded = jwt.decode(token);
if (decoded?.exp) {
const ttl = decoded.exp - Math.floor(Date.now() / 1e3);
if (ttl > 0) {
await this.redis.setex(`blacklist:${token}`, ttl, "1");
}
}
}
/**
* Cleanup resources
*/
async close() {
await this.redis.quit();
}
}
export {
AuthMiddleware
};
//# sourceMappingURL=auth-middleware.js.map