Add byte-compatible Auth & SSO core primitives

Pure, unit-tested primitives the AtomCMS->Next.js login must reproduce exactly
(verified now with round-trip + known vectors; full end-to-end check deferred
until a real DB + APP_KEY + live emulator are available):

- password.ts: argon2id (m=65536,t=4,p=1 via hash-wasm) + bcrypt ($2y$ accepted)
  verify, and the md5->argon2id on-login upgrade gated by convert_passwords
  (mirrors RedirectIfTwoFactorAuthenticatable).
- sso-ticket.ts: '{hotel_name without spaces}-{uuidv4}' written to auth_ticket +
  ip_current (mirrors User::ssoTicket()).
- laravel-encrypter.ts: AES-256-CBC + HMAC-SHA256 payload compatible with
  Laravel encrypt()/encryptString (for existing 2FA secrets) incl. PHP string
  (de)serialization.
- totp.ts: otplib Google2FA-compatible TOTP verify (SHA1/6/30).

Libs: hash-wasm + bcryptjs + otplib (pure JS/WASM, no native build). 28 tests.
This commit is contained in:
Simo committed 2026-06-27 16:00:25 +02:00
1 parent 5fd40feaa6
commit ec2d46e583
11 files changed
+511

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from "vitest";
import { generateSsoTicket, issueSsoTicket } from "./sso-ticket";
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
describe("generateSsoTicket", () => {
it("uses '{hotelName-without-spaces}-{uuidv4}'", () => {
const t = generateSsoTicket("Atom Hotel");
expect(t.startsWith("AtomHotel-")).toBe(true);
expect(UUID_RE.test(t.slice("AtomHotel-".length))).toBe(true);
});
it("strips every space in the hotel name", () => {
expect(generateSsoTicket("My Cool Hotel").startsWith("MyCoolHotel-")).toBe(true);
});
it("produces a fresh ticket each call", () => {
expect(generateSsoTicket("Atom")).not.toBe(generateSsoTicket("Atom"));
});
});
describe("issueSsoTicket", () => {
it("writes auth_ticket AND ip_current and returns the ticket", async () => {
const update = vi.fn().mockResolvedValue(undefined);
const db = { user: { update } };
const ticket = await issueSsoTicket(db, 42, "Atom Hotel", "1.2.3.4");
expect(ticket.startsWith("AtomHotel-")).toBe(true);
expect(update).toHaveBeenCalledWith({
where: { id: 42 },
data: { authTicket: ticket, ipCurrent: "1.2.3.4" },
});
});
});