"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthService = void 0;
const logger_1 = require("../utils/logger");
const ConfigManager_1 = require("./ConfigManager");
const http_1 = require("http");
const accessTokenRefresh_1 = require("./accessTokenRefresh");
const log = (0, logger_1.createLogger)('AuthService');
const LOOPBACK_HOST = '127.0.0.1';
const LOOPBACK_PORT = 0;
const CALLBACK_COMPLETE_PATH = '/complete';
const OAUTH_CALLBACK_TIMEOUT_MS = 120_000;
const MAX_CALLBACK_BODY_BYTES = 64 * 1024;
// Server-side auth validation gates every MCP tool via `ensureAuthenticated()`.
// Without a timeout, a slow or cold-starting proxy makes the profile fetch hang
// indefinitely, which stalls the *entire* tool call (e.g. start_variants) rather
// than failing fast with an "auth could not be verified" error.
const PROFILE_FETCH_TIMEOUT_MS = 10_000;
/** Sends a small text response from the loopback OAuth callback server. */
const sendTextResponse = (response, statusCode, body) => {
    response.statusCode = statusCode;
    response.setHeader('Content-Type', 'text/html; charset=utf-8');
    response.end(body);
};
/** Reads a bounded request body from the local OAuth callback server. */
const readRequestBody = (request) => {
    return new Promise((resolve, reject) => {
        let body = '';
        request.on('data', (chunk) => {
            body += chunk.toString('utf8');
            if (Buffer.byteLength(body) > MAX_CALLBACK_BODY_BYTES) {
                reject(new Error('OAuth callback payload too large'));
                request.destroy();
            }
        });
        request.on('end', () => resolve(body));
        request.on('error', reject);
    });
};
/** Serves the browser page that forwards OAuth hash tokens to localhost. */
const getLoopbackCallbackHtml = () => {
    return `<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Rivet authentication</title>
    <style>
      body {
        margin: 0;
        min-height: 100vh;
        display: grid;
        place-items: center;
        font-family: system-ui, sans-serif;
        background: #0b0b0f;
        color: #fff;
      }
      main {
        max-width: 420px;
        padding: 32px;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Finishing Rivet sign in...</h1>
      <p>You can return to your editor after this page completes.</p>
    </main>
    <script>
      const params = new URLSearchParams(window.location.search);
      const hash = new URLSearchParams(window.location.hash.slice(1));
      if (window.location.hash) {
        window.history.replaceState(
          null,
          document.title,
          window.location.pathname + window.location.search
        );
      }
      fetch('/complete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          sessionId: params.get('session'),
          accessToken: hash.get('access_token'),
          refreshToken: hash.get('refresh_token')
        })
      }).then((response) => {
        document.querySelector('h1').textContent = response.ok
          ? 'Rivet sign in complete'
          : 'Rivet sign in failed';
      }).catch(() => {
        document.querySelector('h1').textContent = 'Rivet sign in failed';
      });
    </script>
  </body>
</html>`;
};
/** Creates a localhost OAuth callback endpoint for browserless MCP login. */
const createLoopbackOAuthCallback = async () => {
    let resolveTokens = () => { };
    let rejectTokens = () => { };
    let timeout = null;
    const clearCallbackTimeout = () => {
        if (timeout) {
            clearTimeout(timeout);
            timeout = null;
        }
    };
    const waitForTokens = new Promise((resolve, reject) => {
        resolveTokens = resolve;
        rejectTokens = reject;
        timeout = setTimeout(() => {
            timeout = null;
            reject(new Error('OAuth callback timed out'));
        }, OAUTH_CALLBACK_TIMEOUT_MS);
    });
    const server = (0, http_1.createServer)((request, response) => {
        const url = new URL(request.url ?? '/', `http://${LOOPBACK_HOST}`);
        if (request.method === 'GET') {
            sendTextResponse(response, 200, getLoopbackCallbackHtml());
            return;
        }
        if (request.method !== 'POST' ||
            url.pathname !== CALLBACK_COMPLETE_PATH) {
            sendTextResponse(response, 404, 'Not found');
            return;
        }
        void readRequestBody(request)
            .then((body) => {
            const payload = JSON.parse(body);
            if (!payload.sessionId || !payload.accessToken) {
                throw new Error('OAuth callback missing session or token');
            }
            clearCallbackTimeout();
            resolveTokens({
                sessionId: payload.sessionId,
                accessToken: payload.accessToken,
                refreshToken: payload.refreshToken,
            });
            sendTextResponse(response, 200, 'Authentication complete');
        })
            .catch((error) => {
            clearCallbackTimeout();
            rejectTokens(error instanceof Error ? error : new Error(String(error)));
            sendTextResponse(response, 400, 'Authentication failed');
        });
    });
    await new Promise((resolve) => {
        server.listen(LOOPBACK_PORT, LOOPBACK_HOST, () => resolve());
    });
    const address = server.address();
    if (!address || typeof address === 'string') {
        throw new Error('Unable to start OAuth callback server');
    }
    return {
        redirectOrigin: `http://${LOOPBACK_HOST}:${address.port}`,
        waitForTokens,
        close: () => new Promise((resolve) => {
            clearCallbackTimeout();
            server.close(() => resolve());
        }),
    };
};
/**
 * AuthService handles authentication with the Rivet proxy server
 */
class AuthService {
    proxyUrl;
    configManager = (0, ConfigManager_1.getConfigManager)();
    constructor(proxyUrl) {
        this.proxyUrl = proxyUrl || this.configManager.getProxyUrl();
        log.debug(`AuthService initialized with proxy URL: ${this.proxyUrl}`);
    }
    /**
     * Sign in with email and password via the proxy's email/signin endpoint.
     * Used in CI when RIVET_E2E_EMAIL / RIVET_E2E_PASSWORD (or RIVET_TEST_*) are set.
     * Uses this.proxyUrl (set via constructor).
     */
    async loginWithEmailPassword(email, password) {
        try {
            const response = await fetch(`${this.proxyUrl}/api/auth/email/signin`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ email: email.trim().toLowerCase(), password }),
            });
            if (!response.ok) {
                const text = await response.text();
                log.warn(`Email sign-in failed with status ${response.status}:`, text);
                return {
                    success: false,
                    error: `HTTP ${response.status}`,
                    message: text.substring(0, 200),
                };
            }
            const result = (await response.json());
            if (!result.success) {
                return {
                    success: false,
                    error: result.error ?? 'Email sign-in failed',
                    message: result.message,
                };
            }
            if (result.token && result.user?.email) {
                this.configManager.setAuth(result.token, result.user.email, result.refreshToken, result.user.id);
                log.info(`Signed in as ${result.user.email} (CI)`);
            }
            return result;
        }
        catch (error) {
            log.warn('Email sign-in error:', error);
            return {
                success: false,
                error: 'Network error',
                message: error instanceof Error ? error.message : 'Unknown error occurred',
            };
        }
    }
    /**
     * Start Google OAuth flow
     * Opens browser for user to authenticate, then polls for token.
     *
     * Accepts either a bare `redirectOrigin` string (legacy call sites) or an
     * options object carrying the login `source`/`editor`/`version` recorded by
     * the proxy for analytics + login_events.
     * redirectOrigin: e.g. http://localhost:4000 or http://localhost:4000/rivet — OAuth callback redirects there with ?session= (used when Rivet runs on a dynamic port).
     */
    async loginWithGoogle(optionsOrRedirectOrigin) {
        const options = typeof optionsOrRedirectOrigin === 'string'
            ? { redirectOrigin: optionsOrRedirectOrigin }
            : optionsOrRedirectOrigin ?? {};
        const requestOptions = { ...options };
        let loopbackCallback = null;
        try {
            if (requestOptions.source === 'mcp' && !requestOptions.redirectOrigin) {
                loopbackCallback = await createLoopbackOAuthCallback();
                requestOptions.redirectOrigin = loopbackCallback.redirectOrigin;
            }
            log.info(`Requesting OAuth URL from: ${this.proxyUrl}/api/auth/google/start`);
            const body = {};
            if (requestOptions.redirectOrigin) {
                body.redirectOrigin = requestOptions.redirectOrigin;
            }
            if (requestOptions.source) {
                body.source = requestOptions.source;
            }
            if (requestOptions.editor) {
                body.editor = requestOptions.editor;
            }
            if (requestOptions.version) {
                body.version = requestOptions.version;
            }
            // Request OAuth URL from proxy
            const response = await fetch(`${this.proxyUrl}/api/auth/google/start`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(body),
            });
            const result = await response.json();
            if (!response.ok || !result.success) {
                log.warn('Failed to start Google OAuth:', result.error, result.message);
                return {
                    success: false,
                    error: result.error || 'Failed to start authentication',
                    message: result.message,
                };
            }
            // Open browser for user to authenticate
            const open = (await Promise.resolve().then(() => __importStar(require('open')))).default;
            await open(result.authUrl);
            log.info('Opened browser for Google authentication');
            log.info('Waiting for authentication...');
            // Poll for completion
            const sessionId = result.sessionId;
            const pollSecret = result.pollSecret;
            const completionSecret = result.completionSecret;
            if (!sessionId || !pollSecret) {
                return {
                    success: false,
                    error: 'Invalid response from server',
                    message: 'Authentication response missing session or poll secret',
                };
            }
            if (loopbackCallback) {
                if (!completionSecret) {
                    return {
                        success: false,
                        error: 'Invalid response from server',
                        message: 'Authentication response missing completion secret',
                    };
                }
                const callbackTokens = await loopbackCallback.waitForTokens;
                if (callbackTokens.sessionId !== sessionId) {
                    return {
                        success: false,
                        error: 'Invalid OAuth callback',
                        message: 'OAuth callback session did not match login session',
                    };
                }
                const completeResponse = await fetch(`${this.proxyUrl}/api/auth/google/complete`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        ...callbackTokens,
                        completionSecret,
                    }),
                });
                const completeResult = await completeResponse.json();
                if (!completeResponse.ok || !completeResult.success) {
                    return {
                        success: false,
                        error: completeResult.error || 'Authentication failed',
                        message: completeResult.message,
                    };
                }
            }
            const tokenResult = await this.pollForToken(sessionId, pollSecret);
            if (tokenResult.success && tokenResult.token && tokenResult.user) {
                this.configManager.setAuth(tokenResult.token, tokenResult.user.email, tokenResult.refreshToken, tokenResult.user.id);
                log.info(`Logged in as ${tokenResult.user.email}`);
            }
            return tokenResult;
        }
        catch (error) {
            log.warn('Google login error:', error);
            return {
                success: false,
                error: 'Network error',
                message: error instanceof Error ? error.message : 'Unknown error occurred',
            };
        }
        finally {
            await loopbackCallback?.close();
        }
    }
    /**
     * Poll the proxy server for authentication token after OAuth
     */
    async pollForToken(sessionId, pollSecret, maxAttempts = 60) {
        for (let i = 0; i < maxAttempts; i++) {
            try {
                const response = await fetch(`${this.proxyUrl}/api/auth/google/token`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        session: sessionId,
                        pollSecret,
                    }),
                });
                const result = await response.json();
                if (result.success) {
                    return result;
                }
                if (result.error && result.error !== 'Pending') {
                    return result;
                }
                // Wait 2 seconds before next poll
                await new Promise((resolve) => setTimeout(resolve, 2000));
            }
            catch (error) {
                log.warn('Error polling for token:', error);
            }
        }
        return {
            success: false,
            error: 'Timeout',
            message: 'Authentication timed out after 2 minutes',
        };
    }
    /**
     * Logout (clear local token and invalidate on server)
     */
    async logout() {
        const token = this.configManager.getAuthToken();
        if (token) {
            try {
                await fetch(`${this.proxyUrl}/api/auth/logout`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        Authorization: `Bearer ${token}`,
                    },
                });
            }
            catch (error) {
                log.warn('Logout error:', error);
                // Continue with local logout even if server request fails
            }
        }
        this.configManager.clearAuth();
        log.info('Logged out successfully');
    }
    /**
     * Get user profile and spending info
     */
    async getProfile() {
        const token = this.configManager.getAuthToken();
        if (!token) {
            log.warn('No authentication token found');
            return null;
        }
        try {
            const response = await fetch(`${this.proxyUrl}/api/user/profile`, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    Authorization: `Bearer ${token}`,
                },
                signal: AbortSignal.timeout(PROFILE_FETCH_TIMEOUT_MS),
            });
            if (!response.ok) {
                const error = await response.json().catch(() => ({}));
                log.warn('Failed to fetch profile:', error);
                return null;
            }
            const result = await response.json();
            return result.success ? result : null;
        }
        catch (error) {
            log.warn('Profile fetch error:', error);
            return null;
        }
    }
    /**
     * Server-validates auth for per-tool MCP gating. A local token only counts as
     * authenticated after the proxy accepts it, and rejected tokens get one
     * refresh-and-retry attempt through `getValidatedProfile()`.
     */
    async ensureAuthenticated() {
        const profile = await this.getValidatedProfile();
        return profile !== null;
    }
    /**
     * Server-validated auth check: fetch the profile, and if the token is
     * rejected (401), refresh once and retry. Returns the profile when the user
     * is genuinely authenticated, otherwise null. Use this instead of
     * `isAuthenticated()` (which only checks for a token's presence) wherever a
     * stale token must be treated as logged-out.
     */
    async getValidatedProfile() {
        const token = this.configManager.getAuthToken();
        if (!token) {
            return null;
        }
        const fetchProfile = async (bearer) => {
            const response = await fetch(`${this.proxyUrl}/api/user/profile`, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    Authorization: `Bearer ${bearer}`,
                },
                signal: AbortSignal.timeout(PROFILE_FETCH_TIMEOUT_MS),
            });
            if (!response.ok) {
                return { status: response.status, profile: null };
            }
            const result = await response.json().catch(() => null);
            return {
                status: response.status,
                profile: result?.success ? result : null,
            };
        };
        try {
            const first = await fetchProfile(token);
            if (first.profile) {
                return first.profile;
            }
            if (first.status !== 401) {
                return null;
            }
            // Token rejected — try a single refresh, then retry.
            const refreshToken = this.configManager.getRefreshToken();
            if (!refreshToken) {
                return null;
            }
            const refreshed = await (0, accessTokenRefresh_1.refreshAccessTokenViaProxy)({
                refreshToken,
                proxyUrl: this.proxyUrl,
            });
            if (!refreshed) {
                return null;
            }
            this.configManager.setAuth(refreshed.token, this.configManager.getEmail() || '', refreshed.refreshToken);
            const retry = await fetchProfile(refreshed.token);
            return retry.profile;
        }
        catch (error) {
            log.warn('Validated profile fetch error:', error);
            return null;
        }
    }
    /**
     * Check if user is authenticated
     */
    isAuthenticated() {
        return this.configManager.isAuthenticated();
    }
    /**
     * Get current auth token
     */
    getAuthToken() {
        return this.configManager.getAuthToken();
    }
}
exports.AuthService = AuthService;
//# sourceMappingURL=AuthService.js.map