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}@auth.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
