/**
 * Service Authentication Middleware
 *
 * Fastify preHandler middleware for service-to-service authentication.
 * Validates HMAC signatures in request headers and optionally issues JWT tokens.
 *
 * @packageDocumentation
 */
import { createLogger } from '../../common/logger.js';
import { UnauthorizedError, ForbiddenError } from '../../common/errors.js';
import { getServiceAccountManager, ServiceAccountStatus, ServiceAccountRevokedError, ServiceAccountSuspendedError, } from './service-account.js';
import { getServiceTokenService, SERVICE_AUTH_HEADERS, } from './service-token.js';
const logger = createLogger({ component: 'service-auth-middleware' });
// =============================================================================
// ERRORS
// =============================================================================
/**
 * Service authentication error
 */
export class ServiceAuthError extends UnauthorizedError {
    code = 'SERVICE_AUTH_ERROR';
    constructor(message, details) {
        super(message, details);
        this.name = 'ServiceAuthError';
    }
}
/**
 * Missing authentication headers error
 */
export class MissingAuthHeadersError extends ServiceAuthError {
    code = 'MISSING_AUTH_HEADERS';
    constructor() {
        super('Missing required service authentication headers');
        this.name = 'MissingAuthHeadersError';
    }
}
/**
 * Invalid signature error
 */
export class InvalidServiceSignatureError extends ServiceAuthError {
    code = 'INVALID_SERVICE_SIGNATURE';
    constructor(reason) {
        super(reason ? `Invalid service signature: ${reason}` : 'Invalid service signature');
        this.name = 'InvalidServiceSignatureError';
    }
}
/**
 * IP not allowed error
 */
export class IpNotAllowedError extends ForbiddenError {
    code = 'IP_NOT_ALLOWED';
    constructor(clientId, ip) {
        super(`IP address not allowed for service: ${ip}`, { clientId, ip });
        this.name = 'IpNotAllowedError';
    }
}
/**
 * Insufficient permissions error
 */
export class InsufficientPermissionsError extends ForbiddenError {
    code = 'INSUFFICIENT_PERMISSIONS';
    constructor(required, actual) {
        super('Insufficient permissions for this operation', { required, actual });
        this.name = 'InsufficientPermissionsError';
    }
}
// =============================================================================
// MIDDLEWARE FACTORY
// =============================================================================
/**
 * Create service authentication middleware
 */
export function createServiceAuthMiddleware(options = {}) {
    const { accountManager = getServiceAccountManager(), allowTokenAuth = true, issueToken = false, skipPaths = [], skipFn, validateIpWhitelist = true, requiredPermissions = [], anyPermissions = [], onError, } = options;
    // Lazy-load token service to allow deferred initialization
    const getTokenSvc = () => {
        if (options.tokenService) {
            return options.tokenService;
        }
        try {
            return getServiceTokenService();
        }
        catch {
            return null;
        }
    };
    return async function serviceAuthMiddleware(request, reply) {
        // Check if path should be skipped
        if (skipPaths.some((path) => request.url.startsWith(path))) {
            return;
        }
        // Check custom skip function
        if (skipFn && skipFn(request)) {
            return;
        }
        const authRequest = request;
        try {
            let authContext = null;
            // Try token-based authentication first (if enabled)
            if (allowTokenAuth) {
                const tokenService = getTokenSvc();
                if (tokenService) {
                    authContext = await tryTokenAuth(request, tokenService, accountManager);
                }
            }
            // Fall back to signature-based authentication
            if (!authContext) {
                const tokenService = getTokenSvc();
                authContext = await trySignatureAuth(request, accountManager, tokenService, issueToken);
            }
            if (!authContext) {
                throw new MissingAuthHeadersError();
            }
            // Validate IP whitelist if enabled
            if (validateIpWhitelist && authContext.serviceAccount.ipWhitelist?.length) {
                const clientIp = request.ip;
                if (!authContext.serviceAccount.ipWhitelist.includes(clientIp)) {
                    logger.warn({ clientId: authContext.clientId, ip: clientIp }, 'Service auth rejected: IP not in whitelist');
                    throw new IpNotAllowedError(authContext.clientId, clientIp);
                }
            }
            // Validate required permissions
            if (requiredPermissions.length > 0) {
                const hasAllRequired = requiredPermissions.every((perm) => authContext.permissions.includes('*') ||
                    authContext.permissions.includes(perm) ||
                    hasWildcardPermission(authContext.permissions, perm));
                if (!hasAllRequired) {
                    throw new InsufficientPermissionsError(requiredPermissions, authContext.permissions);
                }
            }
            // Validate any permissions
            if (anyPermissions.length > 0) {
                const hasAny = anyPermissions.some((perm) => authContext.permissions.includes('*') ||
                    authContext.permissions.includes(perm) ||
                    hasWildcardPermission(authContext.permissions, perm));
                if (!hasAny) {
                    throw new InsufficientPermissionsError(anyPermissions, authContext.permissions);
                }
            }
            // Attach auth context to request
            authRequest.serviceAuth = authContext;
            logger.debug({
                clientId: authContext.clientId,
                tenantId: authContext.tenantId,
                authMethod: authContext.authMethod,
                path: request.url,
            }, 'Service authentication successful');
        }
        catch (error) {
            if (onError) {
                await onError(error, request, reply);
                return;
            }
            logger.warn({
                error: error.message,
                path: request.url,
                ip: request.ip,
            }, 'Service authentication failed');
            throw error;
        }
    };
}
/**
 * Try token-based authentication
 */
async function tryTokenAuth(request, tokenService, accountManager) {
    const authHeader = request.headers.authorization;
    if (!authHeader?.startsWith('Bearer ')) {
        return null;
    }
    const token = authHeader.slice(7);
    const result = await tokenService.verifyToken(token);
    if (!result.valid || !result.payload) {
        if (result.error) {
            logger.debug({ error: result.error }, 'Token verification failed');
        }
        return null;
    }
    // Verify the service account still exists and is active
    const account = await accountManager.findAccount(result.payload.sub);
    if (!account) {
        logger.warn({ clientId: result.payload.sub }, 'Service account not found for valid token');
        return null;
    }
    if (account.status === ServiceAccountStatus.REVOKED) {
        throw new ServiceAccountRevokedError(account.clientId);
    }
    if (account.status === ServiceAccountStatus.SUSPENDED) {
        throw new ServiceAccountSuspendedError(account.clientId);
    }
    return {
        serviceAccount: account,
        clientId: account.clientId,
        tenantId: account.tenantId,
        serviceName: account.name,
        permissions: account.permissions,
        authMethod: 'token',
        token,
        tokenPayload: result.payload,
    };
}
/**
 * Try signature-based authentication
 */
async function trySignatureAuth(request, accountManager, tokenService, issueToken) {
    const headers = request.headers;
    // Extract service auth headers
    const clientId = getHeaderValue(headers, SERVICE_AUTH_HEADERS.SERVICE_ID);
    const signature = getHeaderValue(headers, SERVICE_AUTH_HEADERS.SERVICE_SIGNATURE);
    const timestampStr = getHeaderValue(headers, SERVICE_AUTH_HEADERS.SERVICE_TIMESTAMP);
    // If no service headers, return null (might be token auth)
    if (!clientId && !signature && !timestampStr) {
        return null;
    }
    // If partial headers, that's an error
    if (!clientId || !signature || !timestampStr) {
        throw new MissingAuthHeadersError();
    }
    const timestamp = parseInt(timestampStr, 10);
    if (isNaN(timestamp)) {
        throw new InvalidServiceSignatureError('Invalid timestamp format');
    }
    // Get the service account
    const account = await accountManager.findAccount(clientId);
    if (!account) {
        // Use generic error to prevent enumeration
        throw new InvalidServiceSignatureError('Invalid credentials');
    }
    if (account.status === ServiceAccountStatus.REVOKED) {
        throw new ServiceAccountRevokedError(clientId);
    }
    if (account.status === ServiceAccountStatus.SUSPENDED) {
        throw new ServiceAccountSuspendedError(clientId);
    }
    // We need the plaintext secret to verify, but we only have the hash
    // The signature is created using the plaintext secret, so we need to
    // store a verification key or use a different approach
    //
    // For HMAC verification, the client sends:
    // - signature = HMAC(timestamp + method + path + body, clientSecret)
    //
    // We can't reverse the hash, so we need to:
    // 1. Store a separate verification key (not recommended - defeats purpose)
    // 2. Use the hashed secret as the HMAC key (both sides must agree)
    //
    // We'll use approach 2: both client and server use the same derivation
    // The client must hash their secret before using it for HMAC
    // Get request body as string
    let bodyStr = '';
    if (request.body) {
        if (typeof request.body === 'string') {
            bodyStr = request.body;
        }
        else if (Buffer.isBuffer(request.body)) {
            bodyStr = request.body.toString('utf8');
        }
        else {
            bodyStr = JSON.stringify(request.body);
        }
    }
    if (!tokenService) {
        throw new Error('Token service required for signature verification');
    }
    // Verify the signature using the hashed secret
    // Note: Client must also use hashed secret for HMAC
    const verifyResult = tokenService.verifySignature({
        clientSecret: account.clientSecret, // Using hashed secret as HMAC key
        providedSignature: signature,
        timestamp,
        method: request.method,
        path: request.url.split('?')[0], // Path without query string
        body: bodyStr,
    });
    if (!verifyResult.valid) {
        throw new InvalidServiceSignatureError(verifyResult.error);
    }
    // Update last used timestamp
    accountManager['store'].updateLastUsed(clientId).catch((err) => {
        logger.error({ err, clientId }, 'Failed to update lastUsedAt');
    });
    // Optionally issue a token
    let token;
    if (issueToken && tokenService) {
        token = await tokenService.createToken({
            clientId: account.clientId,
            tenantId: account.tenantId,
            serviceName: account.name,
            permissions: account.permissions,
            ipAddress: request.ip,
        });
    }
    return {
        serviceAccount: account,
        clientId: account.clientId,
        tenantId: account.tenantId,
        serviceName: account.name,
        permissions: account.permissions,
        authMethod: 'signature',
        token,
    };
}
/**
 * Helper to get header value (handles arrays)
 */
