diff options
| author | Adam Soltys <[email protected]> | 2026-10-02 23:07:00 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-02 23:07:09 -0700 |
| commit | 9e12335231f3fa4c4222bea71c4a1737a15a4994 (patch) | |
| tree | ef043ccb2edb285732b6bd5e40ce961328eba859 /tools/marmot-chat-test.js | |
| parent | dccc4a5559f23424d6092009e71375c11efe3784 (diff) | |
| download | coinosv3-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 'tools/marmot-chat-test.js')
| -rw-r--r-- | tools/marmot-chat-test.js | 28 |
1 files changed, 28 insertions, 0 deletions
diff --git a/tools/marmot-chat-test.js b/tools/marmot-chat-test.js index f3a25f7..7310fd0 100644 --- a/tools/marmot-chat-test.js +++ b/tools/marmot-chat-test.js @@ -24,6 +24,12 @@ import * as nip06 from 'nostr-tools/nip06'; import { finalizeEvent } from 'nostr-tools/pure'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { mkdtempSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'; +import { randomBytes, bytesToHex } from '@noble/hashes/utils'; + +// a tiny real PNG with a few random bytes after it, so every run's file is new +const png = () => new Uint8Array([...Uint8Array.from(atob('iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVR4nGP8z8DAwMDAxMDAwMDAAAANHQEDasKb6QAAAABJRU5ErkJggg=='), (c) => c.charCodeAt(0)), ...randomBytes(8)]); +const imgShown = () => page.evaluate(() => [...document.querySelectorAll('.chat-log img.note-img')].filter((i) => i.complete && i.naturalWidth > 0 && i.src.startsWith('blob:')).length); // shots go somewhere every machine has const shot = (name) => join(tmpdir(), name); @@ -169,6 +175,28 @@ try { } await say('hello from coinos'); check('ours reaches wn', !!await until(() => JSON.stringify(wn(['messages', 'list', gid, '--limit', '20'], alice)).includes('hello from coinos'))); + + console.log('\n[pictures]'); + { + const dir = mkdtempSync(join(tmpdir(), 'wn-ui-')); + const theirs = join(dir, 'from-wn.png'); + writeFileSync(theirs, png()); + const before = await imgShown(); + const up = wn(['media', 'upload', gid, theirs, '--send'], alice); + check('wn sends a picture', !up.error, JSON.stringify(up.error || '')); + check('it shows in the group', !!await until(async () => (await imgShown()) > before, SITE ? 90000 : 30000)); + const ours = join(dir, 'from-coinos.png'); + const bytes = png(); + writeFileSync(ours, bytes); + const input = await page.$('#msg-file'); + await input.uploadFile(ours); + const plain = bytesToHex((await import('@noble/hashes/sha256')).sha256(bytes)); + check('ours reaches wn', !!await until(() => JSON.stringify(wn(['media', 'list', gid], alice)).includes(plain), 40000)); + const out = mkdtempSync(join(tmpdir(), 'wn-dl-')); + wn(['media', 'download', gid, plain, '--output', out], alice); + const f = readdirSync(out)[0]; + check('and wn decrypts it', !!f && bytesToHex(readFileSync(join(out, f))) === bytesToHex(bytes)); + } await page.screenshot({ path: shot('marmot-group.png') }); console.log('\n[a first message to a White Noise user founds a direct chat]'); |
