evOo-local/packages/auth/src/identity-trust.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

252 lines
9.8 KiB
TypeScript

import type { TransactionalEmail } from "@rakazo/adapter-kit";
import { bootstrapUserSpace } from "@rakazo/db";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createAuth } from "./index.js";
// Exercise Better Auth's real routing, password hashing, verification and
// session hooks with its official offline adapter. Only persistence is faked.
vi.mock("better-auth/adapters/prisma", async () => {
const { memoryAdapter } = await import("better-auth/adapters/memory");
return {
prismaAdapter: (prisma: { authData: Record<string, unknown[]> }) =>
memoryAdapter(prisma.authData),
};
});
vi.mock("@rakazo/db", () => ({ bootstrapUserSpace: vi.fn(async () => ({ spaceId: "space-1" })) }));
function fixture({
allowlist = "",
delivery = true,
baseURL = "http://auth.example.test",
webOrigin = "http://web.example.test",
requestOrigin,
}: {
allowlist?: string;
delivery?: boolean;
baseURL?: string;
webOrigin?: string;
requestOrigin?: string;
} = {}) {
const data: Record<string, Record<string, unknown>[]> = {
user: [],
account: [],
session: [],
verification: [],
};
const policy = {
signupsEnabled: true,
signupAllowlist: allowlist,
signupPolicyInitialized: true,
};
const messages: TransactionalEmail[] = [];
const members = new Set<string>();
const prisma = {
authData: data,
deploymentSettings: { findUnique: vi.fn(async () => policy) },
spaceMember: {
findFirst: vi.fn(async ({ where }: { where: { userId: string } }) =>
members.has(where.userId) ? { spaceId: "space-1" } : null,
),
},
};
vi.mocked(bootstrapUserSpace).mockImplementation(async (_prisma, user) => {
members.add(user.id);
return { spaceId: "space-1" };
});
const auth = createAuth(prisma as never, {
secret: "offline-auth-secret-at-least-32-characters",
baseURL,
webOrigin,
signupsEnabled: "true",
signupAllowlist: "",
email: delivery
? {
describe: () => ({
id: "offline-email",
contractVersion: "1",
adapterVersion: "1",
capabilities: { transactional: true },
}),
send: async (message) => {
messages.push(message);
},
}
: undefined,
});
const request = (path: string, body?: unknown, token?: string) =>
auth.handler(
new Request(`${baseURL}/api/auth${path}`, {
method: body ? "POST" : "GET",
headers: {
"content-type": "application/json",
origin: requestOrigin ?? webOrigin,
...(token ? { authorization: `Bearer ${token}` } : {}),
},
body: body ? JSON.stringify(body) : undefined,
}),
);
const signup = (email = "approved@example.test") =>
request("/sign-up/email", {
email,
password: "offline-password12",
name: "Test User",
emailVerified: true,
id: "msg-attacker-chosen-id",
});
const signin = (email = "approved@example.test") =>
request("/sign-in/email", {
email,
password: "offline-password12",
});
const verify = () => {
const url = new URL(messages.at(-1)!.text.match(/http:\/\/\S+/)![0]);
return request(`${url.pathname.replace("/api/auth", "")}${url.search}`);
};
return { auth, request, signup, signin, verify, data, policy, messages, members };
}
beforeEach(() => vi.clearAllMocks());
describe("loopback trusted origins", () => {
it("accepts Origin localhost when webOrigin is 127.0.0.1", async () => {
const f = fixture({
delivery: false,
baseURL: "http://127.0.0.1:5173",
webOrigin: "http://127.0.0.1:5173",
requestOrigin: "http://localhost:5173",
});
expect((await f.signup()).status).toBe(200);
});
it("accepts Origin 127.0.0.1 when webOrigin is localhost", async () => {
const f = fixture({
delivery: false,
baseURL: "http://localhost:5173",
webOrigin: "http://localhost:5173",
requestOrigin: "http://127.0.0.1:5173",
});
expect((await f.signup()).status).toBe(200);
});
});
describe("identity trust through auth endpoints", () => {
it("keeps first-owner bootstrap and password signup available without email for open deployments", async () => {
const f = fixture({ delivery: false });
const response = await f.signup();
expect(response.status).toBe(200);
const body = (await response.json()) as {
token: string;
user: { id: string; emailVerified: boolean };
};
expect(body.token).toEqual(expect.any(String));
expect(body.user.emailVerified).toBe(false);
expect(body.user.id).not.toBe("msg-attacker-chosen-id");
expect(bootstrapUserSpace).toHaveBeenCalledTimes(1);
expect((await f.signin()).status).toBe(200);
expect(bootstrapUserSpace).toHaveBeenCalledTimes(1);
});
it("fails closed for a matching allowlisted address when email delivery is unavailable", async () => {
const f = fixture({ allowlist: "@example.test", delivery: false });
expect((await f.signup()).status).toBe(400);
expect(f.data.user).toHaveLength(0);
expect(bootstrapUserSpace).not.toHaveBeenCalled();
});
it("requires mailbox proof before creating a session, space or first-owner claim", async () => {
const f = fixture({ allowlist: "approved@example.test" });
expect((await f.signup("outsider@example.test")).status).toBe(400);
const response = await f.signup();
expect(response.status).toBe(200);
expect(response.headers.get("set-cookie")).toBeNull();
expect(await response.json()).toMatchObject({ token: null, user: { emailVerified: false } });
expect(f.data.session).toHaveLength(0);
expect(bootstrapUserSpace).not.toHaveBeenCalled();
expect((await f.signin()).status).toBe(403);
expect(f.messages).toHaveLength(2);
expect((await f.request("/verify-email?token=forged-token")).status).not.toBe(200);
expect(f.data.user![0]!.emailVerified).toBe(false);
const verified = await f.verify();
expect(verified.status).toBe(302);
expect(verified.headers.get("location")).toBe("http://web.example.test/sign-in");
expect(verified.headers.get("set-cookie")).toBeNull();
expect(f.data.user![0]!.emailVerified).toBe(true);
expect(bootstrapUserSpace).not.toHaveBeenCalled();
const signedIn = await f.signin();
expect(signedIn.status).toBe(200);
const { token } = (await signedIn.json()) as { token: string };
expect(token).toEqual(expect.any(String));
expect(bootstrapUserSpace).toHaveBeenCalledTimes(1);
expect(
await f.auth.api.getSession({ headers: new Headers({ authorization: `Bearer ${token}` }) }),
).toMatchObject({ user: { emailVerified: true } });
});
it("rechecks policy before admitting a verified but unprovisioned signup", async () => {
const f = fixture({ allowlist: "@example.test" });
await f.signup();
await f.verify();
f.policy.signupsEnabled = false;
expect((await f.signin()).status).toBe(403);
expect(bootstrapUserSpace).not.toHaveBeenCalled();
f.policy.signupsEnabled = true;
f.policy.signupAllowlist = "someone-else@example.test";
expect((await f.signin()).status).toBe(403);
expect(bootstrapUserSpace).not.toHaveBeenCalled();
});
it("gates existing unverified sessions and auth mutations when the live allowlist is enabled", async () => {
const f = fixture();
const signedUp = await f.signup();
const cookie = signedUp.headers.get("set-cookie")!.split(";")[0]!;
const { token } = (await signedUp.json()) as { token: string };
expect(await f.auth.api.getSession({ headers: new Headers({ cookie }) })).not.toBeNull();
f.policy.signupAllowlist = "@example.test";
expect(await f.auth.api.getSession({ headers: new Headers({ cookie }) })).toBeNull();
expect(await (await f.request("/get-session", undefined, token)).json()).toBeNull();
expect(
await f.auth.api.getSession({ headers: new Headers({ authorization: `Bearer ${token}` }) }),
).toBeNull();
expect((await f.request("/update-user", { name: "Changed" }, token)).status).toBe(401);
expect((await f.signin()).status).toBe(403);
await f.verify();
expect((await f.signin()).status).toBe(200);
});
it("does not leak request-local verification settings when signup policy changes", async () => {
const f = fixture({ allowlist: "@example.test" });
expect(await (await f.signup()).json()).toMatchObject({ token: null });
f.policy.signupAllowlist = "";
expect(await (await f.signup("open@example.test")).json()).toMatchObject({
token: expect.any(String),
});
expect(f.messages).toHaveLength(1);
f.policy.signupAllowlist = "@example.test";
expect(await (await f.signup("restricted@example.test")).json()).toMatchObject({ token: null });
expect(f.messages).toHaveLength(2);
});
it("reserves internal messaging emails across registration, recovery and email changes", async () => {
const f = fixture();
for (const email of [
"msg-sendblue15550001111@messaging.invalid",
"MSG-Test@MESSAGING.INVALID",
]) {
expect((await f.signup(email)).status).toBe(400);
expect((await f.signin(email)).status).toBe(400);
expect((await f.request("/request-password-reset", { email })).status).toBe(400);
expect((await f.request("/send-verification-email", { email })).status).toBe(400);
}
expect(f.data.user).toHaveLength(0);
const { token } = (await (await f.signup()).json()) as { token: string };
expect(
(await f.request("/change-email", { newEmail: "msg-taken@messaging.invalid" }, token)).status,
).toBe(400);
// An account preclaimed before this upgrade must not keep its session.
f.data.user![0]!.email = "msg-taken@messaging.invalid";
expect(await (await f.request("/get-session", undefined, token)).json()).toBeNull();
expect((await f.request("/update-user", { name: "Changed" }, token)).status).toBe(401);
expect(f.messages).toHaveLength(0);
});
});