Contributing
Thanks for helping! A few ground rules keep Open Lounge Phone dependable enough to hand to a child.
Principles
Section titled “Principles”- The protocol is the contract. The browser emulator, desktop app, and ESP32 firmware all
speak docs/protocol.md. Change
packages/protocoldeliberately: additive fields are fine; anything else bumpsPROTOCOL_VERSION. Runnpm run docs:protocolafter edits. - Default deny. Access-control code must fail closed. New paths that can ring a device go
through
authorizeInbound/authorizeOutboundinpackages/core. - Pure core, thin adapters. Domain logic lives in
packages/corewith no I/O so it runs on Cloudflare Workers, Node, and serves as the reference for firmware. Backends only adapt storage, sockets, and media. - No hosted dependency. Everything must work on a self-hosted box with no third-party accounts. Cloudflare is a first-class deploy target, never a requirement.
Conventions
Section titled “Conventions”These apply to everyone, human or AI.
- npm workspaces, Node 22.18 or newer.
npm installat the root sets up every package; there is no build step for the libraries inpackages/: they exportsrc/index.tsdirectly. - TypeScript:
moduleResolution: Bundler, and relative imports use the.tsextension (import { x } from "./x.ts") so Node runs the sources directly.erasableSyntaxOnlyis on: no enums, namespaces or constructor parameter properties. - Web-standard APIs in shared packages (WebCrypto, fetch, WebSocket) so they run on Node,
Workers and browsers. Node-only code goes in
*/node.tsentry points orapps/server-selfhost. packages/corestays pure: no I/O and no clock reads; passnowin.- Biome for lint and format:
npm run lint, fix withnpx biome check --write .(2 spaces, 100 columns). - Vitest, with tests next to the code as
*.test.ts. - Docs change with the code. The website (openloungephone.app) is built from this repository’s
main: the reference fromdocs/,firmware/andhardware/, and the user guides fromdocs/site/(Markdown with Starlight frontmatter: getting started, the how-tos). A change to a screen, a key or a setting updates its guide in the same commit. - Protocol:
packages/protocol/src/messages.ts(zod) is the source of truth. Messages are flat JSON{ t: "type", id?, ... }of at most 16 KiB. After a change runnpm run docs:protocoland commitdocs/protocol.md; CI fails if it is out of date. Additive optional fields keepPROTOCOL_VERSION; anything else bumps it. - Federation:
docs/federation-spec.mdis the normative/fed/v1spec. Its endpoint, type and stream sections are generated frompackages/federation(the zod schemas andFED_ENDPOINTS): runnpm run docs:federationafter changing them; CI fails if it is out of date. Follow its versioning rules (§9): only additive changes within/fed/v1. - Versions and features: a version number means wire compatibility; features are additive
capabilities within a version. A server advertises both (
.well-knownversions/features,config.server), uses an optional feature with a peer only if the peer lists it, and degrades in plain words otherwise. Receivers ignore unknown fields (no strict schemas on inbound payloads; size limits stay) and answer unknown messages “not supported” without hanging up. New federation features go intoFEATURESinpackages/federation(spec §9.2). - Frozen v1 vectors:
tests/conformance/holds golden vectors for the wire mechanics (RFC 9421 signature bases and signatures,Content-Digest, key-rotation and stream statements, example messages for every endpoint and the device protocol) and a snapshot of every v1 schema. Never edit a v1 vector: a change that needs one is breaking, and breaking changes need v2 (a new federation version orPROTOCOL_VERSION, with new vectors beside the old). The snapshot only grows: after an additive change runnode tests/conformance/generate.ts snapshot, which refuses anything that removes, renames, makes required or narrows a field. A new value in a closed enum is allowed in v1 only behind a feature (list it inGATEDintests/conformance/schemas.ts). - Default deny: every path that can ring a device goes through
authorizeInbound/authorizeOutbound; single-use tokens must be consumed atomically. - Hardware sources are code under
hardware/(see hardware/README.md); generated outputs are not committed:make build/make reviewregenerate them.
Workflow
Section titled “Workflow”npm run checkmust pass (Biome lint/format, TypeScript, Vitest).- The
Interopworkflow runs two servers against each other (self-hosted, and Workers underwrangler dev), and this server against the previous release in both directions (tests/e2e/crossVersion.test.ts); runtests/e2e/cloudflare.test.tslocally when you touch federation. To run the cross-version test locally, check out the pinned release tag somewhere,npm cithere, and setOLP_BASELINE_DIRto it. - Releases of the server are git tags
server-vX.Y.Zon main, whereX.Y.Zis the rootpackage.jsonversion (alsoSERVER_VERSIONinpackages/server-app/src/version.ts; a test keeps them equal, and.well-knownpublishes it assoftware). After tagging, pin the new tag asOLP_BASELINE_REFin.github/workflows/interop.yml, so every later change is tested against it. Firmware (fw-v*) and hardware (hw-v*) have their own tags. - Tests should fail when the behaviour they cover is broken — try reverting your fix and confirm.
- By contributing you agree your work is licensed under AGPL-3.0-or-later (software) or CERN-OHL-S-2.0 (hardware).