Take the product's name out of the cryptography - #32
Merged
Merged
Conversation
Three separate i18n misses of the same shape turned up while shipping E1 and
E2: a string literal inside an interpolated ternary. Angular's i18n marker
covers text nodes, so a literal in `{{ … }}` is invisible to it; the
pseudo-locale reveals them, but only to someone reading a screenshot. Three
in two waves is a pattern, not luck.
So this rejects the shape outright, in the style of no-angular.spec.ts, along
with the sibling failure of a static title/aria-label/placeholder/alt that
never got its i18n- marker. Both compile, both review clean, both are simply
untranslatable.
It found sixteen more on its first run — in the dashboard, groups, group and
menagerie pages — which is the argument for having written it: the sweep, the
pseudo-locale and two rounds of review had all walked past them. All sixteen
are @if/@else with marked spans now, and the template catalogue is 375.
Two details worth keeping:
- The literals are extracted by scanning, not by regex. A greedy quote match
spans from one literal's opener to the next literal's closer, swallowing
the code between them and reporting words that were never strings — which
is exactly the false positive my first throwaway grep produced.
- Punctuation, symbols and emoji are deliberately not copy. '⛔' and ': ' are
the same in every language, and demanding markers for them would make the
guard something people route around rather than obey.
Also fixes the one place the historical internal name still reached a person:
the profile-server field's placeholder read "https://moxy-sync.example.com",
which a self-hoster sees while configuring. Everything else named moxy is
internal and stays that way — the KDF domain strings in particular are
cryptographic input, and renaming one makes every existing phrase open
nothing, which the frozen vectors prove on the spot.
The KDF salts read `moxy.hatch.view.v2` — the old product name, spelled into a value that can never change. Nothing was wrong with it as a salt. What was wrong was that it LOOKED editable: it sat in the same namespace as package aliases, storage keys and CSS selectors, all of which are ordinary mutable things a rebrand or a tidy-up would rightly update. Sooner or later somebody renames the lot in one pass, the frozen vectors go red, and the only thing between that and locking every user out of their own profile is whether the person reading the failure understands why it is there. That is not a safeguard; it is a trap with a note on it. So the meaning now lives in the constant's NAME, which is ordinary TypeScript and may be renamed freely, and the value carries none at all. There is nothing in `rzhy65722j6xubt5` that any future rename could plausibly want to touch. All eight domain strings — four KDF salts, the boop seal's HKDF info, the match-token prefix, the persona colour digest, the metrics token — now live in one file, `crypto/domains.ts`, which is the only place in the repository whose values are frozen. `domains.spec.ts` is the tripwire. The vector specs would also go red, but they go red the way any regression does, and a person under time pressure fixes red tests; this one says in its own failure message what the red actually means. It also rejects any value that reads as a word, so the next person to make these "meaningful" is stopped by a test rather than by luck. Regenerating the vectors this cost — 26 of them, plus the KDF fixture — is a one-time reset that is only safe because there is no production data yet. Every phrase minted against the old salts now opens nothing. Doing this after launch would have been impossible. With the crypto out of the way, the `moxy` invariant stops protecting anything, so the rest of the rename is just work: - browser-storage keys moxy.* -> menagerie.* - wire headers x-moxy-* -> x-menagerie-* - server env vars MOXY_* -> MENAGERIE_* - runtime config file moxy.config.json -> menagerie.config.json - build output dist/moxy -> dist/menagerie - server entrypoint, systemd units, service paths, default DB filename What is left is `@moxy/*` and the `moxy-` selector prefix: 260 references, zero derivations, pure cosmetics — and now safe to sweep whenever, which is the actual win. DEPLOYMENT: the env-var rename needs the operator's hand. The GitHub repo variable MOXY_SERVER_URL becomes MENAGERIE_SERVER_URL, and any MOXY_* set on the profile-server host (MOXY_DB_PATH especially — an unset one silently starts a fresh database at the new default filename) becomes MENAGERIE_*. The systemd units were renamed too, so the old ones need disabling. Ladder green including e2e, which exercises the renamed config file, headers, env vars and dist path end to end.
The rename left three stale references (the Caddyfile's env-var note, the server's own header, a temp-dir prefix) and, more usefully, no answer to the question an operator actually has: my box was installed before this — what breaks? deploy/README now says, with the failure modes ordered by how quietly they fail. MOXY_TRUST_PROXY first, because unread it collapses every client onto one rate-limit bucket and nothing errors; the server just starts refusing people under load.
The rename broke the property the unattended deploy was built for: the installed unit passes MOXY_REPO_DIR, the new update.sh read MENAGERIE_REPO_DIR with a default that matches no existing box, and the first run after the pull would have cd'd into a directory that does not exist and stopped updating — silently, with no way to notice or fix it except logging in. That is exactly the failure an auto-updater exists to prevent, so the script should never have taken the path from a variable in the first place. Three fixes, each closing a way this can wedge: - The clone is located from the script's own path. A unit installed under any older name, or a clone that moves, keeps working; there is no variable to get wrong. - The "is an update needed?" test now compares origin/main against a commit label on the RUNNING container, not against the git ref. Keying off the ref has one unrecoverable state: a run that resets the clone and then dies leaves HEAD at origin/main with the old container still up, so every later run reports "already up to date" while serving pre-failure code. Comparing against what is actually serving makes the updater converge on a desired state, so a half-finished run is just retried next tick. - `git reset --hard` rewrites this script underneath the shell that is part-way through reading it, which is how a half-read script executes garbage. It now hands off to the new copy exactly once, guarded against looping. Plus: the old `moxy-sync` container is retired by the script rather than by hand, since it holds port 8787 and would otherwise make the new container fail to bind — wedging the deploy behind a server nobody can update. Net effect for a box installed before the rename: it converges on its own within two ticks, no shell access needed. The old `moxy-sync-data` volume is left untouched rather than migrated — nothing is deleted, and it can be re-mounted if its contents turn out to matter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The KDF salts read
moxy.hatch.view.v2— the old product name, spelled into a value that can never change. Nothing was wrong with it as a salt. What was wrong was that it looked editable: it sat in the same namespace as package aliases, storage keys and CSS selectors, all of which are ordinary mutable things a rebrand or a tidy-up would rightly update. Sooner or later somebody renames the lot in one pass, the frozen vectors go red, and the only thing between that and locking every user out of their own profile is whether the person reading the failure understands why it's there. That isn't a safeguard; it's a trap with a note on it.The fix is categorical, not cosmetic
The meaning now lives in the constant's name, which is ordinary TypeScript and may be renamed freely. The value carries none at all — there is nothing in
rzhy65722j6xubt5that any future rename could plausibly want to touch.All eight domain strings now live in one file,
crypto/domains.ts: four KDF salts, the boop seal's HKDF info label, the match-token prefix, the persona-colour digest, and the metrics token. Three of those were ones I'd missed when first answering the question —moxy.persona.v3feeds every creature's colour.domains.spec.tsis the tripwire. The vector specs would also go red, but they go red the way any regression does, and a person under time pressure fixes red tests — this one says in its own failure message what the red actually means. It also rejects any value that reads as a word, so the next person to make these "meaningful" is stopped by a test rather than by luck.Cost: 26 vectors regenerated plus the KDF fixture. Every phrase minted against the old salts now opens nothing. This is a one-time reset that is only safe because there is no production data yet — after launch it would have been impossible.
The invariant list is genuinely shorter
With the crypto decoupled, the
moxyinvariant stopped protecting anything, so the rest of the rename is just work: browser-storage keys, wire headers, server env vars, the runtime config filename, the build output path, the server entrypoint, systemd units, service paths, the default DB filename.What's left is
@moxy/*and themoxy-selector prefix — 260 references, zero derivations, pure cosmetics. The win isn't that they're renamed; it's that they're now safe to rename.CLAUDE.mdand the README's historical note are rewritten accordingly — the old note claimed renaming would "churn frozen crypto vectors," which is exactly what is no longer true.The updater had to survive its own rename
The rename broke the property the unattended deploy exists for: the installed unit passes
MOXY_REPO_DIR, the newupdate.shreadMENAGERIE_REPO_DIRwith a default matching no existing box, and the first run after the pull would havecd'd into a directory that doesn't exist and stopped updating — silently, unfixable without shell access.Three fixes, each closing a way it can wedge:
HEADatorigin/mainwith the old container up, so every later run reports "already up to date" while serving pre-failure code. Comparing against what is actually serving makes the updater converge on a desired state, so a half-finished run is just retried.git reset --hardrewrites the script underneath the shell reading it — it now hands off to the new copy exactly once, guarded against looping.Plus the script retires the pre-rename container itself, since it holds port 8787 and would otherwise wedge the deploy behind a server nobody can update.
A box installed before the rename converges on its own within two ticks. The old
moxy-sync-datavolume is left untouched rather than migrated — nothing is deleted, and it can be re-mounted if its contents turn out to matter.Also here
A guard for the copy no text-node pass can see. Three i18n misses of the same shape turned up while shipping the adoption plan — a string literal inside an interpolated ternary, invisible to Angular's marker and visible in the pseudo-locale only to someone reading a screenshot.
i18n-copy.spec.tsrejects that shape and unmarked static attributes outright, and found sixteen more on its first run — in the dashboard, groups, group and menagerie pages, which the sweep, the pseudo-locale and two rounds of review had all walked past.Verification
Full ladder green — format, both typechecks, all four unit suites, production build, and the e2e, which exercises the renamed config file, headers, env vars and dist path end to end.
Operator note: the
MENAGERIE_SERVER_URLrepo variable is already updated. Nothing else needs hands — seedeploy/README.md→ Migrating off themoxynames for what the automatic path is fixing, and what a hand-rolled install would need instead.🤖 Generated with Claude Code
https://claude.ai/code/session_01UaDanzXTm6kVSgbNh1eNrs
Generated by Claude Code