Gate: no PocketBase hooks in the repo

The cloud container pulls the repo live; a pb_hooks/ folder or *.pb.js pushed by any writer would be
server-side JS on the team backend. The container never runs repo hooks (the one sign-in hook is baked
into the image), and this gate keeps a hooks folder from appearing at all. Self-tests on every run.
This commit is contained in:
Doxa Maps 2026-09-27 23:45:31 +00:00
parent 13cf20eccf
commit a08cf1e4d8
2 changed files with 35 additions and 1 deletions

View file

@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
* check-no-repo-pb-hooks.mjs — no PocketBase hooks (pb_hooks, *.pb.js) may live in the repo.
*
* WHY: the cloud container pulls this repo every few seconds. A PocketBase hook is server-side JS; one pushed
* by any plugin writer would run on the team backend within seconds, which is the rogue-writer hole the
* roundtable test closed. The container never points PocketBase at the repo (--hooksDir is outside the
* pulled tree; the one team sign-in hook is baked into the image and pinned, deployment/cloud/Dockerfile).
* This gate keeps anyone from adding a hooks folder that LOOKS like it would run, or that a later recipe
* change could start running.
*
* HOW: every tracked path; a self-test on every run (a poisoned list must fail), so the gate cannot go blind.
*/
import { execFileSync } from 'node:child_process'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = process.env.DOXA_REPO || join(dirname(fileURLToPath(import.meta.url)), '..')
export const offending = (paths) => paths.filter((p) => /(^|\/)pb_hooks(\/|$)/.test(p) || /\.pb\.(js|ts)$/.test(p))
if (offending(['README.md', 'backend/pocketbase/pb_hooks/x.pb.js', 'a/b.pb.ts']).length !== 2) {
console.error('✗ check:no-repo-pb-hooks self-test failed: the matcher went blind'); process.exit(1)
}
const tracked = execFileSync('git', ['ls-files', '-z'], { cwd: ROOT }).toString().split('\0').filter(Boolean)
const bad = offending(tracked)
if (bad.length) {
console.error(`✗ PocketBase hooks in the repo (server-side JS must never ride the live pull):\n${bad.map((p) => ' ' + p).join('\n')}`)
console.error(' The team sign-in hook is baked into the image (deployment/cloud/Dockerfile); anything else needs a design review.')
process.exit(1)
}
console.log(`✓ no PocketBase hooks among ${tracked.length} tracked files`)

View file

@ -168,6 +168,7 @@
"check:one-palette:self-test": "node internal/check-one-palette.mjs --self-test",
"check:optional-t-equivalence": "node internal/check-optional-t-equivalence.mjs",
"check:package-json-unique-keys": "node internal/check-package-json-unique-keys.mjs",
"check:no-repo-pb-hooks": "node internal/check-no-repo-pb-hooks.mjs",
"check:packet-browser": "node internal/check-packet-browser.mjs",
"check:packet-fixture": "node internal/generate-packet-fixture.mjs --check",
"check:packet-render": "node internal/check-packet-render.mjs",
@ -247,7 +248,7 @@
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
@ -395,6 +396,7 @@
"check:no-silent-engine": "A profile must NEVER fall back to a different map engine than its provider asked for. `engineGlobal(provider) || window.mapboxgl` looks defensive and is not: engineGlobal already returns the right global for the requested provider (useMapInstance.js:115 falls back to mapbox's NAME only for an UNKNOWN provider, never a missing engine), so the || fires in exactly one case — the page asked for maplibre and maplibre failed to load — and hands back the TOKEN-GATED engine to a page that asked for the KEYLESS one. Worse, accessToken is gated on !isMaplibre, so the substitute arrives WITHOUT a token: 401s on every tile instead of a legible failure. Found by regions' C3 read 2026-09-26; 2 offenders remained (cities-internet, gospel-resources) after my-upg-100-list was fixed. NOT a duplicate of check:leaf-engine-per-provider / check:provider-switch / check:auto-engine — all three assert the HAPPY path (what loads) and this bug only appears when something does NOT load, so all three were green while it existed. A SOURCE check on purpose: reproducing it live means serving a leaf with MapLibre deliberately blocked, spending a browser slot on what a grep decides. Exits 75 when no file calls engineGlobal — 'found nothing' is not 'found nothing wrong'. Run :self-test to prove the pattern still matches.",
"check:optional-t-equivalence": "C4's shared guarded translator (library/composables/useOptionalT.js) must behave like the twenty hand-written try{useI18n()}catch blocks it replaces: all three host states, and the per-site register asserts each migrated file still passes ITS OWN fallback rather than inheriting the catalogue, because routing them through tStatic would change what seven bare-host bundles render. EVERY LATER C4 WAVE APPENDS A ROW. Extract-and-executes the module source, since useOptionalT imports vue-i18n and an i18n index that calls import.meta.glob.",
"check:package-json-unique-keys": "no object in package.json may carry the same key twice: JSON.parse keeps the last silently, so a duplicate kills the first entry while every gate that reads the PARSED object passes (a //scripts note sat twice after a merge). A small scanner tracks keys per object; its self-test runs every time.",
"check:no-repo-pb-hooks": "no PocketBase hooks (pb_hooks/, *.pb.js) in the repo: the cloud container pulls it live, so a pushed hook would be server-side JS from any writer; the one team sign-in hook is baked into the image",
"check:packet-browser": "the packet the READER gets — the legend's Export > Prayer packet path, proving the shared renderer runs in a BROWSER. Asserts the same 12 points / 12 wheel labels / page-1 geography / print layer as check:packet-render, which it did not until 2026-09-24. CONDITIONAL: SKIPs without a server over the repo root (python3 -m http.server 4898) and a reachable records API.",
"check:packet-fixture": "the committed fixture must still match clusters.json and must never carry image_url (CI would fetch 43 photos from someone else's CDN).",
"check:packet-render:expect-funnel": "check:packet-render with the page-2 FUNNEL declared REQUIRED. Exists because of a real near-miss: a merge resolved with `git checkout --ours` deleted the printPacket call site that supplies the funnel data (60 lines) and EVERY GATE STAYED GREEN — it had to, because the funnel page is OPTIONAL by design (buildPacket omits it when the arg is null), so a packet without page 2 is not malformed, just smaller. Always requiring it would be the wrong fix: the 11 pre-funnel laos packets and any host with no funnel data would go red, and a false red costs as much as a false green. So the CALLER, which knows whether this packet was built with funnel data, declares it — an absence is then either declared (no flag) or a failure (flag passed, page missing), never a silent nothing. Pass it for any packet from a profile that wires packetFunnel(). Browser gate.",