import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { createMagicLink, exchangeMagicLink, InvalidLoginCodeError, type AuthRepository, type PendingLoginCode, } from "../src/index"; class MemoryAuthRepository implements AuthRepository { loginCode: PendingLoginCode | undefined; consumedHash: string | undefined; async saveLoginCode(code: PendingLoginCode) { this.loginCode = code; } async exchangeLoginCode(input: Parameters[0]) { this.consumedHash = input.loginCodeHash; if ( !this.loginCode || this.loginCode.tokenHash !== input.loginCodeHash || this.loginCode.expiresAt <= input.now ) { return null; } this.loginCode = undefined; return { user: { id: "01JQ0000000000000000000000", discordUserId: "123456789012345678", discordUsername: "steve", firstName: null, }, isNewUser: true, }; } } const identity = { id: "123456789012345678", username: "steve", globalName: "Steve", }; const now = new Date("2026-08-01T12:00:00.000Z"); function hash(value: string) { return createHash("sha256").update(value).digest("hex"); } describe("Discord magic-link authentication", () => { it("returns a link while persisting only the token hash", async () => { const repository = new MemoryAuthRepository(); const result = await createMagicLink(identity, { repository, appUrl: "https://accounts.example.com", now: () => now, randomToken: () => "private-login-token", }); expect(result.url).toBe("https://accounts.example.com/auth/discord?code=private-login-token"); expect(result.expiresAt).toEqual(new Date("2026-08-01T12:10:00.000Z")); expect(repository.loginCode).toMatchObject({ tokenHash: hash("private-login-token"), discordUserId: identity.id, discordUsername: identity.username, }); expect(JSON.stringify(repository.loginCode)).not.toContain("private-login-token"); }); it("exchanges a valid one-time code for a session", async () => { const repository = new MemoryAuthRepository(); await createMagicLink(identity, { repository, appUrl: "https://accounts.example.com", now: () => now, randomToken: () => "private-login-token", }); const result = await exchangeMagicLink("private-login-token", { repository, now: () => new Date("2026-08-01T12:01:00.000Z"), randomToken: () => "private-session-token", }); expect(result.sessionToken).toBe("private-session-token"); expect(result.user.discordUserId).toBe(identity.id); expect(repository.consumedHash).toBe(hash("private-login-token")); }); it("rejects an expired or already-consumed code", async () => { const repository = new MemoryAuthRepository(); await expect( exchangeMagicLink("missing-token", { repository, now: () => now, randomToken: () => "private-session-token", }), ).rejects.toBeInstanceOf(InvalidLoginCodeError); }); });