Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

253 lines · 13.2 KBRaw History
1import { spawnSync } from "node:child_process";
2import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3import { tmpdir } from "node:os";
4import { join } from "node:path";
5import { afterEach, describe, expect, it } from "vitest";
6
7/**
8 * scripts/turnstile.sh (run by bootstrap.sh), its use in ensure-secrets.sh, and render-config.mjs's
9 * TURNSTILE_SITE_KEY, against a fake Cloudflare API (curl) and a fake wrangler (pnpm) on PATH.
10 */
11
12const repo = new URL("../../../../", import.meta.url).pathname;
13const SECRET = "0x4AAAAAAAsecret-key-of-the-widget";
14
15type Widget = { sitekey: string; secret: string; name: string; domains: string[]; mode: string };
16type State = {
17 /** Every Turnstile call answers "Authentication error", as for a token without Turnstile Edit. */
18 denied?: boolean;
19 /** Only creating widgets and reading a widget's secret fail. */
20 readOnly?: boolean;
21 widgets: Widget[];
22 /** The deployed Worker's bindings, or null before the first deploy. */
23 worker: Array<{ type: string; name: string; text?: string }> | null;
24 /** Names wrangler secret list returns. */
25 secrets: string[];
26};
27
28// curl -sS -X METHOD URL [-H ...] [--data BODY]: answers from state.json, logs each call.
29const FAKE_CURL = `#!/usr/bin/env node
30const fs = require("node:fs");
31const dir = process.env.FAKE_DIR;
32const state = JSON.parse(fs.readFileSync(dir + "/state.json", "utf8"));
33const a = process.argv.slice(2);
34const method = a[a.indexOf("-X") + 1] ?? "GET";
35const url = a.find((x) => x.startsWith("https://"));
36const body = a.includes("--data") ? a[a.indexOf("--data") + 1] : null;
37fs.appendFileSync(dir + "/calls.log", JSON.stringify({ method, url, body }) + "\\n");
38const path = new URL(url).pathname.replace(/^\\/client\\/v4\\/accounts\\/acct/, "");
39const fail = (code, message) => ({ success: false, errors: [{ code, message }], result: null });
40let out;
41if (path.startsWith("/challenges/widgets")) {
42 const key = path.split("/")[3];
43 if (state.denied) out = fail(10000, "Authentication error");
44 else if (method === "GET" && !key) out = { success: true, result: state.widgets.map(({ secret, ...w }) => w) };
45 else if (state.readOnly) out = fail(10000, "Authentication error");
46 else if (method === "POST") {
47 const w = { ...JSON.parse(body), sitekey: "0x4AAAAAAAnew", secret: "${SECRET}" };
48 state.widgets.push(w);
49 fs.writeFileSync(dir + "/state.json", JSON.stringify(state));
50 out = { success: true, result: w };
51 } else {
52 const w = state.widgets.find((x) => x.sitekey === key);
53 out = w ? { success: true, result: w } : fail(10404, "not found");
54 }
55} else if (path === "/workers/scripts/diffinery/settings") {
56 out = state.worker ? { success: true, result: { bindings: state.worker } } : fail(10007, "This Worker does not exist on your account.");
57} else out = fail(7003, "no route for " + path);
58process.stdout.write(JSON.stringify(out));
59`;
60
61// pnpm --dir APP exec wrangler secret (list|put NAME) ...: logs each put with what came on stdin.
62const FAKE_PNPM = `#!/usr/bin/env node
63const fs = require("node:fs");
64const dir = process.env.FAKE_DIR;
65const state = JSON.parse(fs.readFileSync(dir + "/state.json", "utf8"));
66const a = process.argv.slice(process.argv.indexOf("wrangler") + 1);
67if (a[0] === "secret" && a[1] === "list") process.stdout.write(JSON.stringify(state.secrets.map((name) => ({ name, type: "secret_text" }))));
68else if (a[0] === "secret" && a[1] === "put") {
69 const value = fs.readFileSync(0, "utf8");
70 fs.appendFileSync(dir + "/puts.log", JSON.stringify({ name: a[2], value, args: a.slice(3) }) + "\\n");
71 console.log("Success! Uploaded secret " + a[2]);
72} else { console.error("fake pnpm: unexpected " + a.join(" ")); process.exit(1); }
73`;
74
75let dir = "";
76afterEach(() => {
77 if (dir) rmSync(dir, { recursive: true, force: true });
78 dir = "";
79});
80
81function tree(state: State, wranglerVars: Record<string, string> = {}): string {
82 dir = mkdtempSync(join(tmpdir(), "turnstile-"));
83 for (const d of ["scripts", "apps/diffinery", "bin"]) mkdirSync(join(dir, d), { recursive: true });
84 for (const f of ["turnstile.sh", "ensure-secrets.sh", "jsonc.mjs", "render-config.mjs"]) copyFileSync(join(repo, "scripts", f), join(dir, "scripts", f));
85 const cfg = readFileSync(join(repo, "apps/diffinery/wrangler.jsonc"), "utf8");
86 const vars = Object.entries(wranglerVars).map(([k, v]) => `\n ${JSON.stringify(k)}: ${JSON.stringify(v)},`).join("");
87 writeFileSync(join(dir, "apps/diffinery/wrangler.jsonc"), cfg.replace(/"vars": \{/, `"vars": {${vars}`));
88 writeFileSync(join(dir, "bin/curl"), FAKE_CURL);
89 writeFileSync(join(dir, "bin/pnpm"), FAKE_PNPM);
90 chmodSync(join(dir, "bin/curl"), 0o755);
91 chmodSync(join(dir, "bin/pnpm"), 0o755);
92 writeFileSync(join(dir, "state.json"), JSON.stringify(state));
93 return dir;
94}
95
96function run(cmd: string[], extraEnv: Record<string, string> = {}) {
97 const r = spawnSync(cmd[0]!, cmd.slice(1), {
98 cwd: dir,
99 encoding: "utf8",
100 env: { PATH: `${join(dir, "bin")}:${process.env.PATH}`, HOME: dir, FAKE_DIR: dir, CLOUDFLARE_API_TOKEN: "token", CLOUDFLARE_ACCOUNT_ID: "acct", DIFFINERY_DOMAIN: "git.example.com", ...extraEnv },
101 });
102 const lines = (f: string) => (existsSync(join(dir, f)) ? readFileSync(join(dir, f), "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)) : []);
103 const calls = lines("calls.log") as Array<{ method: string; url: string; body: string | null }>;
104 const puts = lines("puts.log") as Array<{ name: string; value: string; args: string[] }>;
105 // Whatever happens, the secret key never reaches the log.
106 expect(r.stdout + r.stderr).not.toContain(SECRET);
107 return { status: r.status, stdout: r.stdout.trim(), stderr: r.stderr, calls, puts };
108}
109
110const turnstile = () => run(["bash", join(dir, "scripts/turnstile.sh")]);
111const widget = (sitekey = "0x4AAAAAAAold"): Widget => ({ sitekey, secret: SECRET, name: "diffinery git.example.com", domains: ["git.example.com"], mode: "managed" });
112const PERMISSION = "Account → Turnstile → Edit";
113
114describe("scripts/turnstile.sh", () => {
115 it("creates the widget when there is none, and prints its site key", () => {
116 tree({ widgets: [{ ...widget("0x4AAAAAAAother"), name: "someone else's" }], worker: null, secrets: [] });
117 const r = turnstile();
118 expect(r.status).toBe(0);
119 expect(r.stdout).toBe("0x4AAAAAAAnew");
120 expect(r.calls.map((c) => `${c.method} ${new URL(c.url).pathname}${new URL(c.url).search}`)).toEqual([
121 "GET /client/v4/accounts/acct/challenges/widgets?per_page=1000",
122 "POST /client/v4/accounts/acct/challenges/widgets",
123 "GET /client/v4/accounts/acct/workers/scripts/diffinery/settings",
124 ]);
125 expect(JSON.parse(r.calls[1]!.body!)).toEqual({ name: "diffinery git.example.com", domains: ["git.example.com"], mode: "managed" });
126 // The Worker isn't deployed yet: ensure-secrets.sh sets TURNSTILE_SECRET after the deploy.
127 expect(r.puts).toEqual([]);
128 expect(r.stderr).toContain("ensure-secrets.sh sets TURNSTILE_SECRET");
129 });
130
131 it("reuses the widget it finds by name, and leaves a secret that goes with it alone", () => {
132 tree({ widgets: [widget()], worker: [{ type: "plain_text", name: "TURNSTILE_SITE_KEY", text: "0x4AAAAAAAold" }, { type: "secret_text", name: "TURNSTILE_SECRET" }], secrets: [] });
133 const r = turnstile();
134 expect(r.status).toBe(0);
135 expect(r.stdout).toBe("0x4AAAAAAAold");
136 expect(r.calls.some((c) => c.method === "POST")).toBe(false);
137 expect(r.puts).toEqual([]);
138 expect(r.stderr).not.toContain("::warning::");
139 });
140
141 it("leaves a missing TURNSTILE_SECRET to ensure-secrets.sh", () => {
142 tree({ widgets: [widget()], worker: [{ type: "plain_text", name: "TURNSTILE_SITE_KEY", text: "0x4AAAAAAAold" }], secrets: [] });
143 const r = turnstile();
144 expect(r.stdout).toBe("0x4AAAAAAAold");
145 expect(r.puts).toEqual([]);
146 });
147
148 it("replaces a TURNSTILE_SECRET that goes with another site key before the deploy", () => {
149 for (const deployed of ["0x4AAAAAAAgone", undefined]) {
150 tree({ widgets: [widget()], worker: [...(deployed ? [{ type: "plain_text", name: "TURNSTILE_SITE_KEY", text: deployed }] : []), { type: "secret_text", name: "TURNSTILE_SECRET" }], secrets: [] });
151 const r = turnstile();
152 expect(r.status).toBe(0);
153 expect(r.stdout).toBe("0x4AAAAAAAold");
154 expect(r.puts).toEqual([{ name: "TURNSTILE_SECRET", value: SECRET, args: ["--name", "diffinery"] }]);
155 expect(r.calls.some((c) => c.method === "GET" && c.url.endsWith("/challenges/widgets/0x4AAAAAAAold"))).toBe(true);
156 rmSync(dir, { recursive: true, force: true });
157 }
158 });
159
160 it("without the permission, warns naming it, prints no site key and exits 0", () => {
161 tree({ denied: true, widgets: [], worker: null, secrets: [] });
162 const r = turnstile();
163 expect(r.status).toBe(0);
164 expect(r.stdout).toBe("");
165 expect(r.stderr).toContain("::warning::Could not read the account's Turnstile widgets");
166 expect(r.stderr).toContain(PERMISSION);
167 expect(r.stderr).toContain("10000: Authentication error");
168 expect(r.puts).toEqual([]);
169 });
170
171 it("with read-only access, warns when it can't create the widget", () => {
172 tree({ readOnly: true, widgets: [], worker: null, secrets: [] });
173 const r = turnstile();
174 expect(r.status).toBe(0);
175 expect(r.stdout).toBe("");
176 expect(r.stderr).toContain('::warning::Could not create the Turnstile widget "diffinery git.example.com"');
177 expect(r.stderr).toContain(PERMISSION);
178 });
179
180 it("deploys no new site key when a secret that goes with another widget can't be replaced", () => {
181 tree({ readOnly: true, widgets: [widget()], worker: [{ type: "plain_text", name: "TURNSTILE_SITE_KEY", text: "0x4AAAAAAAgone" }, { type: "secret_text", name: "TURNSTILE_SECRET" }], secrets: [] });
182 const r = turnstile();
183 expect(r.status).toBe(0);
184 expect(r.stdout).toBe("");
185 expect(r.puts).toEqual([]);
186 expect(r.stderr).toContain("::warning::Could not read the secret key of Turnstile widget 0x4AAAAAAAold");
187 expect(r.stderr).toContain("TURNSTILE_SECRET goes with another Turnstile widget and couldn't be replaced");
188 });
189
190 it("leaves keys set in wrangler.jsonc alone", () => {
191 tree({ widgets: [], worker: null, secrets: [] }, { TURNSTILE_SITE_KEY: "0x4AAAAAAAmine" });
192 const r = turnstile();
193 expect(r.status).toBe(0);
194 expect(r.stdout).toBe("");
195 expect(r.calls).toEqual([]);
196 });
197});
198
199describe("scripts/ensure-secrets.sh and TURNSTILE_SECRET", () => {
200 const deployConfig = (vars: Record<string, string>) =>
201 writeFileSync(join(dir, "apps/diffinery/wrangler.deploy.jsonc"), `// Generated by scripts/render-config.mjs; do not edit.\n${JSON.stringify({ name: "diffinery", vars })}\n`);
202 const ensure = () => run(["bash", join(dir, "scripts/ensure-secrets.sh")]);
203
204 it("sets it from the deployed site key's widget when it's missing", () => {
205 tree({ widgets: [widget()], worker: [], secrets: ["MASTER_KEY", "OIDC_PRIVATE_KEY"] });
206 deployConfig({ TURNSTILE_SITE_KEY: "0x4AAAAAAAold" });
207 const r = ensure();
208 expect(r.status).toBe(0);
209 expect(r.puts).toEqual([{ name: "TURNSTILE_SECRET", value: SECRET, args: ["-c", "wrangler.deploy.jsonc"] }]);
210 expect(r.stdout).toContain("set TURNSTILE_SECRET");
211 });
212
213 it("leaves it alone when it exists, or when no site key was deployed", () => {
214 tree({ widgets: [widget()], worker: [], secrets: ["MASTER_KEY", "OIDC_PRIVATE_KEY", "TURNSTILE_SECRET"] });
215 deployConfig({ TURNSTILE_SITE_KEY: "0x4AAAAAAAold" });
216 expect(ensure().puts).toEqual([]);
217 rmSync(dir, { recursive: true, force: true });
218 tree({ widgets: [widget()], worker: [], secrets: ["MASTER_KEY", "OIDC_PRIVATE_KEY"] });
219 deployConfig({});
220 const r = ensure();
221 expect(r.puts).toEqual([]);
222 expect(r.calls).toEqual([]);
223 });
224
225 it("warns, naming the permission, and goes on when it can't read the widget's secret", () => {
226 tree({ denied: true, widgets: [], worker: [], secrets: ["MASTER_KEY", "OIDC_PRIVATE_KEY"] });
227 deployConfig({ TURNSTILE_SITE_KEY: "0x4AAAAAAAold" });
228 const r = ensure();
229 expect(r.status).toBe(0);
230 expect(r.puts).toEqual([]);
231 expect(r.stderr).toContain(PERMISSION);
232 expect(r.stderr).toContain("::warning::TURNSTILE_SECRET is not set");
233 });
234});
235
236describe("scripts/render-config.mjs and TURNSTILE_SITE_KEY", () => {
237 const render = (env: Record<string, string>) => {
238 const r = run(["node", join(dir, "scripts/render-config.mjs"), "--d1", "db-id", "--public-url", "https://git.example.com"], env);
239 expect(r.status, r.stderr).toBe(0);
240 return JSON.parse(readFileSync(join(dir, "apps/diffinery/wrangler.deploy.jsonc"), "utf8").split("\n").slice(1).join("\n")) as { vars: Record<string, string> };
241 };
242
243 it("deploys the site key bootstrap passes, unless wrangler.jsonc sets its own", () => {
244 tree({ widgets: [], worker: null, secrets: [] });
245 expect(render({ TURNSTILE_SITE_KEY: "0x4AAAAAAAold" }).vars.TURNSTILE_SITE_KEY).toBe("0x4AAAAAAAold");
246 expect(render({}).vars.TURNSTILE_SITE_KEY).toBeUndefined();
247 rmSync(dir, { recursive: true, force: true });
248 tree({ widgets: [], worker: null, secrets: [] }, { TURNSTILE_SITE_KEY: "0x4AAAAAAAmine" });
249 expect(render({ TURNSTILE_SITE_KEY: "0x4AAAAAAAold" }).vars.TURNSTILE_SITE_KEY).toBe("0x4AAAAAAAmine");
250 // Keys don't open sign-up: render-config never sets SIGNUP.
251 expect(render({ TURNSTILE_SITE_KEY: "0x4AAAAAAAold" }).vars.SIGNUP).toBeUndefined();
252 });
253});