Repository Public MIT
1import AxeBuilder from "@axe-core/playwright";2import { expect, test, type Page } from "@playwright/test";3import { readFileSync } from "node:fs";4import { MANIFEST_FILE } from "@skills/registry-core";5import { packFiles } from "@skills/registry-core/node";67const state = JSON.parse(readFileSync("test-results/e2e-state.json", "utf8")) as { email: string; namespace: string; initialPassword: string; password: string; readonly?: boolean };8test.skip(!!state.readonly, "publisher flows mutate state");9test.describe.configure({ mode: "serial" });1011async function signIn(page: Page, password: string, expectSuccess = true) {12 await page.goto("/login");13 await page.getByLabel("Email").fill(state.email);14 await page.getByLabel("Password").fill(password);15 await page.getByRole("button", { name: "Sign in" }).click();16 if (expectSuccess) await page.waitForURL(/\/dashboard/);17}18let token = "";1920test.describe("publisher console", () => {21 test("rejects a wrong password without revealing whether the account exists", async ({ page }) => {22 await signIn(page, "definitely-the-wrong-password", false);23 await expect(page.getByRole("alert").filter({ hasText: "Invalid" })).toHaveText("Invalid email or password.");24 await expect(page).toHaveURL(/\/login$/);25 });2627 test("forces a password change on first sign-in", async ({ page }) => {28 await signIn(page, state.initialPassword);29 await expect(page).toHaveURL(/\/dashboard\/account\?first=1/);30 await page.goto("/dashboard/tokens");31 await expect(page).toHaveURL(/\/dashboard\/account/); // nothing else is reachable yet32 await page.getByLabel("Current password").fill(state.initialPassword);33 await page.getByLabel("New password", { exact: true }).fill(state.password);34 await page.getByLabel("Repeat new password").fill(state.password);35 await page.getByRole("button", { name: "Change password" }).click();36 await expect(page).toHaveURL(/\/dashboard\?password=changed/);37 const cookies = await page.context().cookies();38 const session = cookies.find((c) => c.name === "skills_session")!;39 expect(session.httpOnly).toBe(true);40 expect(session.sameSite).toBe("Lax");41 });4243 test("creates a scoped token shown exactly once, which then works against the API", async ({ page, request }) => {44 await signIn(page, state.password);45 await page.goto("/dashboard/tokens");46 await page.getByRole("textbox", { name: "Name" }).fill("e2e-publisher");47 await page.getByRole("button", { name: "Create token" }).click();48 const secret = page.getByTestId("one-time-secret").locator("code").first();49 await expect(secret).toHaveText(/^skr_[a-z0-9]{10}_[A-Za-z0-9_-]{43}$/);50 token = (await secret.innerText()).trim();51 const me = await (await request.get("/api/v1/me", { headers: { Authorization: `Bearer ${token}` } })).json();52 expect(me.credential).toMatchObject({ kind: "token", name: "e2e-publisher", namespaces: [state.namespace], elevated: false });53 await page.reload();54 await expect(page.getByTestId("one-time-secret")).toHaveCount(0);55 expect(await page.content()).not.toContain(token);56 await expect(page.getByRole("cell", { name: /e2e-publisher/ })).toBeVisible();57 const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa"]).analyze();58 expect(results.violations.map((v) => `${v.id}: ${v.nodes[0]?.target}`)).toEqual([]);59 });6061 test("creates an artifact and publishes a release from the browser", async ({ page, request }) => {62 await signIn(page, state.password);63 await page.goto("/dashboard/artifacts");64 await page.getByLabel("Slug").fill("browser-skill");65 await page.getByLabel("Summary").fill("Uploaded from the browser console by the end-to-end suite.");66 await page.getByLabel("Visibility").selectOption("public");67 await page.getByLabel(/I confirm this artifact may be publicly visible/).check();68 await page.getByRole("button", { name: "Create artifact" }).click();69 await expect(page).toHaveURL(/\/dashboard\/artifacts\/[0-9a-f-]{36}$/);7071 const name = `@${state.namespace}/browser-skill`;72 const files = (version: string, extra: { path: string; mode: number; data: Buffer }[] = []) => [73 { path: MANIFEST_FILE, mode: 0o644, data: Buffer.from(JSON.stringify({ schemaVersion: "1.0", name, version, type: "skill", description: "Uploaded from the browser console by the end-to-end suite.", license: "MIT", compatibility: ["claude-code"] })) },74 { path: "SKILL.md", mode: 0o644, data: Buffer.from("---\nname: browser-skill\ndescription: Demonstrates browser upload. Use in tests only.\n---\n\n# Browser skill\n\n<script>window.__xss = 1</script>\n\n[bad](javascript:alert(1))\n") },75 { path: "README.md", mode: 0o644, data: Buffer.from("# browser-skill\n") },76 ...extra,77 ];78 // A release that leaks a credential is refused, with the finding shown and nothing published.79 const leaky = packFiles(files("1.0.0", [{ path: "notes.txt", mode: 0o644, data: Buffer.from(`key = ghp_${"Q7".repeat(18)}\n`) }]));80 await page.getByLabel("Version").fill("1.0.0");81 await page.getByLabel(/Release archive/).setInputFiles({ name: "leaky.tar.gz", mimeType: "application/gzip", buffer: leaky.archive });82 await page.getByRole("button", { name: "Upload and publish" }).click();83 await expect(page.getByRole("alert").filter({ hasText: "rejected" })).toContainText("Nothing was published");84 await expect(page.getByLabel("Validation findings")).toContainText("possible GitHub token");85 expect((await request.get(`/api/v1/artifacts/${state.namespace}/browser-skill/versions`)).status()).toBe(404); // still a draft: invisible to the public8687 const good = packFiles(files("1.0.0"));88 await page.getByLabel(/Release archive/).setInputFiles({ name: "browser-skill-1.0.0.tar.gz", mimeType: "application/gzip", buffer: good.archive });89 await page.getByRole("button", { name: "Upload and publish" }).click();90 await expect(page.getByRole("status").filter({ hasText: "published" })).toContainText("Version 1.0.0 is published and immutable");9192 const errors: string[] = [];93 page.on("console", (m) => m.type() === "error" && errors.push(m.text()));94 await page.goto(`/@${state.namespace}/browser-skill`);95 await expect(page.getByTestId("sha256").first()).toHaveText(good.sha256);96 // Untrusted README content is inert: no script ran, no javascript: link survived.97 expect(await page.evaluate(() => (window as unknown as { __xss?: number }).__xss)).toBeUndefined();98 expect(await page.locator('a[href^="javascript:"]').count()).toBe(0);99 expect(await page.locator("#readme script").count()).toBe(0);100 });101102 test("the token is confined to its namespace, and revoking it in the console kills it", async ({ page, request }) => {103 const auth = { Authorization: `Bearer ${token}` };104 const foreign = await request.post("/api/v1/artifacts", { headers: auth, data: { namespace: "spboucher", slug: "intruder", type: "skill", summary: "Should be refused by namespace isolation." } });105 expect(foreign.status()).toBe(403);106 await signIn(page, state.password);107 await page.goto("/dashboard/tokens");108 page.on("dialog", (d) => d.accept());109 await page.getByRole("row", { name: /e2e-publisher/ }).getByRole("button", { name: "Revoke" }).click();110 await expect(page.getByRole("row", { name: /e2e-publisher/ })).toContainText("revoked");111 const after = await request.get("/api/v1/me", { headers: auth });112 expect(after.status()).toBe(401);113 expect((await after.json()).error.code).toBe("token_revoked");114 await page.goto("/dashboard/audit?action=token.");115 await expect(page.getByRole("cell", { name: "token.revoke" }).first()).toBeVisible();116 await page.goto("/dashboard/reports");117 await expect(page.getByRole("heading", { level: 1 })).toHaveText("This page does not exist."); // not an administrator118 });119120 test("sign-out ends the session server-side", async ({ page }) => {121 await signIn(page, state.password);122 const cookie = (await page.context().cookies()).find((c) => c.name === "skills_session")!;123 await page.goto("/dashboard");124 await page.getByRole("button", { name: "Sign out" }).click();125 await expect(page).toHaveURL(/\/$/);126 await page.context().addCookies([cookie]); // replaying the old cookie must not work127 await page.goto("/dashboard");128 await expect(page).toHaveURL(/\/login$/);129 });130});131