1# ✉️ iodine.js - private messages23iodine.js is a miniature encrypted messenger: NIP-17 direct messages sealed and gift wrapped per NIP-59, in about 4kb. A form sends a message to someone's DM relays (their kind 10050 list), and a list shows your inbox, unwrapped and rendered like any other xenon feed. It composes with xenon.js, neon.js and radon.js, and signs and encrypts with any signer that has `nip44`: a NIP-07 extension, chlorine.js, or helium.js with fluorine.js.45```html6<form nio-send nio-to="npub1..."><textarea name="content"></textarea><button>Send</button></form>7<ol nio-inbox nio-with="npub1...">8 <template><li><b nrn-profile="{pubkey}" nrn-field="name">{npub}</b> <p>{content}</p></li></template>9</ol>10```1112The form wraps each message twice, once for the recipient and once for you, and the list shows your conversation with that npub, newest first.1314## Attributes1516| attribute | description |17|---|---|18| `nio-send` | Form that sends its `content` field as a private message on submit |19| `nio-to` | Recipient (`npub`, `nprofile` or hex). Falls back to a form field named `to` |20| `nio-inbox` | Subscribes to your gift wraps (`kinds:[1059]`, `#p:[you]`) and renders the unwrapped messages with the element's template |21| `nio-with` | Only keep messages from or to this pubkey (`npub` or hex) |22| `nio-ignore` | iodine skips this element and its descendants |2324The inbox sets `nxe-filter` and `nar-ignore` on itself and hands over to xenon, so every `nxe-*` attribute, template field and `nxe:*` event works as usual, on the decrypted message. It reads from your own kind 10050 relays when you have them, else from the closest `nhy-relay` or `nhyCfg.relays`, which are also where messages go when a recipient has no DM relays.2526## Events2728| event | cancel to | detail |29|---|---|---|30| `nio:send` | skip sending | `rumor` (unsigned kind 14, editable), `recipients` |31| `nio:sent` | | `rumor`, `recipients`, `results` per relay |32| `nio:error` | | `error` |33| `nio:message` | skip rendering | `wrap`, `rumor` (on the inbox) |3435## API3637| function | returns |38|---|---|39| `iodine.send(to, content, relays?)` | promise of `results`, after wrapping for `to` and yourself |40| `iodine.wrap(rumor, pubkey)` | promise of a signed kind 1059 gift wrap for `pubkey` |41| `iodine.unwrap(wrap)` | promise of the rumor, or `undefined` if it is not for you or does not check out |42| `iodine.relays(pubkey)` | promise of the DM relays in `pubkey`'s kind 10050 list, `[]` after 3s |4344Requires xenon.js and helium.js, fluorine.js for the gift wrap layer, and neon.js for `npub` recipients.4546## Caveats4748Extensions ask for permission to decrypt, often once per message. Messages are never cached by argon.js, so the inbox decrypts everything again on every load. Metadata privacy comes from the gift wraps: relays see only random keys, randomized times and ciphertext, though each wrap's `p` tag names its recipient and relays still see your IP address. Messages have no delivery receipts or deletion.