Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Email sign-up has a Cloudflare Turnstile check, when it is set up. With TURNSTILE_SITE_KEY (a var in apps/web/wrangler.jsonc: the widget's public site key) and the TURNSTILE_SECRET secret both set, the sign-up form draws Turnstile's widget above its button and the server checks its answer with Cloudflare's siteverify before identity is asked; a missing or refused answer gets a plain error. With either unset nothing changes, so an installation of its own and local development sign up without it; when Cloudflare cannot be reached the sign-up goes ahead, the rate limit and email confirmation still in its way. The page's policy allows Turnstile's script and frame from challenges.cloudflare.com, and nothing else is framed. The self-hosted compose file and configs pass both keys through. The accounts and self-hosting guides say so.

syntaqxcommitted Parent7a522b3Browse files
10 files+131−50/10 viewed
+4−1
11401140 were made from your network in the last hour" until the hour is up. A form
11411141 with a mistake in it (a username that cannot be used, an address that is
11421142 not one, a short password) is not counted. Signing up with GitHub is not
1143−counted this way: it needs a GitHub account with a verified address.
1143+counted this way: it needs a GitHub account with a verified address. The
1144+sign-up form also has a Cloudflare Turnstile check above its button, which
1145+usually passes without you doing anything; wait for it to finish before
1146+you select **Create account**.
11441147
11451148 g1t also counts wrong passwords, on the sign-in page, for git over HTTPS and
11461149 when confirming it is you, against the account and against where they come
+1−0
171171 | `BACKUP_S3_BUCKET` | `g1t-backups` | The bucket on the same store that nightly repository backups (a `git bundle` of each repository whose branches or tags changed) are kept in. The bundles are cut by g1t's runner, which this installation does not run yet, so the bucket stays empty for now: copy the volumes, as below. |
172172 | `STATUS_PORT` | `8788` | The port the status page is published on |
173173 | `STATUS_PROBE_REPO` | (none) | A public repository, `workspace/repo`, whose branches the status page lists every minute as a clone would. Empty: git is not checked. |
174+| `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET` | (none) | A [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) widget's site key and secret key, to check that a person fills in the sign-up form. With either unset, sign-up has no such check. |
174175 | `PHONE_PUSH` | `on` | `off`: send [the phone app](#the-phone-app) no notifications, so their words never pass through Expo's push service. Browser and desktop notifications are not affected. |
175176
176177 ## The phone app
+6−4
2828 const files = usercontent && /^http:/.test(usercontent) ? ` ${new URL(usercontent).origin}` : "";
2929 return [
3030 "default-src 'self'",
31− // Cloudflare's Web Analytics beacon, when the zone turns it on, and
32− // HeyCatch's helper for product analytics (lib/analytics.client.ts).
33− `script-src 'self' 'nonce-${nonce}' https://static.cloudflareinsights.com https://in.heycatch.ai`,
31+ // Cloudflare's Web Analytics beacon, when the zone turns it on,
32+ // HeyCatch's helper for product analytics (lib/analytics.client.ts), and
33+ // Cloudflare Turnstile on sign-up when it is on (lib/turnstile.ts).
34+ `script-src 'self' 'nonce-${nonce}' https://static.cloudflareinsights.com https://in.heycatch.ai https://challenges.cloudflare.com`,
3435 "style-src 'self' 'unsafe-inline'",
3536 `img-src 'self' https: data: blob:${files}`,
3637 `media-src 'self' https:${files}`,
3940 // shell, which the page calls through window.g1tDesktop
4041 // (lib/notify-client.ts); no browser resolves either.
4142 "connect-src 'self' https: ipc: http://ipc.localhost",
42− "frame-src 'none'",
43+ // Turnstile's widget draws in a frame of its own; nothing else is framed.
44+ "frame-src https://challenges.cloudflare.com",
4345 "object-src 'none'",
4446 "base-uri 'self'",
4547 "frame-ancestors 'none'",
+37−0
1+import { env } from "cloudflare:workers";
2+
3+import { TURNSTILE_FIELD, TURNSTILE_VERIFY, type TurnstileSettings, turnstilePassed, turnstileSettings } from "./turnstile";
4+
5+/** Turnstile's settings from the Worker's: null when it is off (lib/turnstile.ts). */
6+export function turnstile(): TurnstileSettings {
7+ const vars = env as unknown as { TURNSTILE_SITE_KEY?: string; TURNSTILE_SECRET?: string };
8+ return turnstileSettings(vars.TURNSTILE_SITE_KEY, vars.TURNSTILE_SECRET);
9+}
10+
11+/**
12+ * Whether the form's Turnstile answer is good. True when Turnstile is off.
13+ * A missing or refused answer is false. When Cloudflare cannot be reached
14+ * the sign-up goes ahead, with the rate limit and email confirmation still
15+ * in its way: a check that cannot be made is no reason to stop everyone.
16+ */
17+export async function turnstileOk(form: FormData, client: string | null): Promise<boolean> {
18+ const settings = turnstile();
19+ if (!settings) return true;
20+ const answer = String(form.get(TURNSTILE_FIELD) ?? "").trim();
21+ if (!answer) return false;
22+ const body = new FormData();
23+ body.set("secret", settings.secret);
24+ body.set("response", answer);
25+ if (client) body.set("remoteip", client);
26+ try {
27+ const response = await fetch(TURNSTILE_VERIFY, { method: "POST", body });
28+ if (!response.ok) {
29+ console.error(`turnstile: siteverify answered ${response.status}`);
30+ return true;
31+ }
32+ return turnstilePassed(await response.json());
33+ } catch (error) {
34+ console.error("turnstile: siteverify could not be reached", error);
35+ return true;
36+ }
37+}
+27−0
1+import assert from "node:assert/strict";
2+import { test } from "node:test";
3+
4+import { pagePolicy } from "./page-headers.ts";
5+import { TURNSTILE_SCRIPT, turnstilePassed, turnstileSettings } from "./turnstile.ts";
6+
7+test("Turnstile is on only with both its site key and its secret", () => {
8+ assert.equal(turnstileSettings(undefined, undefined), null);
9+ assert.equal(turnstileSettings("0x4AAA", ""), null);
10+ assert.equal(turnstileSettings("", "0x4BBB"), null);
11+ assert.deepEqual(turnstileSettings(" 0x4AAA ", "0x4BBB"), { siteKey: "0x4AAA", secret: "0x4BBB" });
12+});
13+
14+test("only Cloudflare's success counts as a pass", () => {
15+ assert.equal(turnstilePassed({ success: true }), true);
16+ assert.equal(turnstilePassed({ success: false, "error-codes": ["invalid-input-response"] }), false);
17+ assert.equal(turnstilePassed({ success: "true" }), false);
18+ assert.equal(turnstilePassed(null), false);
19+});
20+
21+test("the page's policy lets the widget's script and frame load, and nothing else framed", () => {
22+ const policy = pagePolicy("n0nce");
23+ const origin = new URL(TURNSTILE_SCRIPT).origin;
24+ assert.match(policy, new RegExp(`script-src [^;]*${origin.replace(/\./g, "\\.")}`));
25+ assert.match(policy, new RegExp(`frame-src ${origin.replace(/\./g, "\\.")};`));
26+ assert.match(policy, /frame-ancestors 'none'/);
27+});
+33−0
1+/**
2+ * Cloudflare Turnstile on email sign-up: a check that a person, not a
3+ * script, filled in the form. It is on when the Worker has both
4+ * `TURNSTILE_SITE_KEY` (a var: the widget's public site key) and
5+ * `TURNSTILE_SECRET` (a secret); without either it is skipped, so an
6+ * installation of its own and local development sign up without one. No
7+ * Workers imports, so it can be tested under Node; turnstile.server.ts
8+ * reads the settings and asks Cloudflare.
9+ */
10+
11+/** The script that draws the widget; the page's policy allows it (page-headers.ts). */
12+export const TURNSTILE_SCRIPT = "https://challenges.cloudflare.com/turnstile/v0/api.js";
13+/** Where a widget's answer is checked. */
14+export const TURNSTILE_VERIFY = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
15+/** The form field the widget fills in. */
16+export const TURNSTILE_FIELD = "cf-turnstile-response";
17+
18+/** What sign-up says when the check failed or was not done. */
19+export const TURNSTILE_FAILED = "We could not tell that a person sent this form. Wait for the check above the button to finish, then try again.";
20+
21+export type TurnstileSettings = { siteKey: string; secret: string } | null;
22+
23+/** The settings, when both are set: otherwise Turnstile is off. */
24+export function turnstileSettings(siteKey: string | undefined, secret: string | undefined): TurnstileSettings {
25+ const key = (siteKey ?? "").trim();
26+ const hidden = (secret ?? "").trim();
27+ return key && hidden ? { siteKey: key, secret: hidden } : null;
28+}
29+
30+/** Whether Cloudflare's answer says the check passed. */
31+export function turnstilePassed(answer: unknown): boolean {
32+ return typeof answer === "object" && answer !== null && (answer as { success?: unknown }).success === true;
33+}
+11−0
1212 import { identity } from "../lib/services.server";
1313 import { cleanCode, cleanProof, invitePath, looksAutomated } from "../lib/invites";
1414 import { clientKey } from "../lib/registration.server";
15+import { TURNSTILE_FAILED, TURNSTILE_SCRIPT } from "../lib/turnstile";
16+import { turnstile, turnstileOk } from "../lib/turnstile.server";
1517 import {
1618 assertSameOrigin,
1719 getViewer,
4042 github: await githubSignInEnabled(),
4143 githubUrl: next !== "/" ? `/auth/github?${new URLSearchParams({ next })}` : "/auth/github",
4244 started: Date.now(),
45+ // Turnstile's public site key when it is on (lib/turnstile.ts).
46+ turnstile: turnstile()?.siteKey ?? null,
4347 };
4448 }
4549
4751 assertSameOrigin(request);
4852 const form = await request.formData();
4953 if (looksAutomated(form)) return data({ error: "Something went wrong. Try again." }, { status: 422 });
54+ if (!(await turnstileOk(form, clientKey(request)))) return data({ error: TURNSTILE_FAILED }, { status: 422 });
5055 const result = await identity.register(
5156 String(form.get("username") ?? ""),
5257 String(form.get("email") ?? ""),
108113 minLength={10}
109114 />
110115 </Field>
116+ {loaderData.turnstile && (
117+ <>
118+ <script src={TURNSTILE_SCRIPT} async defer />
119+ <div className="cf-turnstile" data-sitekey={loaderData.turnstile} data-theme="auto" data-size="flexible" />
120+ </>
121+ )}
111122 <ErrorText>{error}</ErrorText>
112123 <div className="pt-2 *:w-full">
113124 <SubmitButton pending="Creating account…">Create account</SubmitButton>
+4−0
7171 { "name": "WEB_TOKEN_LIMIT", "namespace_id": "4207", "simple": { "limit": 1000, "period": 60 } }
7272 ],
7373 // Logs of a tenth of requests: every page view is one, too many to keep all.
74+ // Cloudflare Turnstile on email sign-up (app/lib/turnstile.ts): the
75+ // widget's public site key here, and its secret as the TURNSTILE_SECRET
76+ // secret. Either one empty turns it off.
77+ "vars": { "TURNSTILE_SITE_KEY": "" },
7478 "observability": { "enabled": true, "head_sampling_rate": 0.1 },
7579 "upload_source_maps": true
7680 }
+5−0
249249 // /.well-known/g1t.json tells the app so.
250250 if (service.web || hosted.name === "g1t-notify") config.vars.PHONE_PUSH = process.env.PHONE_PUSH === "off" ? "off" : "on";
251251 if (hosted.name === "g1t-identity") config.vars.SITE_URL = PUBLIC_URL;
252+ // Cloudflare Turnstile on sign-up, only with both of your widget's keys.
253+ if (service.web) {
254+ config.vars.TURNSTILE_SITE_KEY = process.env.TURNSTILE_SITE_KEY ?? "";
255+ if (process.env.TURNSTILE_SECRET) config.vars.TURNSTILE_SECRET = process.env.TURNSTILE_SECRET;
256+ }
252257 // Nightly backups' bundles go to a bucket of their own on the same
253258 // S3-compatible store, instead of the BACKUPS R2 bucket, and so do the
254259 // packs kept for fresh clones (src/pack_cache.rs), instead of GIT_PACKS.
+3−0
4242 # Your own GitHub App, for "Continue with GitHub" and importing from
4343 # GitHub. Leave these unset to have neither. See the self-hosting guide.
4444 GITHUB_APP_ID: ${GITHUB_APP_ID:-}
45+ # Cloudflare Turnstile on sign-up: both keys, or neither.
46+ TURNSTILE_SITE_KEY: ${TURNSTILE_SITE_KEY:-}
47+ TURNSTILE_SECRET: ${TURNSTILE_SECRET:-}
4548 GITHUB_APP_SLUG: ${GITHUB_APP_SLUG:-}
4649 GITHUB_APP_CLIENT_ID: ${GITHUB_APP_CLIENT_ID:-}
4750 GITHUB_APP_CLIENT_SECRET: ${GITHUB_APP_CLIENT_SECRET:-}

Comments

Sign in to comment.