UNPKG

firebase-tools

Version:
2,217 lines 104 kB
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseBlockingFunctionJwt = exports.setAccountInfoImpl = exports.resetPassword = exports.SESSION_COOKIE_MAX_VALID_DURATION = exports.CUSTOM_TOKEN_AUDIENCE = exports.authOperations = void 0;
const url_1 = require("url");
const jsonwebtoken_1 = require("jsonwebtoken");
const node_fetch_1 = require("node-fetch");
const abort_controller_1 = require("abort-controller");
const utils_1 = require("./utils");
const errors_1 = require("./errors");
const types_1 = require("../types");
const emulatorLogger_1 = require("../emulatorLogger");
const state_1 = require("./state");
exports.authOperations = {
    identitytoolkit: {
        getProjects,
        getRecaptchaParams,
        accounts: {
            createAuthUri,
            delete: deleteAccount,
            lookup,
            resetPassword,
            sendOobCode,
            sendVerificationCode,
            signInWithCustomToken,
            signInWithEmailLink,
            signInWithIdp,
            signInWithPassword,
            signInWithPhoneNumber,
            signUp,
            update: setAccountInfo,
            mfaEnrollment: {
                finalize: mfaEnrollmentFinalize,
                start: mfaEnrollmentStart,
                withdraw: mfaEnrollmentWithdraw,
            },
            mfaSignIn: {
                start: mfaSignInStart,
                finalize: mfaSignInFinalize,
            },
        },
        projects: {
            createSessionCookie,
            queryAccounts,
            getConfig,
            updateConfig,
            accounts: {
                _: signUp,
                delete: deleteAccount,
                lookup,
                query: queryAccounts,
                sendOobCode,
                update: setAccountInfo,
                batchCreate,
                batchDelete,
                batchGet,
            },
            tenants: {
                create: createTenant,
                delete: deleteTenant,
                get: getTenant,
                list: listTenants,
                patch: updateTenant,
                createSessionCookie,
                accounts: {
                    _: signUp,
                    batchCreate,
                    batchDelete,
                    batchGet,
                    delete: deleteAccount,
                    lookup,
                    query: queryAccounts,
                    sendOobCode,
                    update: setAccountInfo,
                },
            },
        },
    },
    securetoken: {
        token: grantToken,
    },
    emulator: {
        projects: {
            accounts: {
                delete: deleteAllAccountsInProject,
            },
            config: {
                get: getEmulatorProjectConfig,
                update: updateEmulatorProjectConfig,
            },
            oobCodes: {
                list: listOobCodesInProject,
            },
            verificationCodes: {
                list: listVerificationCodesInProject,
            },
        },
    },
};
const PASSWORD_MIN_LENGTH = 6;
exports.CUSTOM_TOKEN_AUDIENCE = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
const MFA_INELIGIBLE_PROVIDER = new Set([
    state_1.PROVIDER_ANONYMOUS,
    state_1.PROVIDER_PHONE,
    state_1.PROVIDER_CUSTOM,
    state_1.PROVIDER_GAME_CENTER,
]);
async function signUp(state, reqBody, ctx) {
    var _a, _b, _c, _d;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    let provider;
    const timestamp = new Date();
    let updates = {
        lastLoginAt: timestamp.getTime().toString(),
    };
    if ((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2) {
        if (reqBody.idToken) {
            (0, errors_1.assert)(!reqBody.localId, "UNEXPECTED_PARAMETER : User ID");
        }
        if (reqBody.localId) {
            (0, errors_1.assert)(!state.getUserByLocalId(reqBody.localId), "DUPLICATE_LOCAL_ID");
        }
        updates.displayName = reqBody.displayName;
        updates.photoUrl = reqBody.photoUrl;
        updates.emailVerified = reqBody.emailVerified || false;
        if (reqBody.phoneNumber) {
            (0, errors_1.assert)((0, utils_1.isValidPhoneNumber)(reqBody.phoneNumber), "INVALID_PHONE_NUMBER : Invalid format.");
            (0, errors_1.assert)(!state.getUserByPhoneNumber(reqBody.phoneNumber), "PHONE_NUMBER_EXISTS");
            updates.phoneNumber = reqBody.phoneNumber;
        }
        if (reqBody.disabled) {
            updates.disabled = true;
        }
    }
    else {
        (0, errors_1.assert)(!reqBody.localId, "UNEXPECTED_PARAMETER : User ID");
        if (reqBody.idToken || reqBody.password || reqBody.email) {
            updates.displayName = reqBody.displayName;
            updates.emailVerified = false;
            (0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
            (0, errors_1.assert)(reqBody.password, "MISSING_PASSWORD");
            provider = state_1.PROVIDER_PASSWORD;
            (0, errors_1.assert)(state.allowPasswordSignup, "OPERATION_NOT_ALLOWED");
        }
        else {
            provider = state_1.PROVIDER_ANONYMOUS;
            (0, errors_1.assert)(state.enableAnonymousUser, "ADMIN_ONLY_OPERATION");
        }
    }
    if (reqBody.email || (reqBody.email === "" && provider)) {
        (0, errors_1.assert)((0, utils_1.isValidEmailAddress)(reqBody.email), "INVALID_EMAIL");
        const email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
        (0, errors_1.assert)(!state.getUserByEmail(email), "EMAIL_EXISTS");
        updates.email = email;
    }
    if (reqBody.password) {
        (0, errors_1.assert)(reqBody.password.length >= PASSWORD_MIN_LENGTH, `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`);
        updates.salt = "fakeSalt" + (0, utils_1.randomId)(20);
        updates.passwordHash = hashPassword(reqBody.password, updates.salt);
        updates.passwordUpdatedAt = Date.now();
        updates.validSince = (0, utils_1.toUnixTimestamp)(new Date()).toString();
    }
    if (reqBody.mfaInfo) {
        updates.mfaInfo = getMfaEnrollmentsFromRequest(state, reqBody.mfaInfo, {
            generateEnrollmentIds: true,
        });
    }
    if (state instanceof state_1.TenantProjectState) {
        updates.tenantId = state.tenantId;
    }
    let user;
    if (reqBody.idToken) {
        ({ user } = parseIdToken(state, reqBody.idToken));
    }
    let extraClaims;
    if (!user) {
        updates.createdAt = timestamp.getTime().toString();
        const localId = (_b = reqBody.localId) !== null && _b !== void 0 ? _b : state.generateLocalId();
        if (reqBody.email && !((_c = ctx.security) === null || _c === void 0 ? void 0 : _c.Oauth2)) {
            const userBeforeCreate = Object.assign({ localId }, updates);
            const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_CREATE, userBeforeCreate, { signInMethod: "password" });
            updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
        }
        user = state.createUserWithLocalId(localId, updates);
        (0, errors_1.assert)(user, "DUPLICATE_LOCAL_ID");
        if (reqBody.email && !((_d = ctx.security) === null || _d === void 0 ? void 0 : _d.Oauth2)) {
            if (!user.disabled) {
                const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, { signInMethod: "password" });
                updates = blockingResponse.updates;
                extraClaims = blockingResponse.extraClaims;
                user = state.updateUserByLocalId(user.localId, updates);
            }
            (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
        }
    }
    else {
        user = state.updateUserByLocalId(user.localId, updates);
    }
    return Object.assign({ kind: "identitytoolkit#SignupNewUserResponse", localId: user.localId, displayName: user.displayName, email: user.email }, (provider ? issueTokens(state, user, provider, { extraClaims }) : {}));
}
function lookup(state, reqBody, ctx) {
    var _a, _b, _c, _d, _e;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    const seenLocalIds = new Set();
    const users = [];
    function tryAddUser(maybeUser) {
        if (maybeUser && !seenLocalIds.has(maybeUser.localId)) {
            users.push(maybeUser);
            seenLocalIds.add(maybeUser.localId);
        }
    }
    if ((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2) {
        if (reqBody.initialEmail) {
            throw new errors_1.NotImplementedError("Lookup by initialEmail is not implemented.");
        }
        for (const localId of (_b = reqBody.localId) !== null && _b !== void 0 ? _b : []) {
            tryAddUser(state.getUserByLocalId(localId));
        }
        for (const email of (_c = reqBody.email) !== null && _c !== void 0 ? _c : []) {
            const canonicalizedEmail = (0, utils_1.canonicalizeEmailAddress)(email);
            tryAddUser(state.getUserByEmail(canonicalizedEmail));
        }
        for (const phoneNumber of (_d = reqBody.phoneNumber) !== null && _d !== void 0 ? _d : []) {
            tryAddUser(state.getUserByPhoneNumber(phoneNumber));
        }
        for (const { providerId, rawId } of (_e = reqBody.federatedUserId) !== null && _e !== void 0 ? _e : []) {
            if (!providerId || !rawId) {
                continue;
            }
            tryAddUser(state.getUserByProviderRawId(providerId, rawId));
        }
    }
    else {
        (0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
        const { user } = parseIdToken(state, reqBody.idToken);
        users.push(redactPasswordHash(user));
    }
    return {
        kind: "identitytoolkit#GetAccountInfoResponse",
        users: users.length ? users : undefined,
    };
}
function batchCreate(state, reqBody) {
    var _a, _b;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)((_a = reqBody.users) === null || _a === void 0 ? void 0 : _a.length, "MISSING_USER_ACCOUNT");
    if (reqBody.sanityCheck) {
        if (state.oneAccountPerEmail) {
            const existingEmails = new Set();
            for (const userInfo of reqBody.users) {
                if (userInfo.email) {
                    (0, errors_1.assert)(!existingEmails.has(userInfo.email), `DUPLICATE_EMAIL : ${userInfo.email}`);
                    existingEmails.add(userInfo.email);
                }
            }
        }
        const existingProviderAccounts = new Set();
        for (const userInfo of reqBody.users) {
            for (const { providerId, rawId } of (_b = userInfo.providerUserInfo) !== null && _b !== void 0 ? _b : []) {
                const key = `${providerId}:${rawId}`;
                (0, errors_1.assert)(!existingProviderAccounts.has(key), `DUPLICATE_RAW_ID : Provider id(${providerId}), Raw id(${rawId})`);
                existingProviderAccounts.add(key);
            }
        }
    }
    if (!reqBody.allowOverwrite) {
        const existingLocalIds = new Set();
        for (const userInfo of reqBody.users) {
            const localId = userInfo.localId || "";
            (0, errors_1.assert)(!existingLocalIds.has(localId), `DUPLICATE_LOCAL_ID : ${localId}`);
            existingLocalIds.add(localId);
        }
    }
    const errors = [];
    for (let index = 0; index < reqBody.users.length; index++) {
        const userInfo = reqBody.users[index];
        try {
            (0, errors_1.assert)(userInfo.localId, "localId is missing");
            const uploadTime = new Date();
            const fields = {
                displayName: userInfo.displayName,
                photoUrl: userInfo.photoUrl,
                lastLoginAt: userInfo.lastLoginAt,
            };
            if (userInfo.tenantId) {
                (0, errors_1.assert)(state instanceof state_1.TenantProjectState && state.tenantId === userInfo.tenantId, "Tenant id in userInfo does not match the tenant id in request.");
            }
            if (state instanceof state_1.TenantProjectState) {
                fields.tenantId = state.tenantId;
            }
            if (userInfo.passwordHash) {
                fields.passwordHash = userInfo.passwordHash;
                fields.salt = userInfo.salt;
                fields.passwordUpdatedAt = uploadTime.getTime();
            }
            else if (userInfo.rawPassword) {
                fields.salt = userInfo.salt || "fakeSalt" + (0, utils_1.randomId)(20);
                fields.passwordHash = hashPassword(userInfo.rawPassword, fields.salt);
                fields.passwordUpdatedAt = uploadTime.getTime();
            }
            if (userInfo.customAttributes) {
                validateSerializedCustomClaims(userInfo.customAttributes);
                fields.customAttributes = userInfo.customAttributes;
            }
            if (userInfo.providerUserInfo) {
                fields.providerUserInfo = [];
                for (const providerUserInfo of userInfo.providerUserInfo) {
                    const { providerId, rawId, federatedId } = providerUserInfo;
                    if (providerId === state_1.PROVIDER_PASSWORD || providerId === state_1.PROVIDER_PHONE) {
                        continue;
                    }
                    if (!rawId || !providerId) {
                        if (!federatedId) {
                            (0, errors_1.assert)(false, "federatedId or (providerId & rawId) is required");
                        }
                        else {
                            (0, errors_1.assert)(false, "((Parsing federatedId is not implemented in Auth Emulator; please specify providerId AND rawId as a workaround.))");
                        }
                    }
                    const existingUserWithRawId = state.getUserByProviderRawId(providerId, rawId);
                    (0, errors_1.assert)(!existingUserWithRawId || existingUserWithRawId.localId === userInfo.localId, "raw id exists in other account in database");
                    fields.providerUserInfo.push(Object.assign(Object.assign({}, providerUserInfo), { providerId, rawId }));
                }
            }
            if (userInfo.phoneNumber) {
                (0, errors_1.assert)((0, utils_1.isValidPhoneNumber)(userInfo.phoneNumber), "phone number format is invalid");
                fields.phoneNumber = userInfo.phoneNumber;
            }
            fields.validSince = (0, utils_1.toUnixTimestamp)(uploadTime).toString();
            fields.createdAt = uploadTime.getTime().toString();
            if (fields.createdAt && !isNaN(Number(userInfo.createdAt))) {
                fields.createdAt = userInfo.createdAt;
            }
            if (userInfo.email) {
                const email = userInfo.email;
                (0, errors_1.assert)((0, utils_1.isValidEmailAddress)(email), "email is invalid");
                const existingUserWithEmail = state.getUserByEmail(email);
                (0, errors_1.assert)(!existingUserWithEmail || existingUserWithEmail.localId === userInfo.localId, reqBody.sanityCheck && state.oneAccountPerEmail
                    ? "email exists in other account in database"
                    : `((Auth Emulator does not support importing duplicate email: ${email}))`);
                fields.email = (0, utils_1.canonicalizeEmailAddress)(email);
            }
            fields.emailVerified = !!userInfo.emailVerified;
            fields.disabled = !!userInfo.disabled;
            if (userInfo.mfaInfo && userInfo.mfaInfo.length > 0) {
                fields.mfaInfo = [];
                (0, errors_1.assert)(fields.email, "Second factor account requires email to be presented.");
                (0, errors_1.assert)(fields.emailVerified, "Second factor account requires email to be verified.");
                const existingIds = new Set();
                for (const enrollment of userInfo.mfaInfo) {
                    if (enrollment.mfaEnrollmentId) {
                        (0, errors_1.assert)(!existingIds.has(enrollment.mfaEnrollmentId), "Enrollment id already exists.");
                        existingIds.add(enrollment.mfaEnrollmentId);
                    }
                }
                for (const enrollment of userInfo.mfaInfo) {
                    enrollment.mfaEnrollmentId = enrollment.mfaEnrollmentId || newRandomId(28, existingIds);
                    enrollment.enrolledAt = enrollment.enrolledAt || new Date().toISOString();
                    (0, errors_1.assert)(enrollment.phoneInfo, "Second factor not supported.");
                    (0, errors_1.assert)((0, utils_1.isValidPhoneNumber)(enrollment.phoneInfo), "Phone number format is invalid");
                    enrollment.unobfuscatedPhoneInfo = enrollment.phoneInfo;
                    fields.mfaInfo.push(enrollment);
                }
            }
            if (state.getUserByLocalId(userInfo.localId)) {
                (0, errors_1.assert)(reqBody.allowOverwrite, "localId belongs to an existing account - can not overwrite.");
            }
            state.overwriteUserWithLocalId(userInfo.localId, fields);
        }
        catch (e) {
            if (e instanceof errors_1.BadRequestError) {
                let message = e.message;
                if (message === "INVALID_CLAIMS") {
                    message = "Invalid custom claims provided.";
                }
                else if (message === "CLAIMS_TOO_LARGE") {
                    message = "Custom claims provided are too large.";
                }
                else if (message.startsWith("FORBIDDEN_CLAIM")) {
                    message = "Custom claims provided include a reserved claim.";
                }
                errors.push({
                    index,
                    message,
                });
            }
            else {
                throw e;
            }
        }
    }
    return {
        kind: "identitytoolkit#UploadAccountResponse",
        error: errors,
    };
}
function batchDelete(state, reqBody) {
    var _a;
    const errors = [];
    const localIds = (_a = reqBody.localIds) !== null && _a !== void 0 ? _a : [];
    (0, errors_1.assert)(localIds.length > 0 && localIds.length <= 1000, "LOCAL_ID_LIST_EXCEEDS_LIMIT");
    for (let index = 0; index < localIds.length; index++) {
        const localId = localIds[index];
        const user = state.getUserByLocalId(localId);
        if (!user) {
            continue;
        }
        else if (!user.disabled && !reqBody.force) {
            errors.push({
                index,
                localId,
                message: "NOT_DISABLED : Disable the account before batch deletion.",
            });
        }
        else {
            state.deleteUser(user);
        }
    }
    return { errors: errors.length ? errors : undefined };
}
function batchGet(state, reqBody, ctx) {
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    const maxResults = Math.min(Math.floor(ctx.params.query.maxResults) || 20, 1000);
    const users = state.queryUsers({}, { sortByField: "localId", order: "ASC", startToken: ctx.params.query.nextPageToken });
    let newPageToken = undefined;
    if (maxResults >= 0 && users.length >= maxResults) {
        users.length = maxResults;
        if (users.length) {
            newPageToken = users[users.length - 1].localId;
        }
    }
    return {
        kind: "identitytoolkit#DownloadAccountResponse",
        users,
        nextPageToken: newPageToken,
    };
}
function createAuthUri(state, reqBody) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    const sessionId = reqBody.sessionId || (0, utils_1.randomId)(27);
    if (reqBody.providerId) {
        throw new errors_1.NotImplementedError("Sign-in with IDP is not yet supported.");
    }
    (0, errors_1.assert)(reqBody.identifier, "MISSING_IDENTIFIER");
    (0, errors_1.assert)(reqBody.continueUri, "MISSING_CONTINUE_URI");
    (0, errors_1.assert)((0, utils_1.isValidEmailAddress)(reqBody.identifier), "INVALID_IDENTIFIER");
    const email = (0, utils_1.canonicalizeEmailAddress)(reqBody.identifier);
    (0, errors_1.assert)((0, utils_1.parseAbsoluteUri)(reqBody.continueUri), "INVALID_CONTINUE_URI");
    const allProviders = [];
    const signinMethods = [];
    let registered = false;
    const users = state.getUsersByEmailOrProviderEmail(email);
    if (state.oneAccountPerEmail) {
        if (users.length) {
            registered = true;
            (_a = users[0].providerUserInfo) === null || _a === void 0 ? void 0 : _a.forEach(({ providerId }) => {
                if (providerId === state_1.PROVIDER_PASSWORD) {
                    allProviders.push(providerId);
                    if (users[0].passwordHash) {
                        signinMethods.push(state_1.PROVIDER_PASSWORD);
                    }
                    if (users[0].emailLinkSignin) {
                        signinMethods.push(state_1.SIGNIN_METHOD_EMAIL_LINK);
                    }
                }
                else if (providerId !== state_1.PROVIDER_PHONE) {
                    allProviders.push(providerId);
                    signinMethods.push(providerId);
                }
            });
        }
    }
    else {
        const user = users.find((u) => u.email);
        if (user) {
            registered = true;
            if (user.passwordHash || user.emailLinkSignin) {
                allProviders.push(state_1.PROVIDER_PASSWORD);
                if (users[0].passwordHash) {
                    signinMethods.push(state_1.PROVIDER_PASSWORD);
                }
                if (users[0].emailLinkSignin) {
                    signinMethods.push(state_1.SIGNIN_METHOD_EMAIL_LINK);
                }
            }
        }
    }
    if (state.enableImprovedEmailPrivacy) {
        return {
            kind: "identitytoolkit#CreateAuthUriResponse",
            sessionId,
        };
    }
    else {
        return {
            kind: "identitytoolkit#CreateAuthUriResponse",
            registered,
            allProviders,
            sessionId,
            signinMethods,
        };
    }
}
const SESSION_COOKIE_MIN_VALID_DURATION = 5 * 60;
exports.SESSION_COOKIE_MAX_VALID_DURATION = 14 * 24 * 60 * 60;
function createSessionCookie(state, reqBody) {
    (0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
    const validDuration = Number(reqBody.validDuration) || exports.SESSION_COOKIE_MAX_VALID_DURATION;
    (0, errors_1.assert)(validDuration >= SESSION_COOKIE_MIN_VALID_DURATION &&
        validDuration <= exports.SESSION_COOKIE_MAX_VALID_DURATION, "INVALID_DURATION");
    const { payload } = parseIdToken(state, reqBody.idToken);
    const issuedAt = (0, utils_1.toUnixTimestamp)(new Date());
    const expiresAt = issuedAt + validDuration;
    const sessionCookie = (0, jsonwebtoken_1.sign)(Object.assign(Object.assign({}, payload), { iat: issuedAt, exp: expiresAt, iss: `https://session.firebase.google.com/${payload.aud}` }), "fake-secret", {
        algorithm: "none",
    });
    return { sessionCookie };
}
function deleteAccount(state, reqBody, ctx) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    let user;
    if ((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2) {
        (0, errors_1.assert)(reqBody.localId, "MISSING_LOCAL_ID");
        const maybeUser = state.getUserByLocalId(reqBody.localId);
        (0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
        user = maybeUser;
    }
    else {
        (0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
        user = parseIdToken(state, reqBody.idToken).user;
    }
    state.deleteUser(user);
    return {
        kind: "identitytoolkit#DeleteAccountResponse",
    };
}
function getProjects(state) {
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(state instanceof state_1.AgentProjectState, "UNSUPPORTED_TENANT_OPERATION");
    return {
        projectId: state.projectNumber,
        authorizedDomains: [
            "localhost",
        ],
    };
}
function getRecaptchaParams(state) {
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    return {
        kind: "identitytoolkit#GetRecaptchaParamResponse",
        recaptchaStoken: "This-is-a-fake-token__Dont-send-this-to-the-Recaptcha-service__The-Auth-Emulator-does-not-support-Recaptcha",
        recaptchaSiteKey: "Fake-key__Do-not-send-this-to-Recaptcha_",
    };
}
function queryAccounts(state, reqBody) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    if ((_a = reqBody.expression) === null || _a === void 0 ? void 0 : _a.length) {
        throw new errors_1.NotImplementedError("expression is not implemented.");
    }
    if (reqBody.returnUserInfo === false) {
        return {
            recordsCount: state.getUserCount().toString(),
        };
    }
    if (reqBody.limit) {
        throw new errors_1.NotImplementedError("limit is not implemented.");
    }
    reqBody.offset = reqBody.offset || "0";
    if (reqBody.offset !== "0") {
        throw new errors_1.NotImplementedError("offset is not implemented.");
    }
    if (!reqBody.order || reqBody.order === "ORDER_UNSPECIFIED") {
        reqBody.order = "ASC";
    }
    if (!reqBody.sortBy || reqBody.sortBy === "SORT_BY_FIELD_UNSPECIFIED") {
        reqBody.sortBy = "USER_ID";
    }
    let sortByField;
    if (reqBody.sortBy === "USER_ID") {
        sortByField = "localId";
    }
    else {
        throw new errors_1.NotImplementedError("Only sorting by USER_ID is implemented.");
    }
    const users = state.queryUsers({}, { order: reqBody.order, sortByField });
    return {
        recordsCount: users.length.toString(),
        userInfo: users,
    };
}
function resetPassword(state, reqBody) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(state.allowPasswordSignup, "PASSWORD_LOGIN_DISABLED");
    (0, errors_1.assert)(reqBody.oobCode, "MISSING_OOB_CODE");
    const oob = state.validateOobCode(reqBody.oobCode);
    (0, errors_1.assert)(oob, "INVALID_OOB_CODE");
    if (reqBody.newPassword) {
        (0, errors_1.assert)(oob.requestType === "PASSWORD_RESET", "INVALID_OOB_CODE");
        (0, errors_1.assert)(reqBody.newPassword.length >= PASSWORD_MIN_LENGTH, `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`);
        state.deleteOobCode(reqBody.oobCode);
        let user = state.getUserByEmail(oob.email);
        (0, errors_1.assert)(user, "INVALID_OOB_CODE");
        const salt = "fakeSalt" + (0, utils_1.randomId)(20);
        const passwordHash = hashPassword(reqBody.newPassword, salt);
        user = state.updateUserByLocalId(user.localId, {
            emailVerified: true,
            passwordHash,
            salt,
            passwordUpdatedAt: Date.now(),
            validSince: (0, utils_1.toUnixTimestamp)(new Date()).toString(),
        }, { deleteProviders: (_a = user.providerUserInfo) === null || _a === void 0 ? void 0 : _a.map((info) => info.providerId) });
    }
    return {
        kind: "identitytoolkit#ResetPasswordResponse",
        requestType: oob.requestType,
        email: oob.requestType === "EMAIL_SIGNIN" ? undefined : oob.email,
        newEmail: oob.newEmail,
    };
}
exports.resetPassword = resetPassword;
function sendOobCode(state, reqBody, ctx) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(reqBody.requestType && reqBody.requestType !== "OOB_REQ_TYPE_UNSPECIFIED", "MISSING_REQ_TYPE");
    if (reqBody.returnOobLink) {
        (0, errors_1.assert)((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2, "INSUFFICIENT_PERMISSION");
    }
    if (reqBody.continueUrl) {
        (0, errors_1.assert)((0, utils_1.parseAbsoluteUri)(reqBody.continueUrl), "INVALID_CONTINUE_URI : ((expected an absolute URI with valid scheme and host))");
    }
    let email;
    let newEmail;
    let mode;
    switch (reqBody.requestType) {
        case "EMAIL_SIGNIN":
            (0, errors_1.assert)(state.enableEmailLinkSignin, "OPERATION_NOT_ALLOWED");
            mode = "signIn";
            (0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
            email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
            break;
        case "PASSWORD_RESET":
            mode = "resetPassword";
            (0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
            email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
            const maybeUser = state.getUserByEmail(email);
            if (state.enableImprovedEmailPrivacy && !maybeUser) {
                return {
                    kind: "identitytoolkit#GetOobConfirmationCodeResponse",
                    email,
                };
            }
            (0, errors_1.assert)(maybeUser, "EMAIL_NOT_FOUND");
            break;
        case "VERIFY_EMAIL":
            mode = "verifyEmail";
            if (reqBody.returnOobLink && !reqBody.idToken) {
                (0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
                email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
                const maybeUser = state.getUserByEmail(email);
                (0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
            }
            else {
                const user = parseIdToken(state, reqBody.idToken || "").user;
                (0, errors_1.assert)(user.email, "MISSING_EMAIL");
                email = user.email;
            }
            break;
        case "VERIFY_AND_CHANGE_EMAIL":
            mode = "verifyAndChangeEmail";
            (0, errors_1.assert)(reqBody.newEmail, "MISSING_NEW_EMAIL");
            newEmail = (0, utils_1.canonicalizeEmailAddress)(reqBody.newEmail);
            if (reqBody.returnOobLink && !reqBody.idToken) {
                (0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
                email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
                const maybeUser = state.getUserByEmail(email);
                (0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
            }
            else {
                (0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
                const user = parseIdToken(state, reqBody.idToken).user;
                (0, errors_1.assert)(user.email, "MISSING_EMAIL");
                email = user.email;
            }
            (0, errors_1.assert)(!state.getUserByEmail(newEmail), "EMAIL_EXISTS");
            break;
        default:
            throw new errors_1.NotImplementedError(reqBody.requestType);
    }
    if (reqBody.canHandleCodeInApp) {
        emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("WARN", "canHandleCodeInApp is unsupported in Auth Emulator. All OOB operations will complete via web.");
    }
    const url = (0, utils_1.authEmulatorUrl)(ctx.req);
    const oobRecord = createOobRecord(state, email, url, {
        requestType: reqBody.requestType,
        mode,
        continueUrl: reqBody.continueUrl,
        newEmail,
    });
    if (reqBody.returnOobLink) {
        return {
            kind: "identitytoolkit#GetOobConfirmationCodeResponse",
            email,
            oobCode: oobRecord.oobCode,
            oobLink: oobRecord.oobLink,
        };
    }
    else {
        logOobMessage(oobRecord);
        return {
            kind: "identitytoolkit#GetOobConfirmationCodeResponse",
            email,
        };
    }
}
function sendVerificationCode(state, reqBody) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(state instanceof state_1.AgentProjectState, "UNSUPPORTED_TENANT_OPERATION");
    (0, errors_1.assert)(reqBody.phoneNumber && (0, utils_1.isValidPhoneNumber)(reqBody.phoneNumber), "INVALID_PHONE_NUMBER : Invalid format.");
    const user = state.getUserByPhoneNumber(reqBody.phoneNumber);
    (0, errors_1.assert)(!((_a = user === null || user === void 0 ? void 0 : user.mfaInfo) === null || _a === void 0 ? void 0 : _a.length), "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user.");
    const { sessionInfo, phoneNumber, code } = state.createVerificationCode(reqBody.phoneNumber);
    emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("BULLET", `To verify the phone number ${phoneNumber}, use the code ${code}.`);
    return {
        sessionInfo,
    };
}
function setAccountInfo(state, reqBody, ctx) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    const url = (0, utils_1.authEmulatorUrl)(ctx.req);
    return setAccountInfoImpl(state, reqBody, {
        privileged: !!((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2),
        emulatorUrl: url,
    });
}
function setAccountInfoImpl(state, reqBody, { privileged = false, emulatorUrl = undefined } = {}) {
    var _a, _b;
    const unimplementedFields = ["provider", "upgradeToFederatedLogin"];
    for (const field of unimplementedFields) {
        if (field in reqBody) {
            throw new errors_1.NotImplementedError(`${field} is not implemented yet.`);
        }
    }
    if (!privileged) {
        (0, errors_1.assert)(reqBody.idToken || reqBody.oobCode, "INVALID_REQ_TYPE : Unsupported request parameters.");
        (0, errors_1.assert)(reqBody.customAttributes == null, "INSUFFICIENT_PERMISSION");
    }
    else {
        (0, errors_1.assert)(reqBody.localId, "MISSING_LOCAL_ID");
    }
    if (reqBody.customAttributes) {
        validateSerializedCustomClaims(reqBody.customAttributes);
    }
    reqBody.deleteAttribute = reqBody.deleteAttribute || [];
    for (const attr of reqBody.deleteAttribute) {
        if (attr === "PROVIDER" || attr === "RAW_USER_INFO") {
            throw new errors_1.NotImplementedError(`deleteAttribute: ${attr}`);
        }
    }
    const updates = {};
    let user;
    let signInProvider;
    let isEmailUpdate = false;
    let newEmail;
    if (reqBody.oobCode) {
        const oob = state.validateOobCode(reqBody.oobCode);
        (0, errors_1.assert)(oob, "INVALID_OOB_CODE");
        switch (oob.requestType) {
            case "VERIFY_EMAIL": {
                state.deleteOobCode(reqBody.oobCode);
                signInProvider = state_1.PROVIDER_PASSWORD;
                const maybeUser = state.getUserByEmail(oob.email);
                (0, errors_1.assert)(maybeUser, "INVALID_OOB_CODE");
                user = maybeUser;
                updates.emailVerified = true;
                if (oob.email !== user.email) {
                    updates.email = oob.email;
                }
                break;
            }
            case "VERIFY_AND_CHANGE_EMAIL":
                state.deleteOobCode(reqBody.oobCode);
                const maybeUser = state.getUserByEmail(oob.email);
                (0, errors_1.assert)(maybeUser, "INVALID_OOB_CODE");
                (0, errors_1.assert)(oob.newEmail, "INVALID_OOB_CODE");
                (0, errors_1.assert)(!state.getUserByEmail(oob.newEmail), "EMAIL_EXISTS");
                user = maybeUser;
                if (oob.newEmail !== user.email) {
                    updates.email = oob.newEmail;
                    updates.emailVerified = true;
                    newEmail = oob.newEmail;
                }
                break;
            case "RECOVER_EMAIL": {
                state.deleteOobCode(reqBody.oobCode);
                const maybeUser = state.getUserByInitialEmail(oob.email);
                (0, errors_1.assert)(maybeUser, "INVALID_OOB_CODE");
                (0, errors_1.assert)(!state.getUserByEmail(oob.email), "EMAIL_EXISTS");
                user = maybeUser;
                if (oob.email !== user.email) {
                    updates.email = oob.email;
                    updates.emailVerified = true;
                }
                break;
            }
            default:
                throw new errors_1.NotImplementedError(oob.requestType);
        }
    }
    else {
        if (reqBody.idToken) {
            ({ user, signInProvider } = parseIdToken(state, reqBody.idToken));
            (0, errors_1.assert)(reqBody.disableUser == null, "OPERATION_NOT_ALLOWED");
        }
        else {
            (0, errors_1.assert)(reqBody.localId, "MISSING_LOCAL_ID");
            const maybeUser = state.getUserByLocalId(reqBody.localId);
            (0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
            user = maybeUser;
        }
        if (reqBody.email) {
            (0, errors_1.assert)((0, utils_1.isValidEmailAddress)(reqBody.email), "INVALID_EMAIL");
            newEmail = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
            if (newEmail !== user.email) {
                (0, errors_1.assert)(!state.getUserByEmail(newEmail), "EMAIL_EXISTS");
                updates.email = newEmail;
                updates.emailVerified = false;
                isEmailUpdate = true;
                if (signInProvider !== state_1.PROVIDER_ANONYMOUS && user.email && !user.initialEmail) {
                    updates.initialEmail = user.email;
                }
            }
        }
        if (reqBody.password) {
            (0, errors_1.assert)(reqBody.password.length >= PASSWORD_MIN_LENGTH, `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`);
            updates.salt = "fakeSalt" + (0, utils_1.randomId)(20);
            updates.passwordHash = hashPassword(reqBody.password, updates.salt);
            updates.passwordUpdatedAt = Date.now();
            signInProvider = state_1.PROVIDER_PASSWORD;
        }
        if (reqBody.password || reqBody.validSince || updates.email) {
            updates.validSince = (0, utils_1.toUnixTimestamp)(new Date()).toString();
        }
        if (reqBody.mfa) {
            if (reqBody.mfa.enrollments && reqBody.mfa.enrollments.length > 0) {
                updates.mfaInfo = getMfaEnrollmentsFromRequest(state, reqBody.mfa.enrollments);
            }
            else {
                updates.mfaInfo = undefined;
            }
        }
        const fieldsToCopy = [
            "displayName",
            "photoUrl",
        ];
        if (privileged) {
            if (reqBody.disableUser != null) {
                updates.disabled = reqBody.disableUser;
            }
            if (reqBody.phoneNumber && reqBody.phoneNumber !== user.phoneNumber) {
                (0, errors_1.assert)((0, utils_1.isValidPhoneNumber)(reqBody.phoneNumber), "INVALID_PHONE_NUMBER : Invalid format.");
                (0, errors_1.assert)(!state.getUserByPhoneNumber(reqBody.phoneNumber), "PHONE_NUMBER_EXISTS");
                updates.phoneNumber = reqBody.phoneNumber;
            }
            fieldsToCopy.push("emailVerified", "customAttributes", "createdAt", "lastLoginAt", "validSince");
        }
        for (const field of fieldsToCopy) {
            if (reqBody[field] != null) {
                (0, utils_1.mirrorFieldTo)(updates, field, reqBody);
            }
        }
        for (const attr of reqBody.deleteAttribute) {
            switch (attr) {
                case "USER_ATTRIBUTE_NAME_UNSPECIFIED":
                    continue;
                case "DISPLAY_NAME":
                    updates.displayName = undefined;
                    break;
                case "PHOTO_URL":
                    updates.photoUrl = undefined;
                    break;
                case "PASSWORD":
                    updates.passwordHash = undefined;
                    updates.salt = undefined;
                    break;
                case "EMAIL":
                    updates.email = undefined;
                    updates.emailVerified = undefined;
                    updates.emailLinkSignin = undefined;
                    break;
            }
        }
        if ((_a = reqBody.deleteProvider) === null || _a === void 0 ? void 0 : _a.includes(state_1.PROVIDER_PASSWORD)) {
            updates.email = undefined;
            updates.emailVerified = undefined;
            updates.emailLinkSignin = undefined;
            updates.passwordHash = undefined;
            updates.salt = undefined;
        }
        if ((_b = reqBody.deleteProvider) === null || _b === void 0 ? void 0 : _b.includes(state_1.PROVIDER_PHONE)) {
            updates.phoneNumber = undefined;
        }
    }
    if (reqBody.linkProviderUserInfo) {
        (0, errors_1.assert)(reqBody.linkProviderUserInfo.providerId, "MISSING_PROVIDER_ID");
        (0, errors_1.assert)(reqBody.linkProviderUserInfo.rawId, "MISSING_RAW_ID");
    }
    user = state.updateUserByLocalId(user.localId, updates, {
        deleteProviders: reqBody.deleteProvider,
        upsertProviders: reqBody.linkProviderUserInfo
            ? [reqBody.linkProviderUserInfo]
            : undefined,
    });
    if (signInProvider !== state_1.PROVIDER_ANONYMOUS && user.initialEmail && isEmailUpdate) {
        if (!emulatorUrl) {
            throw new Error("Internal assertion error: missing emulatorUrl param");
        }
        sendOobForEmailReset(state, user.initialEmail, emulatorUrl);
    }
    return redactPasswordHash(Object.assign({ kind: "identitytoolkit#SetAccountInfoResponse", localId: user.localId, emailVerified: user.emailVerified, providerUserInfo: user.providerUserInfo, email: user.email, displayName: user.displayName, photoUrl: user.photoUrl, newEmail, passwordHash: user.passwordHash }, (updates.validSince && signInProvider ? issueTokens(state, user, signInProvider) : {})));
}
exports.setAccountInfoImpl = setAccountInfoImpl;
function sendOobForEmailReset(state, initialEmail, url) {
    const oobRecord = createOobRecord(state, initialEmail, url, {
        requestType: "RECOVER_EMAIL",
        mode: "recoverEmail",
    });
    logOobMessage(oobRecord);
}
function createOobRecord(state, email, url, params) {
    const oobRecord = state.createOob(email, params.newEmail, params.requestType, (oobCode) => {
        url.pathname = "/emulator/action";
        url.searchParams.set("mode", params.mode);
        url.searchParams.set("lang", "en");
        url.searchParams.set("oobCode", oobCode);
        url.searchParams.set("apiKey", "fake-api-key");
        if (params.continueUrl) {
            url.searchParams.set("continueUrl", params.continueUrl);
        }
        if (state instanceof state_1.TenantProjectState) {
            url.searchParams.set("tenantId", state.tenantId);
        }
        return url.toString();
    });
    return oobRecord;
}
function logOobMessage(oobRecord) {
    const oobLink = oobRecord.oobLink;
    const email = oobRecord.email;
    let maybeMessage;
    switch (oobRecord.requestType) {
        case "EMAIL_SIGNIN":
            maybeMessage = `To sign in as ${email}, follow this link: ${oobLink}`;
            break;
        case "PASSWORD_RESET":
            maybeMessage = `To reset the password for ${email}, follow this link: ${oobLink}&newPassword=NEW_PASSWORD_HERE`;
            break;
        case "VERIFY_EMAIL":
            maybeMessage = `To verify the email address ${email}, follow this link: ${oobLink}`;
            break;
        case "VERIFY_AND_CHANGE_EMAIL":
            maybeMessage = `To verify and change the email address from ${email} to ${oobRecord.newEmail}, follow this link: ${oobLink}`;
            break;
        case "RECOVER_EMAIL":
            maybeMessage = `To reset your email address to ${email}, follow this link: ${oobLink}`;
            break;
    }
    if (maybeMessage) {
        emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("BULLET", maybeMessage);
    }
}
function signInWithCustomToken(state, reqBody) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(reqBody.token, "MISSING_CUSTOM_TOKEN");
    let payload;
    if (reqBody.token.startsWith("{")) {
        try {
            payload = JSON.parse(reqBody.token);
        }
        catch (_b) {
            throw new errors_1.BadRequestError("INVALID_CUSTOM_TOKEN : ((Auth Emulator only accepts strict JSON or JWTs as fake custom tokens.))");
        }
    }
    else {
        const decoded = (0, jsonwebtoken_1.decode)(reqBody.token, { complete: true });
        if (state instanceof state_1.TenantProjectState) {
            (0, errors_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.payload.tenant_id) === state.tenantId, "TENANT_ID_MISMATCH");
        }
        (0, errors_1.assert)(decoded, "INVALID_CUSTOM_TOKEN : Invalid assertion format");
        if (decoded.header.alg !== "none") {
            emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("WARN", "Received a signed custom token. Auth Emulator does not validate JWTs and IS NOT SECURE");
        }
        (0, errors_1.assert)(decoded.payload.aud === exports.CUSTOM_TOKEN_AUDIENCE, `INVALID_CUSTOM_TOKEN : ((Invalid aud (audience): ${decoded.payload.aud} ` +
            "Note: Firebase ID Tokens / third-party tokens cannot be used with signInWithCustomToken.))");
        payload = decoded.payload;
    }
    const localId = (_a = coercePrimitiveToString(payload.uid)) !== null && _a !== void 0 ? _a : coercePrimitiveToString(payload.user_id);
    (0, errors_1.assert)(localId, "MISSING_IDENTIFIER");
    let extraClaims = {};
    if ("claims" in payload) {
        validateCustomClaims(payload.claims);
        extraClaims = payload.claims;
    }
    let user = state.getUserByLocalId(localId);
    const isNewUser = !user;
    const timestamp = new Date();
    const updates = {
        customAuth: true,
        lastLoginAt: timestamp.getTime().toString(),
        tenantId: state instanceof state_1.TenantProjectState ? state.tenantId : undefined,
    };
    if (user) {
        (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
        user = state.updateUserByLocalId(localId, updates);
    }
    else {
        updates.createdAt = timestamp.getTime().toString();
        user = state.createUserWithLocalId(localId, updates);
        if (!user) {
            throw new Error(`Internal assertion error: trying to create duplicate localId: ${localId}`);
        }
    }
    return Object.assign({ kind: "identitytoolkit#VerifyCustomTokenResponse", isNewUser }, issueTokens(state, user, state_1.PROVIDER_CUSTOM, { extraClaims }));
}
async function signInWithEmailLink(state, reqBody) {
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(state.enableEmailLinkSignin, "OPERATION_NOT_ALLOWED");
    const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined;
    (0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
    const email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
    (0, errors_1.assert)(reqBody.oobCode, "MISSING_OOB_CODE");
    const oob = state.validateOobCode(reqBody.oobCode);
    (0, errors_1.assert)(oob && oob.requestType === "EMAIL_SIGNIN", "INVALID_OOB_CODE");
    (0, errors_1.assert)(email === oob.email, "INVALID_EMAIL : The email provided does not match the sign-in email address.");
    state.deleteOobCode(reqBody.oobCode);
    const userFromEmail = state.getUserByEmail(email);
    let user = userFromIdToken || userFromEmail;
    const isNewUser = !user;
    const timestamp = new Date();
    let updates = {
        email,
        emailVerified: true,
        emailLinkSignin: true,
    };
    if (state instanceof state_1.TenantProjectState) {
        updates.tenantId = state.tenantId;
    }
    let extraClaims;
    if (!user) {
        updates.createdAt = timestamp.getTime().toString();
        const localId = state.generateLocalId();
        const userBeforeCreate = Object.assign({ localId }, updates);
        const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_CREATE, userBeforeCreate, { signInMethod: "emailLink" });
        updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
        user = state.createUserWithLocalId(localId, updates);
        if (!user.disabled && !isMfaEnabled(state, user)) {
            const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, { signInMethod: "emailLink" });
            updates = blockingResponse.updates;
            extraClaims = blockingResponse.extraClaims;
            user = state.updateUserByLocalId(user.localId, updates);
        }
    }
    else {
        (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
        if (userFromIdToken && userFromEmail) {
            (0, errors_1.assert)(userFromIdToken.localId === userFromEmail.localId, "EMAIL_EXISTS");
        }
        if (!user.disabled && !isMfaEnabled(state, user)) {
            const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, Object.assign(Object.assign({}, user), updates), { signInMethod: "emailLink" });
            updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
            extraClaims = blockingResponse.extraClaims;
        }
        user = state.updateUserByLocalId(user.localId, updates);
    }
    const response = {
        kind: "identitytoolkit#EmailLinkSigninResponse",
        email,
        localId: user.localId,
        isNewUser,
    };
    (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
    if (isMfaEnabled(state, user)) {
        return Object.assign(Object.assign({}, response), mfaPending(state, user, state_1.PROVIDER_PASSWORD));
    }
    else {
        user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() });
        return Object.assign(Object.assign({}, response), issueTokens(state, user, state_1.PROVIDER_PASSWORD, { extraClaims }));
    }
}
async function signInWithIdp(state, reqBody) {
    var _a, _b;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    if (reqBody.returnRefreshToken) {
        throw new errors_1.NotImplementedError("returnRefreshToken is not implemented yet.");
    }
    if (reqBody.pendingIdToken) {
        throw new errors_1.NotImplementedError("pendingIdToken is not implemented yet.");
    }
    const normalizedUri = getNormalizedUri(reqBody);
    const providerId = (_a = normalizedUri.searchParams.get("providerId")) === null || _a === void 0 ? void 0 : _a.toLowerCase();
    (0, errors_1.assert)(providerId, `INVALID_CREDENTIAL_OR_PROVIDER_ID : Invalid IdP response/credential: ${normalizedUri.toString()}`);
    const oauthIdToken = normalizedUri.searchParams.get("id_token") || undefined;
    const oauthAccessToken = normalizedUri.searchParams.get("access_token") || undefined;
    const claims = parseClaims(oauthIdToken) || parseClaims(oauthAccessToken);
    if (!claims) {
        if (oauthIdToken) {
            throw new errors_1.BadRequestError(`INVALID_IDP_RESPONSE : Unable to parse id_token: ${oauthIdToken} ((Auth Emulator only accepts strict JSON or JWTs as fake id_tokens.))`);
        }
        else if (oauthAccessToken) {
            if (providerId === "google.com" || providerId === "apple.com") {
                throw new errors_1.NotImplementedError(`The Auth Emulator only support sign-in with ${providerId} using id_token, not access_token. Please update your code to use id_token.`);
            }
            else {
                throw new errors_1.NotImplementedError(`The Auth Emulator does not support ${providerId} sign-in with credentials.`);
            }
        }
        else {
            throw new errors_1.NotImplementedError("The Auth Emulator only supports sign-in with credentials (id_token required).");
        }
    }
    let samlResponse;
    let signInAttributes = undefined;
    if (normalizedUri.searchParams.get("SAMLResponse")) {
        samlResponse = JSON.parse(normalizedUri.searchParams.get("SAMLResponse"));
        signInAttributes = (_b = samlResponse.assertion) === null || _b === void 0 ? void 0 : _b.attributeStatements;
        (0, errors_1.assert)(samlResponse.assertion, "INVALID_IDP_RESPONSE ((Missing assertion in SAMLResponse.))");
        (0, errors_1.assert)(samlResponse.assertion.subject, "INVALID_IDP_RESPONSE ((Missing assertion.subject in SAMLResponse.))");
        (0, errors_1.assert)(samlResponse.assertion.subject.nameId, "INVALID_IDP_RESPONSE ((Missing assertion.subject.nameId in SAMLResponse.))");
    }
    let { response, rawId } = fakeFetchUserInfoFromIdp(providerId, claims, samlResponse);
    response.oauthAccessToken =
        oauthAccessToken || `FirebaseAuthEmulatorFakeAccessToken_${providerId}`;
    response.oauthIdToken = oauthIdToken;
    const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined;
    const userMatchingProvider = state.getUserByProviderRawId(providerId, rawId);
    let accountUpdates;
    try {
        if (userFromIdToken) {
            (0, errors_1.assert)(!userMatchingProvider, "FEDERATED_USER_ID_ALREADY_LINKED");
            ({ accountUpdates, response } = handleLinkIdp(state, response, userFromIdToken));
        }
        else if (state.oneAccountPerEmail) {
            const userMatchingEmail = response.email ? state.getUserByEmail(response.email) : undefined;
            ({ accountUpdates, response } = handleIdpSigninEmailRequired(response, rawId, userMatchingProvider, userMatchingEmail));
        }
        else {
            ({ accountUpdates, response } = handleIdpSigninEmailNotRequired(response, userMatchingProvider));
        }
    }
    catch (err) {
        if (reqBody.returnIdpCredential && err instanceof errors_1.BadRequestError) {
            response.errorMessage = err.message;
            return response;
        }
        else {
            throw err;
        }
    }
    if (response.needConfirmation) {
        return response;
    }
    const providerUserInfo = {
        providerId,
        rawId,
        federatedId: rawId,
        displayName: response.displayName,
        photoUrl: response.photoUrl,
        email: response.email,
        screenName: response.screenName,
    };
    let user;
    let extraClaims;
    const oauthTokens = {
        oauthIdToken: response.oauthIdToken,
        oauthAccessToken: response.oauthAccessToken,
        oauthRefreshToken: response.oauthRefreshToken,
        oauthTokenSecret: response.oauthTokenSecret,
        oauthExpiresIn: coercePrimitiveToString(response.oauthExpireIn),
    };
    if (response.isNewUser) {
        const timestamp = new Date();
        let updates = Object.assign(Object.assign({}, accountUpdates.fields), { createdAt: timestamp.getTime().toString(), lastLoginAt: timestamp.getTime().toString(), providerUserInfo: [providerUserInfo], tenantId: state instanceof state_1.TenantProjectState ? state.tenantId : undefined });
        const localId = state.generateLocalId();
        const userBeforeCreate = Object.assign({ localId }, updates);
        const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_CREATE, userBeforeCreate, {
            signInMethod: response.providerId,
            rawUserInfo: response.rawUserInfo,
            signInAttributes: JSON.stringify(signInAttributes),
        }, oauthTokens);
        updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
        user = state.createUserWithLocalId(localId, updates);
        response.localId = user.localId;
        if (!user.disabled && !isMfaEnabled(state, user)) {
            const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, {
                signInMethod: response.providerId,
                rawUserInfo: response.rawUserInfo,
                signInAttributes: JSON.stringify(signInAttributes),
            }, oauthTokens);
            updates = blockingResponse.updates;
            extraClaims = blockingResponse.extraClaims;
            user = state.updateUserByLocalId(user.localId, updates);
        }
    }
    else {
        if (!response.localId) {
            throw new Error("Internal assertion error: localId not set for existing user.");
        }
        const maybeUser = state.getUserByLocalId(response.localId);
        (0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
        user = maybeUser;
        let updates = Object.assign({}, accountUpdates.fields);
        if (!user.disabled && !isMfaEnabled(state, user)) {
            const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, Object.assign(Object.assign({}, user), updates), {
                signInMethod: response.providerId,
                rawUserInfo: response.rawUserInfo,
                signInAttributes: JSON.stringify(signInAttributes),
            }, oauthTokens);
            extraClaims = blockingResponse.extraClaims;
            updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
        }
        user = state.updateUserByLocalId(response.localId, updates, {
            upsertProviders: [providerUserInfo],
        });
    }
    if (user.email === response.email) {
        response.emailVerified = user.emailVerified;
    }
    if (state instanceof state_1.TenantProjectState) {
        response.tenantId = state.tenantId;
    }
    if (isMfaEnabled(state, user)) {
        return Object.assign(Object.assign({}, response), mfaPending(state, user, providerId));
    }
    else {
        user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() });
        (0, errors_1.assert)(!(user === null || user === void 0 ? void 0 : user.disabled), "USER_DISABLED");
        return Object.assign(Object.assign({}, response), issueTokens(state, user, providerId, { signInAttributes, extraClaims }));
    }
}
async function signInWithPassword(state, reqBody) {
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(state.allowPasswordSignup, "PASSWORD_LOGIN_DISABLED");
    (0, errors_1.assert)(reqBody.email !== undefined, "MISSING_EMAIL");
    (0, errors_1.assert)((0, utils_1.isValidEmailAddress)(reqBody.email), "INVALID_EMAIL");
    (0, errors_1.assert)(reqBody.password, "MISSING_PASSWORD");
    if (reqBody.captchaResponse || reqBody.captchaChallenge) {
        throw new errors_1.NotImplementedError("captcha unimplemented");
    }
    if (reqBody.idToken || reqBody.pendingIdToken) {
        throw new errors_1.NotImplementedError("idToken / pendingIdToken is no longer in use and unsupported by the Auth Emulator.");
    }
    const email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
    let user = state.getUserByEmail(email);
    if (state.enableImprovedEmailPrivacy) {
        (0, errors_1.assert)(user, "INVALID_LOGIN_CREDENTIALS");
        (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
        (0, errors_1.assert)(user.passwordHash && user.salt, "INVALID_LOGIN_CREDENTIALS");
        (0, errors_1.assert)(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_LOGIN_CREDENTIALS");
    }
    else {
        (0, errors_1.assert)(user, "EMAIL_NOT_FOUND");
        (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
        (0, errors_1.assert)(user.passwordHash && user.salt, "INVALID_PASSWORD");
        (0, errors_1.assert)(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_PASSWORD");
    }
    const response = {
        kind: "identitytoolkit#VerifyPasswordResponse",
        registered: true,
        localId: user.localId,
        email,
    };
    if (isMfaEnabled(state, user)) {
        return Object.assign(Object.assign({}, response), mfaPending(state, user, state_1.PROVIDER_PASSWORD));
    }
    else {
        const { updates, extraClaims } = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, { signInMethod: "password" });
        user = state.updateUserByLocalId(user.localId, Object.assign(Object.assign({}, updates), { lastLoginAt: Date.now().toString() }));
        (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
        return Object.assign(Object.assign({}, response), issueTokens(state, user, state_1.PROVIDER_PASSWORD, { extraClaims }));
    }
}
async function signInWithPhoneNumber(state, reqBody) {
    var _a;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(state instanceof state_1.AgentProjectState, "UNSUPPORTED_TENANT_OPERATION");
    let phoneNumber;
    if (reqBody.temporaryProof) {
        (0, errors_1.assert)(reqBody.phoneNumber, "MISSING_PHONE_NUMBER");
        const proof = state.validateTemporaryProof(reqBody.temporaryProof, reqBody.phoneNumber);
        (0, errors_1.assert)(proof, "INVALID_TEMPORARY_PROOF");
        ({ phoneNumber } = proof);
    }
    else {
        (0, errors_1.assert)(reqBody.sessionInfo, "MISSING_SESSION_INFO");
        (0, errors_1.assert)(reqBody.code, "MISSING_CODE");
        phoneNumber = verifyPhoneNumber(state, reqBody.sessionInfo, reqBody.code);
    }
    const userFromPhoneNumber = state.getUserByPhoneNumber(phoneNumber);
    const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined;
    if (userFromPhoneNumber && userFromIdToken) {
        if (userFromPhoneNumber.localId !== userFromIdToken.localId) {
            (0, errors_1.assert)(!reqBody.temporaryProof, "PHONE_NUMBER_EXISTS");
            return Object.assign({}, state.createTemporaryProof(phoneNumber));
        }
    }
    let user = userFromIdToken || userFromPhoneNumber;
    const isNewUser = !user;
    const timestamp = new Date();
    let updates = {
        phoneNumber,
        lastLoginAt: timestamp.getTime().toString(),
    };
    let extraClaims;
    if (!user) {
        updates.createdAt = timestamp.getTime().toString();
        const localId = state.generateLocalId();
        const userBeforeCreate = Object.assign({ localId }, updates);
        const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_CREATE, userBeforeCreate, { signInMethod: "phone" });
        updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
        user = state.createUserWithLocalId(localId, updates);
        if (!user.disabled) {
            const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, { signInMethod: "phone" });
            updates = blockingResponse.updates;
            extraClaims = blockingResponse.extraClaims;
            user = state.updateUserByLocalId(user.localId, updates);
        }
    }
    else {
        (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
        (0, errors_1.assert)(!((_a = user.mfaInfo) === null || _a === void 0 ? void 0 : _a.length), "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user.");
        if (!user.disabled) {
            const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, Object.assign(Object.assign({}, user), updates), { signInMethod: "phone" });
            updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
            extraClaims = blockingResponse.extraClaims;
        }
        user = state.updateUserByLocalId(user.localId, updates);
    }
    (0, errors_1.assert)(!(user === null || user === void 0 ? void 0 : user.disabled), "USER_DISABLED");
    const tokens = issueTokens(state, user, state_1.PROVIDER_PHONE, {
        extraClaims,
    });
    return Object.assign({ isNewUser,
        phoneNumber, localId: user.localId }, tokens);
}
function grantToken(state, reqBody) {
    (0, errors_1.assert)(reqBody.grantType, "MISSING_GRANT_TYPE");
    (0, errors_1.assert)(reqBody.grantType === "refresh_token", "INVALID_GRANT_TYPE");
    (0, errors_1.assert)(reqBody.refreshToken, "MISSING_REFRESH_TOKEN");
    const refreshTokenRecord = state.validateRefreshToken(reqBody.refreshToken);
    (0, errors_1.assert)(!refreshTokenRecord.user.disabled, "USER_DISABLED");
    const tokens = issueTokens(state, refreshTokenRecord.user, refreshTokenRecord.provider, {
        extraClaims: refreshTokenRecord.extraClaims,
        secondFactor: refreshTokenRecord.secondFactor,
    });
    return {
        id_token: tokens.idToken,
        access_token: tokens.idToken,
        expires_in: tokens.expiresIn,
        refresh_token: tokens.refreshToken,
        token_type: "Bearer",
        user_id: refreshTokenRecord.user.localId,
        project_id: state.projectNumber,
    };
}
function deleteAllAccountsInProject(state) {
    state.deleteAllAccounts();
    return {};
}
function getEmulatorProjectConfig(state) {
    return {
        signIn: {
            allowDuplicateEmails: !state.oneAccountPerEmail,
        },
        emailPrivacyConfig: {
            enableImprovedEmailPrivacy: state.enableImprovedEmailPrivacy,
        },
    };
}
function updateEmulatorProjectConfig(state, reqBody, ctx) {
    var _a, _b;
    const updateMask = [];
    if (((_a = reqBody.signIn) === null || _a === void 0 ? void 0 : _a.allowDuplicateEmails) != null) {
        updateMask.push("signIn.allowDuplicateEmails");
    }
    if (((_b = reqBody.emailPrivacyConfig) === null || _b === void 0 ? void 0 : _b.enableImprovedEmailPrivacy) != null) {
        updateMask.push("emailPrivacyConfig.enableImprovedEmailPrivacy");
    }
    ctx.params.query.updateMask = updateMask.join();
    updateConfig(state, reqBody, ctx);
    return getEmulatorProjectConfig(state);
}
function listOobCodesInProject(state) {
    return {
        oobCodes: [...state.listOobCodes()],
    };
}
function listVerificationCodesInProject(state) {
    return {
        verificationCodes: [...state.listVerificationCodes()],
    };
}
function mfaEnrollmentStart(state, reqBody) {
    var _a, _b;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)((state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") &&
        ((_a = state.mfaConfig.enabledProviders) === null || _a === void 0 ? void 0 : _a.includes("PHONE_SMS")), "OPERATION_NOT_ALLOWED : SMS based MFA not enabled.");
    (0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
    const { user, signInProvider } = parseIdToken(state, reqBody.idToken);
    (0, errors_1.assert)(!MFA_INELIGIBLE_PROVIDER.has(signInProvider), "UNSUPPORTED_FIRST_FACTOR : MFA is not available for the given first factor.");
    (0, errors_1.assert)(user.emailVerified, "UNVERIFIED_EMAIL : Need to verify email first before enrolling second factors.");
    (0, errors_1.assert)(reqBody.phoneEnrollmentInfo, "INVALID_ARGUMENT : ((Missing phoneEnrollmentInfo.))");
    const phoneNumber = reqBody.phoneEnrollmentInfo.phoneNumber;
    (0, errors_1.assert)(phoneNumber && (0, utils_1.isValidPhoneNumber)(phoneNumber), "INVALID_PHONE_NUMBER : Invalid format.");
    (0, errors_1.assert)(!((_b = user.mfaInfo) === null || _b === void 0 ? void 0 : _b.some((enrollment) => enrollment.unobfuscatedPhoneInfo === phoneNumber)), "SECOND_FACTOR_EXISTS : Phone number already enrolled as second factor for this account.");
    const { sessionInfo, code } = state.createVerificationCode(phoneNumber);
    emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("BULLET", `To enroll MFA with ${phoneNumber}, use the code ${code}.`);
    return {
        phoneSessionInfo: {
            sessionInfo,
        },
    };
}
function mfaEnrollmentFinalize(state, reqBody) {
    var _a, _b;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)((state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") &&
        ((_a = state.mfaConfig.enabledProviders) === null || _a === void 0 ? void 0 : _a.includes("PHONE_SMS")), "OPERATION_NOT_ALLOWED : SMS based MFA not enabled.");
    (0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
    let { user, signInProvider } = parseIdToken(state, reqBody.idToken);
    (0, errors_1.assert)(!MFA_INELIGIBLE_PROVIDER.has(signInProvider), "UNSUPPORTED_FIRST_FACTOR : MFA is not available for the given first factor.");
    (0, errors_1.assert)(reqBody.phoneVerificationInfo, "INVALID_ARGUMENT : ((Missing phoneVerificationInfo.))");
    if (reqBody.phoneVerificationInfo.androidVerificationProof) {
        throw new errors_1.NotImplementedError("androidVerificationProof is unsupported!");
    }
    const { code, sessionInfo } = reqBody.phoneVerificationInfo;
    (0, errors_1.assert)(code, "MISSING_CODE");
    (0, errors_1.assert)(sessionInfo, "MISSING_SESSION_INFO");
    const phoneNumber = verifyPhoneNumber(state, sessionInfo, code);
    (0, errors_1.assert)(!((_b = user.mfaInfo) === null || _b === void 0 ? void 0 : _b.some((enrollment) => enrollment.unobfuscatedPhoneInfo === phoneNumber)), "SECOND_FACTOR_EXISTS : Phone number already enrolled as second factor for this account.");
    const existingFactors = user.mfaInfo || [];
    const existingIds = new Set();
    for (const { mfaEnrollmentId } of existingFactors) {
        if (mfaEnrollmentId) {
            existingIds.add(mfaEnrollmentId);
        }
    }
    const enrollment = {
        displayName: reqBody.displayName,
        enrolledAt: new Date().toISOString(),
        mfaEnrollmentId: newRandomId(28, existingIds),
        phoneInfo: phoneNumber,
        unobfuscatedPhoneInfo: phoneNumber,
    };
    user = state.updateUserByLocalId(user.localId, {
        mfaInfo: [...existingFactors, enrollment],
    });
    const { idToken, refreshToken } = issueTokens(state, user, signInProvider, {
        secondFactor: { identifier: enrollment.mfaEnrollmentId, provider: state_1.PROVIDER_PHONE },
    });
    return {
        idToken,
        refreshToken,
    };
}
function mfaEnrollmentWithdraw(state, reqBody) {
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
    let { user, signInProvider } = parseIdToken(state, reqBody.idToken);
    (0, errors_1.assert)(user.mfaInfo, "MFA_ENROLLMENT_NOT_FOUND");
    const updatedList = user.mfaInfo.filter((enrollment) => enrollment.mfaEnrollmentId !== reqBody.mfaEnrollmentId);
    (0, errors_1.assert)(updatedList.length < user.mfaInfo.length, "MFA_ENROLLMENT_NOT_FOUND");
    user = state.updateUserByLocalId(user.localId, { mfaInfo: updatedList });
    return Object.assign({}, issueTokens(state, user, signInProvider));
}
function mfaSignInStart(state, reqBody) {
    var _a, _b;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)((state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") &&
        ((_a = state.mfaConfig.enabledProviders) === null || _a === void 0 ? void 0 : _a.includes("PHONE_SMS")), "OPERATION_NOT_ALLOWED : SMS based MFA not enabled.");
    (0, errors_1.assert)(reqBody.mfaPendingCredential, "MISSING_MFA_PENDING_CREDENTIAL : Request does not have MFA pending credential.");
    (0, errors_1.assert)(reqBody.mfaEnrollmentId, "MISSING_MFA_ENROLLMENT_ID : No second factor identifier is provided.");
    const { user } = parsePendingCredential(state, reqBody.mfaPendingCredential);
    const enrollment = (_b = user.mfaInfo) === null || _b === void 0 ? void 0 : _b.find((factor) => factor.mfaEnrollmentId === reqBody.mfaEnrollmentId);
    (0, errors_1.assert)(enrollment, "MFA_ENROLLMENT_NOT_FOUND");
    const phoneNumber = enrollment.unobfuscatedPhoneInfo;
    (0, errors_1.assert)(phoneNumber, "INVALID_ARGUMENT : MFA provider not supported!");
    const { sessionInfo, code } = state.createVerificationCode(phoneNumber);
    emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("BULLET", `To sign in with MFA using ${phoneNumber}, use the code ${code}.`);
    return {
        phoneResponseInfo: {
            sessionInfo,
        },
    };
}
async function mfaSignInFinalize(state, reqBody) {
    var _a, _b;
    (0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
    (0, errors_1.assert)((state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") &&
        ((_a = state.mfaConfig.enabledProviders) === null || _a === void 0 ? void 0 : _a.includes("PHONE_SMS")), "OPERATION_NOT_ALLOWED : SMS based MFA not enabled.");
    (0, errors_1.assert)(reqBody.mfaPendingCredential, "MISSING_CREDENTIAL : Please set MFA Pending Credential.");
    (0, errors_1.assert)(reqBody.phoneVerificationInfo, "INVALID_ARGUMENT : MFA provider not supported!");
    if (reqBody.phoneVerificationInfo.androidVerificationProof) {
        throw new errors_1.NotImplementedError("androidVerificationProof is unsupported!");
    }
    const { code, sessionInfo } = reqBody.phoneVerificationInfo;
    (0, errors_1.assert)(code, "MISSING_CODE");
    (0, errors_1.assert)(sessionInfo, "MISSING_SESSION_INFO");
    const phoneNumber = verifyPhoneNumber(state, sessionInfo, code);
    let { user, signInProvider } = parsePendingCredential(state, reqBody.mfaPendingCredential);
    const enrollment = (_b = user.mfaInfo) === null || _b === void 0 ? void 0 : _b.find((enrollment) => enrollment.unobfuscatedPhoneInfo === phoneNumber);
    const { updates, extraClaims } = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, { signInMethod: signInProvider, signInSecondFactor: "phone" });
    user = state.updateUserByLocalId(user.localId, Object.assign(Object.assign({}, updates), { lastLoginAt: Date.now().toString() }));
    (0, errors_1.assert)(enrollment && enrollment.mfaEnrollmentId, "MFA_ENROLLMENT_NOT_FOUND");
    (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
    const { idToken, refreshToken } = issueTokens(state, user, signInProvider, {
        extraClaims,
        secondFactor: { identifier: enrollment.mfaEnrollmentId, provider: state_1.PROVIDER_PHONE },
    });
    return {
        idToken,
        refreshToken,
    };
}
function getConfig(state) {
    (0, errors_1.assert)(state instanceof state_1.AgentProjectState, "((Can only get top-level configurations on agent projects.))");
    return state.config;
}
function updateConfig(state, reqBody, ctx) {
    var _a;
    (0, errors_1.assert)(state instanceof state_1.AgentProjectState, "((Can only update top-level configurations on agent projects.))");
    for (const event in (_a = reqBody.blockingFunctions) === null || _a === void 0 ? void 0 : _a.triggers) {
        if (Object.prototype.hasOwnProperty.call(reqBody.blockingFunctions.triggers, event)) {
            (0, errors_1.assert)(Object.values(state_1.BlockingFunctionEvents).includes(event), "INVALID_BLOCKING_FUNCTION : ((Event type is invalid.))");
            (0, errors_1.assert)((0, utils_1.parseAbsoluteUri)(reqBody.blockingFunctions.triggers[event].functionUri), "INVALID_BLOCKING_FUNCTION : ((Expected an absolute URI with valid scheme and host.))");
        }
    }
    return state.updateConfig(reqBody, ctx.params.query.updateMask);
}
function coercePrimitiveToString(value) {
    switch (typeof value) {
        case "string":
            return value;
        case "number":
        case "boolean":
            return value.toString();
        default:
            return undefined;
    }
}
function redactPasswordHash(user) {
    return user;
}
function hashPassword(password, salt) {
    return `fakeHash:salt=${salt}:password=${password}`;
}
function issueTokens(state, user, signInProvider, { extraClaims, secondFactor, signInAttributes, } = {}) {
    user = state.updateUserByLocalId(user.localId, { lastRefreshAt: new Date().toISOString() });
    const tenantId = state instanceof state_1.TenantProjectState ? state.tenantId : undefined;
    const expiresInSeconds = 60 * 60;
    const idToken = generateJwt(user, {
        projectId: state.projectId,
        signInProvider,
        expiresInSeconds,
        extraClaims,
        secondFactor,
        tenantId,
        signInAttributes,
    });
    const refreshToken = state.createRefreshTokenFor(user, signInProvider, {
        extraClaims,
        secondFactor,
    });
    return {
        idToken,
        refreshToken,
        expiresIn: expiresInSeconds.toString(),
    };
}
function parseIdToken(state, idToken) {
    const decoded = (0, jsonwebtoken_1.decode)(idToken, { complete: true });
    (0, errors_1.assert)(decoded, "INVALID_ID_TOKEN");
    if (decoded.header.alg !== "none") {
        emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("WARN", "Received a signed JWT. Auth Emulator does not validate JWTs and IS NOT SECURE");
    }
    if (decoded.payload.firebase.tenant) {
        (0, errors_1.assert)(state instanceof state_1.TenantProjectState, "((Parsed token that belongs to tenant in a non-tenant project.))");
        (0, errors_1.assert)(decoded.payload.firebase.tenant === state.tenantId, "TENANT_ID_MISMATCH");
    }
    const user = state.getUserByLocalId(decoded.payload.user_id);
    (0, errors_1.assert)(user, "USER_NOT_FOUND");
    (0, errors_1.assert)(!user.validSince || decoded.payload.iat >= Number(user.validSince), "TOKEN_EXPIRED");
    (0, errors_1.assert)(!user.disabled, "USER_DISABLED");
    const signInProvider = decoded.payload.firebase.sign_in_provider;
    return { user, signInProvider, payload: decoded.payload };
}
function generateJwt(user, { projectId, signInProvider, expiresInSeconds, extraClaims = {}, secondFactor, tenantId, signInAttributes, }) {
    const identities = {};
    if (user.email) {
        identities["email"] = [user.email];
    }
    if (user.providerUserInfo) {
        for (const providerInfo of user.providerUserInfo) {
            if (providerInfo.providerId &&
                providerInfo.providerId !== state_1.PROVIDER_PASSWORD &&
                providerInfo.rawId) {
                const ids = identities[providerInfo.providerId] || [];
                ids.push(providerInfo.rawId);
                identities[providerInfo.providerId] = ids;
            }
        }
    }
    const customAttributes = JSON.parse(user.customAttributes || "{}");
    const customPayloadFields = Object.assign(Object.assign(Object.assign({ name: user.displayName, picture: user.photoUrl }, customAttributes), extraClaims), { email: user.email, email_verified: user.emailVerified, phone_number: user.phoneNumber, provider_id: signInProvider === "anonymous" ? signInProvider : undefined, auth_time: (0, utils_1.toUnixTimestamp)(getAuthTime(user)), user_id: user.localId, firebase: {
            identities,
            sign_in_provider: signInProvider,
            second_factor_identifier: secondFactor === null || secondFactor === void 0 ? void 0 : secondFactor.identifier,
            sign_in_second_factor: secondFactor === null || secondFactor === void 0 ? void 0 : secondFactor.provider,
            tenant: tenantId,
            sign_in_attributes: signInAttributes,
        } });
    const jwtStr = (0, jsonwebtoken_1.sign)(customPayloadFields, "fake-secret", {
        algorithm: "none",
        expiresIn: expiresInSeconds,
        subject: user.localId,
        issuer: `https://securetoken.google.com/${projectId}`,
        audience: projectId,
    });
    return jwtStr;
}
function getAuthTime(user) {
    if (user.lastLoginAt != null) {
        const millisSinceEpoch = parseInt(user.lastLoginAt, 10);
        const authTime = new Date(millisSinceEpoch);
        if (isNaN(authTime.getTime())) {
            throw new Error(`Internal assertion error: invalid user.lastLoginAt = ${user.lastLoginAt}`);
        }
        return authTime;
    }
    else if (user.lastRefreshAt != null) {
        const authTime = new Date(user.lastRefreshAt);
        if (isNaN(authTime.getTime())) {
            throw new Error(`Internal assertion error: invalid user.lastRefreshAt = ${user.lastRefreshAt}`);
        }
        return authTime;
    }
    else {
        throw new Error(`Internal assertion error: Missing user.lastLoginAt and user.lastRefreshAt`);
    }
}
function verifyPhoneNumber(state, sessionInfo, code) {
    const verification = state.getVerificationCodeBySessionInfo(sessionInfo);
    (0, errors_1.assert)(verification, "INVALID_SESSION_INFO");
    (0, errors_1.assert)(verification.code === code, "INVALID_CODE");
    state.deleteVerificationCodeBySessionInfo(sessionInfo);
    return verification.phoneNumber;
}
const CUSTOM_ATTRIBUTES_MAX_LENGTH = 1000;
function validateSerializedCustomClaims(claims) {
    (0, errors_1.assert)(claims.length <= CUSTOM_ATTRIBUTES_MAX_LENGTH, "CLAIMS_TOO_LARGE");
    let parsed;
    try {
        parsed = JSON.parse(claims);
    }
    catch (_a) {
        throw new errors_1.BadRequestError("INVALID_CLAIMS");
    }
    validateCustomClaims(parsed);
}
const FORBIDDEN_CUSTOM_CLAIMS = [
    "iss",
    "aud",
    "sub",
    "iat",
    "exp",
    "nbf",
    "jti",
    "nonce",
    "azp",
    "acr",
    "amr",
    "cnf",
    "auth_time",
    "firebase",
    "at_hash",
    "c_hash",
];
function validateCustomClaims(claims) {
    (0, errors_1.assert)(typeof claims === "object" && claims != null && !Array.isArray(claims), "INVALID_CLAIMS");
    for (const reservedField of FORBIDDEN_CUSTOM_CLAIMS) {
        (0, errors_1.assert)(!(reservedField in claims), `FORBIDDEN_CLAIM : ${reservedField}`);
    }
}
function newRandomId(length, existingIds) {
    for (let i = 0; i < 10; i++) {
        const id = (0, utils_1.randomId)(length);
        if (!(existingIds === null || existingIds === void 0 ? void 0 : existingIds.has(id))) {
            return id;
        }
    }
    throw new errors_1.InternalError("INTERNAL_ERROR : Failed to generate a random ID after 10 attempts", "INTERNAL");
}
function getMfaEnrollmentsFromRequest(state, request, options) {
    const enrollments = [];
    const phoneNumbers = new Set();
    const enrollmentIds = new Set();
    for (const enrollment of request) {
        (0, errors_1.assert)(enrollment.phoneInfo && (0, utils_1.isValidPhoneNumber)(enrollment.phoneInfo), "INVALID_MFA_PHONE_NUMBER : Invalid format.");
        if (!phoneNumbers.has(enrollment.phoneInfo)) {
            const mfaEnrollmentId = (options === null || options === void 0 ? void 0 : options.generateEnrollmentIds)
                ? newRandomId(28, enrollmentIds)
                : enrollment.mfaEnrollmentId;
            (0, errors_1.assert)(mfaEnrollmentId, "INVALID_MFA_ENROLLMENT_ID : mfaEnrollmentId must be defined.");
            (0, errors_1.assert)(!enrollmentIds.has(mfaEnrollmentId), "DUPLICATE_MFA_ENROLLMENT_ID");
            enrollments.push(Object.assign(Object.assign({}, enrollment), { mfaEnrollmentId, unobfuscatedPhoneInfo: enrollment.phoneInfo }));
            phoneNumbers.add(enrollment.phoneInfo);
            enrollmentIds.add(mfaEnrollmentId);
        }
    }
    return state.validateMfaEnrollments(enrollments);
}
function getNormalizedUri(reqBody) {
    (0, errors_1.assert)(reqBody.requestUri, "MISSING_REQUEST_URI");
    const normalizedUri = (0, utils_1.parseAbsoluteUri)(reqBody.requestUri);
    (0, errors_1.assert)(normalizedUri, "INVALID_REQUEST_URI");
    if (reqBody.postBody) {
        const postBodyParams = new url_1.URLSearchParams(reqBody.postBody);
        for (const key of postBodyParams.keys()) {
            normalizedUri.searchParams.set(key, postBodyParams.get(key));
        }
    }
    const fragment = normalizedUri.hash.replace(/^#/, "");
    if (fragment) {
        const fragmentParams = new url_1.URLSearchParams(fragment);
        for (const key of fragmentParams.keys()) {
            normalizedUri.searchParams.set(key, fragmentParams.get(key));
        }
        normalizedUri.hash = "";
    }
    return normalizedUri;
}
function parseClaims(idTokenOrJsonClaims) {
    if (!idTokenOrJsonClaims) {
        return undefined;
    }
    let claims;
    if (idTokenOrJsonClaims.startsWith("{")) {
        try {
            claims = JSON.parse(idTokenOrJsonClaims);
        }
        catch (_a) {
            throw new errors_1.BadRequestError(`INVALID_IDP_RESPONSE : Unable to parse id_token: ${idTokenOrJsonClaims} ((Auth Emulator failed to parse fake id_token as strict JSON.))`);
        }
    }
    else {
        const decoded = (0, jsonwebtoken_1.decode)(idTokenOrJsonClaims, { json: true });
        if (!decoded) {
            return undefined;
        }
        claims = decoded;
    }
    (0, errors_1.assert)(claims.sub, 'INVALID_IDP_RESPONSE : Invalid Idp Response: id_token missing required fields. ((Missing "sub" field. This field is required and must be a unique identifier.))');
    (0, errors_1.assert)(typeof claims.sub === "string", 'INVALID_IDP_RESPONSE : ((The "sub" field must be a string.))');
    return claims;
}
function fakeFetchUserInfoFromIdp(providerId, claims, samlResponse) {
    var _a, _b, _c, _d, _e;
    const rawId = claims.sub;
    const email = claims.email ? (0, utils_1.canonicalizeEmailAddress)(claims.email) : undefined;
    const emailVerified = !!claims.email_verified;
    const displayName = claims.name;
    const photoUrl = claims.picture;
    const response = {
        kind: "identitytoolkit#VerifyAssertionResponse",
        context: "",
        providerId,
        displayName,
        fullName: displayName,
        screenName: claims.screen_name,
        email,
        emailVerified,
        photoUrl,
    };
    let federatedId = rawId;
    switch (providerId) {
        case "google.com": {
            federatedId = `https://accounts.google.com/${rawId}`;
            let grantedScopes = "openid https://www.googleapis.com/auth/userinfo.profile";
            if (email) {
                grantedScopes += " https://www.googleapis.com/auth/userinfo.email";
            }
            response.firstName = claims.given_name;
            response.lastName = claims.family_name;
            response.rawUserInfo = JSON.stringify({
                granted_scopes: grantedScopes,
                id: rawId,
                name: displayName,
                given_name: claims.given_name,
                family_name: claims.family_name,
                verified_email: emailVerified,
                locale: "en",
                email,
                picture: photoUrl,
            });
            break;
        }
        case (_a = providerId.match(/^saml\./)) === null || _a === void 0 ? void 0 : _a.input:
            const nameId = (_c = (_b = samlResponse === null || samlResponse === void 0 ? void 0 : samlResponse.assertion) === null || _b === void 0 ? void 0 : _b.subject) === null || _c === void 0 ? void 0 : _c.nameId;
            response.email = nameId && (0, utils_1.isValidEmailAddress)(nameId) ? nameId : response.email;
            response.emailVerified = true;
            response.rawUserInfo = JSON.stringify((_d = samlResponse === null || samlResponse === void 0 ? void 0 : samlResponse.assertion) === null || _d === void 0 ? void 0 : _d.attributeStatements);
            break;
        case (_e = providerId.match(/^oidc\./)) === null || _e === void 0 ? void 0 : _e.input:
        default:
            response.rawUserInfo = JSON.stringify(claims);
            break;
    }
    response.federatedId = federatedId;
    return { response, rawId };
}
function handleLinkIdp(state, response, userFromIdToken) {
    if (state.oneAccountPerEmail && response.email) {
        const userMatchingEmail = state.getUserByEmail(response.email);
        (0, errors_1.assert)(!userMatchingEmail || userMatchingEmail.localId === userFromIdToken.localId, "EMAIL_EXISTS");
    }
    response.localId = userFromIdToken.localId;
    const fields = {};
    if (state.oneAccountPerEmail && response.email && !userFromIdToken.email) {
        fields.email = response.email;
        fields.emailVerified = response.emailVerified;
    }
    if (response.email &&
        response.emailVerified &&
        (fields.email || userFromIdToken.email) === response.email) {
        fields.emailVerified = true;
    }
    return { accountUpdates: { fields }, response };
}
function handleIdpSigninEmailNotRequired(response, userMatchingProvider) {
    if (userMatchingProvider) {
        return {
            response: Object.assign(Object.assign({}, response), { localId: userMatchingProvider.localId }),
            accountUpdates: {},
        };
    }
    else {
        return handleIdpSignUp(response, { emailRequired: false });
    }
}
function handleIdpSigninEmailRequired(response, rawId, userMatchingProvider, userMatchingEmail) {
    var _a, _b, _c;
    if (userMatchingProvider) {
        return {
            response: Object.assign(Object.assign({}, response), { localId: userMatchingProvider.localId }),
            accountUpdates: {},
        };
    }
    else if (userMatchingEmail) {
        if (response.emailVerified) {
            if ((_a = userMatchingEmail.providerUserInfo) === null || _a === void 0 ? void 0 : _a.some((info) => info.providerId === response.providerId && info.rawId !== rawId)) {
                response.emailRecycled = true;
            }
            response.localId = userMatchingEmail.localId;
            const accountUpdates = {
                fields: {},
            };
            if (!userMatchingEmail.emailVerified) {
                accountUpdates.fields.passwordHash = undefined;
                accountUpdates.fields.phoneNumber = undefined;
                accountUpdates.fields.validSince = (0, utils_1.toUnixTimestamp)(new Date()).toString();
                accountUpdates.deleteProviders = (_b = userMatchingEmail.providerUserInfo) === null || _b === void 0 ? void 0 : _b.map((info) => info.providerId);
            }
            accountUpdates.fields.dateOfBirth = response.dateOfBirth;
            accountUpdates.fields.displayName = response.displayName;
            accountUpdates.fields.language = response.language;
            accountUpdates.fields.photoUrl = response.photoUrl;
            accountUpdates.fields.screenName = response.screenName;
            accountUpdates.fields.emailVerified = true;
            return { response, accountUpdates };
        }
        else {
            response.needConfirmation = true;
            response.localId = userMatchingEmail.localId;
            response.verifiedProvider = (_c = userMatchingEmail.providerUserInfo) === null || _c === void 0 ? void 0 : _c.map((info) => info.providerId).filter((id) => id !== state_1.PROVIDER_PASSWORD && id !== state_1.PROVIDER_PHONE);
            return { response, accountUpdates: {} };
        }
    }
    else {
        return handleIdpSignUp(response, { emailRequired: true });
    }
}
function handleIdpSignUp(response, options) {
    const accountUpdates = {
        fields: {
            dateOfBirth: response.dateOfBirth,
            displayName: response.displayName,
            language: response.language,
            photoUrl: response.photoUrl,
            screenName: response.screenName,
        },
    };
    if (options.emailRequired && response.email) {
        accountUpdates.fields.email = response.email;
        accountUpdates.fields.emailVerified = response.emailVerified;
    }
    return {
        response: Object.assign(Object.assign({}, response), { isNewUser: true }),
        accountUpdates,
    };
}
function mfaPending(state, user, signInProvider) {
    if (!user.mfaInfo) {
        throw new Error("Internal assertion error: mfaPending called on user without MFA.");
    }
    const pendingCredentialPayload = {
        _AuthEmulatorMfaPendingCredential: "DO NOT MODIFY",
        localId: user.localId,
        signInProvider,
        projectId: state.projectId,
    };
    if (state instanceof state_1.TenantProjectState) {
        pendingCredentialPayload.tenantId = state.tenantId;
    }
    const mfaPendingCredential = Buffer.from(JSON.stringify(pendingCredentialPayload), "utf8").toString("base64");
    return { mfaPendingCredential, mfaInfo: user.mfaInfo.map(redactMfaInfo) };
}
function redactMfaInfo(mfaInfo) {
    return {
        displayName: mfaInfo.displayName,
        enrolledAt: mfaInfo.enrolledAt,
        mfaEnrollmentId: mfaInfo.mfaEnrollmentId,
        phoneInfo: mfaInfo.unobfuscatedPhoneInfo
            ? obfuscatePhoneNumber(mfaInfo.unobfuscatedPhoneInfo)
            : undefined,
    };
}
function obfuscatePhoneNumber(phoneNumber) {
    const split = phoneNumber.split("");
    let digitsEncountered = 0;
    for (let i = split.length - 1; i >= 0; i--) {
        if (/[0-9]/.test(split[i])) {
            digitsEncountered++;
            if (digitsEncountered > 4) {
                split[i] = "*";
            }
        }
    }
    return split.join("");
}
function parsePendingCredential(state, pendingCredential) {
    let pendingCredentialPayload;
    try {
        const json = Buffer.from(pendingCredential, "base64").toString("utf8");
        pendingCredentialPayload = JSON.parse(json);
    }
    catch (_a) {
        (0, errors_1.assert)(false, "((Invalid phoneVerificationInfo.mfaPendingCredential.))");
    }
    (0, errors_1.assert)(pendingCredentialPayload._AuthEmulatorMfaPendingCredential, "((Invalid phoneVerificationInfo.mfaPendingCredential.))");
    (0, errors_1.assert)(pendingCredentialPayload.projectId === state.projectId, "INVALID_PROJECT_ID : Project ID does not match MFA pending credential.");
    if (state instanceof state_1.TenantProjectState) {
        (0, errors_1.assert)(pendingCredentialPayload.tenantId === state.tenantId, "INVALID_PROJECT_ID : Project ID does not match MFA pending credential.");
    }
    const { localId, signInProvider } = pendingCredentialPayload;
    const user = state.getUserByLocalId(localId);
    (0, errors_1.assert)(user, "((User in pendingCredentialPayload does not exist.))");
    return { user, signInProvider };
}
function createTenant(state, reqBody) {
    var _a, _b, _c, _d, _e;
    if (!(state instanceof state_1.AgentProjectState)) {
        throw new errors_1.InternalError("INTERNAL_ERROR : Can only create tenant in agent project", "INTERNAL");
    }
    const mfaConfig = (_a = reqBody.mfaConfig) !== null && _a !== void 0 ? _a : {};
    if (!("state" in mfaConfig)) {
        mfaConfig.state = "DISABLED";
    }
    if (!("enabledProviders" in mfaConfig)) {
        mfaConfig.enabledProviders = [];
    }
    const tenant = {
        displayName: reqBody.displayName,
        allowPasswordSignup: (_b = reqBody.allowPasswordSignup) !== null && _b !== void 0 ? _b : false,
        enableEmailLinkSignin: (_c = reqBody.enableEmailLinkSignin) !== null && _c !== void 0 ? _c : false,
        enableAnonymousUser: (_d = reqBody.enableAnonymousUser) !== null && _d !== void 0 ? _d : false,
        disableAuth: (_e = reqBody.disableAuth) !== null && _e !== void 0 ? _e : false,
        mfaConfig: mfaConfig,
        tenantId: "",
    };
    return state.createTenant(tenant);
}
function listTenants(state, reqBody, ctx) {
    (0, errors_1.assert)(state instanceof state_1.AgentProjectState, "((Can only list tenants in agent project.))");
    const pageSize = Math.min(Math.floor(ctx.params.query.pageSize) || 20, 1000);
    const tenants = state.listTenants(ctx.params.query.pageToken);
    let nextPageToken = undefined;
    if (pageSize > 0 && tenants.length >= pageSize) {
        tenants.length = pageSize;
        nextPageToken = tenants[tenants.length - 1].tenantId;
    }
    return {
        nextPageToken,
        tenants,
    };
}
function deleteTenant(state) {
    (0, errors_1.assert)(state instanceof state_1.TenantProjectState, "((Can only delete tenant on tenant projects.))");
    state.delete();
    return {};
}
function getTenant(state) {
    (0, errors_1.assert)(state instanceof state_1.TenantProjectState, "((Can only get tenant on tenant projects.))");
    return state.tenantConfig;
}
function updateTenant(state, reqBody, ctx) {
    (0, errors_1.assert)(state instanceof state_1.TenantProjectState, "((Can only update tenant on tenant projects.))");
    return state.updateTenant(reqBody, ctx.params.query.updateMask);
}
function isMfaEnabled(state, user) {
    var _a;
    return ((state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") &&
        ((_a = user.mfaInfo) === null || _a === void 0 ? void 0 : _a.length));
}
async function fetchBlockingFunction(state, event, user, options = {}, oauthTokens = {}, timeoutMs = 60000) {
    const url = state.getBlockingFunctionUri(event);
    if (!url) {
        return { updates: {} };
    }
    const jwt = generateBlockingFunctionJwt(state, event, url, timeoutMs, user, options, oauthTokens);
    const reqBody = {
        data: {
            jwt,
        },
    };
    const controller = new abort_controller_1.default();
    const timeout = setTimeout(() => {
        controller.abort();
    }, timeoutMs);
    let response;
    let ok;
    let status;
    let text;
    try {
        const res = await (0, node_fetch_1.default)(url, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(reqBody),
            signal: controller.signal,
        });
        ok = res.ok;
        status = res.status;
        text = await res.text();
    }
    catch (thrown) {
        const err = thrown instanceof Error ? thrown : new Error(thrown);
        const isAbortError = err.name.includes("AbortError");
        if (isAbortError) {
            throw new errors_1.InternalError(`BLOCKING_FUNCTION_ERROR_RESPONSE : ((Deadline exceeded making request to ${url}.))`, err.message);
        }
        throw new errors_1.InternalError(`BLOCKING_FUNCTION_ERROR_RESPONSE : ((Failed to make request to ${url}.))`, err.message);
    }
    finally {
        clearTimeout(timeout);
    }
    (0, errors_1.assert)(ok, `BLOCKING_FUNCTION_ERROR_RESPONSE : ((HTTP request to ${url} returned HTTP error ${status}: ${text}))`);
    try {
        response = JSON.parse(text);
    }
    catch (thrown) {
        const err = thrown instanceof Error ? thrown : new Error(thrown);
        throw new errors_1.InternalError(`BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response body is not valid JSON.))`, err.message);
    }
    return processBlockingFunctionResponse(event, response);
}
function processBlockingFunctionResponse(event, response) {
    let extraClaims;
    const updates = {};
    if (response.userRecord) {
        const userRecord = response.userRecord;
        (0, errors_1.assert)(userRecord.updateMask, "BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response UserRecord is missing updateMask.))");
        const mask = userRecord.updateMask;
        const fields = mask.split(",");
        for (const field of fields) {
            switch (field) {
                case "displayName":
                case "photoUrl":
                    updates[field] = coercePrimitiveToString(userRecord[field]);
                    break;
                case "disabled":
                case "emailVerified":
                    updates[field] = !!userRecord[field];
                    break;
                case "customClaims":
                    const customClaims = JSON.stringify(userRecord.customClaims);
                    validateSerializedCustomClaims(customClaims);
                    updates.customAttributes = customClaims;
                    break;
                case "sessionClaims":
                    if (event !== state_1.BlockingFunctionEvents.BEFORE_SIGN_IN) {
                        break;
                    }
                    try {
                        extraClaims = userRecord.sessionClaims;
                    }
                    catch (_a) {
                        throw new errors_1.BadRequestError("BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response has malformed session claims.))");
                    }
                    break;
                default:
                    break;
            }
        }
    }
    return { updates, extraClaims };
}
function generateBlockingFunctionJwt(state, event, url, timeoutMs, user, options, oauthTokens) {
    const issuedAt = (0, utils_1.toUnixTimestamp)(new Date());
    const jwt = {
        iss: `https://securetoken.google.com/${state.projectId}`,
        aud: url,
        iat: issuedAt,
        exp: issuedAt + timeoutMs / 100,
        event_id: (0, utils_1.randomBase64UrlStr)(16),
        event_type: event,
        user_agent: "NotYetSupportedInFirebaseAuthEmulator",
        ip_address: "127.0.0.1",
        locale: "en",
        user_record: {
            uid: user.localId,
            email: user.email,
            email_verified: user.emailVerified,
            display_name: user.displayName,
            photo_url: user.photoUrl,
            disabled: user.disabled,
            phone_number: user.phoneNumber,
            custom_claims: JSON.parse(user.customAttributes || "{}"),
        },
        sub: user.localId,
        sign_in_method: options.signInMethod,
        sign_in_second_factor: options.signInSecondFactor,
        sign_in_attributes: options.signInAttributes,
        raw_user_info: options.rawUserInfo,
    };
    if (state instanceof state_1.TenantProjectState) {
        jwt.tenant_id = state.tenantId;
        jwt.user_record.tenant_id = state.tenantId;
    }
    const providerData = [];
    if (user.providerUserInfo) {
        for (const providerUserInfo of user.providerUserInfo) {
            const provider = {
                provider_id: providerUserInfo.providerId,
                display_name: providerUserInfo.displayName,
                photo_url: providerUserInfo.photoUrl,
                email: providerUserInfo.email,
                uid: providerUserInfo.rawId,
                phone_number: providerUserInfo.phoneNumber,
            };
            providerData.push(provider);
        }
    }
    jwt.user_record.provider_data = providerData;
    if (user.mfaInfo) {
        const enrolledFactors = [];
        for (const mfaEnrollment of user.mfaInfo) {
            if (!mfaEnrollment.mfaEnrollmentId) {
                continue;
            }
            const enrolledFactor = {
                uid: mfaEnrollment.mfaEnrollmentId,
                display_name: mfaEnrollment.displayName,
                enrollment_time: mfaEnrollment.enrolledAt,
                phone_number: mfaEnrollment.phoneInfo,
                factor_id: state_1.PROVIDER_PHONE,
            };
            enrolledFactors.push(enrolledFactor);
        }
        jwt.user_record.multi_factor = {
            enrolled_factors: enrolledFactors,
        };
    }
    if (user.lastLoginAt || user.createdAt) {
        jwt.user_record.metadata = {
            last_sign_in_time: user.lastLoginAt,
            creation_time: user.createdAt,
        };
    }
    if (state.shouldForwardCredentialToBlockingFunction("accessToken")) {
        jwt.oauth_access_token = oauthTokens.oauthAccessToken;
        jwt.oauth_token_secret = oauthTokens.oauthTokenSecret;
        jwt.oauth_expires_in = oauthTokens.oauthExpiresIn;
    }
    if (state.shouldForwardCredentialToBlockingFunction("idToken")) {
        jwt.oauth_id_token = oauthTokens.oauthIdToken;
    }
    if (state.shouldForwardCredentialToBlockingFunction("refreshToken")) {
        jwt.oauth_refresh_token = oauthTokens.oauthRefreshToken;
    }
    const jwtStr = (0, jsonwebtoken_1.sign)(jwt, "fake-secret", {
        algorithm: "none",
    });
    return jwtStr;
}
function parseBlockingFunctionJwt(jwt) {
    const decoded = (0, jsonwebtoken_1.decode)(jwt, { json: true });
    (0, errors_1.assert)(decoded, "((Invalid blocking function jwt.))");
    (0, errors_1.assert)(decoded.iss, "((Invalid blocking function jwt, missing `iss` claim.))");
    (0, errors_1.assert)(decoded.aud, "((Invalid blocking function jwt, missing `aud` claim.))");
    (0, errors_1.assert)(decoded.user_record, "((Invalid blocking function jwt, missing `user_record` claim.))");
    return decoded;
}
exports.parseBlockingFunctionJwt = parseBlockingFunctionJwt;