import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { execFileSync, spawn } from "node:child_process"; import fs from "node:fs"; import net from "node:net"; import path from "node:path"; import Database from "better-sqlite3"; const PNG_BYTES = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4]); const dataDir = path.join(process.cwd(), ".data", `phase1-auth-smoke-${Date.now()}`); const databasePath = path.join(dataDir, "neta.db"); const port = await getAvailablePort(); const baseUrl = `http://127.0.0.1:${port}`; const env = { ...process.env, NODE_ENV: "development", DATA_DIR: dataDir, DATABASE_PATH: databasePath, APP_URL: baseUrl, NEXT_PUBLIC_SITE_URL: baseUrl, BETTER_AUTH_SECRET: "phase1-auth-smoke-secret-is-longer-than-32-characters", TRUSTED_ORIGINS: baseUrl, NETA_MINIMUM_MOBILE_VERSION: "1.2.3-smoke.1", NEXT_TELEMETRY_DISABLED: "1", }; fs.mkdirSync(dataDir, { recursive: true }); execFileSync(process.execPath, ["scripts/migrate.mjs"], { cwd: process.cwd(), env, stdio: "inherit", }); const server = spawn( process.execPath, ["node_modules/next/dist/bin/next", "dev", "--hostname", "127.0.0.1", "--port", String(port)], { cwd: process.cwd(), env, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32", }, ); let serverOutput = ""; server.stdout.on("data", (chunk) => { serverOutput = `${serverOutput}${chunk}`.slice(-12000); }); server.stderr.on("data", (chunk) => { serverOutput = `${serverOutput}${chunk}`.slice(-12000); }); try { await waitForServer(); const discoveryResponses = await Promise.all( Array.from({ length: 4 }, () => fetch(`${baseUrl}/.well-known/neta`)), ); const discoveryDocuments = await Promise.all( discoveryResponses.map(async (response) => { assert.equal(response.status, 200, "Neta discovery must be public"); assert.match( response.headers.get("cache-control") ?? "", /public/, "Discovery must declare its public cache policy", ); assert.equal(response.headers.get("set-cookie"), null, "Discovery must not create a session"); return response.json(); }), ); const discovery = discoveryDocuments[0]; assert.equal(discovery.protocol, "neta"); assert.equal(discovery.discoveryVersion, 1); assert.match(discovery.instanceId, /^[0-9a-f-]{36}$/i); assert.equal(discovery.api.version, "1"); assert.equal(discovery.api.baseUrl, `${baseUrl}/api/v1`); assert.equal(discovery.api.metaUrl, `${baseUrl}/api/v1/meta`); assert.equal(discovery.security.httpsRequired, true); assert.deepEqual( new Set(discoveryDocuments.map((document) => document.instanceId)), new Set([discovery.instanceId]), "Concurrent discovery must return one stable instance id", ); const publicMeta = await jsonRequest("/api/v1/meta"); assert.equal(publicMeta.response.status, 200); assert.equal(publicMeta.response.headers.get("x-neta-api-version"), "1"); assert.equal(publicMeta.payload.ok, true); assert.equal(publicMeta.payload.data.instance.id, discovery.instanceId); assert.match( publicMeta.payload.data.instance.createdAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, "Instance timestamps must use UTC ISO-8601", ); assert.equal(publicMeta.payload.data.client.minimumSupportedVersion, "1.2.3-smoke.1"); assert.equal(publicMeta.payload.data.links.me, `${baseUrl}/api/v1/me`); assert.deepEqual(publicMeta.payload.data.client.platforms, ["ios", "android"]); assert.deepEqual( publicMeta.payload.data.capabilities.find( (capability) => capability.id === "auth.device-pairing", ), { id: "auth.device-pairing", version: 1, status: "planned", access: "freelancer", }, ); const publicV1Health = await jsonRequest("/api/v1/health"); assert.equal(publicV1Health.response.status, 200); assert.deepEqual( { ok: publicV1Health.payload.ok, status: publicV1Health.payload.data.status, migrationsApplied: publicV1Health.payload.data.checks.migrationsApplied, }, { ok: true, status: "ok", migrationsApplied: true }, ); const anonymousMe = await jsonRequest("/api/v1/me"); assert.equal(anonymousMe.response.status, 401); assert.equal(anonymousMe.response.headers.get("x-neta-api-version"), "1"); assert.deepEqual( { ok: anonymousMe.payload.ok, code: anonymousMe.payload.error.code }, { ok: false, code: "UNAUTHENTICATED" }, ); const setupAttempts = await Promise.all([ authPost("/api/auth/sign-up/email", { name: "Owner One", email: "owner-one@example.com", password: "OwnerOne-Password-123", }), authPost("/api/auth/sign-up/email", { name: "Owner Two", email: "owner-two@example.com", password: "OwnerTwo-Password-123", }), ]); const successfulSetups = setupAttempts.filter((attempt) => attempt.response.ok); assert.equal(successfulSetups.length, 1, "Concurrent setup must create exactly one owner"); const setupPayload = successfulSetups[0].payload; const ownerEmail = setupPayload.user.email; let ownerCookie = cookieHeader(successfulSetups[0].response); const db = new Database(databasePath); try { assert.equal( db.prepare("select count(*) as value from app_profiles where role = 'freelancer'").get().value, 1, "Exactly one freelancer profile must exist", ); const ownerUserId = db .prepare("select auth_user_id as authUserId from app_profiles where role = 'freelancer'") .get().authUserId; assert.deepEqual( db.prepare("select instance_id as instanceId from instance_settings").all(), [{ instanceId: discovery.instanceId }], "Discovery identity must be persisted exactly once", ); const ownerMe = await jsonRequest("/api/v1/me", { cookie: ownerCookie }); assert.equal(ownerMe.response.status, 200); assert.deepEqual( { id: ownerMe.payload.data.user.id, email: ownerMe.payload.data.user.email, role: ownerMe.payload.data.user.role, clientId: ownerMe.payload.data.user.clientId, }, { id: ownerUserId, email: ownerEmail, role: "freelancer", clientId: null, }, ); assert.doesNotMatch( JSON.stringify(ownerMe.payload), /token|password/i, "The me contract must not expose session tokens or password material", ); const insertClient = db.prepare( "insert into clients (id, owner_user_id, name) values (?, ?, ?)", ); for (const [clientId, name] of [ ["client-alpha", "Alpha Client"], ["client-expired", "Expired Client"], ["client-revoked", "Revoked Client"], ]) { insertClient.run(clientId, ownerUserId, name); } db.prepare( "insert into projects (id, owner_user_id, client_id, name, status) values (?, ?, ?, ?, ?)", ).run("project-alpha", ownerUserId, "client-alpha", "Alpha Project", "active"); db.prepare("update projects set revision_quota = ? where id = ?").run(2, "project-alpha"); db.prepare( "insert into projects (id, owner_user_id, client_id, name, status) values (?, ?, ?, ?, ?)", ).run("project-foreign", ownerUserId, "client-expired", "Foreign Project", "active"); db.prepare( "insert into tasks (id, owner_user_id, client_id, project_id, title, status, priority, is_public_to_client) values (?, ?, ?, ?, ?, ?, ?, ?)", ).run("task-portal-public", ownerUserId, "client-alpha", "project-alpha", "Portal Public Task", "todo", "medium", 1); db.prepare( "insert into tasks (id, owner_user_id, client_id, project_id, title, status, priority, is_public_to_client) values (?, ?, ?, ?, ?, ?, ?, ?)", ).run("task-portal-private", ownerUserId, "client-alpha", "project-alpha", "Portal Private Task", "todo", "medium", 0); db.prepare( "insert into project_planning_sections (id, owner_user_id, project_id, category, title, content, metadata, sort_order) values (?, ?, ?, ?, ?, ?, ?, ?)", ).run("planning-portal", ownerUserId, "project-alpha", "overview", "Portal Plan", "Visible planning content", "{}", 0); db.prepare( "insert into finance_transactions (id, owner_user_id, type, amount_minor, currency, transaction_date, payment_status) values (?, ?, ?, ?, ?, ?, ?)", ).run( "phase7-finance", ownerUserId, "income", 10_000, "TRY", new Date().toISOString().slice(0, 10), "paid", ); db.prepare( "insert into chat_sessions (id, owner_user_id, title) values (?, ?, ?)", ).run("phase7-chat", ownerUserId, "Phase 7 Chat"); db.prepare( "insert into proposals (id, owner_user_id, client_id, project_id, title, amount_minor, currency, status) values (?, ?, ?, ?, ?, ?, ?, ?)", ).run("phase7-proposal", ownerUserId, "client-alpha", "project-alpha", "Phase 7 Proposal", 25_000, "TRY", "draft"); db.prepare( "insert into invoices (id, owner_user_id, client_id, project_id, invoice_number, amount_minor, currency, status, issue_date) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", ).run("phase7-invoice", ownerUserId, "client-alpha", "project-alpha", "P7-001", 25_000, "TRY", "draft", "2026-07-17"); db.prepare( "insert into subscriptions (id, owner_user_id, name, amount_minor, currency, billing_cycle, status) values (?, ?, ?, ?, ?, ?, ?)", ).run("phase7-subscription", ownerUserId, "Phase 7 Hosting", 5_000, "TRY", "monthly", "active"); const rejectedRegistration = await authPost("/api/auth/sign-up/email", { name: "Public Attacker", email: "attacker@example.com", password: "Attacker-Password-123", }); assert.equal(rejectedRegistration.response.ok, false, "Public registration must close after setup"); if (!ownerCookie) { const signedIn = await authPost("/api/auth/sign-in/email", { email: ownerEmail, password: ownerEmail.startsWith("owner-one") ? "OwnerOne-Password-123" : "OwnerTwo-Password-123", }); assert.equal(signedIn.response.ok, true, "Owner must be able to sign in"); ownerCookie = cookieHeader(signedIn.response); } assert.ok(ownerCookie, "Owner session cookie must be issued"); for (const pathname of [ "/", "/clients", "/clients/client-alpha", "/projects", "/projects/project-alpha", "/tasks", "/calendar", "/finance", "/journal", "/analytics", "/settings", "/chat", "/business/proposals", "/business/invoices", "/business/subscriptions", ]) { const page = await fetch(`${baseUrl}${pathname}`, { headers: { cookie: ownerCookie }, redirect: "manual", }); assert.equal(page.status, 200, `Freelancer SSR route failed: ${pathname}`); assert.doesNotMatch(await page.text(), /lib\/supabase|supabase\.co/i, `SSR output leaked Supabase: ${pathname}`); } for (const [pathname, body] of [ ["/api/finance-analysis", undefined], ["/api/project-risk", { projectId: "project-alpha" }], ]) { const anonymousAi = await jsonRequest(pathname, { method: "POST", body, }); assert.equal(anonymousAi.response.status, 401, `Anonymous AI request must fail: ${pathname}`); const missingAiSettings = await jsonRequest(pathname, { method: "POST", cookie: ownerCookie, body, }); assert.equal(missingAiSettings.response.status, 400, `Missing AI key must fail: ${pathname}`); } const chatBody = { sessionId: "phase7-chat", messages: [{ id: "phase7-user-message", role: "user", parts: [{ type: "text", text: "Projeyi özetle" }], }], }; const anonymousChat = await fetch(`${baseUrl}/api/chat`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl }, body: JSON.stringify(chatBody), }); assert.equal(anonymousChat.status, 401, "Anonymous chat request must fail"); const missingChatSettings = await fetch(`${baseUrl}/api/chat`, { method: "POST", headers: { "content-type": "application/json", cookie: ownerCookie, origin: baseUrl, }, body: JSON.stringify(chatBody), }); assert.equal(missingChatSettings.status, 400, "Missing AI key must fail: /api/chat"); assert.equal( db.prepare("select count(*) as value from chat_messages where session_id = ?") .get("phase7-chat").value, 0, "A rejected AI request must not append chat messages", ); const anonymousUpload = await uploadFile("avatar", { fileName: "anonymous.png" }); assert.equal(anonymousUpload.response.status, 401, "Anonymous file upload must fail"); assert.deepEqual( { ok: anonymousUpload.payload.ok, code: anonymousUpload.payload.error.code }, { ok: false, code: "UNAUTHENTICATED" }, "File API errors must use the standard envelope", ); const logoUpload = await uploadFile("branding_logo", { cookie: ownerCookie, fileName: "logo.png", }); assert.equal(logoUpload.response.status, 201, JSON.stringify(logoUpload.payload)); assert.equal(logoUpload.payload.ok, true, "File API success must use the standard envelope"); const logoFileId = logoUpload.payload.data.id; const brandingUpdate = await jsonRequest("/api/branding", { method: "PATCH", cookie: ownerCookie, body: { applicationName: "Neta Smoke Studio", primaryColor: "#336699", accentColor: "#F0CC22", lightLogoFileId: logoFileId, }, }); assert.equal(brandingUpdate.response.ok, true, JSON.stringify(brandingUpdate.payload)); assert.equal(brandingUpdate.payload.data.applicationName, "Neta Smoke Studio"); const brandedLoginHtml = await (await fetch(`${baseUrl}/login`)).text(); assert.match(brandedLoginHtml, /Neta Smoke Studio/, "Branding metadata must be server-rendered"); assert.match(brandedLoginHtml, /--poyraz-primary:#336699/, "Brand tokens must be present in first HTML response"); const dynamicManifest = await (await fetch(`${baseUrl}/manifest.webmanifest`)).json(); assert.equal(dynamicManifest.name, "Neta Smoke Studio", "Manifest must use instance branding"); const brandedMeta = await jsonRequest("/api/v1/meta"); assert.equal(brandedMeta.payload.data.instance.applicationName, "Neta Smoke Studio"); assert.equal( brandedMeta.payload.data.branding.lightLogoUrl, `${baseUrl}/api/branding/assets/${logoFileId}`, "Mobile metadata must expose absolute branding asset URLs", ); const publicLogo = await fetch(`${baseUrl}/api/branding/assets/${logoFileId}`); assert.equal(publicLogo.status, 200, "Referenced branding asset must be publicly readable"); assert.equal(publicLogo.headers.get("x-content-type-options"), "nosniff"); assert.deepEqual(new Uint8Array(await publicLogo.arrayBuffer()), PNG_BYTES); const portalAssetUpload = await uploadFile("project_asset", { cookie: ownerCookie, fileName: "portal.png", projectId: "project-alpha", portalVisible: true, }); assert.equal(portalAssetUpload.response.status, 201, JSON.stringify(portalAssetUpload.payload)); const portalAssetFileId = portalAssetUpload.payload.data.id; const privateAssetUpload = await uploadFile("project_asset", { cookie: ownerCookie, fileName: "private.png", projectId: "project-alpha", portalVisible: false, }); assert.equal(privateAssetUpload.response.status, 201, JSON.stringify(privateAssetUpload.payload)); const privateAssetFileId = privateAssetUpload.payload.data.id; const anonymousInvite = await jsonRequest("/api/portal-invitations", { method: "POST", body: { clientId: "anonymous-client", email: "anonymous@example.com" }, }); assert.equal(anonymousInvite.response.status, 401, "Anonymous invitation creation must fail"); const invalidInvite = await jsonRequest("/api/portal-invitations", { method: "POST", cookie: ownerCookie, body: { clientId: "", email: "not-an-email" }, }); assert.equal(invalidInvite.response.status, 400, "Invalid invitation input must fail"); const missingClientInvite = await jsonRequest("/api/portal-invitations", { method: "POST", cookie: ownerCookie, body: { clientId: "missing-client", email: "missing@example.com" }, }); assert.equal(missingClientInvite.response.status, 404, "Invitation target must be an owned client"); const firstInvite = await jsonRequest("/api/portal-invitations", { method: "POST", cookie: ownerCookie, body: { clientId: "client-alpha", email: "client@example.com" }, }); assert.equal(firstInvite.response.status, 201); const rawFirstToken = tokenFromUrl(firstInvite.payload.invitation.invitationUrl); const storedFirst = db .prepare("select token_hash as tokenHash, status from portal_invitations where id = ?") .get(firstInvite.payload.invitation.id); assert.notEqual(storedFirst.tokenHash, rawFirstToken, "Raw invitation token must not be stored"); assert.equal(storedFirst.tokenHash, sha256(rawFirstToken)); const secondInvite = await jsonRequest("/api/portal-invitations", { method: "POST", cookie: ownerCookie, body: { clientId: "client-alpha", email: "client@example.com" }, }); assert.equal(secondInvite.response.status, 201); assert.equal( db.prepare("select status from portal_invitations where id = ?").get(firstInvite.payload.invitation.id).status, "revoked", "A replacement invitation must revoke the prior active invitation", ); const revokedByReplacement = await acceptInvite(rawFirstToken); assert.equal(revokedByReplacement.response.status, 409); const rawClientToken = tokenFromUrl(secondInvite.payload.invitation.invitationUrl); const accepted = await acceptInvite(rawClientToken); assert.equal(accepted.response.status, 201, JSON.stringify(accepted.payload)); const clientAuthUserId = db .prepare("select auth_user_id as authUserId from app_profiles where email = ?") .get("client@example.com").authUserId; assert.deepEqual( db .prepare("select role, client_id as clientId, disabled from app_profiles where email = ?") .get("client@example.com"), { role: "client", clientId: "client-alpha", disabled: 0 }, ); assert.equal( db.prepare("select auth_user_id as authUserId from clients where id = ?").get("client-alpha") .authUserId, clientAuthUserId, "Accepted invitation must atomically link the domain client", ); assert.notEqual( db.prepare("select password from account where user_id = ?").get(clientAuthUserId).password, "Client-Password-123", "Client password must be hashed", ); db.prepare( "insert into project_revisions (id, owner_user_id, project_id, client_id, requested_by_user_id, description, status) values (?, ?, ?, ?, ?, ?, ?)", ).run( "revision-portal", ownerUserId, "project-alpha", "client-alpha", clientAuthUserId, "Portal Revision Request", "pending", ); const replayed = await acceptInvite(rawClientToken); assert.equal(replayed.response.status, 409, "Accepted invitation must be single-use"); const clientSignIn = await authPost("/api/auth/sign-in/email", { email: "client@example.com", password: "Client-Password-123", }); assert.equal(clientSignIn.response.ok, true, JSON.stringify(clientSignIn.payload)); const clientCookie = cookieHeader(clientSignIn.response); const clientMe = await jsonRequest("/api/v1/me", { cookie: clientCookie }); assert.equal(clientMe.response.status, 200); assert.deepEqual( { role: clientMe.payload.data.user.role, clientId: clientMe.payload.data.user.clientId, }, { role: "client", clientId: "client-alpha" }, ); for (const [pathname, body] of [ ["/api/finance-analysis", undefined], ["/api/project-risk", { projectId: "project-alpha" }], ]) { const forbiddenAi = await jsonRequest(pathname, { method: "POST", cookie: clientCookie, body, }); assert.equal(forbiddenAi.response.status, 403, `Client AI access must fail: ${pathname}`); } const forbiddenClientChat = await fetch(`${baseUrl}/api/chat`, { method: "POST", headers: { "content-type": "application/json", cookie: clientCookie, origin: baseUrl, }, body: JSON.stringify(chatBody), }); assert.equal(forbiddenClientChat.status, 403, "Client chat access must fail"); for (const pathname of [ "/portal", "/portal/projects", "/portal/projects/project-alpha", "/portal/tasks", "/portal/revisions", ]) { const page = await fetch(`${baseUrl}${pathname}`, { headers: { cookie: clientCookie }, redirect: "manual", }); assert.equal(page.status, 200, `Portal SSR route failed: ${pathname}`); const html = await page.text(); assert.doesNotMatch(html, /lib\/supabase|supabase\.co/i, `Portal SSR output leaked Supabase: ${pathname}`); if (pathname === "/portal") assert.match(html, /Neta Smoke Studio/, "Portal must render local branding"); if (pathname === "/portal/tasks" || pathname === "/portal/projects/project-alpha") { assert.match(html, /Portal Public Task/, `Public task missing from ${pathname}`); assert.doesNotMatch(html, /Portal Private Task/, `Private task leaked from ${pathname}`); } if (pathname === "/portal/projects/project-alpha") { assert.match(html, /Visible planning content/, "Portal planning section must be visible"); } if (pathname === "/portal/revisions") { assert.match(html, /Portal Revision Request/, "Portal revision history must be visible"); } } const foreignProject = await fetch(`${baseUrl}/portal/projects/project-foreign`, { headers: { cookie: clientCookie }, redirect: "manual", }); assert.equal(foreignProject.status, 404, "Client must not open another client's project"); const clientPortalAsset = await fetch(`${baseUrl}/api/files/${portalAssetFileId}`, { headers: { cookie: clientCookie }, }); assert.equal(clientPortalAsset.status, 200, "Client must read portal-visible project asset"); const clientPrivateAsset = await fetch(`${baseUrl}/api/files/${privateAssetFileId}`, { headers: { cookie: clientCookie }, }); assert.equal(clientPrivateAsset.status, 404, "Client must not read private project asset"); const forbiddenProjectUpload = await uploadFile("project_asset", { cookie: clientCookie, fileName: "forbidden.png", projectId: "project-alpha", portalVisible: true, }); assert.equal(forbiddenProjectUpload.response.status, 403, "Client must not upload project assets"); const clientAvatarUpload = await uploadFile("avatar", { cookie: clientCookie, fileName: "client-avatar.png", }); assert.equal(clientAvatarUpload.response.status, 201, JSON.stringify(clientAvatarUpload.payload)); const clientAvatarFileId = clientAvatarUpload.payload.data.id; const clientAvatar = await fetch(`${baseUrl}/api/files/${clientAvatarFileId}`, { headers: { cookie: clientCookie }, }); assert.equal(clientAvatar.status, 200, "Client must read own avatar"); const deletedAvatar = await fetch(`${baseUrl}/api/files/${clientAvatarFileId}`, { method: "DELETE", headers: { cookie: clientCookie, origin: baseUrl }, }); assert.equal(deletedAvatar.status, 204, "Client must delete own avatar"); const roleViolation = await jsonRequest("/api/portal-invitations", { method: "POST", cookie: clientCookie, body: { clientId: "forbidden-client", email: "forbidden@example.com" }, }); assert.equal(roleViolation.response.status, 403, "Client must not create invitations"); const disableClient = await jsonRequest("/api/portal-clients/client-alpha", { method: "PATCH", cookie: ownerCookie, body: { enabled: false }, }); assert.equal(disableClient.response.ok, true); const revokedSession = await fetch(`${baseUrl}/api/auth/get-session`, { headers: { cookie: clientCookie }, }); assert.equal((await revokedSession.json()), null, "Disabling a client must revoke active sessions"); const revokedClientMe = await jsonRequest("/api/v1/me", { cookie: clientCookie }); assert.equal(revokedClientMe.response.status, 401, "Disabled client API session must be rejected"); const disabledSignIn = await authPost("/api/auth/sign-in/email", { email: "client@example.com", password: "Client-Password-123", }); assert.equal(disabledSignIn.response.ok, false, "Disabled client must not create a session directly"); const enableClient = await jsonRequest("/api/portal-clients/client-alpha", { method: "PATCH", cookie: ownerCookie, body: { enabled: true }, }); assert.equal(enableClient.response.ok, true); const enabledSignIn = await authPost("/api/auth/sign-in/email", { email: "client@example.com", password: "Client-Password-123", }); assert.equal(enabledSignIn.response.ok, true, "Re-enabled client must be able to sign in"); const expiringInvite = await jsonRequest("/api/portal-invitations", { method: "POST", cookie: ownerCookie, body: { clientId: "client-expired", email: "expired@example.com" }, }); const rawExpiredToken = tokenFromUrl(expiringInvite.payload.invitation.invitationUrl); db.prepare("update portal_invitations set expires_at = ? where id = ?").run( Date.now() - 1000, expiringInvite.payload.invitation.id, ); const expired = await acceptInvite(rawExpiredToken); assert.equal(expired.response.status, 409); assert.equal(expired.payload.code, "INVITATION_EXPIRED"); assert.equal( db.prepare("select status from portal_invitations where id = ?").get(expiringInvite.payload.invitation.id).status, "expired", ); const manualRevokeInvite = await jsonRequest("/api/portal-invitations", { method: "POST", cookie: ownerCookie, body: { clientId: "client-revoked", email: "revoked@example.com" }, }); const rawRevokedToken = tokenFromUrl(manualRevokeInvite.payload.invitation.invitationUrl); const revoked = await jsonRequest( `/api/portal-invitations/${manualRevokeInvite.payload.invitation.id}`, { method: "DELETE", cookie: ownerCookie }, ); assert.equal(revoked.response.ok, true); assert.equal((await acceptInvite(rawRevokedToken)).response.status, 409); const auditTypes = new Set( db.prepare("select distinct type from auth_audit_events").all().map((row) => row.type), ); for (const requiredType of [ "setup_started", "setup_completed", "registration_rejected", "login_succeeded", "login_failed", "invitation_created", "invitation_accepted", "invitation_revoked", "invitation_expired", "client_access_disabled", "client_access_enabled", ]) { assert.ok(auditTypes.has(requiredType), `Missing audit event: ${requiredType}`); } const signOut = await authPost("/api/auth/sign-out", {}, ownerCookie); assert.equal(signOut.response.ok, true); const ownerSessionAfterLogout = await fetch(`${baseUrl}/api/auth/get-session`, { headers: { cookie: ownerCookie }, }); assert.equal(await ownerSessionAfterLogout.json(), null, "Logout must revoke owner session"); } finally { db.close(); } console.log("Phase 1 auth and invitation smoke passed."); } catch (error) { console.error(serverOutput); throw error; } finally { if (server.pid && process.platform !== "win32") { try { process.kill(-server.pid, "SIGTERM"); } catch {} } else { server.kill("SIGTERM"); } await Promise.race([ new Promise((resolve) => server.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 5000)), ]); } async function acceptInvite(token) { return jsonRequest("/api/portal-invitations/accept", { method: "POST", body: { token, displayName: "Portal Client", password: "Client-Password-123" }, }); } async function authPost(pathname, body, cookie) { return jsonRequest(pathname, { method: "POST", body, cookie }); } async function uploadFile(kind, { cookie, fileName, projectId, portalVisible } = {}) { const formData = new FormData(); formData.set("kind", kind); formData.set("file", new Blob([PNG_BYTES], { type: "image/png" }), fileName ?? "upload.png"); if (projectId) formData.set("projectId", projectId); if (portalVisible !== undefined) formData.set("portalVisible", String(portalVisible)); const headers = { origin: baseUrl }; if (cookie) headers.cookie = cookie; const response = await fetch(`${baseUrl}/api/files`, { method: "POST", headers, body: formData }); const text = await response.text(); return { response, payload: text ? JSON.parse(text) : null }; } async function jsonRequest(pathname, { method, body, cookie } = {}) { const headers = { origin: baseUrl }; if (body !== undefined) headers["content-type"] = "application/json"; if (cookie) headers.cookie = cookie; const response = await fetch(`${baseUrl}${pathname}`, { method: method ?? "GET", headers, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await response.text(); const payload = text ? JSON.parse(text) : null; return { response, payload }; } function cookieHeader(response) { const values = response.headers.getSetCookie?.() ?? []; const fallback = response.headers.get("set-cookie"); return (values.length > 0 ? values : fallback ? [fallback] : []) .map((value) => value.split(";", 1)[0]) .join("; "); } function tokenFromUrl(value) { return new URL(value).pathname.split("/").at(-1); } function sha256(value) { return createHash("sha256").update(value, "utf8").digest("hex"); } async function waitForServer() { const deadline = Date.now() + 60_000; while (Date.now() < deadline) { if (server.exitCode !== null) { throw new Error(`Next.js server exited early (${server.exitCode}).\n${serverOutput}`); } try { const response = await fetch(`${baseUrl}/api/health/live`); if (response.ok) return; } catch {} await new Promise((resolve) => setTimeout(resolve, 250)); } throw new Error(`Timed out waiting for Next.js server.\n${serverOutput}`); } function getAvailablePort() { return new Promise((resolve, reject) => { const listener = net.createServer(); listener.once("error", reject); listener.listen(0, "127.0.0.1", () => { const address = listener.address(); listener.close(() => resolve(address.port)); }); }); }