mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
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