From da42b98bb389925c11486bd5aecd08666897aace Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:09:01 -0500 Subject: [PATCH] Add DM privacy and signing adapter documentation --- docs/auth-architecture.md | 1 + docs/dm-privacy-model.md | 70 +++++++++++++++++++++++++++++++ docs/nostr-event-schemas.md | 7 ++++ docs/signing-adapter.md | 82 +++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 docs/dm-privacy-model.md create mode 100644 docs/signing-adapter.md diff --git a/docs/auth-architecture.md b/docs/auth-architecture.md index 33ea4077..a5135dcb 100644 --- a/docs/auth-architecture.md +++ b/docs/auth-architecture.md @@ -135,3 +135,4 @@ attempting privileged calls. - [`docs/nostr-auth.md`](nostr-auth.md) - [`docs/nip46-remote-signers.md`](nip46-remote-signers.md) +- [`docs/signing-adapter.md`](signing-adapter.md) diff --git a/docs/dm-privacy-model.md b/docs/dm-privacy-model.md new file mode 100644 index 00000000..40a9fdbc --- /dev/null +++ b/docs/dm-privacy-model.md @@ -0,0 +1,70 @@ +# Direct message privacy model (NIP-17 / NIP-59 / NIP-44) + +This guide summarizes how bitvid delivers private direct messages, how relay +hints are chosen, and which user controls affect delivery. It is meant to be +read alongside the protocol references in `docs/nips/17.md`, `docs/nips/59.md`, +and `docs/nips/44.md`. + +## Privacy layers: rumor → seal → gift wrap + +When privacy mode is enabled, bitvid uses the NIP-17 pipeline: + +1. **Rumor (kind 14 / 15)**: a plaintext DM or attachment payload is created. +2. **Seal (kind 13)**: the rumor is encrypted with NIP-44 to produce a sealed + event signed by the sender. +3. **Gift wrap (kind 1059)**: the seal is encrypted again with NIP-44 using a + freshly generated ephemeral keypair. The outer wrap is what relays receive. + +This layered approach hides the sender/recipient metadata and message timing +from public relays while still allowing the receiver to unwrap the message. +bitvid randomizes timestamps for seals and wraps to reduce correlation, and +publishes a sender copy of the wrap so the sender can sync their own history. + +## Relay hint selection + +Relay hints decide where gift-wrapped NIP-17 events are published. Selection +uses the following priority order: + +1. **Explicit hints** – If the caller provides `recipientRelayHints` or + `senderRelayHints`, those are used immediately. +2. **Recipient discovery** – Otherwise, the client queries the configured read + relays for the recipient's `kind:10050` relay list and uses any `relay` tags + it finds. +3. **Fallback relays** – If no relay list is found, the client falls back to the + configured write relays (or the default relay list), and flags the selection + as a privacy fallback. + +This logic is shared by the DM service helper `resolveDmRelaySelection()` and +by `NostrClient.sendDirectMessage()` when `useNip17` is enabled. + +### Legacy NIP-04 relay selection + +When privacy mode is disabled, bitvid sends a traditional kind `4` DM: + +- It attempts to load the recipient's NIP-65 relay list (`kind:10002`) from the + configured read relays. +- If no relay list is available, it falls back to the configured write relays + (or the default relay list). + +The final relay set is where the signed NIP-04 ciphertext is published. + +## User controls that affect privacy + +### Privacy toggle (NIP-17 vs NIP-04) + +The DM composer exposes a privacy toggle that switches between NIP-04 and +NIP-17 delivery. When the toggle is enabled, the UI explains that NIP-17 +gift-wraps the message so relays only see the wrapper and relay hints. + +### Relay hint management + +Users can curate their own DM relay hints list in the profile modal. The UI +lets them add and remove WSS relay URLs and publish the resulting list as a +`kind:10050` event. This list is what other clients query during the relay +selection phase above. + +### Read receipts & typing indicators + +Additional DM privacy toggles (read receipts and typing indicators) live in the +same profile modal and persist to local state. Turning them off reduces +metadata leaks by preventing extra ephemeral events from being sent. diff --git a/docs/nostr-event-schemas.md b/docs/nostr-event-schemas.md index 0534ae3c..c92d69da 100644 --- a/docs/nostr-event-schemas.md +++ b/docs/nostr-event-schemas.md @@ -79,6 +79,9 @@ sign/encrypt calls. Call `clearActiveSigner()` on logout if your integration manages session state manually; the built-in logout handler already does this for the default extension flow. +For adapter implementation details and test guidance, see +[`docs/signing-adapter.md`](signing-adapter.md). + ### Accessing raw events `NostrClient` now keeps a lightweight cache of parsed videos alongside a @@ -189,6 +192,10 @@ bitvid now consumes both legacy direct messages (kind `4`) and modern gift-wrap The `isPrivate` flag in Content Schema v3 marks cards that should stay off shared or public grids. Clients should suppress these events for everyone except the owner, even though the payload stays in clear text for compatibility. +For the privacy model, relay hint selection flow, and user controls (privacy +toggle, relay list publishing, and metadata toggles), see +[`docs/dm-privacy-model.md`](dm-privacy-model.md). + If you introduce a new Nostr feature, add its schema to `js/nostrEventSchemas.js` so that the catalogue stays complete and so existing builders inherit the same debugging knobs. diff --git a/docs/signing-adapter.md b/docs/signing-adapter.md new file mode 100644 index 00000000..4e23e8a6 --- /dev/null +++ b/docs/signing-adapter.md @@ -0,0 +1,82 @@ +# SigningAdapter guide + +`SigningAdapter` is the lightweight interface bitvid uses to sign direct-message +payloads and message metadata without depending on a specific auth provider. +The canonical implementations live in `js/auth/signingAdapter.js`, and callers +such as `NostrClient.sendDirectMessage()` and the DM composer accept a +`signingAdapter` option so they can work with NIP-07 extensions, remote signers, +or test doubles. + +## Interface contract + +A `SigningAdapter` is an object with the following async methods: + +- `getPubkey()` → resolves the hex public key that should be used as the DM + author. +- `getDisplayName()` → optional display name string for UI metadata. +- `signEvent(unsignedEvent)` → returns a fully signed Nostr event object. +- `signMessage(message)` → returns a signature for an arbitrary string. + +`signEvent()` is required for DM publishing; `signMessage()` is optional but is +used by the DM composer to attach a signature to the outgoing payload metadata. + +## Implementing a new adapter + +1. **Normalize the pubkey**: resolve and return a lowercased hex pubkey from + `getPubkey()`. The NIP-07 adapter does this by delegating to + `window.nostr.getPublicKey()` and normalizing the result. +2. **Provide signing primitives**: + - `signEvent()` must return an event with `id` and `sig` populated. + - `signMessage()` should return a stable signature string for the message + body when you need message-level signatures (for example, the DM composer + includes this for UI payloads). +3. **Expose a type label**: include a `type` field (for example `nip07`, `dev`, + or `test`) to make debugging easier. + +Reference implementations: + +- `createNip07SigningAdapter({ extension })` uses a browser extension signer. +- `createEphemeralDevSigningAdapter()` generates a local, throwaway keypair for + development flows. +- `createTestSigningAdapter()` returns deterministic signatures for tests. + +## Testing a SigningAdapter + +### Unit tests + +For Node-based tests, use `createTestSigningAdapter()` so signatures are stable +and deterministic. You can pass it directly to `NostrClient.sendDirectMessage()` +through the `options.signingAdapter` field to avoid relying on active signers: + +```js +import { createTestSigningAdapter } from "../js/auth/signingAdapter.js"; +import { nostrClient } from "../js/nostrClientFacade.js"; + +const adapter = createTestSigningAdapter({ + pubkey: "f".repeat(64), +}); + +const result = await nostrClient.sendDirectMessage( + "npub...", + "hello", + null, + { signingAdapter: adapter } +); +``` + +Assertions can then verify that `result.ok` is `true` and that the signed event +was built with the adapter's pubkey and signatures. + +### Manual smoke checks + +For browser-only checks, wire the adapter into the DM composer by passing it in +as the `signingAdapter` prop. The composer will call `getPubkey()`, +`getDisplayName()`, and `signMessage()` while building the outgoing payload +object so you can inspect the resulting metadata in your handler. + +## Usage reminders + +- When the signer does **not** implement NIP-44 encryption, NIP-17 sends are + blocked; keep the privacy toggle disabled until encryption is available. +- Always surface errors from `getPubkey()` or `signMessage()` to the caller so + the UI can display a meaningful failure state.