"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccountManager = exports.formatAccountStatus = exports.AccountStatus = void 0;
const common_1 = require("@atproto/common");
const syntax_1 = require("@atproto/syntax");
const xrpc_server_1 = require("@atproto/xrpc-server");
const auth_scope_1 = require("../auth-scope");
const db_1 = require("../db");
const explicit_slurs_1 = require("../handle/explicit-slurs");
const index_1 = require("../handle/index");
const db_2 = require("./db");
const account = __importStar(require("./helpers/account"));
const account_1 = require("./helpers/account");
const auth = __importStar(require("./helpers/auth"));
const emailToken = __importStar(require("./helpers/email-token"));
const invite = __importStar(require("./helpers/invite"));
const password = __importStar(require("./helpers/password"));
const repo = __importStar(require("./helpers/repo"));
const scrypt = __importStar(require("./helpers/scrypt"));
const token = __importStar(require("./helpers/token"));
var account_2 = require("./helpers/account");
Object.defineProperty(exports, "AccountStatus", { enumerable: true, get: function () { return account_2.AccountStatus; } });
Object.defineProperty(exports, "formatAccountStatus", { enumerable: true, get: function () { return account_2.formatAccountStatus; } });
class AccountManager {
    constructor(idResolver, jwtKey, serviceDid, serviceHandleDomains, db) {
        Object.defineProperty(this, "idResolver", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: idResolver
        });
        Object.defineProperty(this, "jwtKey", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: jwtKey
        });
        Object.defineProperty(this, "serviceDid", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: serviceDid
        });
        Object.defineProperty(this, "serviceHandleDomains", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: serviceHandleDomains
        });
        Object.defineProperty(this, "db", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        this.db = (0, db_2.getDb)(db.accountDbLoc, db.disableWalAutoCheckpoint);
    }
    async migrateOrThrow() {
        await this.db.ensureWal();
        await (0, db_2.getMigrator)(this.db).migrateToLatestOrThrow();
    }
    close() {
        this.db.close();
    }
    // Account
    // ----------
    async getAccount(handleOrDid, flags) {
        return account.getAccount(this.db, handleOrDid, flags);
    }
    async getAccounts(dids, flags) {
        return account.getAccounts(this.db, dids, flags);
    }
    async getAccountByEmail(email, flags) {
        return account.getAccountByEmail(this.db, email, flags);
    }
    async isAccountActivated(did) {
        const account = await this.getAccount(did, { includeDeactivated: true });
        if (!account)
            return false;
        return !account.deactivatedAt;
    }
    async getDidForActor(handleOrDid, flags) {
        const got = await this.getAccount(handleOrDid, flags);
        return got?.did ?? null;
    }
    async getAccountStatus(handleOrDid) {
        const got = await this.getAccount(handleOrDid, {
            includeDeactivated: true,
            includeTakenDown: true,
        });
        const res = account.formatAccountStatus(got);
        return res.active ? account_1.AccountStatus.Active : res.status;
    }
    async normalizeAndValidateHandle(handle, { did, allowAnyValid, } = {}) {
        const normalized = (0, index_1.baseNormalizeAndValidate)(handle);
        // tld validation
        if (!(0, syntax_1.isValidTld)(normalized)) {
            throw new xrpc_server_1.InvalidRequestError('Handle TLD is invalid or disallowed', 'InvalidHandle');
        }
        // slur check
        if (!allowAnyValid && (0, explicit_slurs_1.hasExplicitSlur)(normalized)) {
            throw new xrpc_server_1.InvalidRequestError('Inappropriate language in handle', 'InvalidHandle');
        }
        if ((0, index_1.isServiceDomain)(normalized, this.serviceHandleDomains)) {
            // verify constraints on a service domain
            (0, index_1.ensureHandleServiceConstraints)(normalized, this.serviceHandleDomains, allowAnyValid);
        }
        else {
            if (did == null) {
                throw new xrpc_server_1.InvalidRequestError('Not a supported handle domain', 'UnsupportedDomain');
            }
            // verify resolution of a non-service domain
            const resolvedDid = await this.idResolver.handle.resolve(normalized);
            if (resolvedDid !== did) {
                throw new xrpc_server_1.InvalidRequestError('External handle did not resolve to DID');
            }
        }
        return normalized;
    }
    async createAccount({ did, handle, email, password, repoCid, repoRev, inviteCode, deactivated, refreshJwt, }) {
        if (password && password.length > scrypt.NEW_PASSWORD_MAX_LENGTH) {
            throw new xrpc_server_1.InvalidRequestError('Password too long');
        }
        const passwordScrypt = password
            ? await scrypt.genSaltAndHash(password)
            : undefined;
        const now = new Date().toISOString();
        await this.db.transaction(async (dbTxn) => {
            if (inviteCode) {
                await invite.ensureInviteIsAvailable(dbTxn, inviteCode);
            }
            await Promise.all([
                account.registerActor(dbTxn, { did, handle, deactivated }),
                email && passwordScrypt
                    ? account.registerAccount(dbTxn, { did, email, passwordScrypt })
                    : Promise.resolve(),
                invite.recordInviteUse(dbTxn, {
                    did,
                    inviteCode,
                    now,
                }),
                refreshJwt &&
                    auth.storeRefreshToken(dbTxn, auth.decodeRefreshToken(refreshJwt), null),
                repo.updateRoot(dbTxn, did, repoCid, repoRev),
            ]);
        });
    }
    async createAccountAndSession(opts) {
        const { accessJwt, refreshJwt } = await auth.createTokens({
            did: opts.did,
            jwtKey: this.jwtKey,
            serviceDid: this.serviceDid,
            scope: auth_scope_1.AuthScope.Access,
        });
        await this.createAccount({ ...opts, refreshJwt });
        return { accessJwt, refreshJwt };
    }
    // @NOTE should always be paired with a sequenceHandle().
    // the token output from this method should be passed to sequenceHandle().
    async updateHandle(did, handle) {
        return account.updateHandle(this.db, did, handle);
    }
    async deleteAccount(did) {
        return account.deleteAccount(this.db, did);
    }
    async takedownAccount(did, takedown) {
        await this.db.transaction(async (dbTxn) => Promise.all([
            account.updateAccountTakedownStatus(dbTxn, did, takedown),
            auth.revokeRefreshTokensByDid(dbTxn, did),
            token.removeByDidQB(dbTxn, did).execute(),
        ]));
    }
    async getAccountAdminStatus(did) {
        return account.getAccountAdminStatus(this.db, did);
    }
    async updateRepoRoot(did, cid, rev) {
        return repo.updateRoot(this.db, did, cid, rev);
    }
    async deactivateAccount(did, deleteAfter) {
        return account.deactivateAccount(this.db, did, deleteAfter);
    }
    async activateAccount(did) {
        return account.activateAccount(this.db, did);
    }
    // Auth
    // ----------
    async createSession(did, appPassword, isSoftDeleted = false) {
        const { accessJwt, refreshJwt } = await auth.createTokens({
            did,
            jwtKey: this.jwtKey,
            serviceDid: this.serviceDid,
            scope: auth.formatScope(appPassword, isSoftDeleted),
        });
        // For soft deleted accounts don't store refresh token so that it can't be rotated.
        if (!isSoftDeleted) {
            const refreshPayload = auth.decodeRefreshToken(refreshJwt);
            await auth.storeRefreshToken(this.db, refreshPayload, appPassword);
        }
        return { accessJwt, refreshJwt };
    }
    async rotateRefreshToken(id) {
        const token = await auth.getRefreshToken(this.db, id);
        if (!token)
            return null;
        const now = new Date();
        // take the chance to tidy all of a user's expired tokens
        // does not need to be transactional since this is just best-effort
        await auth.deleteExpiredRefreshTokens(this.db, token.did, now.toISOString());
        // Shorten the refresh token lifespan down from its
        // original expiration time to its revocation grace period.
        const prevExpiresAt = new Date(token.expiresAt);
        const REFRESH_GRACE_MS = 2 * common_1.HOUR;
        const graceExpiresAt = new Date(now.getTime() + REFRESH_GRACE_MS);
        const expiresAt = graceExpiresAt < prevExpiresAt ? graceExpiresAt : prevExpiresAt;
        if (expiresAt <= now) {
            return null;
        }
        // Determine the next refresh token id: upon refresh token
        // reuse you always receive a refresh token with the same id.
        const nextId = token.nextId ?? auth.getRefreshTokenId();
        const { accessJwt, refreshJwt } = await auth.createTokens({
            did: token.did,
            jwtKey: this.jwtKey,
            serviceDid: this.serviceDid,
            scope: auth.formatScope(token.appPassword),
            jti: nextId,
        });
        const refreshPayload = auth.decodeRefreshToken(refreshJwt);
        try {
            await this.db.transaction((dbTxn) => Promise.all([
                auth.addRefreshGracePeriod(dbTxn, {
                    id,
                    expiresAt: expiresAt.toISOString(),
                    nextId,
                }),
                auth.storeRefreshToken(dbTxn, refreshPayload, token.appPassword),
            ]));
        }
        catch (err) {
            if (err instanceof auth.ConcurrentRefreshError) {
                return this.rotateRefreshToken(id);
            }
            throw err;
        }
        return { accessJwt, refreshJwt };
    }
    async revokeRefreshToken(id) {
        return auth.revokeRefreshToken(this.db, id);
    }
    // Login
    // ----------
    async login({ identifier, password, }) {
        const start = Date.now();
        try {
            const identifierNormalized = identifier.toLowerCase();
            const user = identifierNormalized.includes('@')
                ? await this.getAccountByEmail(identifierNormalized, {
                    includeDeactivated: true,
                    includeTakenDown: true,
                })
                : await this.getAccount(identifierNormalized, {
                    includeDeactivated: true,
                    includeTakenDown: true,
                });
            if (!user) {
                throw new xrpc_server_1.AuthRequiredError('Invalid identifier or password');
            }
            const isSoftDeleted = (0, db_1.softDeleted)(user);
            let appPassword = null;
            const validAccountPass = await this.verifyAccountPassword(user.did, password);
            if (!validAccountPass) {
                // takendown/suspended accounts cannot login with app password
                if (isSoftDeleted) {
                    throw new xrpc_server_1.AuthRequiredError('Invalid identifier or password');
                }
                appPassword = await this.verifyAppPassword(user.did, password);
                if (appPassword === null) {
                    throw new xrpc_server_1.AuthRequiredError('Invalid identifier or password');
                }
            }
            return { user, appPassword, isSoftDeleted };
        }
        finally {
            // Mitigate timing attacks
            await (0, common_1.wait)(350 - (Date.now() - start));
        }
    }
    // Passwords
    // ----------
    async createAppPassword(did, name, privileged) {
        return password.createAppPassword(this.db, did, name, privileged);
    }
    async listAppPasswords(did) {
        return password.listAppPasswords(this.db, did);
    }
    async verifyAccountPassword(did, passwordStr) {
        return password.verifyAccountPassword(this.db, did, passwordStr);
    }
    async verifyAppPassword(did, passwordStr) {
        return password.verifyAppPassword(this.db, did, passwordStr);
    }
    async revokeAppPassword(did, name) {
        await this.db.transaction(async (dbTxn) => Promise.all([
            password.deleteAppPassword(dbTxn, did, name),
            auth.revokeAppPasswordRefreshToken(dbTxn, did, name),
        ]));
    }
    // Invites
    // ----------
    async ensureInviteIsAvailable(code) {
        return invite.ensureInviteIsAvailable(this.db, code);
    }
    async createInviteCodes(toCreate, useCount) {
        return invite.createInviteCodes(this.db, toCreate, useCount);
    }
    async createAccountInviteCodes(forAccount, codes, expectedTotal, disabled) {
        return invite.createAccountInviteCodes(this.db, forAccount, codes, expectedTotal, disabled);
    }
    async getAccountInvitesCodes(did) {
        const inviteCodes = await invite.getAccountsInviteCodes(this.db, [did]);
        return inviteCodes.get(did) ?? [];
    }
    async getAccountsInvitesCodes(dids) {
        return invite.getAccountsInviteCodes(this.db, dids);
    }
    async getInvitedByForAccounts(dids) {
        return invite.getInvitedByForAccounts(this.db, dids);
    }
    async getInviteCodesUses(codes) {
        return invite.getInviteCodesUses(this.db, codes);
    }
    async setAccountInvitesDisabled(did, disabled) {
        return invite.setAccountInvitesDisabled(this.db, did, disabled);
    }
    async disableInviteCodes(opts) {
        return invite.disableInviteCodes(this.db, opts);
    }
    // Email Tokens
    // ----------
    async createEmailToken(did, purpose) {
        return emailToken.createEmailToken(this.db, did, purpose);
    }
    async assertValidEmailToken(did, purpose, token) {
        return emailToken.assertValidToken(this.db, did, purpose, token);
    }
    async assertValidEmailTokenAndCleanup(did, purpose, token) {
        await emailToken.assertValidToken(this.db, did, purpose, token);
        await emailToken.deleteEmailToken(this.db, did, purpose);
    }
    async confirmEmail(opts) {
        const { did, token } = opts;
        await emailToken.assertValidToken(this.db, did, 'confirm_email', token);
        const now = new Date().toISOString();
        await this.db.transaction((dbTxn) => Promise.all([
            emailToken.deleteEmailToken(dbTxn, did, 'confirm_email'),
            account.setEmailConfirmedAt(dbTxn, did, now),
        ]));
    }
    async updateEmail(opts) {
        const { did, email } = opts;
        await this.db.transaction((dbTxn) => Promise.all([
            account.updateEmail(dbTxn, did, email),
            emailToken.deleteAllEmailTokens(dbTxn, did),
        ]));
    }
    async resetPassword(opts) {
        const did = await emailToken.assertValidTokenAndFindDid(this.db, 'reset_password', opts.token);
        await this.updateAccountPassword({ did, password: opts.password });
        return did;
    }
    async updateAccountPassword(opts) {
        const { did } = opts;
        const passwordScrypt = await scrypt.genSaltAndHash(opts.password);
        await this.db.transaction(async (dbTxn) => Promise.all([
            password.updateUserPassword(dbTxn, { did, passwordScrypt }),
            emailToken.deleteEmailToken(dbTxn, did, 'reset_password'),
            auth.revokeRefreshTokensByDid(dbTxn, did),
        ]));
    }
}
exports.AccountManager = AccountManager;
//# sourceMappingURL=account-manager.js.map