# Contributing
Bug reports and pull requests are welcome. Security reports are not — those go
through [SECURITY.md](SECURITY.md), privately.
```bash
npm install
npm test # unit suite, offline, about 2s
npm run typecheck # two tsc runs; both must pass, see below
npm start # build, then serve the site on :4173
```
[`docs/protocol.md`](docs/protocol.md) and [`docs/threat-model.md`](docs/threat-model.md)
are the long-form explanation of the protocol, what it protects, and why the
transport seam is where it is. Read them before changing anything under
`src/core/` or `src/transport/`. `CLAUDE.md` is the
same territory written as a list of things that have already broken once; it is
addressed to an agent, but the invariants in it are real and apply to everyone.
## The rules that are not negotiable
Each of these exists because it was broken before, usually invisibly.
**Both typecheck passes, always.** `src/core/` and `src/transport/` are checked
once with Node's globals and once without them. That double pass is the only
thing making "isomorphic" a property of the build rather than a claim in a
comment — a stray `Buffer.from` in core passes one and fails the other. Run
`npm run typecheck`, never a single `tsc`.
**Neither safety gesture may be softened.** The sender confirms the SAS before
a manifest goes out, because the manifest alone leaks the filename and size.
The receiver's Accept click is also the user activation that lets
`showSaveFilePicker` open, so nothing may be `await`ed ahead of `createSink` in
that handler. `--yes` skips the accept prompt and must never skip the SAS.
**The code comes from the URL fragment, never the query string.** A fragment is
the one part of a URL that is never sent to a server, which is the entire reason
a link may carry a key at all.
**No new runtime dependencies.** Four, plus one optional. Every dependency is
one more thing between `npm install` and a working transfer, which is why the
virtual DOM in `src/web/vdom.js` is hand-rolled rather than being preact.
devDependencies are a different question and a much easier one.
## House style
Two-space indent, no semicolons, single quotes, JSDoc types throughout —
`checkJs` over plain ES modules, with no build step for the published code.
Comments are long, and they explain **why**. The useful ones name the
alternative that was rejected and the failure it would have caused; a comment
restating what the line does is worse than no comment. Read the file you are
editing and match its register before writing.
Commit subjects are `type(scope): lowercase clause, and a second clause`.
Bodies are prose paragraphs rather than bullet lists, and say what was
reasoned, measured, or ruled out.
## Tests
`npm test` is offline and must stay that way — it is what CI runs.
The two end-to-end suites are not in CI and are expected to be run by hand:
```bash
npm run test:e2e # two real browsers over public relays
npm run test:e2e:interop # two Node processes driving the CLI
```
They depend on public Nostr relays, so they fail for reasons that have nothing
to do with your change — a relay being unreachable is weather. A red tick for
weather teaches everyone to ignore red ticks, which is why they are kept out.
A failure that is not a relay is worth chasing: the interop suite used to fail
on most runs for a real reason, and the note at the foot of `CLAUDE.md` records
what it was.
Why the interop suite spawns two processes
Trystero computes `selfId` once per module instance, so two rooms sharing a
process also share an identity: each sees the other's announcement carrying its
own id, discards it as itself, and they wait for each other until the timeout.
That is a property of Trystero rather than a bug here, but it is invisible
until you try it.
Type checking without a build step
`tsc --noEmit` with `checkJs` over the JSDoc. The published sources are plain ES
modules, unbundled and untranspiled; only the site's browser bundle is built.
There are three configs because there are three runtimes, and `src/core/` and
`src/transport/` deliberately appear in two of them. Being checked once without
Node globals and once with them is what makes "isomorphic" a property the build
enforces rather than a claim in a comment.
## Changing the web UI
`patch()` owns every child of the shadow root, so anything the view does not
describe is removed as stale, and the test suite reads text and visibility
rather than paint. Look at every screen when you change the view:
`node scripts/check-layout.mjs --shots` walks all of them, in Chromium and
Firefox, at four viewports, with a mouse and with touch, and fails on a page
that scrolls or a button off screen. Its assertions only see layouts that are
illegal, never ones that are legal and wrong, so read the pictures too. A whole
component reverting to unstyled browser defaults is invisible to a green test
run, and has happened. (`make-screenshots.mjs` renders the README's three
pictures and nothing else.)
All user-facing copy lives in `src/web/view.js`. If you are adding a string,
that is where it goes.
## Regenerating the images
Five hand-run scripts, none of them wired into `npm run build` — that runs in
CI and in `prepublishOnly`, neither of which should download a browser:
```bash
node scripts/make-og.mjs # site/og.png, the social card
node scripts/make-icon.mjs # the app mark and site/favicon.png
node scripts/make-diagrams.mjs # docs/diagrams/*.png, from the .mmd sources
npm run build && node scripts/make-screenshots.mjs # docs/screenshots/*.png, for the README
node scripts/make-store-screenshots.mjs # the store listing's phone screenshots
```
Their output is committed. Run the relevant one when the palette, the copy on
the card, or a diagram source changes.
## Where things are
```
src/index.js the isomorphic entry -- no DOM, no fs
src/core/secret.js QR secret, HKDF derivations
src/core/session.js ephemeral ECDH, directional keys, SAS
src/core/frame.js per-chunk AEAD, nonce construction
src/core/control.js ordered, sealed control messages
src/core/sender.js chunking, backpressure, accept handshake
src/core/receiver.js demux, verification, sink management
src/core/source.js the FileSource contract
src/core/beam.js beam's fountain code and framing
src/transport/room.js Trystero pairing, relay list, ICE
src/transport/channel.js the transport seam, on its own
src/web/view.js every screen, and all user-facing copy
src/web/element.js the qr-drop element: sessions, teardown, native events
src/web/vdom.js h() and patch(), hand-rolled
src/web/styles.js the component's CSS; tokens.js holds the design tokens
src/web/beam.js beam's live halves: the canvas player, the camera collector
src/web/sink.js File System Access, with a Blob fallback
src/node/ fs sink, fs source, terminal QR, WebRTC polyfill
src/cli.js the qrdrop command
site/index.html the page; CSP placeholder filled in at build
scripts/build-site.mjs esbuild, and the generated CSP
scripts/check-layout.mjs every screen, both engines, every viewport
types/qrdrop.d.ts the Channel contract, and shared types
app/src/ the Tauri shell's JS: native sink, deep links
app/src-tauri/ the Rust crate, and gen/ (committed; see CLAUDE.md)
docs/ protocol, threat model, beam, app, hosting
```