mirror of
https://github.com/layer-systems/website.git
synced 2026-09-12 13:43:01 +02:00
Add NIP-17 DM docs and fix test imports
Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
d82adbfe0a
commit
087a2df3a3
22
NIP.md
22
NIP.md
@@ -18,6 +18,23 @@ protocols:
|
||||
NIP-86 authorization (with the NIP-86-required `payload` tag) and for Blossom uploads.
|
||||
- [NIP-11: Relay Information Document](https://github.com/nostr-protocol/nips/blob/master/11.md)
|
||||
— read at connect time to show relay identity in Relay Admin.
|
||||
- [NIP-17: Private Direct Messages](https://github.com/nostr-protocol/nips/blob/master/17.md)
|
||||
(draft, optional) — the Messages app (`src/apps/messages/`, protocol layer in
|
||||
`src/lib/dm.ts`). Chat messages are unsigned kind `14` rumors, sealed by the sender
|
||||
into kind `13` with [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md)
|
||||
and gift-wrapped (kind `1059`, [NIP-59](https://github.com/nostr-protocol/nips/blob/master/59.md))
|
||||
with a throwaway key — one wrap for the recipient and one sender copy, so history is
|
||||
recoverable on any client holding the keys. Seal/wrap timestamps are randomized up to
|
||||
two days into the past. The recipient copy is published to the relays from the
|
||||
recipient's kind `10050` inbox list; a sender without that list gets the message on
|
||||
their own relays plus a warning that the recipient may not be reachable. The
|
||||
seal↔rumor pubkey check mandated by the NIP is enforced on unwrap, and rumors whose
|
||||
room (author + `p` tags) does not include the viewer are dropped.
|
||||
- [NIP-04: Encrypted Direct Messages](https://github.com/nostr-protocol/nips/blob/master/04.md)
|
||||
(deprecated) — kind `4` legacy DMs are read for backwards compatibility and used for
|
||||
sending only when the signer offers no NIP-44 encryption; the UI labels that fallback
|
||||
visibly (weaker crypto, public sender/recipient metadata) and never downgrades a
|
||||
NIP-44-capable signer to it.
|
||||
|
||||
## Adopted third-party kinds
|
||||
|
||||
@@ -25,5 +42,6 @@ protocols:
|
||||
[Grimoire](https://github.com/purrgrammer/grimoire) client, adopted as-is for interop.
|
||||
See `docs/apps.md` ("Spells are a third-party kind") and `src/hooks/useSpells.ts`.
|
||||
|
||||
Anything else (kinds 0, 1, 3, 5, 6, 16, 9802, 10002, 10003, 22242, 30023, 30311, 31337,
|
||||
39701, …) follows the official NIPs as implemented in `src/hooks/` and `src/lib/`.
|
||||
Anything else (kinds 0, 1, 3, 4, 5, 6, 13, 14, 16, 9802, 10002, 10003, 10050, 22242,
|
||||
30023, 30311, 31337, 39701, …) follows the official NIPs as implemented in `src/hooks/`
|
||||
and `src/lib/`.
|
||||
|
||||
32
docs/apps.md
32
docs/apps.md
@@ -89,7 +89,7 @@ export default function ExampleApp({ setTitle }: AppProps) {
|
||||
}
|
||||
```
|
||||
|
||||
## The eleven apps
|
||||
## The twelve apps
|
||||
|
||||
| App | `id` | Params | Notes |
|
||||
|---|---|---|---|
|
||||
@@ -100,6 +100,7 @@ export default function ExampleApp({ setTitle }: AppProps) {
|
||||
| Bookmarks | `bookmarks` | — | NIP-51 kind 10003 list — bookmarked notes and articles |
|
||||
| Web Bookmarks | `web-bookmarks` | — | NIP-B0 kind 39701 — one addressable event per saved URL |
|
||||
| Live | `live` | `pubkey?`, `identifier?` | NIP-53 kind 30311 live events + kind 1311 chat |
|
||||
| Messages | `messages` | `peer?` | NIP-17 gift-wrapped DMs (kinds 14/13/1059), legacy NIP-04 fallback |
|
||||
| Spells | `spells` | `id?` | Saved/shareable REQ filters — kind 777, a third-party draft NIP |
|
||||
| Relays | `relays` | — | Connection state, subscription count, measured latency |
|
||||
| Relay Admin | `relay-admin` | `relay?` | NIP-86 management console for relays you operate |
|
||||
@@ -168,6 +169,35 @@ whether the relay offers a reverse operation. Every operation is written to a pe
|
||||
audit log (method, target, result, operator-safe error) that never contains the
|
||||
authorization header or any key material; signing secrets stay inside the user's signer.
|
||||
|
||||
### Messages keep plaintext and metadata off the wire — and out of storage
|
||||
|
||||
The Messages app implements NIP-17 (`src/lib/dm.ts` + `src/hooks/useDirectMessages.ts`):
|
||||
plaintext only ever exists inside an unsigned kind 14 rumor, which is NIP-44-sealed by
|
||||
the sender and gift-wrapped with a throwaway key. One wrap goes to the recipient's kind
|
||||
10050 inbox relays, one sender copy to the sender's own relays — the sender copy is what
|
||||
makes history recoverable on another device, so it is always written. Decryption happens
|
||||
on demand and plaintexts live in memory only; nothing message-shaped is persisted to
|
||||
localStorage. The conversation list shows no received plaintext either ("New message",
|
||||
not a snippet), because a lock-screen-glance at the window title bar or an over-the-shoulder
|
||||
view should not leak content.
|
||||
|
||||
Two compatibility decisions are deliberately visible rather than silent:
|
||||
|
||||
- **Legacy NIP-04 (kind 4)** is read for old messages and used for sending only when the
|
||||
signer has no NIP-44 (some extensions). A banner and a "legacy" label on the affected
|
||||
bubbles say exactly what that costs: sender/recipient metadata is public. A
|
||||
NIP-44-capable signer is never downgraded.
|
||||
- **No kind 10050 inbox list** means NIP-17 cannot say where the recipient receives DMs.
|
||||
The message is still stored (on the sender's relays, so the sender keeps their
|
||||
history) but the send reports a `DmRecipientUnreachableError` with recovery guidance
|
||||
instead of claiming delivery. Bubble states are honest for the same reason:
|
||||
pending → sent means "a relay accepted the gift wrap", never "the recipient read it".
|
||||
|
||||
Hiding a conversation publishes a NIP-09 kind 5 deletion request for the sender's own
|
||||
copies (relays may refuse; nothing recalls a message the recipient already fetched) and
|
||||
hides the peer locally. Received gift wraps are signed by throwaway keys and cannot be
|
||||
deletion-requested by design.
|
||||
|
||||
### Relay latency is a real round trip
|
||||
|
||||
A WebSocket gives the browser no ping, so the Relays app times an actual `REQ`/`EOSE`
|
||||
|
||||
@@ -5,9 +5,9 @@ import { NSecSigner, type NostrEvent } from '@nostrify/nostrify';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
|
||||
import { TestApp } from '@/test/TestApp';
|
||||
import { buildGiftWraps, DM_GIFT_WRAP_KIND, LEGACY_DM_KIND } from '@/lib/dm';
|
||||
import { buildGiftWraps, DM_GIFT_WRAP_KIND, LEGACY_DM_KIND, type DmMessage } from '@/lib/dm';
|
||||
import { useLoginActions } from './useLoginActions';
|
||||
import { groupIntoConversations, useDmConversations, type DmMessage } from './useDirectMessages';
|
||||
import { groupIntoConversations, useDmConversations } from './useDirectMessages';
|
||||
|
||||
const aliceSecret = generateSecretKey();
|
||||
const alice = getPublicKey(aliceSecret);
|
||||
|
||||
@@ -81,10 +81,11 @@ describe('parseRecipient', () => {
|
||||
});
|
||||
|
||||
it('rejects note/nevent/naddr identifiers — they are not people', () => {
|
||||
const note = nip19.noteEncode('a'.repeat(64));
|
||||
expect(parseRecipient(note).type).toBe('invalid');
|
||||
const nevent = nip19.neventEncode({ id: 'a'.repeat(64) });
|
||||
expect(parseRecipient(nevent.type ? nevent : '').type).toBe('invalid');
|
||||
expect(parseRecipient(nip19.noteEncode('a'.repeat(64))).type).toBe('invalid');
|
||||
expect(parseRecipient(nip19.neventEncode({ id: 'a'.repeat(64) })).type).toBe('invalid');
|
||||
expect(
|
||||
parseRecipient(nip19.naddrEncode({ pubkey: alice, kind: 30023, identifier: 'x' })).type,
|
||||
).toBe('invalid');
|
||||
});
|
||||
|
||||
it('rejects an nsec — a secret must never be typed into a recipient field', () => {
|
||||
|
||||
Reference in New Issue
Block a user