Add DM privacy and signing adapter documentation

This commit is contained in:
thePR0M3TH3AN committed 2026-01-19 14:09:01 -05:00
1 parent 9606c8ef0d
commit da42b98bb3
4 files changed
+160

No files matched your search

+1
View File
@@ -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)
+70
View File
@@ -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.
+7
View File
@@ -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.
+82
View File
@@ -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.