/**
 * OIDC Single Sign-On
 *
 * Supports enterprise SSO with Okta, Auth0, Azure AD, and generic OIDC providers.
 * Handles user authentication, role mapping, and SCIM provisioning.
 *
 * @module enterprise/auth/oidc
 */
import { createHash, randomBytes } from "crypto";
// ============================================================================
// Provider Configuration
// ============================================================================
/**
 * Well-known OIDC discovery URL suffix
 */
const OIDC_DISCOVERY_PATH = "/.well-known/openid-configuration";
/**
 * Default scopes
 */
const DEFAULT_SCOPES = ["openid", "profile", "email"];
/**
 * Provider-specific configurations
 */
const PROVIDER_CONFIGS = {
    okta: {
        scopes: [...DEFAULT_SCOPES, "groups"],
    },
    auth0: {
        scopes: [...DEFAULT_SCOPES],
    },
    "azure-ad": {
        scopes: [...DEFAULT_SCOPES, "offline_access"],
    },
    google: {
        scopes: [...DEFAULT_SCOPES],
    },
    custom: {},
};
// ============================================================================
// OIDC Client
// ============================================================================
/**
 * OIDC Client for enterprise authentication
 */
export class OIDCClient {
    config;
    discoveryCache = null;
    pendingStates = new Map();
    constructor(config) {
        this.config = {
            ...PROVIDER_CONFIGS[config.provider],
            ...config,
            scopes: config.scopes || PROVIDER_CONFIGS[config.provider].scopes || DEFAULT_SCOPES,
        };
    }
    /**
     * Discover OIDC endpoints from issuer
     */
    async discover() {
        if (this.discoveryCache) {
            return this.discoveryCache;
        }
        const discoveryUrl = new URL(OIDC_DISCOVERY_PATH, this.config.issuerUrl);
        const response = await fetch(discoveryUrl.toString());
        if (!response.ok) {
            throw new Error(`OIDC discovery failed: ${response.status} ${response.statusText}`);
        }
        this.discoveryCache = (await response.json());
        return this.discoveryCache;
    }
    /**
     * Get authorization URL
     */
    async getAuthorizationUrl() {
        const discovery = await this.discover();
        const authEndpoint = this.config.endpoints?.authorization ||
            discovery.authorization_endpoint;
        // Generate PKCE code verifier and challenge
        const codeVerifier = base64URLEncode(randomBytes(32));
        const codeChallenge = base64URLEncode(createHash("sha256").update(codeVerifier).digest());
        // Generate state and nonce
        const state = base64URLEncode(randomBytes(16));
        const nonce = base64URLEncode(randomBytes(16));
        // Build authorization URL
        const params = new URLSearchParams({
            response_type: "code",
            client_id: this.config.clientId,
            redirect_uri: this.config.redirectUri,
            scope: this.config.scopes.join(" "),
            state,
            nonce,
            code_challenge: codeChallenge,
            code_challenge_method: "S256",
        });
        const url = `${authEndpoint}?${params.toString()}`;
        // Store state
        const authState = {
            codeVerifier,
            state,
            nonce,
            redirectUri: this.config.redirectUri,
            expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
        };
        this.pendingStates.set(state, authState);
        return { url, state: authState };
    }
    /**
     * Exchange authorization code for tokens
     */
    async exchangeCode(code, state) {
        // Validate state
        const authState = this.pendingStates.get(state);
        if (!authState) {
            throw new Error("Invalid or expired authorization state");
        }
        if (Date.now() > authState.expiresAt) {
            this.pendingStates.delete(state);
            throw new Error("Authorization state expired");
        }
        const discovery = await this.discover();
        const tokenEndpoint = this.config.endpoints?.token || discovery.token_endpoint;
        // Exchange code for tokens
        const response = await fetch(tokenEndpoint, {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
            body: new URLSearchParams({
                grant_type: "authorization_code",
                code,
                client_id: this.config.clientId,
                client_secret: this.config.clientSecret,
                redirect_uri: authState.redirectUri,
                code_verifier: authState.codeVerifier,
            }),
        });
        if (!response.ok) {
            const error = await response.text();
            throw new Error(`Token exchange failed: ${error}`);
        }
        const tokenData = (await response.json());
        const tokens = {
            accessToken: tokenData.access_token,
            idToken: tokenData.id_token,
            refreshToken: tokenData.refresh_token,
            tokenType: tokenData.token_type,
            expiresIn: tokenData.expires_in,
            scope: tokenData.scope,
        };
        // Parse ID token and get user info
        const user = await this.getUserInfo(tokens, authState.nonce);
        // Cleanup state
        this.pendingStates.delete(state);
        return { tokens, user };
    }
    /**
     * Get user information from tokens
     */
    async getUserInfo(tokens, expectedNonce) {
        // Parse ID token claims
        const claims = parseJWT(tokens.idToken);
        // Validate nonce if provided
        if (expectedNonce && claims.nonce !== expectedNonce) {
            throw new Error("ID token nonce mismatch");
        }
        // Map role
        const role = this.mapRole(claims);
        return {
            id: claims.sub,
            email: claims.email,
            name: claims.name,
            givenName: claims.given_name,
            familyName: claims.family_name,
            picture: claims.picture,
            role,
            claims,
            provider: this.config.provider,
            expiresAt: new Date(Date.now() + tokens.expiresIn * 1000).toISOString(),
        };
    }
    /**
     * Refresh tokens
     */
    async refreshTokens(refreshToken) {
        const discovery = await this.discover();
        const tokenEndpoint = this.config.endpoints?.token || discovery.token_endpoint;
        const response = await fetch(tokenEndpoint, {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
            body: new URLSearchParams({
                grant_type: "refresh_token",
                refresh_token: refreshToken,
                client_id: this.config.clientId,
                client_secret: this.config.clientSecret,
            }),
        });
        if (!response.ok) {
            throw new Error("Token refresh failed");
        }
        const tokenData = (await response.json());
        return {
            accessToken: tokenData.access_token,
            idToken: tokenData.id_token,
            refreshToken: tokenData.refresh_token,
            tokenType: tokenData.token_type,
            expiresIn: tokenData.expires_in,
            scope: tokenData.scope,
        };
    }
    /**
     * Map OIDC claims to Vaspera role
     */
    mapRole(claims) {
        if (!this.config.roleMapping) {
            return "viewer"; // Default role
        }
        const { rolesClaim, mappings, defaultRole } = this.config.roleMapping;
        // Get roles/groups from claims
        const roleValues = claims[rolesClaim];
        if (!roleValues) {
            return defaultRole;
        }
        // Handle both string and array
        const roles = Array.isArray(roleValues) ? roleValues : [roleValues];
        // Find first matching mapping
        for (const role of roles) {
            const mappedRole = mappings[role];
            if (mappedRole) {
                return mappedRole;
            }
        }
        return defaultRole;
    }
    /**
     * Validate ID token
     */
    async validateIdToken(idToken) {
        const claims = parseJWT(idToken);
        // Check issuer
        if (claims.iss !== this.config.issuerUrl) {
            return false;
        }
        // Check audience
        if (claims.aud !== this.config.clientId) {
            return false;
        }
        // Check expiration
        const exp = claims.exp;
        if (Date.now() / 1000 > exp) {
            return false;
        }
        // Note: For production, would also verify signature using JWKS
        return true;
    }
}
/**
 * SCIM client for user provisioning
 */
