import { createHash, randomBytes } from "node:crypto"; type AccountInviteContext = { verificationToken: { identifier: string; expires: Date }; user: { id: string; email: string | null; name: string | null; passwordHash: string | null; emailVerified: Date | null; accounts: Array<{ id: string }>; }; }; type AccountInviteOptions = { prisma: any; tokenTtlMs?: number; identifierPrefix?: string; }; const DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX = "account-invite:"; const DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; function hashAccountInviteToken(token: string): string { return createHash("sha256").update(token).digest("hex"); } function buildAccountInviteIdentifier(prefix: string, userId: string): string { return `${prefix}${userId}`; } export async function createAccountInviteToken( options: AccountInviteOptions & { userId: string } ): Promise<{ token: string; expiresAt: Date }> { const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX; const identifier = buildAccountInviteIdentifier(identifierPrefix, options.userId); const token = randomBytes(32).toString("hex"); const expiresAt = new Date(Date.now() + (options.tokenTtlMs ?? DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS)); await options.prisma.verificationToken.deleteMany({ where: { OR: [{ identifier }, { expires: { lt: new Date() } }] } }); await options.prisma.verificationToken.create({ data: { identifier, token: hashAccountInviteToken(token), expires: expiresAt } }); return { token, expiresAt }; } export async function getAccountInviteContext( options: AccountInviteOptions & { token: string } ): Promise { const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX; const verificationToken = await options.prisma.verificationToken.findUnique({ where: { token: hashAccountInviteToken(options.token) }, select: { identifier: true, expires: true } }); if ( !verificationToken || verificationToken.expires <= new Date() || !verificationToken.identifier.startsWith(identifierPrefix) ) { return null; } const userId = verificationToken.identifier.slice(identifierPrefix.length); if (!userId) { return null; } const user = await options.prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, name: true, passwordHash: true, emailVerified: true, accounts: { select: { id: true }, take: 1 } } }); if (!user?.email) { return null; } return { verificationToken, user }; } export { DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX, DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS, buildAccountInviteIdentifier, hashAccountInviteToken };