/** * The joint public-steal contract (addendum A2, item 8d): one machine publishes a repo to a bucket, * Kepr serves it read-only over HTTP in Koh's own layout, a second `koh` steals it — and later, * once that machine has signed in, offers its work back to the bucket. * * There is no S3 on a test host, so the bucket is a fake one served here; koh is run as the real * installed binary (`koh` on PATH, the same one this suite is pinned to). The account of what the * fake bucket accepts is in its own class: signatures are ignored rather than verified, which is * the one way this test is easier than the real thing. */ import { describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Hono } from "hono"; import { openBucket } from "../src/bucket/reader.ts"; import { syncAll } from "../src/bucket/sync.ts"; import { createApp } from "../src/server/app.ts"; import { openKeprDb } from "../src/server/db.ts"; import { Browser, PASSWORD } from "./browser.ts"; import { platform, stripPath } from "./fixture.ts"; const REPO = "shipped"; const PRIVATE_REPO = "kept"; const BUCKET = "kohbucket"; const ACCESS = "fake-access"; const SECRET = "fake-secret"; /** The command deadline: two koh runs talk to a local server, so this is generous. */ const RUN = 60_000; /** * The fake bucket runs as its own process: the koh calls below are synchronous (`koh` blocks the * event loop), so an in-process server would never answer them. `tests/fake-s3.ts` prints its * endpoint on stdout. */ async function startBucket(): Promise<{ endpoint: string; stop: () => void }> { const proc = Bun.spawn(["bun", "run", join(import.meta.dir, "fake-s3.ts")], { stdout: "pipe", stderr: "inherit", }); const reader = proc.stdout.getReader(); const first = await reader.read(); const endpoint = new TextDecoder() .decode(first.value ?? new Uint8Array()) .trim() .split("\n")[0] ?? ""; reader.releaseLock(); expect(endpoint).toStartWith("http://127.0.0.1:"); return { endpoint, stop: () => proc.kill() }; } type Ran = { code: number; out: string; err: string }; /** The real koh, with a throwaway HOME so no test ever touches the operator's config. */ async function koh( cwd: string, home: string, args: string[], env: Record = {}, ): Promise { const proc = Bun.spawn(["koh", ...args], { cwd, env: { PATH: process.env.PATH, HOME: home, NO_COLOR: "1", ...env }, stdout: "pipe", stderr: "pipe", }); const [out, err, code] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited, ]); return { code, out, err }; } /** The whole line, every field of every save: what a stolen copy must reproduce exactly. */ async function line(repo: string, home: string): Promise { return JSON.parse((await koh(repo, home, ["view", "--json"])).out).saves; } describe.serial("stealing a public repo from Kepr over HTTP", () => { test( "a stolen copy comes over the wire, stays read-only, and follows its source", async () => { const root = await mkdtemp(join(tmpdir(), "kepr-steal-")); const bucketServer = await startBucket(); const endpoint = bucketServer.endpoint; const keys = { KOH_ACCESS_KEY: ACCESS, KOH_SECRET_KEY: SECRET }; try { // The owner: a small repo in a bucket, offered on every save. const ownerHome = join(root, "owner-home"); const ownerDir = join(root, "owner"); const repoDir = join(ownerDir, REPO); const keptDir = join(ownerDir, PRIVATE_REPO); await mkdir(ownerHome, { recursive: true }); await mkdir(repoDir, { recursive: true }); await mkdir(keptDir, { recursive: true }); expect((await koh(repoDir, ownerHome, ["init"], keys)).code).toBe(0); expect( (await koh(repoDir, ownerHome, ["remote", "s3", endpoint, BUCKET, "fr-par"], keys)).code, ).toBe(0); await writeFile(join(repoDir, "README.md"), "# shipped\n\nA repo to steal.\n"); await writeFile(join(repoDir, "src.v"), "fn main() {}\n"); expect((await koh(repoDir, ownerHome, ["save", "first save"])).code).toBe(0); await writeFile(join(repoDir, "README.md"), "# shipped\n\nA repo to steal, edited.\n"); expect((await koh(repoDir, ownerHome, ["save", "second save"])).code).toBe(0); // A second repo, never made public on Kepr: only a signed-in machine may have it. expect((await koh(keptDir, ownerHome, ["init"], keys)).code).toBe(0); expect( (await koh(keptDir, ownerHome, ["remote", "s3", endpoint, BUCKET, "fr-par"], keys)).code, ).toBe(0); await writeFile(join(keptDir, "secret.txt"), "kept in the bucket\n"); expect((await koh(keptDir, ownerHome, ["save", "a private repo"])).code).toBe(0); // Kepr, reading that bucket: the shipped repo is public, the kept one is not. const dataDir = await mkdtemp(join(tmpdir(), "kepr-steal-kepr-")); const kepr = openKeprDb(dataDir); const bucket = { kind: "s3" as const, endpoint, name: BUCKET, region: "fr-par", accessKeyId: ACCESS, secretAccessKey: SECRET, }; /** Kepr syncs on its own loop in production; here each new save is synced by hand. */ const sync = () => syncAll({ bucket: openBucket(bucket), kepr, mirrorDir: join(dataDir, "mirror"), }); await sync(); kepr.query("UPDATE repos SET public = 1 WHERE name = ?").run(REPO); let app: Hono; const server = Bun.serve({ port: 0, fetch: (request) => app.fetch(request), }); app = createApp( { domain: `127.0.0.1:${server.port}`, dataDir, adminPasswordHash: await Bun.password.hash(PASSWORD, "argon2id"), bucket: { ...bucket, write: { accessKeyId: ACCESS, secretAccessKey: SECRET } }, }, kepr, ); const host = `http://127.0.0.1:${server.port}`; // The second machine: nothing but a HOME and the address it was told. const thiefHome = join(root, "thief-home"); const thiefDir = join(root, "thief"); await mkdir(thiefHome, { recursive: true }); await mkdir(thiefDir, { recursive: true }); const stolen = await koh(thiefDir, thiefHome, ["steal", `${host}/${REPO}`]); expect(stolen.code, stolen.err + stolen.out).toBe(0); const copy = join(thiefDir, REPO); expect(await readFile(join(copy, "README.md"), "utf8")).toContain("edited"); expect((await koh(copy, thiefHome, ["doctor"])).code).toBe(0); // Save for save, the copy is the line the owner published. expect(await line(copy, thiefHome)).toEqual(await line(repoDir, ownerHome)); // The source is remembered, and shown above the machine's remote. expect((await koh(copy, thiefHome, ["remote"])).out).toContain(`${host}/${REPO}`); // And it cannot be offered: it has no bucket of its own. const refused = await koh(copy, thiefHome, ["offer"]); expect(refused.code).toBe(1); expect(refused.err + refused.out).toContain("which is read-only"); // A later public save arrives on a bare `koh steal`. await writeFile(join(repoDir, "README.md"), "# shipped\n\nA third save.\n"); expect((await koh(repoDir, ownerHome, ["save", "third save"])).code).toBe(0); await sync(); expect((await koh(copy, thiefHome, ["steal"])).code).toBe(0); expect(await readFile(join(copy, "README.md"), "utf8")).toContain("A third save"); // A strip on the source side reaches the mirror, and is honoured: history no longer // holds the path, so a fresh steal never sees the file at all. On the machine that // already had it the file stays on disk — a strip is not a delete — but it is banned: // not unsaved work, and never saved again. await stripPath(repoDir, ownerHome, "src.v"); // The strip wrote `/src.v` into `.kohignore`; the next save carries the rule everywhere. expect(await readFile(join(repoDir, ".kohignore"), "utf8")).toContain("/src.v\n"); expect((await koh(repoDir, ownerHome, ["save", "ignore the stripped src.v"])).code).toBe(0); await sync(); expect((await koh(copy, thiefHome, ["steal"])).code).toBe(0); expect((await koh(copy, thiefHome, ["doctor"])).code).toBe(0); expect(await Bun.file(join(copy, "src.v")).exists()).toBe(true); // Not listed as a change; the status line may still quote a save message naming it. const status = (await koh(copy, thiefHome, ["status"])).out; expect(status).not.toContain("+ src.v"); expect(status).toContain("no changes"); const fresh = join(root, "thief-again"); await mkdir(fresh, { recursive: true }); expect((await koh(fresh, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0); expect(await Bun.file(join(fresh, REPO, "src.v")).exists()).toBe(false); // A third copy stays stolen to the end, for `offer --json` to upgrade. const third = join(root, "thief-three"); await mkdir(third, { recursive: true }); expect((await koh(third, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0); // And a fourth, for `offer --builds --json` to upgrade. const fourth = join(root, "thief-four"); await mkdir(fourth, { recursive: true }); expect((await koh(fourth, thiefHome, ["steal", `${host}/${REPO}`])).code).toBe(0); // Signing in: a code from the connect page, read the way `koh` reads it. // This Kepr's domain is the loopback address it is served on, so the same-origin // checks want that origin, not the Browser's default. const origin = `https://127.0.0.1:${server.port}`; const browser = new Browser(app); const signInPage = await browser.get("/sign-in"); const form = /name="form" value="([^"]+)"/.exec(await signInPage.text())?.[1] ?? ""; const signed = await browser.request("/sign-in", { method: "POST", headers: { origin }, body: new URLSearchParams({ form, password: PASSWORD }), }); expect(signed.status).toBe(303); const machine = "thief-machine"; expect((await browser.get(`/connect?machine=${machine}`)).status).toBe(200); const shown = await browser.post("/connect", { machine }, origin); const code = /class="mono code">([^<]+) out/app-1.0.0-${platform()}`, "output = out/app-{version}-{platform}", `platforms = ${platform()}`, "", ].join("\n"), ); const risky = JSON.parse( (await koh(repoDir, ownerHome, ["save", "risky", "--json"])).out, ).id; const flag = await koh(repoDir, ownerHome, ["flag", risky, "--reason", "broke the build"]); expect(flag.code, flag.err + flag.out).toBe(0); expect((await koh(repoDir, ownerHome, ["offer"])).code).toBe(0); const flaggedOffer = await koh(join(third, REPO), thiefHome, ["offer", "--json"]); expect(flaggedOffer.code, flaggedOffer.err + flaggedOffer.out).toBe(0); expect(JSON.parse(flaggedOffer.out).flagged).toEqual([risky]); const flaggedBuilds = await koh(join(fourth, REPO), thiefHome, [ "offer", "--builds", "--json", ]); expect(flaggedBuilds.code, flaggedBuilds.err + flaggedBuilds.out).toBe(0); expect(JSON.parse(flaggedBuilds.out).flagged).toEqual([risky]); } finally { bucketServer.stop(); } }, RUN, ); });