Files
amethyst/amethyst
Claude 401ba8b5de refactor(cordn): move the transport wiring into commons, and test what had none
Three classes carried the cordn transport and had no test between them.
Two of the three were untestable where they sat, which is most of why.

**The relay pool and the link factory move to commons.** `amy` needs the
same ContextVM transport the Android app builds, and Rule 5 of the CLI's
contract says extract before adding — so `NostrClientCvmRelayPool` and
`CordnRuntime.realLinks` are now `commons`' `NostrClientCvmRelayPool` and
`CordnLinks.over`. Neither had an Android dependency; they were in
`amethyst/` because that is where the first caller was.

Worth stating why the factory is shared rather than copied per front end:
everything it assembles is a protocol decision — which signer signs which
call, that gift wrapping stays REQUIRED, that `initialize` is not performed
at open — and a second copy is a second place those can drift apart.

`CordnCoordinatorLink`/`…Factory` move from jvmAndroid to commonMain with
them. They were only there because `FileBackedCordnScopeFactory` shares the
file, and nothing about a link is JVM-bound. `CordnBlobCipher` moves for the
same reason.

**`KeyedCordnBlobCipher`**, new: ChaCha20-Poly1305 under a caller-supplied
key, for the platforms with no OS key store to hide one in — the desktop app
and `amy`. Android keeps its KeyStore cipher. The nonce is random per blob
and the KDoc says why that is the parameter to be careful about: one key
covers every blob, and a repeat leaks the XOR of two `MlsGroupState`
serialisations, which is epoch secrets.

**`CordnBlobUpload`**, new: everything a blob host is told about a cordn
attachment, as a value instead of seven literal arguments. This is the one
with a real reason beyond testability. Each of those arguments is a privacy
decision, and every one of them fails *silently* if it regresses — the
upload still succeeds, the group still sees the file, and the only
difference is what a server learned. There is no downstream check that would
notice. `CordnMediaService` now computes the descriptor and forwards it, so
the decisions have one home and can be asserted.

28 tests. What they pin, and what killing each mutation proved:

- the REQ goes to the coordinator's relays and matches on the recipient
  `p` tag, not on `authors` — a gift-wrapped response is signed by a
  one-time key, so an `authors` filter matches nothing and the call simply
  times out
- a response arriving with `isLive=false` is still delivered; kind 25910 is
  ephemeral, so branching on it would drop the answer
- closing a subscription unsubscribes that id and no other
- the blob host gets `sha256(ciphertext)`, `application/octet-stream`, a hex
  filename, no alt text, no content warning, and `/upload` rather than
  `/media`
- `initialize` claims survive the trip, and each way a server can decline to
  make one leaves a null — including JSON `null`, which
  `jsonPrimitive.content` renders as the four-character string "null" and
  would otherwise put a coordinator called "null" on a settings screen

One mutation survived twice and is worth recording. Removing the
`require(bytes.size > NONCE_LENGTH)` from `decrypt` changed no outcome: a
short blob already throws, either from the AEAD's own tag-length check or
from slicing a 12-byte nonce out of a shorter array, and both are
`IllegalArgumentException`. So the guard buys no failure — it buys the
reason. The test now asserts the message, because that is the only thing
there is to assert: "fromIndex(12) > toIndex(0)" from inside a `copyOfRange`
reads as a bug in Amethyst, while naming the size points at the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
2026-09-22 02:16:33 +00:00
..