Skip to content

Take the product's name out of the cryptography - #32

Merged
Randolin merged 4 commits into
mainfrom
claude/product-strategy-positioning-u1vwig
Aug 29, 2026
Merged

Randolin merged 4 commits into
mainfrom
claude/product-strategy-positioning-u1vwig

Conversation

@Randolin

Copy link
Copy Markdown
Owner

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 rzhy65722j6xubt5 that 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.v3 feeds every creature's colour.

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.

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 moxy invariant 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 the moxy- 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.md and 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 new update.sh read MENAGERIE_REPO_DIR with a default matching no existing box, and the first run after the pull would have cd'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:

  • The clone is located from the script's own path. No variable to get wrong.
  • The update test compares against a commit label on the running container, not the git ref. Keying off the ref has one unrecoverable state: a run that resets the clone then dies leaves HEAD at origin/main with 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 --hard rewrites 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-data volume 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.ts rejects 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_URL repo variable is already updated. Nothing else needs hands — see deploy/README.mdMigrating off the moxy names 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

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.
@Randolin
Randolin merged commit 5c4beec into main Aug 29, 2026
2 checks passed
@Randolin
Randolin deleted the claude/product-strategy-positioning-u1vwig branch August 29, 2026 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant