Commit Graph

4 Commits

Author SHA1 Message Date
null 29864ccc0a feat(secrets): --built, because the repository is the wrong place to stop
--staged and --tracked scan what is in git. Neither sees the bundle, which is
the only artifact a user receives -- and a key reaches it without ever being
committed, inlined from an environment variable at build time. An auditor of
applications of this kind reported hardcoded credentials in the frontend bundle
of seven of eight in a single week.

Two tiers, because one would have been useless:

  findings (exit 1)  eyJ, service_role, apikey=, plus every pattern the other
                     modes already use
  noted (exit 0)     NEXT_PUBLIC_, VITE_, REACT_APP_, anon

The second tier is printed and fails nothing. Those prefixes mean "deliberately
shipped to the browser", so failing on them would be a permanently red gate, and
a gate that is always red is one everybody has learned to ignore. But a Supabase
anon key is safe exactly as far as row-level security makes it safe, and knowing
it is out there is the input to that judgement rather than a substitute for it.

eyJ is confined to --built on purpose: it is the base64 of the `{"` every JWT
header starts with, and against source it matches ordinary base64 constantly.

Verified: a planted JWT and service_role in a scratch dist/ are found and exit
1; removing them exits 0 with the public references still listed; a directory
that does not exist exits 2, because nothing scanned is not a pass. Findings are
reported relative to the build directory -- an absolute path consumed the whole
truncation budget and left findings that named a file and showed nothing.

One correction shipped with it: the comment above the report claimed the match
is never echoed in full. It is not redacted at all, only truncated at 120
characters, so a short credential is printed whole. The comment now says what
the code does. Masking the matched span is the real fix and is filed separately.

closes #8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:59:39 -05:00
null 2dca89e635 feat(docs): OPERATIONS.md, the runbook that did not exist
The template shipped scripts to back up, deploy, check health and read the
deployed version, and no document saying where errors go, what alerts, who
receives it, or what to run first when it is down. Grep across docs/** found
zero mentions of error tracking, observability or database restore.

Five sections. Where errors go, with the distinction that matters at 3am --
healthcheck.sh answers "is it up", error tracking answers "is it working", and a
service returning 500 to everything is up. What alerts and to whom, naming a
person rather than a channel nobody owns. Backups, whose last row is the date of
the last verified restore, because a backup nobody has restored is a guess.
Rate limits and cost ceilings, *(precautionary)*. And an ordered "it is down,
what now" where every step is a command that changes nothing.

Marked *(only for a deployed service)*, with the instruction to delete rather
than keep the headings unanswered: an empty runbook reads as one nobody wrote,
which is worse than one that never applied.

scaffold.sh now lays it down (17 files, 0 skipped) and DOC_TRUST_MAP.md points
at it from both tables. Two prose counts in scaffold.sh's header said
"thirteen documents" and were already stale; they no longer carry a number,
since a count in prose beside a list in code drifts the moment the list grows --
which is what just happened.

closes #6

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:57:20 -05:00
null 912c470bbb docs(template): the owner is _null
README.md's status header named a person; it now names _null.

healthcheck.sh's crontab example hard-coded one operator's home directory in
two lines. Those are now $HOME, rather than a literal /home/_null, because a
template copied into every project should not carry anyone's home path and an
invented one would be a path that does not exist -- which this tree refuses
everywhere else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:47:56 -05:00
null 6965915dbd chore(repo): put the template under version control
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.

This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.

secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:44:26 -05:00