aboutsummaryrefslogtreecommitdiffstats
path: root/src/marmot-client.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-02 23:07:00 -0700
committerAdam Soltys <[email protected]>2026-10-02 23:07:09 -0700
commit9e12335231f3fa4c4222bea71c4a1737a15a4994 (patch)
treeef043ccb2edb285732b6bd5e40ce961328eba859 /src/marmot-client.js
parentdccc4a5559f23424d6092009e71375c11efe3784 (diff)
downloadcoinosv3-9e12335231f3fa4c4222bea71c4a1737a15a4994.tar.gz
coinosv3-9e12335231f3fa4c4222bea71c4a1737a15a4994.zip
White Noise attachments (encrypted-media-v2)
Pictures and files now travel in White Noise groups and direct chats, both ways. A file is sealed under the group's media secret for the epoch its message goes out in (MLS exporter "encrypted-media" -> HKDF -> ChaCha20-Poly1305, AAD bound to hash, type and name), uploaded to Blossom, and sent as a kind 9 with a v2 imeta tag. A commit landing mid-upload moves the epoch, so the file is sealed again rather than sent under a stale key. Receivers keep each epoch's media secret for as long as a message from it is in the log, so older pictures still open. Groups founded here now carry and require marmot.group.encrypted-media.v2 (blossom-v1 on blossom.ditto.pub and nostr.download). Checked against MDK's wn: our pictures decrypt in wn, wn's in ours, and wn uploads into our groups by our policy. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/marmot-client.js')
-rw-r--r--src/marmot-client.js12
1 files changed, 12 insertions, 0 deletions
diff --git a/src/marmot-client.js b/src/marmot-client.js
index 7352e05..d63cb04 100644
--- a/src/marmot-client.js
+++ b/src/marmot-client.js
@@ -400,6 +400,18 @@ export function marmotClient({ scope, pubkey, identity, on, relays = GROUP_RELAY
pubkey, groups, load, start, stop, resubscribe, ready, announce, lookup, welcome, accept,
create, send, add, kick, rename, leave, forget,
event: (kind, content, tags) => M.appEvent(pubkey, kind, content, tags),
+ // attachments: seal for the epoch a message is about to go out in, and
+ // open with the secret of the epoch it came in
+ media: {
+ read: M.readMediaTag,
+ tag: M.mediaTag,
+ seal: (g, plain, meta) => ({ epoch: g.tip.epoch, ...M.sealMedia(M.mediaSecret(g.tip), plain, meta) }),
+ open(g, epoch, cipher, ref) {
+ const secret = M.mediaSecretAt(g, epoch) || (g.tip.epoch === epoch ? M.mediaSecret(g.tip) : null);
+ if (!secret) throw new Error('media key for this epoch is gone');
+ return M.openMedia(secret, cipher, ref);
+ },
+ },
view: (g) => M.groupView(g.tip),
peer: (g) => M.directPeer(g),
accounts: (g) => M.accounts(g.tip),