export class SCIMClient {
    baseUrl;
    bearerToken;
    constructor(baseUrl, bearerToken) {
        this.baseUrl = baseUrl.replace(/\/$/, "");
        this.bearerToken = bearerToken;
    }
    /**
     * Get all users
     */
    async getUsers() {
        const response = await this.request("GET", "/Users");
        const data = (await response.json());
        return data.Resources || [];
    }
    /**
     * Get user by ID
     */
    async getUser(id) {
        try {
            const response = await this.request("GET", `/Users/${id}`);
            return (await response.json());
        }
        catch {
            return null;
        }
    }
    /**
     * Create user
     */
    async createUser(user) {
        const response = await this.request("POST", "/Users", user);
        return (await response.json());
    }
    /**
     * Update user
     */
    async updateUser(id, user) {
        const response = await this.request("PATCH", `/Users/${id}`, user);
        return (await response.json());
    }
    /**
     * Delete user
     */
    async deleteUser(id) {
        await this.request("DELETE", `/Users/${id}`);
    }
    /**
     * Get all groups
     */
    async getGroups() {
        const response = await this.request("GET", "/Groups");
        const data = (await response.json());
        return data.Resources || [];
    }
    /**
     * Make SCIM request
     */
    async request(method, path, body) {
        const response = await fetch(`${this.baseUrl}${path}`, {
            method,
            headers: {
                Authorization: `Bearer ${this.bearerToken}`,
                "Content-Type": "application/scim+json",
                Accept: "application/scim+json",
            },
            body: body ? JSON.stringify(body) : undefined,
        });
        if (!response.ok) {
            throw new Error(`SCIM request failed: ${response.status}`);
        }
        return response;
    }
}
// ============================================================================
// Helpers
// ============================================================================
/**
 * Base64 URL encode
 */
function base64URLEncode(buffer) {
    return buffer
        .toString("base64")
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=/g, "");
}
/**
 * Parse JWT without verification
 */
function parseJWT(token) {
    const parts = token.split(".");
    if (parts.length !== 3) {
        throw new Error("Invalid JWT format");
    }
    const payload = parts[1];
    const decoded = Buffer.from(payload, "base64url").toString("utf-8");
    return JSON.parse(decoded);
}
/**
 * Create OIDC client for a provider
 */
export function createOIDCClient(config) {
    return new OIDCClient(config);
}
/**
 * Create SCIM client
 */
export function createSCIMClient(baseUrl, bearerToken) {
    return new SCIMClient(baseUrl, bearerToken);
}
//# sourceMappingURL=oidc.js.map