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);
};
import DatabaseService from "./DatabaseService";
import Model from "../../Models/DatabaseModels/UserWebAuthn";
import UserService from "./UserService";
import BadDataException from "../../Types/Exception/BadDataException";
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse, } from "@simplewebauthn/server";
import { Host, HttpProtocol } from "../EnvironmentConfig";
import ObjectID from "../../Types/ObjectID";
import UserTotpAuthService from "./UserTotpAuthService";
import OneUptimeDate from "../../Types/Date";
const WEBAUTHN_CHALLENGE_TTL_MINUTES = 5;
export class Service extends DatabaseService {
    constructor() {
        super(Model);
    }
    async generateRegistrationOptions(data) {
        const user = await UserService.findOneById({
            id: data.userId,
            select: {
                email: true,
                name: true,
            },
            props: {
                isRoot: true,
            },
        });
        if (!user) {
            throw new BadDataException("User not found");
        }
        if (!user.email) {
            throw new BadDataException("User email not found");
        }
        // Get existing credentials for this user
        const existingCredentials = await this.findBy({
            query: {
                userId: data.userId,
            },
            select: {
                credentialId: true,
            },
            limit: LIMIT_PER_PROJECT,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        const options = await generateRegistrationOptions({
            rpName: "OneUptime",
            rpID: Host.toString(),
            userID: new Uint8Array(Buffer.from(data.userId.toString())),
            userName: user.email.toString(),
            userDisplayName: user.name ? user.name.toString() : user.email.toString(),
            attestationType: "none",
            excludeCredentials: existingCredentials
                .filter((cred) => {
                return cred.credentialId;
            })
                .map((cred) => {
                return {
                    id: cred.credentialId,
                    type: "public-key",
                };
            }),
            authenticatorSelection: {
                residentKey: "discouraged",
                userVerification: "preferred",
            },
        });
        // Convert to JSON serializable format
        options.challenge = Buffer.from(options.challenge).toString("base64url");
        if (options.excludeCredentials) {
            options.excludeCredentials = options.excludeCredentials.map((cred) => {
                return Object.assign(Object.assign({}, cred), { id: typeof cred.id === "string"
                        ? cred.id
                        : Buffer.from(cred.id).toString("base64url") });
            });
        }
        // Store the challenge server-side so verification uses a trusted value
        await UserService.updateOneById({
            id: data.userId,
            data: {
                webauthnChallenge: options.challenge,
                webauthnChallengeExpiresAt: OneUptimeDate.addRemoveMinutes(OneUptimeDate.getCurrentDate(), WEBAUTHN_CHALLENGE_TTL_MINUTES),
            },
            props: {
                isRoot: true,
            },
        });
        return {
            options: options,
            challenge: options.challenge,
        };
    }
    async verifyRegistration(data) {
        if (!data.props.userId) {
            throw new BadDataException("User ID not found in request");
        }
        // Retrieve the challenge from the server-side store
        const storedChallenge = await this.getAndClearStoredChallenge(data.props.userId);
        const expectedOrigin = `${HttpProtocol}${Host.toString()}`;
        const verification = await verifyRegistrationResponse({
            response: data.credential,
            expectedChallenge: storedChallenge,
            expectedOrigin: expectedOrigin,
            expectedRPID: Host.toString(),
        });
        if (!verification.verified) {
            throw new BadDataException("Registration verification failed");
        }
        const { registrationInfo } = verification;
        if (!registrationInfo) {
            throw new BadDataException("Registration info not found");
        }
        // Save the credential
        const userWebAuthn = Model.fromJSON({
            name: data.name,
            credentialId: registrationInfo.credential.id,
            publicKey: Buffer.from(registrationInfo.credential.publicKey).toString("base64"),
            counter: "0",
            transports: JSON.stringify([]),
            isVerified: true,
            userId: data.props.userId,
        }, Model);
        await this.create({
            data: userWebAuthn,
            props: data.props,
        });
    }
    async generateAuthenticationOptions(data) {
        const user = await UserService.findOneBy({
            query: { email: data.email },
            select: {
                _id: true,
            },
            props: {
                isRoot: true,
            },
        });
        if (!user) {
            throw new BadDataException("User not found");
        }
        // Get user's WebAuthn credentials
        const credentials = await this.findBy({
            query: {
                userId: user.id,
                isVerified: true,
            },
            select: {
                credentialId: true,
            },
            limit: LIMIT_PER_PROJECT,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        if (credentials.length === 0) {
            throw new BadDataException("No WebAuthn credentials found for this user");
        }
        const options = await generateAuthenticationOptions({
            rpID: Host.toString(),
            allowCredentials: credentials.map((cred) => {
                return {
                    id: cred.credentialId,
                    type: "public-key",
                };
            }),
            userVerification: "preferred",
        });
        // Convert to JSON serializable format
        options.challenge = Buffer.from(options.challenge).toString("base64url");
        // allowCredentials id is already base64url string
        // Store the challenge server-side so verification uses a trusted value
        await UserService.updateOneById({
            id: user.id,
            data: {
                webauthnChallenge: options.challenge,
                webauthnChallengeExpiresAt: OneUptimeDate.addRemoveMinutes(OneUptimeDate.getCurrentDate(), WEBAUTHN_CHALLENGE_TTL_MINUTES),
            },
            props: {
                isRoot: true,
            },
        });
        return {
            options: options,
            challenge: options.challenge,
            userId: user.id.toString(),
        };
    }
    async verifyAuthentication(data) {
        // Retrieve the challenge from the server-side store
        const storedChallenge = await this.getAndClearStoredChallenge(new ObjectID(data.userId));
        const user = await UserService.findOneById({
            id: new ObjectID(data.userId),
            select: {
                _id: true,
                email: true,
            },
            props: {
                isRoot: true,
            },
        });
        if (!user) {
            throw new BadDataException("User not found");
        }
        // Get the credential from database
        const dbCredential = await this.findOneBy({
            query: {
                credentialId: data.credential.id,
                userId: new ObjectID(data.userId),
                isVerified: true,
            },
            select: {
                credentialId: true,
                publicKey: true,
                counter: true,
                _id: true,
            },
            props: {
                isRoot: true,
            },
        });
        if (!dbCredential) {
            throw new BadDataException("Credential not found");
        }
        const expectedOrigin = `${HttpProtocol}${Host.toString()}`;
        const verification = await verifyAuthenticationResponse({
            response: data.credential,
            expectedChallenge: storedChallenge,
            expectedOrigin: expectedOrigin,
            expectedRPID: Host.toString(),
            credential: {
                id: dbCredential.credentialId,
                publicKey: Buffer.from(dbCredential.publicKey, "base64"),
                counter: parseInt(dbCredential.counter),
            },
        });
        if (!verification.verified) {
            throw new BadDataException("Authentication verification failed");
        }
        // Update counter
        await this.updateOneById({
            id: dbCredential.id,
            data: {
                counter: verification.authenticationInfo.newCounter.toString(),
            },
            props: {
                isRoot: true,
            },
        });
        return user;
    }
    /**
     * Retrieves the stored WebAuthn challenge for the given user,
     * validates it has not expired, and clears it so it cannot be reused.
     */
    async getAndClearStoredChallenge(userId) {
        const user = await UserService.findOneById({
            id: userId,
            select: {
                webauthnChallenge: true,
                webauthnChallengeExpiresAt: true,
            },
            props: {
                isRoot: true,
            },
        });
        if (!user) {
            throw new BadDataException("User not found");
        }
        if (!user.webauthnChallenge || !user.webauthnChallengeExpiresAt) {
            throw new BadDataException("No pending WebAuthn challenge found. Please initiate the WebAuthn flow again.");
        }
        // Check expiry
        if (OneUptimeDate.isBefore(user.webauthnChallengeExpiresAt, OneUptimeDate.getCurrentDate())) {
            // Clear the expired challenge
            await UserService.updateOneById({
                id: userId,
                data: {
                    webauthnChallenge: null,
                    webauthnChallengeExpiresAt: null,
                },
                props: {
                    isRoot: true,
                },
            });
            throw new BadDataException("WebAuthn challenge has expired. Please initiate the WebAuthn flow again.");
        }
        const challenge = user.webauthnChallenge;
        // Clear the challenge immediately so it cannot be reused (one-time use)
        await UserService.updateOneById({
            id: userId,
            data: {
                webauthnChallenge: null,
                webauthnChallengeExpiresAt: null,
            },
            props: {
                isRoot: true,
            },
        });
        return challenge;
    }
    async onBeforeCreate(createBy) {
        if (!createBy.props.userId) {
            throw new BadDataException("User id is required");
        }
        createBy.data.userId = createBy.props.userId;
        const user = await UserService.findOneById({
            id: createBy.data.userId,
            props: {
                isRoot: true,
            },
            select: {
                email: true,
            },
        });
        if (!user) {
            throw new BadDataException("User not found");
        }
        if (!user.email) {
            throw new BadDataException("User email is required");
        }
        // by default secuirty keys are always verified. You can't add an unverified security key.
        createBy.data.isVerified = true;
        return {
            createBy: createBy,
            carryForward: {},
        };
    }
    async onBeforeDelete(deleteBy) {
        const itemsToBeDeleted = await this.findBy({
            query: deleteBy.query,
            select: {
                userId: true,
                _id: true,
                isVerified: true,
            },
            limit: LIMIT_MAX,
            skip: 0,
            props: deleteBy.props,
        });
        for (const item of itemsToBeDeleted) {
            if (item.isVerified) {
                // check if user two auth is enabled.
                const user = await UserService.findOneById({
                    id: item.userId,
                    props: {
                        isRoot: true,
                    },
                    select: {
                        enableTwoFactorAuth: true,
                    },
                });
                if (!user) {
                    throw new BadDataException("User not found");
                }
                if (user.enableTwoFactorAuth) {
                    // if enabled then check if this is the only verified 2FA method for this user.
                    const verifiedWebAuthnItems = await this.findBy({
                        query: {
                            userId: item.userId,
                            isVerified: true,
                        },
                        select: {
                            _id: true,
                        },
                        limit: LIMIT_MAX,
                        skip: 0,
                        props: deleteBy.props,
                    });
                    const verifiedTotpItems = await UserTotpAuthService.findBy({
                        query: {
                            userId: item.userId,
                            isVerified: true,
                        },
                        select: {
                            _id: true,
                        },
                        limit: LIMIT_MAX,
                        skip: 0,
                        props: deleteBy.props,
                    });
                    const totalVerified2FA = verifiedWebAuthnItems.length + verifiedTotpItems.length;
                    if (totalVerified2FA === 1) {
                        throw new BadDataException("You must have atleast one verified two factor auth. Please disable two factor auth before deleting this item.");
                    }
                }
            }
        }
        return {
            deleteBy: deleteBy,
            carryForward: {},
        };
    }
}
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "generateRegistrationOptions", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "verifyRegistration", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "generateAuthenticationOptions", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "verifyAuthentication", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "onBeforeCreate", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "onBeforeDelete", null);
export default new Service();
//# sourceMappingURL=UserWebAuthnService.js.map