"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.decodeRefreshToken = exports.encodeRefreshToken = exports.BlockingFunctionEvents = exports.TenantProjectState = exports.AgentProjectState = exports.ProjectState = exports.SIGNIN_METHOD_EMAIL_LINK = exports.PROVIDER_GAME_CENTER = exports.PROVIDER_CUSTOM = exports.PROVIDER_ANONYMOUS = exports.PROVIDER_PHONE = exports.PROVIDER_PASSWORD = void 0; const utils_1 = require("./utils"); const cloudFunctions_1 = require("./cloudFunctions"); const errors_1 = require("./errors"); exports.PROVIDER_PASSWORD = "password"; exports.PROVIDER_PHONE = "phone"; exports.PROVIDER_ANONYMOUS = "anonymous"; exports.PROVIDER_CUSTOM = "custom"; exports.PROVIDER_GAME_CENTER = "gc.apple.com"; exports.SIGNIN_METHOD_EMAIL_LINK = "emailLink"; class ProjectState { constructor(projectId) { this.projectId = projectId; this.users = new Map(); this.localIdForEmail = new Map(); this.localIdForInitialEmail = new Map(); this.localIdForPhoneNumber = new Map(); this.localIdsForProviderEmail = new Map(); this.userIdForProviderRawId = new Map(); this.oobs = new Map(); this.verificationCodes = new Map(); this.temporaryProofs = new Map(); this.pendingLocalIds = new Set(); } get projectNumber() { return "12345"; } generateLocalId() { for (let i = 0; i < 10; i++) { const localId = (0, utils_1.randomId)(28); if (!this.users.has(localId) && !this.pendingLocalIds.has(localId)) { this.pendingLocalIds.add(localId); return localId; } } throw new Error("Cannot generate a random unique localId after 10 tries."); } createUserWithLocalId(localId, props) { if (this.users.has(localId)) { return undefined; } this.users.set(localId, { localId, }); this.pendingLocalIds.delete(localId); const user = this.updateUserByLocalId(localId, props, { upsertProviders: props.providerUserInfo, }); this.authCloudFunction.dispatch("create", user); return user; } overwriteUserWithLocalId(localId, props) { const userInfoBefore = this.users.get(localId); if (userInfoBefore) { this.removeUserFromIndex(userInfoBefore); } const timestamp = new Date(); this.users.set(localId, { localId, createdAt: props.createdAt || timestamp.getTime().toString(), lastLoginAt: timestamp.getTime().toString(), }); const user = this.updateUserByLocalId(localId, props, { upsertProviders: props.providerUserInfo, }); return user; } deleteUser(user) { this.users.delete(user.localId); this.removeUserFromIndex(user); this.authCloudFunction.dispatch("delete", user); } updateUserByLocalId(localId, fields, options = {}) { var _a, _b; const upsertProviders = (_a = options.upsertProviders) !== null && _a !== void 0 ? _a : []; const deleteProviders = (_b = options.deleteProviders) !== null && _b !== void 0 ? _b : []; const user = this.users.get(localId); if (!user) { throw new Error(`Internal assertion error: trying to update nonexistent user: ${localId}`); } const oldEmail = user.email; const oldPhoneNumber = user.phoneNumber; for (const field of Object.keys(fields)) { (0, utils_1.mirrorFieldTo)(user, field, fields); } if (oldEmail && oldEmail !== user.email) { this.localIdForEmail.delete(oldEmail); } if (user.email) { this.localIdForEmail.set(user.email, user.localId); } if (user.email && (user.passwordHash || user.emailLinkSignin)) { upsertProviders.push({ providerId: exports.PROVIDER_PASSWORD, email: user.email, federatedId: user.email, rawId: user.email, displayName: user.displayName, photoUrl: user.photoUrl, }); } else { deleteProviders.push(exports.PROVIDER_PASSWORD); } if (user.initialEmail) { this.localIdForInitialEmail.set(user.initialEmail, user.localId); } if (oldPhoneNumber && oldPhoneNumber !== user.phoneNumber) { this.localIdForPhoneNumber.delete(oldPhoneNumber); } if (user.phoneNumber) { this.localIdForPhoneNumber.set(user.phoneNumber, user.localId); upsertProviders.push({ providerId: exports.PROVIDER_PHONE, phoneNumber: user.phoneNumber, rawId: user.phoneNumber, }); } else { deleteProviders.push(exports.PROVIDER_PHONE); } if (user.mfaInfo) { this.validateMfaEnrollments(user.mfaInfo); } return this.updateUserProviderInfo(user, upsertProviders, deleteProviders); } validateMfaEnrollments(enrollments) { const phoneNumbers = new Set(); const enrollmentIds = new Set(); for (const enrollment of enrollments) { (0, errors_1.assert)(enrollment.phoneInfo && (0, utils_1.isValidPhoneNumber)(enrollment.phoneInfo), "INVALID_MFA_PHONE_NUMBER : Invalid format."); (0, errors_1.assert)(enrollment.mfaEnrollmentId, "INVALID_MFA_ENROLLMENT_ID : mfaEnrollmentId must be defined."); (0, errors_1.assert)(!enrollmentIds.has(enrollment.mfaEnrollmentId), "DUPLICATE_MFA_ENROLLMENT_ID"); (0, errors_1.assert)(!phoneNumbers.has(enrollment.phoneInfo), "INTERNAL_ERROR : MFA Enrollment Phone Numbers must be unique."); phoneNumbers.add(enrollment.phoneInfo); enrollmentIds.add(enrollment.mfaEnrollmentId); } return enrollments; } updateUserProviderInfo(user, upsertProviders, deleteProviders) { var _a, _b; const oldProviderEmails = getProviderEmailsForUser(user); if (user.providerUserInfo) { const updatedProviderUserInfo = []; for (const info of user.providerUserInfo) { if (deleteProviders.includes(info.providerId)) { (_a = this.userIdForProviderRawId.get(info.providerId)) === null || _a === void 0 ? void 0 : _a.delete(info.rawId); } else { updatedProviderUserInfo.push(info); } } user.providerUserInfo = updatedProviderUserInfo; } if (upsertProviders.length) { user.providerUserInfo = (_b = user.providerUserInfo) !== null && _b !== void 0 ? _b : []; for (const upsert of upsertProviders) { const providerId = upsert.providerId; let users = this.userIdForProviderRawId.get(providerId); if (!users) { users = new Map(); this.userIdForProviderRawId.set(providerId, users); } users.set(upsert.rawId, user.localId); const index = user.providerUserInfo.findIndex((info) => info.providerId === upsert.providerId); if (index < 0) { user.providerUserInfo.push(upsert); } else { user.providerUserInfo[index] = upsert; } } } for (const email of getProviderEmailsForUser(user)) { oldProviderEmails.delete(email); let localIds = this.localIdsForProviderEmail.get(email); if (!localIds) { localIds = new Set(); this.localIdsForProviderEmail.set(email, localIds); } localIds.add(user.localId); } for (const oldEmail of oldProviderEmails) { this.removeProviderEmailForUser(oldEmail, user.localId); } return user; } getUserByEmail(email) { const localId = this.localIdForEmail.get(email); if (!localId) { return undefined; } return this.getUserByLocalIdAssertingExists(localId); } getUserByInitialEmail(initialEmail) { const localId = this.localIdForInitialEmail.get(initialEmail); if (!localId) { return undefined; } return this.getUserByLocalIdAssertingExists(localId); } getUserByLocalIdAssertingExists(localId) { const userInfo = this.getUserByLocalId(localId); if (!userInfo) { throw new Error(`Internal state invariant broken: no user with ID: ${localId}`); } return userInfo; } getUsersByEmailOrProviderEmail(email) { var _a; const users = []; const seenLocalIds = new Set(); const localId = this.localIdForEmail.get(email); if (localId) { users.push(this.getUserByLocalIdAssertingExists(localId)); seenLocalIds.add(localId); } for (const localId of (_a = this.localIdsForProviderEmail.get(email)) !== null && _a !== void 0 ? _a : []) { if (!seenLocalIds.has(localId)) { users.push(this.getUserByLocalIdAssertingExists(localId)); seenLocalIds.add(localId); } } return users; } getUserByPhoneNumber(phoneNumber) { const localId = this.localIdForPhoneNumber.get(phoneNumber); if (!localId) { return undefined; } return this.getUserByLocalIdAssertingExists(localId); } removeProviderEmailForUser(email, localId) { const localIds = this.localIdsForProviderEmail.get(email); if (!localIds) { return; } localIds.delete(localId); if (localIds.size === 0) { this.localIdsForProviderEmail.delete(email); } } getUserByProviderRawId(provider, rawId) { var _a; const localId = (_a = this.userIdForProviderRawId.get(provider)) === null || _a === void 0 ? void 0 : _a.get(rawId); if (!localId) { return undefined; } return this.getUserByLocalIdAssertingExists(localId); } listProviderInfosByProviderId(provider) { var _a; const users = this.userIdForProviderRawId.get(provider); if (!users) { return []; } const infos = []; for (const localId of users.values()) { const user = this.getUserByLocalIdAssertingExists(localId); const info = (_a = user.providerUserInfo) === null || _a === void 0 ? void 0 : _a.find((info) => info.providerId === provider); if (!info) { throw new Error(`Internal assertion error: User ${localId} does not have providerInfo ${provider}.`); } infos.push(info); } return infos; } getUserByLocalId(localId) { return this.users.get(localId); } createRefreshTokenFor(userInfo, provider, { extraClaims = {}, secondFactor, } = {}) { const localId = userInfo.localId; const refreshTokenRecord = { _AuthEmulatorRefreshToken: "DO NOT MODIFY", localId, provider, extraClaims, projectId: this.projectId, secondFactor, tenantId: userInfo.tenantId, }; const refreshToken = encodeRefreshToken(refreshTokenRecord); return refreshToken; } validateRefreshToken(refreshToken) { const record = decodeRefreshToken(refreshToken); (0, errors_1.assert)(record.projectId === this.projectId, "INVALID_REFRESH_TOKEN"); if (this instanceof TenantProjectState) { (0, errors_1.assert)(record.tenantId === this.tenantId, "TENANT_ID_MISMATCH"); } const user = this.getUserByLocalId(record.localId); (0, errors_1.assert)(user, "INVALID_REFRESH_TOKEN"); return { user, provider: record.provider, extraClaims: record.extraClaims, secondFactor: record.secondFactor, }; } createOob(email, requestType, generateLink) { const oobCode = (0, utils_1.randomBase64UrlStr)(54); const oobLink = generateLink(oobCode); const oob = { email, requestType, oobCode, oobLink, }; this.oobs.set(oobCode, oob); return oob; } validateOobCode(oobCode) { return this.oobs.get(oobCode); } deleteOobCode(oobCode) { return this.oobs.delete(oobCode); } listOobCodes() { return this.oobs.values(); } createVerificationCode(phoneNumber) { const sessionInfo = (0, utils_1.randomBase64UrlStr)(226); const verification = { code: (0, utils_1.randomDigits)(6), phoneNumber, sessionInfo, }; this.verificationCodes.set(sessionInfo, verification); return verification; } getVerificationCodeBySessionInfo(sessionInfo) { return this.verificationCodes.get(sessionInfo); } deleteVerificationCodeBySessionInfo(sessionInfo) { return this.verificationCodes.delete(sessionInfo); } listVerificationCodes() { return this.verificationCodes.values(); } deleteAllAccounts() { this.users.clear(); this.localIdForEmail.clear(); this.localIdForPhoneNumber.clear(); this.localIdsForProviderEmail.clear(); this.userIdForProviderRawId.clear(); } getUserCount() { return this.users.size; } queryUsers(filter, options) { const users = []; for (const user of this.users.values()) { if (!options.startToken || user.localId > options.startToken) { filter; users.push(user); } } users.sort((a, b) => { if (options.sortByField === "localId") { if (a.localId < b.localId) { return -1; } else if (a.localId > b.localId) { return 1; } } return 0; }); return options.order === "DESC" ? users.reverse() : users; } createTemporaryProof(phoneNumber) { const record = { phoneNumber, temporaryProof: (0, utils_1.randomBase64UrlStr)(119), temporaryProofExpiresIn: "3600", }; this.temporaryProofs.set(record.temporaryProof, record); return record; } validateTemporaryProof(temporaryProof, phoneNumber) { const record = this.temporaryProofs.get(temporaryProof); if (!record || record.phoneNumber !== phoneNumber) { return undefined; } return record; } removeUserFromIndex(user) { var _a, _b; if (user.email) { this.localIdForEmail.delete(user.email); } if (user.initialEmail) { this.localIdForInitialEmail.delete(user.initialEmail); } if (user.phoneNumber) { this.localIdForPhoneNumber.delete(user.phoneNumber); } for (const info of (_a = user.providerUserInfo) !== null && _a !== void 0 ? _a : []) { (_b = this.userIdForProviderRawId.get(info.providerId)) === null || _b === void 0 ? void 0 : _b.delete(info.rawId); if (info.email) { this.removeProviderEmailForUser(info.email, user.localId); } } } } exports.ProjectState = ProjectState; class AgentProjectState extends ProjectState { constructor(projectId) { super(projectId); this.tenantProjectForTenantId = new Map(); this._authCloudFunction = new cloudFunctions_1.AuthCloudFunction(this.projectId); this._config = { signIn: { allowDuplicateEmails: false }, blockingFunctions: {}, }; } get authCloudFunction() { return this._authCloudFunction; } get oneAccountPerEmail() { return !this._config.signIn.allowDuplicateEmails; } set oneAccountPerEmail(oneAccountPerEmail) { this._config.signIn.allowDuplicateEmails = !oneAccountPerEmail; } get allowPasswordSignup() { return true; } get disableAuth() { return false; } get mfaConfig() { return { state: "ENABLED", enabledProviders: ["PHONE_SMS"] }; } get enableAnonymousUser() { return true; } get enableEmailLinkSignin() { return true; } get config() { return this._config; } get blockingFunctionsConfig() { return this._config.blockingFunctions; } set blockingFunctionsConfig(blockingFunctions) { this._config.blockingFunctions = blockingFunctions; } shouldForwardCredentialToBlockingFunction(type) { var _a, _b, _c, _d, _e, _f; switch (type) { case "accessToken": return (_b = (_a = this._config.blockingFunctions.forwardInboundCredentials) === null || _a === void 0 ? void 0 : _a.accessToken) !== null && _b !== void 0 ? _b : false; case "idToken": return (_d = (_c = this._config.blockingFunctions.forwardInboundCredentials) === null || _c === void 0 ? void 0 : _c.idToken) !== null && _d !== void 0 ? _d : false; case "refreshToken": return (_f = (_e = this._config.blockingFunctions.forwardInboundCredentials) === null || _e === void 0 ? void 0 : _e.refreshToken) !== null && _f !== void 0 ? _f : false; } } getBlockingFunctionUri(event) { const triggers = this.blockingFunctionsConfig.triggers; if (triggers) { return Object.prototype.hasOwnProperty.call(triggers, event) ? triggers[event].functionUri : undefined; } return undefined; } updateConfig(update, updateMask) { var _a, _b, _c; if (!updateMask) { this.oneAccountPerEmail = (_b = !((_a = update.signIn) === null || _a === void 0 ? void 0 : _a.allowDuplicateEmails)) !== null && _b !== void 0 ? _b : true; this.blockingFunctionsConfig = (_c = update.blockingFunctions) !== null && _c !== void 0 ? _c : {}; return this.config; } return applyMask(updateMask, this.config, update); } getTenantProject(tenantId) { if (!this.tenantProjectForTenantId.has(tenantId)) { this.createTenantWithTenantId(tenantId, { tenantId, allowPasswordSignup: true, disableAuth: false, mfaConfig: { state: "ENABLED", enabledProviders: ["PHONE_SMS"], }, enableAnonymousUser: true, enableEmailLinkSignin: true, }); } return this.tenantProjectForTenantId.get(tenantId); } listTenants(startToken) { const tenantProjects = []; for (const tenantProject of this.tenantProjectForTenantId.values()) { if (!startToken || tenantProject.tenantId > startToken) { tenantProjects.push(tenantProject); } } tenantProjects.sort((a, b) => { if (a.tenantId < b.tenantId) { return -1; } else if (a.tenantId > b.tenantId) { return 1; } return 0; }); return tenantProjects.map((tenantProject) => tenantProject.tenantConfig); } createTenant(tenant) { for (let i = 0; i < 10; i++) { const tenantId = (0, utils_1.randomId)(28); const createdTenant = this.createTenantWithTenantId(tenantId, tenant); if (createdTenant) { return createdTenant; } } throw new Error("Could not generate a random unique tenantId after 10 tries"); } createTenantWithTenantId(tenantId, tenant) { if (this.tenantProjectForTenantId.has(tenantId)) { return undefined; } tenant.name = `projects/${this.projectId}/tenants/${tenantId}`; tenant.tenantId = tenantId; this.tenantProjectForTenantId.set(tenantId, new TenantProjectState(this.projectId, tenantId, tenant, this)); return tenant; } deleteTenant(tenantId) { this.tenantProjectForTenantId.delete(tenantId); } } exports.AgentProjectState = AgentProjectState; class TenantProjectState extends ProjectState { constructor(projectId, tenantId, _tenantConfig, parentProject) { super(projectId); this.tenantId = tenantId; this._tenantConfig = _tenantConfig; this.parentProject = parentProject; } get oneAccountPerEmail() { return this.parentProject.oneAccountPerEmail; } get authCloudFunction() { return this.parentProject.authCloudFunction; } get tenantConfig() { return this._tenantConfig; } get allowPasswordSignup() { return this._tenantConfig.allowPasswordSignup; } get disableAuth() { return this._tenantConfig.disableAuth; } get mfaConfig() { return this._tenantConfig.mfaConfig; } get enableAnonymousUser() { return this._tenantConfig.enableAnonymousUser; } get enableEmailLinkSignin() { return this._tenantConfig.enableEmailLinkSignin; } shouldForwardCredentialToBlockingFunction(type) { return this.parentProject.shouldForwardCredentialToBlockingFunction(type); } getBlockingFunctionUri(event) { return this.parentProject.getBlockingFunctionUri(event); } delete() { this.parentProject.deleteTenant(this.tenantId); } updateTenant(update, updateMask) { var _a, _b, _c, _d, _e; if (!updateMask) { const mfaConfig = (_a = update.mfaConfig) !== null && _a !== void 0 ? _a : {}; if (!("state" in mfaConfig)) { mfaConfig.state = "DISABLED"; } if (!("enabledProviders" in mfaConfig)) { mfaConfig.enabledProviders = []; } this._tenantConfig = { tenantId: this.tenantId, name: this.tenantConfig.name, allowPasswordSignup: (_b = update.allowPasswordSignup) !== null && _b !== void 0 ? _b : false, disableAuth: (_c = update.disableAuth) !== null && _c !== void 0 ? _c : false, mfaConfig: mfaConfig, enableAnonymousUser: (_d = update.enableAnonymousUser) !== null && _d !== void 0 ? _d : false, enableEmailLinkSignin: (_e = update.enableEmailLinkSignin) !== null && _e !== void 0 ? _e : false, displayName: update.displayName, }; return this.tenantConfig; } return applyMask(updateMask, this.tenantConfig, update); } } exports.TenantProjectState = TenantProjectState; var BlockingFunctionEvents; (function (BlockingFunctionEvents) { BlockingFunctionEvents["BEFORE_CREATE"] = "beforeCreate"; BlockingFunctionEvents["BEFORE_SIGN_IN"] = "beforeSignIn"; })(BlockingFunctionEvents = exports.BlockingFunctionEvents || (exports.BlockingFunctionEvents = {})); function encodeRefreshToken(refreshTokenRecord) { return Buffer.from(JSON.stringify(refreshTokenRecord), "utf8").toString("base64"); } exports.encodeRefreshToken = encodeRefreshToken; function decodeRefreshToken(refreshTokenString) { let refreshTokenRecord; try { const json = Buffer.from(refreshTokenString, "base64").toString("utf8"); refreshTokenRecord = JSON.parse(json); } catch (_a) { throw new errors_1.BadRequestError("INVALID_REFRESH_TOKEN"); } (0, errors_1.assert)(refreshTokenRecord._AuthEmulatorRefreshToken, "INVALID_REFRESH_TOKEN"); return refreshTokenRecord; } exports.decodeRefreshToken = decodeRefreshToken; function getProviderEmailsForUser(user) { var _a; const emails = new Set(); (_a = user.providerUserInfo) === null || _a === void 0 ? void 0 : _a.forEach(({ email }) => { if (email) { emails.add(email); } }); return emails; } function applyMask(updateMask, dest, update) { const paths = updateMask.split(","); for (const path of paths) { const fields = path.split("."); let updateField = update; let existingField = dest; let field; for (let i = 0; i < fields.length - 1; i++) { field = fields[i]; if (updateField[field] == null) { console.warn(`Unable to find field '${field}' in update '${updateField}`); break; } if (Array.isArray(updateField[field]) || Object(updateField[field]) !== updateField[field]) { console.warn(`Field '${field}' is singular and cannot have sub-fields`); break; } if (!existingField[field]) { existingField[field] = {}; } updateField = updateField[field]; existingField = existingField[field]; } field = fields[fields.length - 1]; if (updateField[field] == null) { console.warn(`Unable to find field '${field}' in update '${JSON.stringify(updateField)}`); continue; } existingField[field] = updateField[field]; } return dest; }