From 3c53965da43beb02e1005b3f97638ac12b7efa2d Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:01:22 -0400 Subject: [PATCH 01/12] Add pre-commit guard against ETHERPAD_DOMAIN leaks Fails the commit if ETHERPAD_DOMAIN is unset (and not in .env), or if its value appears in any file in the staged index. Backstop against re-leaking the pad host into public history. Co-Authored-By: Claude Opus 4.7 (1M context) --- .husky/pre-commit | 1 + scripts/check-no-etherpad-domain.sh | 38 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100755 scripts/check-no-etherpad-domain.sh diff --git a/.husky/pre-commit b/.husky/pre-commit index 5ee7abd8..db31338c 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ pnpm exec lint-staged +./scripts/check-no-etherpad-domain.sh diff --git a/scripts/check-no-etherpad-domain.sh b/scripts/check-no-etherpad-domain.sh new file mode 100755 index 00000000..20eca155 --- /dev/null +++ b/scripts/check-no-etherpad-domain.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fail the commit if ETHERPAD_DOMAIN is unset, or if its value appears in any +# file that would be in the resulting tree. This prevents the production +# pad host from being leaked into the public repo (it has happened before — +# see the May 2026 history scrub). + +if [ -z "${ETHERPAD_DOMAIN:-}" ] && [ -f .env ]; then + ETHERPAD_DOMAIN=$( + grep -E '^[[:space:]]*ETHERPAD_DOMAIN[[:space:]]*=' .env \ + | head -1 \ + | sed -E 's/^[[:space:]]*ETHERPAD_DOMAIN[[:space:]]*=[[:space:]]*//' \ + | sed -E 's/^"(.*)"$/\1/' \ + | sed -E "s/^'(.*)'\$/\\1/" + ) +fi + +if [ -z "${ETHERPAD_DOMAIN:-}" ]; then + echo "pre-commit: ETHERPAD_DOMAIN is unset and not found in .env." >&2 + echo "Set it (e.g. in .env) so this hook can verify no staged file contains the value." >&2 + exit 1 +fi + +# Search the staged index — what the commit would actually contain. +# Exclude .env files (gitignored but defensive) and this script itself +# (which intentionally references the variable name, not its value). +matches=$(git grep --cached -n -F "$ETHERPAD_DOMAIN" -- \ + ':!.env' ':!.env.*' \ + ':!scripts/check-no-etherpad-domain.sh' 2>/dev/null || true) + +if [ -n "$matches" ]; then + echo "pre-commit: ETHERPAD_DOMAIN value found in staged content:" >&2 + echo "$matches" >&2 + echo "" >&2 + echo "Refusing to commit. Remove the literal value before committing." >&2 + exit 1 +fi From 15d126ae327189687ae120dcebac7bd0bb8b205a Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:03:57 -0400 Subject: [PATCH 02/12] Drop scrub reference from guard script comment Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/check-no-etherpad-domain.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/check-no-etherpad-domain.sh b/scripts/check-no-etherpad-domain.sh index 20eca155..db1aaf86 100755 --- a/scripts/check-no-etherpad-domain.sh +++ b/scripts/check-no-etherpad-domain.sh @@ -2,9 +2,8 @@ set -euo pipefail # Fail the commit if ETHERPAD_DOMAIN is unset, or if its value appears in any -# file that would be in the resulting tree. This prevents the production -# pad host from being leaked into the public repo (it has happened before — -# see the May 2026 history scrub). +# file that would be in the resulting tree. Keeps the production pad host +# out of the public repo. if [ -z "${ETHERPAD_DOMAIN:-}" ] && [ -f .env ]; then ETHERPAD_DOMAIN=$( From f92e363cbb0b4c76da69cadd6958c27b107bb261 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:06:12 -0400 Subject: [PATCH 03/12] Reformat agent docs to satisfy prettier Table column padding shifted after recent content edits. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/agents/build-pipeline.md | 2 +- docs/agents/debugging-stale-content.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/agents/build-pipeline.md b/docs/agents/build-pipeline.md index 832b3d1a..65774e47 100644 --- a/docs/agents/build-pipeline.md +++ b/docs/agents/build-pipeline.md @@ -40,7 +40,7 @@ Production `ETHERPAD_DOMAIN` is ``. Only `` actual | Var | Effect | | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `ETHERPAD_DOMAIN` | Host to fetch pads from. Required. `` in prod, `` locally if you want real content. | +| `ETHERPAD_DOMAIN` | Host to fetch pads from. Required. `` in prod, `` locally if you want real content. | | `RENDER` | Set by Render automatically. Triggers in-memory caching paths instead of disk. | | `FILE_SYSTEM_CACHE=false` | Manual override to disable disk caches when developing locally. Useful for snapshot regeneration after pad edits. | diff --git a/docs/agents/debugging-stale-content.md b/docs/agents/debugging-stale-content.md index e70cc8aa..9a04e2ca 100644 --- a/docs/agents/debugging-stale-content.md +++ b/docs/agents/debugging-stale-content.md @@ -16,7 +16,7 @@ There are at least seven places content can be stale. Cheapest checks first. | 4 | Render build cache: `.cache/parsed-markdown/` | inside the build container | keyed on markdown content hash, busts naturally on pad edits — safe | | 5 | `node-fetch-cache` | local disk only | bypassed when `RENDER=true` (see `src/lib/fetchPost.ts`) | | 6 | Astro image cache (`node_modules/.astro/assets/*.json`) | local + Render `node_modules` cache | extended weekly by `scripts/image-cache.mjs`. Only affects images | -| 7 | Etherpad export endpoint | | no cache headers as of last check — fresh per request | +| 7 | Etherpad export endpoint | | no cache headers as of last check — fresh per request | ## Decision tree From c804197a2181fd19759e59fb411c173171b36ed2 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:11:39 -0400 Subject: [PATCH 04/12] fix(scripts): handle inline comments in .env parser Strip whitespace and # comments when extracting ETHERPAD_DOMAIN from .env so the guard doesn't silently match a value that includes a trailing comment. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/check-no-etherpad-domain.sh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/check-no-etherpad-domain.sh b/scripts/check-no-etherpad-domain.sh index db1aaf86..ba98e385 100755 --- a/scripts/check-no-etherpad-domain.sh +++ b/scripts/check-no-etherpad-domain.sh @@ -6,13 +6,20 @@ set -euo pipefail # out of the public repo. if [ -z "${ETHERPAD_DOMAIN:-}" ] && [ -f .env ]; then - ETHERPAD_DOMAIN=$( - grep -E '^[[:space:]]*ETHERPAD_DOMAIN[[:space:]]*=' .env \ - | head -1 \ - | sed -E 's/^[[:space:]]*ETHERPAD_DOMAIN[[:space:]]*=[[:space:]]*//' \ - | sed -E 's/^"(.*)"$/\1/' \ - | sed -E "s/^'(.*)'\$/\\1/" - ) + raw=$(grep -E '^[[:space:]]*ETHERPAD_DOMAIN[[:space:]]*=' .env | head -1 || true) + if [ -n "$raw" ]; then + raw="${raw#*=}" # drop key + = + raw="${raw#"${raw%%[![:space:]]*}"}" # lstrip + if [[ "$raw" == \"* ]]; then + raw="${raw#\"}"; raw="${raw%%\"*}" # content between double quotes + elif [[ "$raw" == \'* ]]; then + raw="${raw#\'}"; raw="${raw%%\'*}" # content between single quotes + else + raw="${raw%%#*}" # strip inline comment + raw="${raw%"${raw##*[![:space:]]}"}" # rstrip + fi + ETHERPAD_DOMAIN="$raw" + fi fi if [ -z "${ETHERPAD_DOMAIN:-}" ]; then From efaec19079ed667917afd67fd6296c887937a9a5 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:19:14 -0400 Subject: [PATCH 05/12] refactor(scripts): port etherpad-domain guard to tsx + dotenv Replaces the hand-rolled bash .env parser with `dotenv/config`, which already handles comments, quoting, and escapes correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .husky/pre-commit | 2 +- scripts/check-no-etherpad-domain.sh | 44 ----------------------------- scripts/check-no-etherpad-domain.ts | 44 +++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 45 deletions(-) delete mode 100755 scripts/check-no-etherpad-domain.sh create mode 100644 scripts/check-no-etherpad-domain.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index db31338c..c104df3a 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ pnpm exec lint-staged -./scripts/check-no-etherpad-domain.sh +pnpm exec tsx ./scripts/check-no-etherpad-domain.ts diff --git a/scripts/check-no-etherpad-domain.sh b/scripts/check-no-etherpad-domain.sh deleted file mode 100755 index ba98e385..00000000 --- a/scripts/check-no-etherpad-domain.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Fail the commit if ETHERPAD_DOMAIN is unset, or if its value appears in any -# file that would be in the resulting tree. Keeps the production pad host -# out of the public repo. - -if [ -z "${ETHERPAD_DOMAIN:-}" ] && [ -f .env ]; then - raw=$(grep -E '^[[:space:]]*ETHERPAD_DOMAIN[[:space:]]*=' .env | head -1 || true) - if [ -n "$raw" ]; then - raw="${raw#*=}" # drop key + = - raw="${raw#"${raw%%[![:space:]]*}"}" # lstrip - if [[ "$raw" == \"* ]]; then - raw="${raw#\"}"; raw="${raw%%\"*}" # content between double quotes - elif [[ "$raw" == \'* ]]; then - raw="${raw#\'}"; raw="${raw%%\'*}" # content between single quotes - else - raw="${raw%%#*}" # strip inline comment - raw="${raw%"${raw##*[![:space:]]}"}" # rstrip - fi - ETHERPAD_DOMAIN="$raw" - fi -fi - -if [ -z "${ETHERPAD_DOMAIN:-}" ]; then - echo "pre-commit: ETHERPAD_DOMAIN is unset and not found in .env." >&2 - echo "Set it (e.g. in .env) so this hook can verify no staged file contains the value." >&2 - exit 1 -fi - -# Search the staged index — what the commit would actually contain. -# Exclude .env files (gitignored but defensive) and this script itself -# (which intentionally references the variable name, not its value). -matches=$(git grep --cached -n -F "$ETHERPAD_DOMAIN" -- \ - ':!.env' ':!.env.*' \ - ':!scripts/check-no-etherpad-domain.sh' 2>/dev/null || true) - -if [ -n "$matches" ]; then - echo "pre-commit: ETHERPAD_DOMAIN value found in staged content:" >&2 - echo "$matches" >&2 - echo "" >&2 - echo "Refusing to commit. Remove the literal value before committing." >&2 - exit 1 -fi diff --git a/scripts/check-no-etherpad-domain.ts b/scripts/check-no-etherpad-domain.ts new file mode 100644 index 00000000..f9a45571 --- /dev/null +++ b/scripts/check-no-etherpad-domain.ts @@ -0,0 +1,44 @@ +import "dotenv/config"; +import { spawnSync } from "node:child_process"; + +const domain = process.env.ETHERPAD_DOMAIN; + +if (!domain) { + console.error( + "pre-commit: ETHERPAD_DOMAIN is unset and not found in .env.\n" + + "Set it (e.g. in .env) so this hook can verify no staged file " + + "contains the value.", + ); + process.exit(1); +} + +const result = spawnSync( + "git", + [ + "grep", + "--cached", + "-n", + "-F", + domain, + "--", + ":!.env", + ":!.env.*", + ":!scripts/check-no-etherpad-domain.ts", + ], + { encoding: "utf8" }, +); + +// git grep exits 0 on match, 1 on no match, >1 on error. +if (result.status === 0 && result.stdout) { + console.error("pre-commit: ETHERPAD_DOMAIN value found in staged content:"); + console.error(result.stdout.trimEnd()); + console.error( + "\nRefusing to commit. Remove the literal value before committing.", + ); + process.exit(1); +} + +if (result.status !== null && result.status > 1) { + console.error("pre-commit: git grep failed:", result.stderr.trim()); + process.exit(result.status); +} From 68d2ef38b79696e323e9460a400d74fd84caf6e5 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:21:39 -0400 Subject: [PATCH 06/12] fix(scripts): expose secrets guard as a pnpm script Lets knip discover it and gives the hook a friendlier invocation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .husky/pre-commit | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index c104df3a..53b48197 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ pnpm exec lint-staged -pnpm exec tsx ./scripts/check-no-etherpad-domain.ts +pnpm run check:secrets diff --git a/package.json b/package.json index a164cbfc..a505aa08 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:snapshot:update": "vitest --config ./vitest.snapshot.config.ts --update", "cache:clear": "rm -rf .cache && rm -rf node_modules/.astro", "check": "astro check", + "check:secrets": "tsx ./scripts/check-no-etherpad-domain.ts", "check:ts": "tsc --noEmit", "lint": "eslint .", "format": "prettier --write .", From a52284df0b9efdab416a3f6f7faff1f0c74fc331 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:27:23 -0400 Subject: [PATCH 07/12] fix(render): install deps explicitly in build command Render was relying on a cached node_modules; the recent history rewrite invalidated the cache and there's no auto-install step, so \`pnpm run build\` errored with \`astro: not found\`. Make install explicit so the build is reproducible from a clean checkout. Co-Authored-By: Claude Opus 4.7 (1M context) --- render.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.yaml b/render.yaml index b0bcbb56..d6d3121b 100644 --- a/render.yaml +++ b/render.yaml @@ -2,7 +2,7 @@ services: - type: web name: beeblog env: static - buildCommand: pnpm run build + buildCommand: pnpm install --frozen-lockfile && pnpm run build staticPublishPath: ./dist pullRequestPreviewsEnabled: true routes: From 0f8d761fa4c0cd47d466b9694d4e8781b979c688 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:30:16 -0400 Subject: [PATCH 08/12] fix(build): add prebuild hook to install deps when missing Render's render.yaml buildCommand is snapshotted at service creation and not re-read on push, so changes there don't take effect without a manual Blueprint sync. Moving the install fallback into a pnpm prebuild hook keeps \`pnpm run build\` self-contained: locally it's a no-op (astro already in node_modules/.bin), on a clean checkout it installs first. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a505aa08..5578d2a8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", + "prebuild": "[ -x node_modules/.bin/astro ] || pnpm install --frozen-lockfile", "build": "node ./scripts/image-cache.mjs && astro build", "preview": "astro preview", "astro": "astro", From 96b3a0fd26f83cf21d2a8f9d79d2c2ffd9878d2d Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:32:04 -0400 Subject: [PATCH 09/12] fix(pnpm): allow pixelteer build scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the expost approval — pixelteer is also a git-hosted dep that runs prepare scripts during install, which Render rejects under the tightened onlyBuiltDependencies allowlist. Co-Authored-By: Claude Opus 4.7 (1M context) --- pnpm-workspace.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b3b33f8d..1861e842 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,4 @@ packages: onlyBuiltDependencies: - expost + - pixelteer From 928bbc307e8e4d8e075b27ee07f806e72b26c436 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:34:18 -0400 Subject: [PATCH 10/12] fix(build): install only prod deps in the prebuild fallback Builds don't reach any devDependency (pixelteer is used only in the manual puppeteer scripts; vitest/diffable-html only in snapshot specs), so installing dev deps just added install time and forced us to whitelist git-hosted dev deps in onlyBuiltDependencies. Switch the prebuild to \`--prod\` and drop the pixelteer entry. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- pnpm-workspace.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 5578d2a8..eeedf820 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", - "prebuild": "[ -x node_modules/.bin/astro ] || pnpm install --frozen-lockfile", + "prebuild": "[ -x node_modules/.bin/astro ] || pnpm install --prod --frozen-lockfile", "build": "node ./scripts/image-cache.mjs && astro build", "preview": "astro preview", "astro": "astro", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1861e842..b3b33f8d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,3 @@ packages: onlyBuiltDependencies: - expost - - pixelteer From 774b34bcccf1a81688aaba406d495172943ce4cf Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:41:16 -0400 Subject: [PATCH 11/12] fix(build): gate prebuild install on \$RENDER Previous file-existence check failed silently inside pnpm's script shell. \$RENDER is set by Render and only by Render, so this scopes the install to where it's needed and stays a no-op locally. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eeedf820..64905bc7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", - "prebuild": "[ -x node_modules/.bin/astro ] || pnpm install --prod --frozen-lockfile", + "prebuild": "[ -z \"$RENDER\" ] || pnpm install --prod --frozen-lockfile", "build": "node ./scripts/image-cache.mjs && astro build", "preview": "astro preview", "astro": "astro", From 11aeedb7550b26d88bcc26c19d3d530204ee9299 Mon Sep 17 00:00:00 2001 From: Nathan Arthur Date: Wed, 13 May 2026 16:45:59 -0400 Subject: [PATCH 12/12] fix(build): revert --prod, restore pixelteer in onlyBuiltDependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \`pnpm install --prod\` skips devDependencies inside git-hosted deps too — and expost's own prepare script needs tsc (a devDep) to build itself from source. So the install has to include dev deps, which means pixelteer (also git-hosted with build scripts) needs to stay in the allowlist. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- pnpm-workspace.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 64905bc7..25cfb2d7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", - "prebuild": "[ -z \"$RENDER\" ] || pnpm install --prod --frozen-lockfile", + "prebuild": "[ -z \"$RENDER\" ] || pnpm install --frozen-lockfile", "build": "node ./scripts/image-cache.mjs && astro build", "preview": "astro preview", "astro": "astro", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b3b33f8d..1861e842 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,4 @@ packages: onlyBuiltDependencies: - expost + - pixelteer