Files
openwork/evals/specs/enterprise-invite-install-connect.e2e.test.ts

536 lines
25 KiB
TypeScript

import { spawn } from "node:child_process";
import { mkdtemp, writeFile } from "node:fs/promises";
import net from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect } from "vitest";
import { denFetch, evalIn, signIn, waitFor } from "@openwork/behaviors";
import { attachSurface, callFunctionOnSurface, navigate } from "@openwork/cdp";
import { screenshot, validate } from "@openwork/test-evidence";
import { chrome, localHost } from "@openwork/hosts";
import { startEgressLab } from "@openwork/labs";
import { needs, server, test, unmetNeeds } from "@openwork/testkit";
import { resolveSystemCaEnv } from "../../apps/desktop/electron/runtime.mjs";
import type { TestNeeds } from "@openwork/testkit";
const requirements: TestNeeds = {
optIn: ["OPENWORK_EVAL_E2E_TESTS"],
};
const missingRequirements = unmetNeeds(requirements, process.env);
const title = missingRequirements.length > 0
? `enterprise invite→install→connect skipped — needs: ${missingRequirements.join(", ")}`
: "an invited teammate signs up, reaches the token-free install guide, and links the enterprise desktop by workspace address";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function stringField(value: unknown, key: string): string | null {
if (!isRecord(value)) return null;
const field = value[key];
return typeof field === "string" ? field : null;
}
const chainErrorPattern = /UNABLE_TO_VERIFY_LEAF_SIGNATURE|unable to verify the first certificate|unable to get local issuer/i;
// Async spawn on purpose: the egress lab's TLS and AIA servers run inside this
// vitest process, so a sync spawn would deadlock the child's TLS handshake.
async function fetchLabInChild(url: string, env: NodeJS.ProcessEnv): Promise<{ status: number | null; output: string }> {
const script = `
fetch(process.argv[1]).then(async (response) => {
console.log(JSON.stringify({ ok: response.ok, status: response.status }));
process.exit(response.ok ? 0 : 1);
}).catch((error) => {
console.log(JSON.stringify({ ok: false, message: error?.message ?? String(error), causeCode: error?.cause?.code ?? null }));
process.exit(1);
});
`;
const child = spawn(process.execPath, ["--eval", script, url], { env });
const timer = setTimeout(() => child.kill("SIGKILL"), 15_000);
let output = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
output += chunk;
});
child.stderr.on("data", (chunk: string) => {
output += chunk;
});
const status = await new Promise<number | null>((resolve) => {
child.on("error", () => resolve(null));
child.on("close", (code) => resolve(code));
});
clearTimeout(timer);
return { status, output: output.trim() };
}
// Exactly the record the enterprise gate stamps after a confirmed exchange
// (enterprise-activation-gate.tsx, exchangeConfirmedGrant).
async function writeStampedBootstrap(denBaseUrl: string): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), "openwork-invite-connect-activation-"));
const bootstrapPath = join(dir, "desktop-bootstrap.json");
const stamped = {
baseUrl: denBaseUrl,
requireSignin: true,
enterpriseActivation: { activatedAt: new Date().toISOString(), denBaseUrl },
};
await writeFile(bootstrapPath, `${JSON.stringify(stamped, null, 2)}\n`, "utf8");
return bootstrapPath;
}
async function resolveCaEnvFromRecord(options: {
bootstrapPath: string;
rootPem: string;
}): Promise<{ caEnv: NodeJS.ProcessEnv; logs: string[] }> {
const userDataDir = await mkdtemp(join(tmpdir(), "openwork-invite-connect-chain-"));
const logs: string[] = [];
const caEnv: NodeJS.ProcessEnv = await resolveSystemCaEnv({
tlsModule: { getCACertificates: () => [] },
userDataDir,
parentEnv: {},
logInfo(message: unknown) {
logs.push(String(message));
},
loadPlatformCertificates: async () => [options.rootPem],
platformSourceName: "egress-lab-root",
chainRepair: {
bootstrapPath: options.bootstrapPath,
rootsProvider: () => [options.rootPem],
},
});
return { caEnv, logs };
}
async function reserveClosedPort(): Promise<number> {
const srv = net.createServer();
const port = await new Promise<number>((resolve, reject) => {
srv.once("error", reject);
srv.listen(0, "127.0.0.1", () => {
const address = srv.address();
if (!address || typeof address === "string") {
reject(new Error("port reservation did not bind to a TCP port"));
return;
}
resolve(address.port);
});
});
await new Promise<void>((resolve) => srv.close(() => resolve()));
return port;
}
/**
* The whole new journey in one tape: an admin invites a teammate; the teammate
* creates the account THROUGH the invite (never a sign-in against a
* nonexistent account), lands on the clean authenticated /install guide with
* no installer token anywhere, and links a real blank-slate Enterprise desktop
* by typing the workspace address the guide showed — with the pasted
* openwork:// URL as the same field's silent recovery. The one-time grant dies
* after use, and the activation record this flow stamps is exactly what the
* TLS chain-repair seam consumes, unlocking only the stamped origin on a
* broken-chain, semi-airgapped network.
*/
test(title, { timeout: 1_800_000 }, async ({ evidence, place }) => {
needs(requirements);
const runId = `${Date.now().toString(36)}${process.pid.toString(36)}`;
const orgName = `Acme Robotics ${runId}`;
const invitee = {
email: `maya+${runId}@openwork.test`,
name: "Maya Chen",
password: "OpenWorkEval123!",
};
await using den = await server({
place,
org: { name: orgName, admin: { name: "Jordan Chen" } },
});
const invitation = await denFetch(den.ref, "/v1/invitations", {
method: "POST",
headers: { authorization: `Bearer ${den.admin.token}` },
body: JSON.stringify({ email: invitee.email, role: "member" }),
});
const inviteToken = stringField(invitation.body, "inviteToken");
if (!invitation.response.ok || !inviteToken) {
throw new Error(`Invitation failed: HTTP ${invitation.response.status} ${invitation.text.slice(0, 500)}`);
}
// localhost instead of 127.0.0.1 so the Better-Auth session cookie set at
// sign-up is host-only on "localhost" and therefore travels to the Den API
// origin too (cookies ignore ports), which the authenticated download needs.
const webOrigin = den.ref.webUrl.replace("127.0.0.1", "localhost");
const apiOrigin = den.ref.apiUrl.replace("127.0.0.1", "localhost");
await using browser = await chrome({
name: "enterprise-invite-install-connect",
startUrl: webOrigin,
headless: true,
});
await browser.client.send("Emulation.setDeviceMetricsOverride", {
width: 1280,
height: 900,
deviceScaleFactor: 1,
mobile: false,
});
// ── Frame 1: the invite signs the newcomer UP, never IN ──────────────────
await navigate(browser.client, `${webOrigin}/join-org?invite=${encodeURIComponent(inviteToken)}`);
await waitFor(
browser,
`Boolean(document.querySelector('[data-testid="join-org-auth"] input[type="password"]'))
&& [...document.querySelectorAll("button")].some((button) => (button.textContent ?? "").trim() === "Create account")`,
{ timeoutMs: 180_000, label: "invite sign-up form with Create account" },
);
await screenshot(browser);
const authCaptureReady = await evalIn(browser, `(() => {
window.__authCalls = [];
const original = window.fetch.bind(window);
window.fetch = async (...args) => {
const input = args[0];
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("/api/auth/")) window.__authCalls.push(url);
return original(...args);
};
return true;
})()`);
expect(authCaptureReady).toBe(true);
const submitted = await callFunctionOnSurface(browser, `function (nameValue, passwordValue) {
const scope = document.querySelector('[data-testid="join-org-auth"]');
if (!scope) return "no auth scope";
const inputs = [...scope.querySelectorAll("input")];
const name = inputs.find((input) => input.type === "text");
const password = inputs.find((input) => input.type === "password");
if (!name || !password) return "missing fields";
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set;
setValue.call(name, nameValue);
name.dispatchEvent(new Event("input", { bubbles: true }));
setValue.call(password, passwordValue);
password.dispatchEvent(new Event("input", { bubbles: true }));
password.closest("form")?.requestSubmit();
return "submitted";
}`, [invitee.name, invitee.password]);
expect(submitted).toBe("submitted");
const joinLabel = `Join ${orgName}`;
await expect.poll(
() => callFunctionOnSurface(browser, `function (expectedLabel) {
return [...document.querySelectorAll("button")]
.some((button) => (button.textContent ?? "").trim() === expectedLabel && !button.disabled);
}`, [joinLabel]),
{
message: "signed-in one-click join step did not appear after in-invite sign-up",
timeout: 90_000,
interval: 250,
},
).toBe(true);
const authCalls: unknown = await evalIn(browser, "JSON.stringify(window.__authCalls ?? [])");
const calls: string[] = JSON.parse(String(authCalls)) as string[];
const signUpCall = calls.find((url) => url.includes("/api/auth/sign-up/email"));
expect(signUpCall ?? "").toContain("invite=");
expect(calls.some((url) => url.includes("/api/auth/sign-in/email"))).toBe(false);
evidence.recordAssertionEvidence(
"The invite creates the account instead of demanding a sign-in",
`Captured auth calls ${JSON.stringify(calls)}: the form posted /api/auth/sign-up/email carrying the invite and never touched /api/auth/sign-in/email for the not-yet-existing account.`,
Boolean(signUpCall?.includes("invite=")) && !calls.some((url) => url.includes("/api/auth/sign-in/email")),
);
// ── Frame 2: joining lands on the clean token-free install guide ─────────
const joined = await callFunctionOnSurface(browser, `function (expectedLabel) {
const button = [...document.querySelectorAll("button")]
.find((candidate) => (candidate.textContent ?? "").trim() === expectedLabel && !candidate.disabled);
if (!(button instanceof HTMLButtonElement)) return false;
button.click();
return true;
}`, [joinLabel]);
expect(joined).toBe(true);
await waitFor(
browser,
`location.pathname === "/install"
&& location.search === ""
&& Boolean(document.querySelector('[data-testid="install-page"]'))
&& !document.body.textContent?.includes("Loading your install link")`,
{ timeoutMs: 90_000, label: "clean authenticated /install after joining" },
);
await screenshot(browser);
const rawInstall = await evalIn(browser, `(async () => {
const resources = performance.getEntriesByType("resource").map((entry) => entry.name);
const hrefs = [...document.querySelectorAll("a[href]")].map((anchor) => anchor.href);
const headers = new Headers({ Accept: "application/json" });
const storedToken = localStorage.getItem("openwork:web:auth-token")?.trim();
if (storedToken) headers.set("Authorization", "Bearer " + storedToken);
const configResponse = await fetch("/api/den/v1/me/install-config", {
credentials: "include",
headers,
});
const config = await configResponse.json();
return JSON.stringify({
search: location.search,
configLoaded: resources.some((url) => url.includes("/v1/me/install-config")),
mintedToken: resources.some((url) => url.includes("/install-links") || url.includes("/v1/install-config?token=")),
configStatus: configResponse.status,
distribution: config?.distribution,
downloadHrefs: hrefs.filter((href) => href.includes("/v1/me/install/")),
cloudDownloadSurface: [...document.querySelectorAll("h1")]
.some((heading) => (heading.textContent ?? "").trim() === "Download OpenWork"),
cloudReturnControl: [...document.querySelectorAll("a")]
.some((anchor) => (anchor.textContent ?? "").trim() === "I already installed OpenWork"),
enterpriseGuide: Boolean(document.querySelector('[data-testid="install-guide"]')),
skipControl: Boolean(document.querySelector('[data-testid="install-skip-download"]')),
workspaceControl: Boolean(document.querySelector('[data-testid="install-workspace-address"]')),
text: document.body.innerText.replace(/\\s+/g, " ").slice(0, 400),
});
})()`, { awaitPromise: true });
const install: unknown = JSON.parse(String(rawInstall));
if (!isRecord(install) || !Array.isArray(install.downloadHrefs) || !install.downloadHrefs.every((href) => typeof href === "string")) {
throw new Error(`Install facts had an unexpected shape: ${JSON.stringify(install)}`);
}
const distribution = install.distribution;
if (distribution !== "cloud" && distribution !== "enterprise") {
throw new Error(`Install config returned an invalid distribution: ${JSON.stringify(distribution)}`);
}
const downloadHref = install.downloadHrefs[0];
if (typeof downloadHref !== "string") throw new Error("Install page exposed no authenticated download link");
expect(install.search).toBe("");
expect(install.configLoaded).toBe(true);
expect(install.mintedToken).toBe(false);
expect(install.configStatus).toBe(200);
expect(install.downloadHrefs.length).toBeGreaterThan(0);
for (const href of install.downloadHrefs) {
expect(href).toContain("/v1/me/install/");
expect(href).not.toContain("token=");
}
if (distribution === "cloud") {
expect(install.cloudDownloadSurface).toBe(true);
expect(install.cloudReturnControl).toBe(true);
expect(install.enterpriseGuide).toBe(false);
expect(install.skipControl).toBe(false);
expect(install.workspaceControl).toBe(false);
} else {
expect(install.enterpriseGuide).toBe(true);
expect(install.skipControl).toBe(true);
expect(install.cloudReturnControl).toBe(false);
}
const member = await signIn(den.ref, { email: invitee.email, password: invitee.password });
// Daytona preview duplicates CORS headers on Origin-bearing responses; top-level downloads bypass CORS, so probe through the session directly.
const downloadPath = new URL(downloadHref).pathname;
const authedDownload = await denFetch(den.ref, downloadPath, {
headers: { authorization: `Bearer ${member.token}` },
redirect: "manual",
});
const anonymousDownload = await denFetch(den.ref, downloadPath, { redirect: "manual" });
expect([200, 302], `authenticated download answered HTTP ${authedDownload.response.status}`).toContain(authedDownload.response.status);
expect(anonymousDownload.response.ok).toBe(false);
expect([401, 403]).toContain(anonymousDownload.response.status);
evidence.recordAssertionEvidence(
"The install guide is authenticated and token-free end to end",
`URL stayed /install with empty search; authenticated config reported ${distribution}; no /install-links mint and no ?token= anywhere; the session-scoped download path ${downloadPath} answered ${authedDownload.response.status} and the same URL without a session was rejected with ${anonymousDownload.response.status}.`,
true,
);
// ── Frame 3: the guide hands over the workspace address ──────────────────
if (distribution === "enterprise") {
const skipped = await evalIn(browser, `(() => {
const skip = document.querySelector('[data-testid="install-skip-download"]');
if (!(skip instanceof HTMLButtonElement)) return false;
skip.click();
return true;
})()`);
expect(skipped).toBe(true);
await waitFor(
browser,
`Boolean(document.querySelector('[data-testid="install-workspace-address"] input'))`,
{ timeoutMs: 30_000, label: "connect step showing the workspace address" },
);
const shownAddress = String(await evalIn(
browser,
`document.querySelector('[data-testid="install-workspace-address"] input')?.value ?? ""`,
));
expect(shownAddress).toBe(webOrigin);
evidence.recordAssertionEvidence(
"Connect hands the member the exact workspace address",
`Step 3 shows ${shownAddress}, exactly the Den web origin the browser is signed in on, with a copy control and no credential.`,
shownAddress === webOrigin,
);
}
// ── Frames 4-5: the real blank-slate Enterprise desktop links by address ──
const handoff = await denFetch(den.ref, "/v1/auth/desktop-handoff", {
method: "POST",
headers: { authorization: `Bearer ${member.token}` },
body: JSON.stringify({ desktopScheme: "openwork" }),
});
const openworkUrl = stringField(handoff.body, "openworkUrl");
const grant = stringField(handoff.body, "grant");
if (!handoff.response.ok || !openworkUrl || !grant) {
throw new Error(`Desktop handoff mint failed: HTTP ${handoff.response.status} ${handoff.text.slice(0, 400)}`);
}
const placedHost = place.host();
await using localElectronHost = placedHost ? undefined : localHost();
const host = placedHost ?? localElectronHost;
if (!host) throw new Error("No Electron host: neither placement host nor local fallback is available.");
const handle = await host.spawnElectron("enterprise-invite-connect", {
profile: "fresh",
env: { OPENWORK_DESKTOP_DISTRIBUTION: "enterprise" },
});
const desktopSurface = await attachSurface(handle, { timeoutMs: 240_000 });
try {
await waitFor(
desktopSurface,
`Boolean(document.querySelector('[data-testid="enterprise-activation-root"]'))`,
{ timeoutMs: 240_000, label: "blank-slate enterprise gate" },
);
await screenshot(desktopSurface);
const gate: unknown = JSON.parse(String(await evalIn(desktopSurface, `(() => {
const text = document.body.innerText;
return JSON.stringify({
heading: text.includes("Link this app to your organization"),
serverField: Boolean(document.querySelector('#organization-server-input')),
linkField: Boolean(document.querySelector('#enterprise-openwork-link')),
methodToggle: Boolean(document.querySelector('[data-testid="enterprise-connection-method-toggle"]')),
});
})()`)));
expect(isRecord(gate) && gate.heading === true).toBe(true);
expect(isRecord(gate) && gate.serverField === true).toBe(true);
expect(isRecord(gate) && gate.linkField === false).toBe(true);
expect(isRecord(gate) && gate.methodToggle === false).toBe(true);
evidence.recordAssertionEvidence(
"The enterprise blank slate asks one question: the workspace address",
"The packaged-policy gate renders Link this app to your organization with only the organization-server-input form; no OpenWork link field and no method toggle exist.",
true,
);
const typeIntoGate = (value: string) => callFunctionOnSurface(desktopSurface, `function (inputValue) {
const input = document.querySelector('#organization-server-input');
if (!(input instanceof HTMLInputElement)) return "no input";
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set;
setValue.call(input, inputValue);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.closest("form")?.requestSubmit();
return "submitted";
}`, [value]);
// Server-first path up to the browser handoff: typing the address reaches
// the explicit origin-naming confirmation with Continue in browser.
expect(await typeIntoGate(webOrigin)).toBe("submitted");
await expect.poll(
() => callFunctionOnSurface(desktopSurface, `function (expectedOrigin) {
return Boolean(document.querySelector('[data-testid="organization-server-confirm"]'))
&& document.body.innerText.includes(expectedOrigin)
&& document.body.innerText.includes("Continue in browser");
}`, [webOrigin]),
{
message: "origin-naming confirmation did not appear for the typed address",
timeout: 30_000,
interval: 250,
},
).toBe(true);
await screenshot(desktopSurface);
evidence.recordAssertionEvidence(
"Typing the workspace address reaches the named-origin confirmation",
`The gate names ${webOrigin} and offers Continue in browser before anything is persisted or exchanged; the headless run stops short of the OS browser round trip on purpose.`,
true,
);
const wentBack = await evalIn(desktopSurface, `(() => {
const back = [...document.querySelectorAll("button")].find((button) => (button.textContent ?? "").trim() === "Back");
if (!(back instanceof HTMLButtonElement)) return false;
back.click();
return true;
})()`);
expect(wentBack).toBe(true);
// Recovery seam of the SAME field: a pasted openwork:// URL carries the
// origin and one-time grant, still passes the named confirmation, and
// signs the desktop in — sign-in IS activation.
expect(await typeIntoGate(openworkUrl)).toBe("submitted");
await waitFor(
desktopSurface,
`Boolean(document.querySelector('[data-testid="organization-server-confirm"]'))
&& document.body.innerText.includes("Confirm and finish sign-in")`,
{ timeoutMs: 30_000, label: "manual confirmation for the pasted openwork:// URL" },
);
const confirmed = await evalIn(desktopSurface, `(() => {
const confirm = document.querySelector('[data-testid="organization-server-confirm"]');
if (!(confirm instanceof HTMLButtonElement)) return false;
confirm.click();
return true;
})()`);
expect(confirmed).toBe(true);
await waitFor(
desktopSurface,
`!document.querySelector('[data-testid="enterprise-activation-root"]')`,
{ timeoutMs: 120_000, label: "gate unmounts after the confirmed exchange" },
);
await screenshot(desktopSurface);
const reused = await denFetch(den.ref, "/v1/auth/desktop-handoff/exchange", {
method: "POST",
body: JSON.stringify({ grant }),
});
expect(reused.response.ok).toBe(false);
evidence.recordAssertionEvidence(
"Pasting the complete OpenWork URL signs the desktop in exactly once",
`After confirming ${webOrigin} the enterprise gate unmounted (sign-in is activation), and replaying the same grant against /v1/auth/desktop-handoff/exchange failed with HTTP ${reused.response.status}: the credential is single-use.`,
!reused.response.ok,
);
} finally {
await desktopSurface.stop().catch(() => undefined);
await host.disposeSurface(handle).catch(() => undefined);
}
// ── Frame 6: the stamped record unlocks ONLY its origin on a broken-chain,
// semi-airgapped network ─────────────────────────────────────
await using lab = await startEgressLab({ profile: "broken-chain" });
if (!lab.caPath || !lab.rootPem) throw new Error("egress lab did not expose its private root material");
const labOrigin = new URL(lab.url).origin;
const before = await fetchLabInChild(lab.url, { ...process.env, NODE_EXTRA_CA_CERTS: lab.caPath });
expect(before.status).not.toBe(0);
expect(before.output).toMatch(chainErrorPattern);
const repaired = await resolveCaEnvFromRecord({
bootstrapPath: await writeStampedBootstrap(labOrigin),
rootPem: lab.rootPem,
});
expect(repaired.logs.some((line) => line.includes(`chain repaired for ${labOrigin}`))).toBe(true);
const healed = await fetchLabInChild(lab.url, { ...process.env, ...repaired.caEnv });
expect(healed.status).toBe(0);
const mismatchedOrigin = `https://127.0.0.1:${await reserveClosedPort()}`;
const mismatched = await resolveCaEnvFromRecord({
bootstrapPath: await writeStampedBootstrap(mismatchedOrigin),
rootPem: lab.rootPem,
});
expect(mismatched.logs.some((line) => /chain repaired/.test(line))).toBe(false);
const stillBroken = await fetchLabInChild(lab.url, { ...process.env, ...mismatched.caEnv });
expect(stillBroken.status).not.toBe(0);
expect(stillBroken.output).toMatch(chainErrorPattern);
evidence.recordAssertionEvidence(
"The activation record from this flow repairs TLS only for its own origin",
`With the exact bootstrap shape the gate stamps, a leaf-only broken chain at ${labOrigin} failed (${chainErrorPattern.exec(before.output)?.[0] ?? "chain error"}), was repaired solely from the stamped origin (exit 0), and a record naming ${mismatchedOrigin} left the lab unreachable — the semi-airgapped network stays closed to everything but the stamped workspace.`,
true,
);
const finalShot = await screenshot(browser);
const seen = await validate(finalShot, distribution === "cloud"
? [
"The page is an OpenWork download guide",
"Desktop download choices are visible",
]
: [
"The page is an OpenWork install or setup guide",
"A step mentions connecting or a workspace address",
]);
expect(seen.ok, seen.why).toBe(true);
});