mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 01:07:46 +00:00
The private community list (kind 13302) is documented as wire-compatible with Armada's `communityList.ts`, whose entry type ends in `[k: string]: unknown` — unknown keys are part of the contract, and `ConcordJson`'s own KDoc says shapes are "deliberately client-extensible (CORD-03/04)". But `ignoreUnknownKeys = true` plus closed `@Serializable` DTOs meant decode dropped every unmodelled key and encode never restored it, so **every Amethyst write of a user's list silently stripped fields another client had written**, across every community in it. Already proven, not hypothetical: `JoinMaterialWire` declared a `refounder` field that nothing in the repo reads, so it was parsed and destroyed on the first write. We only avoided destroying Armada's `invite_ref`/`excluded_at_epoch` because those were modelled hours ago, for stranded recovery — the anchor recovery depends on would otherwise have been deleted on every write. Each wire DTO's compiler-generated serializer is now wrapped in a shared `JsonTransformingSerializer` that lifts unknown keys into a bag on decode and merges them back on encode, with declared fields winning on conflict. The known-key set is read from the descriptor rather than hand-listed, so it cannot drift from the DTO. Preserved at the document root, each entry, the `current` join material, each channel, each held_root, each tombstone, and everything nested inside `seed`. `refounder`'s typed field is removed so it round-trips generically. Two further data-loss bugs surfaced while doing it, both fixed here: - **`seed` was overwritten with `current` on every write**, destroying the immutable join anchor. It is now kept and re-emitted verbatim as a raw JsonObject — we never hydrate from it while `current` exists, so we have no business rewriting it, and keeping it raw preserves everything nested inside for free. - **`tombstones` were re-encoded as an empty list**, which did not just lose their unknown keys: it RESURRECTED communities another client had deliberately removed. They are now carried verbatim. Verified by four separate sabotage passes (no-op the transform, re-mint `seed`, restore the empty-tombstone write, flip the merge order); each new test fails under at least one, and every mechanism is covered. Control-plane re-serialization was audited too and is NOT fixed here: `compactControlPlane` is safe (it re-wraps the original seal verbatim), but the user-facing *edit* paths — `editConcordMetadata`, `grant`, and the channel edits — construct fresh typed entities and re-encode, so they drop extensions the same way. Fixing those means merging into the head edition's raw JsonObject on each edit path, which is a larger change than this should carry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>