function getHeaderValue(headers, name) {
    const value = headers[name] ?? headers[name.toLowerCase()];
    if (Array.isArray(value)) {
        return value[0] ?? null;
    }
    return value ?? null;
}
/**
 * Check if permissions list contains a wildcard that matches the required permission
 */
function hasWildcardPermission(permissions, required) {
    for (const perm of permissions) {
        if (perm.endsWith(':*')) {
            const prefix = perm.slice(0, -1); // Remove '*'
            if (required.startsWith(prefix)) {
                return true;
            }
        }
    }
    return false;
}
// =============================================================================
// HELPER MIDDLEWARE FACTORIES
// =============================================================================
/**
 * Create middleware that requires specific permissions
 */
export function requireServicePermissions(permissions, options = { all: true }) {
    const baseOptions = options.all
        ? { requiredPermissions: permissions }
        : { anyPermissions: permissions };
    return createServiceAuthMiddleware(baseOptions);
}
/**
 * Create middleware that requires any of the specified permissions
 */
export function requireAnyServicePermission(permissions) {
    return createServiceAuthMiddleware({ anyPermissions: permissions });
}
/**
 * Create middleware that validates service belongs to a specific tenant
 */
export function requireServiceTenant(tenantId) {
    return async function (request, reply) {
        const authRequest = request;
        if (!authRequest.serviceAuth) {
            throw new ServiceAuthError('Service authentication required');
        }
        if (authRequest.serviceAuth.tenantId !== tenantId) {
            throw new ForbiddenError('Service not authorized for this tenant', {
                serviceTenant: authRequest.serviceAuth.tenantId,
                requiredTenant: tenantId,
            });
        }
    };
}
// =============================================================================
// REQUEST HELPERS
// =============================================================================
/**
 * Get service auth context from request
 */
export function getServiceAuth(request) {
    return request.serviceAuth;
}
/**
 * Require service auth context from request
 */
export function requireServiceAuth(request) {
    const auth = getServiceAuth(request);
    if (!auth) {
        throw new ServiceAuthError('Service authentication required');
    }
    return auth;
}
/**
 * Check if request has service auth
 */
export function hasServiceAuth(request) {
    return !!request.serviceAuth;
}
/**
 * Get service client ID from request
 */
export function getServiceClientId(request) {
    return getServiceAuth(request)?.clientId;
}
/**
 * Get service tenant ID from request
 */
export function getServiceTenantId(request) {
    return getServiceAuth(request)?.tenantId;
}
/**
 * Get service permissions from request
 */
export function getServicePermissions(request) {
    return getServiceAuth(request)?.permissions ?? [];
}
/**
 * Check if service has a specific permission
 */
export function serviceHasPermission(request, permission) {
    const permissions = getServicePermissions(request);
    if (permissions.includes('*')) {
        return true;
    }
    if (permissions.includes(permission)) {
        return true;
    }
    return hasWildcardPermission(permissions, permission);
}
// =============================================================================
// FASTIFY PLUGIN
// =============================================================================
import fp from 'fastify-plugin';
/**
 * Service auth Fastify plugin
 */
const serviceAuthPluginAsync = async (fastify, options) => {
    const { global = false, decorateRequest = true, ...middlewareOptions } = options;
    // Create middleware
    const middleware = createServiceAuthMiddleware(middlewareOptions);
    // Register globally if requested
    if (global) {
        fastify.addHook('preHandler', middleware);
    }
    // Decorate request with helpers
    if (decorateRequest) {
        fastify.decorateRequest('serviceAuth', null);
        fastify.decorateRequest('getServiceAuth', function () {
            return getServiceAuth(this);
        });
        fastify.decorateRequest('requireServiceAuth', function () {
            return requireServiceAuth(this);
        });
        fastify.decorateRequest('hasServiceAuth', function () {
            return hasServiceAuth(this);
        });
        fastify.decorateRequest('serviceHasPermission', function (permission) {
            return serviceHasPermission(this, permission);
        });
    }
    // Decorate fastify with middleware factory
    fastify.decorate('serviceAuthMiddleware', createServiceAuthMiddleware);
    fastify.decorate('requireServicePermissions', requireServicePermissions);
    fastify.decorate('requireAnyServicePermission', requireAnyServicePermission);
    logger.info({ global }, 'Service auth plugin registered');
};
/**
 * Service auth Fastify plugin
 */
export const serviceAuthPlugin = fp(serviceAuthPluginAsync, {
    name: 'service-auth',
    fastify: '>=4.x',
});
//# sourceMappingURL=service-auth-middleware.js.map