"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdentityResolutionService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const auth_error_1 = require("../../../errors/response-errors/auth.error");
const event_service_1 = require("../../../events/event.service");
const user_service_1 = require("../../../services/user.service");
const INVALID_PASSWORD_PLACEHOLDER = '!token-exchange-no-password';
const MAX_NAME_LENGTH = 32;
function isGlobalRole(role) {
    return role in db_1.GLOBAL_ROLES;
}
function trimName(value, fallback = '') {
    return (value ?? fallback).slice(0, MAX_NAME_LENGTH);
}
let IdentityResolutionService = class IdentityResolutionService {
    constructor(logger, userRepository, authIdentityRepository, eventService, userService) {
        this.userRepository = userRepository;
        this.authIdentityRepository = authIdentityRepository;
        this.eventService = eventService;
        this.userService = userService;
        this.logger = logger.scoped('token-exchange');
    }
    async resolve(claims, allowedRoles, tokenContext) {
        const email = claims.email?.toLowerCase();
        const identity = await this.authIdentityRepository.findOne({
            where: { providerId: claims.sub, providerType: 'token-exchange' },
            relations: { user: { role: true } },
        });
        if (identity) {
            return await this.resolveByIdentity(claims, identity, allowedRoles, tokenContext);
        }
        if (email) {
            const existingUser = await this.userRepository.findOne({
                where: { email },
                relations: ['authIdentities', 'role'],
            });
            if (existingUser) {
                return await this.resolveByEmail(claims, email, existingUser, allowedRoles, tokenContext);
            }
        }
        if (!email) {
            throw new auth_error_1.AuthError('Email claim is required for user provisioning');
        }
        return await this.provisionUser(claims, email, allowedRoles, tokenContext);
    }
    async resolveByIdentity(claims, identity, allowedRoles, tokenContext) {
        this.logger.debug('Resolved user by auth identity', { sub: claims.sub });
        const resolvedRole = this.resolveRoleForExistingUser(claims.role, allowedRoles, identity.user.role?.slug);
        return await this.syncProfile(identity.user, claims, resolvedRole, tokenContext);
    }
    async resolveByEmail(claims, email, existingUser, allowedRoles, tokenContext) {
        this.logger.debug('Linking external identity to existing user by email', {
            sub: claims.sub,
            email,
        });
        const resolvedRole = this.resolveRoleForExistingUser(claims.role, allowedRoles, existingUser.role?.slug);
        await this.authIdentityRepository.save(db_1.AuthIdentity.create(existingUser, claims.sub, 'token-exchange'));
        this.eventService.emit('token-exchange-identity-linked', {
            userId: existingUser.id,
            sub: claims.sub,
            email,
            kid: tokenContext?.kid ?? '',
            issuer: tokenContext?.issuer ?? claims.iss,
        });
        return await this.syncProfile(existingUser, claims, resolvedRole, tokenContext);
    }
    async provisionUser(claims, email, allowedRoles, tokenContext) {
        this.logger.debug('JIT provisioning new user', { sub: claims.sub, email });
        const jitRole = this.resolveRoleForNewUser(claims.role, allowedRoles);
        const targetRole = jitRole ? { slug: jitRole } : db_1.GLOBAL_MEMBER_ROLE;
        const user = await this.userRepository.manager.transaction(async (trx) => {
            const { user: newUser } = await this.userRepository.createUserWithProject({
                email,
                firstName: trimName(claims.given_name),
                lastName: trimName(claims.family_name),
                role: targetRole,
                password: INVALID_PASSWORD_PLACEHOLDER,
            }, trx);
            await trx.save(trx.create(db_1.AuthIdentity, {
                providerId: claims.sub,
                providerType: 'token-exchange',
                userId: newUser.id,
            }));
            return newUser;
        });
        this.eventService.emit('token-exchange-user-provisioned', {
            userId: user.id,
            sub: claims.sub,
            email,
            role: targetRole.slug,
            kid: tokenContext?.kid ?? '',
            issuer: tokenContext?.issuer ?? claims.iss,
        });
        return user;
    }
    resolveRoleForExistingUser(roleClaim, allowedRoles, currentRole) {
        if (roleClaim === undefined)
            return undefined;
        if (currentRole === 'global:owner') {
            this.logger.debug('Skipping role sync for existing owner');
            return undefined;
        }
        const role = roleClaim;
        if (role === 'global:owner') {
            this.logger.warn('Ignoring global:owner role claim for existing user');
            return undefined;
        }
        if (!isGlobalRole(role)) {
            this.logger.warn('Unknown role claim ignored', { role });
            return undefined;
        }
        if (allowedRoles && allowedRoles.length > 0 && !allowedRoles.includes(role)) {
            throw new auth_error_1.AuthError(`Role '${role}' is not allowed for this token exchange key`);
        }
        return role;
    }
    resolveRoleForNewUser(roleClaim, allowedRoles) {
        if (roleClaim === undefined)
            return undefined;
        const role = roleClaim;
        if (role === 'global:owner') {
            throw new auth_error_1.AuthError('Cannot provision global:owner role via token exchange');
        }
        if (!isGlobalRole(role)) {
            throw new auth_error_1.AuthError(`Unrecognized role '${role}' cannot be assigned to new user`);
        }
        if (allowedRoles && allowedRoles.length > 0 && !allowedRoles.includes(role)) {
            throw new auth_error_1.AuthError(`Role '${role}' is not allowed for this token exchange key`);
        }
        return role;
    }
    async syncProfile(user, claims, resolvedRole, tokenContext) {
        let needsReload = false;
        const profileUpdates = {};
        if (claims.given_name !== undefined) {
            const trimmed = trimName(claims.given_name);
            if (trimmed !== user.firstName) {
                profileUpdates.firstName = trimmed;
            }
        }
        if (claims.family_name !== undefined) {
            const trimmed = trimName(claims.family_name);
            if (trimmed !== user.lastName) {
                profileUpdates.lastName = trimmed;
            }
        }
        if (Object.keys(profileUpdates).length > 0) {
            await this.userRepository.update(user.id, profileUpdates);
            needsReload = true;
        }
        const previousRole = user.role?.slug;
        if (resolvedRole && resolvedRole !== previousRole) {
            await this.userService.changeUserRole(user, { newRoleName: resolvedRole });
            needsReload = true;
            if (previousRole) {
                this.eventService.emit('token-exchange-role-updated', {
                    userId: user.id,
                    previousRole,
                    newRole: resolvedRole,
                    kid: tokenContext?.kid ?? '',
                    issuer: tokenContext?.issuer ?? claims.iss,
                });
            }
        }
        if (needsReload) {
            return await this.userRepository.findOneOrFail({
                where: { id: user.id },
                relations: ['role'],
            });
        }
        return user;
    }
};
exports.IdentityResolutionService = IdentityResolutionService;
exports.IdentityResolutionService = IdentityResolutionService = __decorate([
    (0, di_1.Service)(),
    __metadata("design:paramtypes", [backend_common_1.Logger,
        db_1.UserRepository,
        db_1.AuthIdentityRepository,
        event_service_1.EventService,
        user_service_1.UserService])
], IdentityResolutionService);
//# sourceMappingURL=identity-resolution.service.js.map