PRD: v1 reference implementation #1

Open
opened 2026-05-06 21:12:43 +02:00 by arne · 0 comments
Owner

Problem Statement

The wire protocol and design rationale are documented in SPEC.md, CONTEXT.md, README.md, and CLAUDE.md, but no implementation exists. Users cannot send or receive messages; the protocol is paper, not running software. Anyone wanting to integrate URL-to-URL messaging with cryptographic origin authentication has nothing to install, run, or import. There are also no test vectors yet, so the spec's normative conformance contract — declared in SPEC.md §14 — is not enforceable.

Solution

Build the v1 reference implementation as a Go single binary with a public package for embedding (pkg/msg), an HTTPS daemon (msg serve), a bubbletea TUI (msg) sharing SQLite state with the daemon, and a normative test-vector suite under testdata/vectors/. Ship example clients in Python, JavaScript, and Bash to prove the spec is implementable in <100 lines elsewhere.

After this PRD is delivered, a user can:

  • Run msg init to set up a URL identity and TLS configuration.
  • Run msg serve to start receiving at their URL.
  • Run msg to read inbox and compose messages from a TUI.
  • Pipe a payload to msg send <url> for shell automation.
  • Implement the protocol in any language using SPEC.md and the test vectors.
  • Embed the protocol in a Go program via pkg/msg.

User Stories

Operator / first-time setup

  1. As a first-time user, I want to set up a working URL identity with msg init, so I don't have to manually generate keys, write a config, or pick a TLS mode.
  2. As a first-time user, I want auto-TLS via Let's Encrypt to be the default, so I get a working HTTPS endpoint without manual cert management.
  3. As an operator behind a reverse proxy, I want to disable built-in TLS and listen on loopback, so I can let Caddy or nginx terminate.
  4. As an operator with existing certs, I want to point the daemon at PEM files, so I can keep my existing cert workflow.
  5. As an operator, I want the daemon to verify on startup that its configured URL matches what it can serve, so misconfiguration fails loudly.
  6. As an operator, I want sensible XDG-compliant default paths for config, db, and keys, so I don't have to figure out where things go.
  7. As an operator, I want flag, env var, and config-file overrides in a clear precedence order (flag > env > config > default), so I can configure the daemon however my deploy expects.

Sender

  1. As a sender, I want to compose a message in the TUI and have it cryptographically signed and delivered without thinking about keys, so messaging feels as simple as email.
  2. As a sender, I want to see the live status of my outgoing message (queued / delivered / receipt-confirmed / failed), so I know whether the recipient got it.
  3. As a sender, I want to optionally request a signed receipt, so I can prove delivery later.
  4. As a sender, I want failed messages to retry automatically with exponential backoff, so a transient blip doesn't drop my message.
  5. As a sender, I want to see the specific error code on failure (bad-signature, wrong-recipient, stale-timestamp, unknown-key, duplicate-id, malformed-envelope, unsupported-version), so I can debug.
  6. As a sender, I want to retry a failed message manually with one keystroke in the TUI, so I don't have to recompose.
  7. As a sender, I want to send to a URL via a shell pipeline (echo … | msg send <url>), so I can automate notifications from scripts.
  8. As a sender, I want to reply to a message in the TUI and have inReplyTo set automatically, so threads work without ceremony.
  9. As a sender, I want my outbound to be queued before delivery so I can compose offline, and the daemon flushes when reachability returns.

Receiver

  1. As a receiver, I want messages from any URL whose signature verifies to arrive in my inbox automatically, so I don't have to whitelist senders.
  2. As a receiver, I want every message verified for origin authenticity per the §7 verification procedure before it is persisted, so I can trust who sent what.
  3. As a receiver, I want messages that fail verification rejected with the specific stable error code from SPEC.md §9, so senders can debug.
  4. As a receiver, I want my daemon to keep receiving even when the TUI is closed, so I am reachable while away from my terminal.
  5. As a receiver, I want the daemon to re-fetch the sender's actor doc once when an unknown keyId arrives, so key rotation works transparently.
  6. As a receiver, I want every accepted message durably persisted before the 2xx response is returned, so my acknowledgments mean something across restarts.
  7. As a receiver, I want to optionally restrict inbound to URLs in my contacts table (--acl=contacts), so I can run a stricter policy.
  8. As a receiver, I want a per-sender rate limit by default, so a misbehaving peer can't flood my inbox.
  9. As a receiver, I want a (sender, id) duplicate to be rejected as duplicate-id, so replays are cleanly visible in error responses.

TUI

  1. As a TUI user, I want a two-pane contacts/thread layout, so I can navigate conversations with one keystroke per move.
  2. As a TUI user, I want vim-style and arrow-key navigation, so I can use it without learning a new mental model.
  3. As a TUI user, I want full-text search across all my messages, so I can find a phrase from last month.
  4. As a TUI user, I want to add a contact by pasting a URL and confirming with the actor's display name, so adding people is one step.
  5. As a TUI user, I want clear status indicators for queued/delivered/receipt-confirmed/failed messages, so I can scan the inbox at a glance.
  6. As a TUI user, I want a help screen, so I don't have to remember key bindings.
  7. As a TUI user, I want unread messages visually distinguished, so I notice new arrivals.
  8. As a TUI user, I want the TUI to surface the name from a peer's actor doc as a label alongside their URL, never as a substitute for the URL, so I can't be fooled by a spoofed display name.

Identity and key management

  1. As a user, I want my private keys stored at mode 0600 in my config dir, so they're protected by filesystem permissions.
  2. As a user, I want to rotate my key with one command (msg keys rotate), so I can recover from suspected compromise.
  3. As a user, I want the old key to remain valid in my actor doc during a configurable retain window, so in-flight messages aren't lost during rotation.
  4. As a user, I want to prune retired keys past their retain window with one command (msg keys prune), so my actor doc stays clean.
  5. As a user, I want the daemon to generate a fresh keypair on first run if none exists, so I don't have to remember an init step.

Embedder / protocol implementer

  1. As a Go developer, I want a public package (pkg/msg) for embedding sign / verify / actor logic, so I can integrate URL-to-URL messaging into my Go service without running a sidecar.
  2. As an implementer of another language, I want a 1-page wire spec and a directory of normative test vectors, so I can build a conformant client in under 100 lines.
  3. As an implementer, I want every observable wire behavior captured by a vector, so my tests can prove parity with the reference implementation.
  4. As a web developer, I want to send messages from my web app by signing and POSTing directly per the spec, so I don't need a Go sidecar.
  5. As an implementer of a third-party client, I want example clients in Python, JavaScript, and Bash that round-trip with the Go binary, so I have working reference code in a familiar language.

Maintainer

  1. As a contributor, I want CLAUDE.md's spec-first discipline (vectors first, code second) enforced in PR review, so wire behavior doesn't quietly diverge.
  2. As a contributor, I want CONTEXT.md to surface the design rationale for non-obvious choices, so I can make informed changes.
  3. As a contributor, I want every vector exercised in go test ./..., so a regression in protocol behavior fails CI.

Operations

  1. As an operator, I want structured logging (slog) by default, so I can debug without parsing free-form output.
  2. As an operator, I want signature failures, retries, and rate-limit hits logged at appropriate levels, so I can spot trouble.
  3. As an operator, I want message bodies excluded from logs by default, so private content does not end up in log aggregators.

Implementation Decisions

Architecture

  • Go single binary, module rooted at code.bas.es/arne/msg. Public package at pkg/msg; daemon, TUI, and store internals under internal/.
  • SQLite via modernc.org/sqlite (pure Go) — keeps the binary cgo-free for clean cross-compilation.
  • Two binary modes share state via SQLite only: msg serve (daemon) and msg (TUI). No IPC, no Unix sockets, no localhost HTTP between them.
  • TUI watches for new rows by polling SQLite at ~250ms; daemon polls the outbox at the same cadence.

Deep modules

These encapsulate behavior behind a small, stable interface and carry the protocol's logic. They are individually unit-testable and composable.

  • envelope — envelope and actor-doc schema, JSON encode/decode, structural validation. Pure.
  • signing — Ed25519 sign and verify over arbitrary byte sequences, plus key encoding. Pure crypto.
  • verifier — composes envelope + signing + actor cache + clock + dedup-set into the full SPEC.md §7 verification procedure. Returns a structured outcome with stable SPEC.md §9 error codes. Single entry point: takes raw body bytes, signature header, claimed recipient URL, and dependencies. This is the protocol's central rejection-logic module — every vector tests it.
  • sender — symmetric: builds an envelope, signs the raw body bytes, POSTs to the recipient, parses the response (status, error code, optional receipt). Receipt verification reuses the same verifier path.
  • actor_cache — TTL-based cache of actor docs keyed by URL, encapsulating the SPEC.md §7.3 "refetch once on unknown keyId" rule. Hides fetch + cache + refetch policy.
  • keys — load / save keys.json, generate, rotate (add new + retire old with retain window), prune. File-mode 0600 enforcement.
  • outbox — send-loop state machine: pending → sending → delivered | failed-permanent | failed-pending-user. Backoff schedule 5s, 30s, 5m, 30m. Hard cap: 4 attempts or 24h elapsed.

Shallow / glue modules

  • store — SQLite migrations and typed DAO methods over the schema (unified messages table with direction, plus actor_cache, contacts, receipts, FTS5 virtual table messages_fts).
  • httpnet/http handler wiring the verifier into request handling; serves the actor doc on GET <url>; handles per-sender rate limiting and the --acl=contacts mode.
  • tls — config helper for the three TLS modes (auto via certmagic / autocert; cert+key files; HTTP loopback for reverse-proxy fronting).
  • config — TOML loading at ${XDG_CONFIG_HOME:-~/.config}/msg/config.toml, with override stack flag > env (MSG_*) > config > default.
  • tui — bubbletea views, lipgloss styles, key bindings; address-book add flow; r for retry; / for FTS search; ? for help.
  • cli — cobra command tree for serve, init, send, inbox list, keys init|rotate|prune, version.
  • examples — Python, JavaScript, and Bash reference clients exercising the same vectors against the Go binary.

Wire protocol behavior

  • All wire-observable behavior is captured by vectors under testdata/vectors/. Vectors are normative: a behavior change updates the vector first, the code second.
  • Receiver returns 2xx only after the verified message is durably committed.
  • Receipts are opt-in per-message via Msg-Receipt: required. Receivers may decline (return 204); senders tolerate the absence.
  • Ed25519 only in v1; the algorithm field on each key in the actor doc is the source of truth for what verifies a given keyId.
  • Msg-Signature carries plain padded-base64 (RFC 4648 §4), no algorithm prefix.
  • Error code strings (bad-signature, stale-timestamp, unknown-key, duplicate-id, wrong-recipient, malformed-envelope, unsupported-version) are stable. Renaming is breaking.

Defaults and policy

  • Timestamp skew window: ±5 minutes. Configurable.
  • Per-sender rate limit: 60 messages / minute. Configurable.
  • Key retain window on rotation: 30 days.
  • Outbound retry: 5s, 30s, 5m, 30m; hard cap 4 attempts or 24h.
  • Listen address default :443 when TLS mode is auto or files; 127.0.0.1:8088 when none.

Storage schema (high-level)

  • messages — unified inbox + outbox. direction column. Outbound rows additionally carry send_status, attempts, next_attempt_at, error_code. Raw signed envelope bytes stored on every row for re-verification, audit-grade export, and migration.
  • actor_cache — URL → cached actor doc + fetched_at.
  • contacts — URL → user-defined display_name, pinned status.
  • receipts — signed delivery acks for outbound messages, FK to messages.rowid.
  • messages_fts — FTS5 virtual table over payload and peer_url for TUI search.

Logging

  • slog JSON output by default. Levels: error, warn, info, debug.
  • Message payloads excluded from logs at info level. Debug level may include them.

Testing Decisions

A good test asserts external behavior at the public boundary, not implementation details. For msg, this means:

  • Vector tests against verifier and sender. Every file under testdata/vectors/ is a black-box case (inputs + expected status code, error code, or expected signature bytes). Vectors are the conformance contract; a vector change is a behavior change.
  • Unit tests on the deep modules: envelope, signing, actor_cache, keys, outbox. Each has a small, pure-ish interface that is comfortable to test in isolation.
  • One end-to-end integration test that boots two daemons in-process, has one POST to the other, verifies receipt round-trip, and exercises persistence, the real net/http stack, and signature verification end-to-end.
  • Not unit-tested in v1: tui, cli, tls, config. Covered transitively by the integration test or are too presentation-heavy to test in isolation cheaply.
  • Examples directory CI: each example client (Python, JavaScript, Bash) is exercised against the Go binary using the same vectors. Cross-language interop is enforced in CI, not assumed.

Prior art: this is a greenfield Go repo; no in-repo prior art exists. Reference patterns are the Go standard library's *_test.go table-driven test convention, the testdata/ directory convention, and black-box testing via pkg_test packages.

Out of Scope

The following are explicitly out of scope for v1, per CONTEXT.md:

  • Group messaging / multi-recipient envelopes. Future direction is boards-as-URLs (a board is just another URL whose daemon re-broadcasts received posts to subscriber URLs) — a deployment pattern, not a protocol extension.
  • End-to-end payload encryption. Origin authentication only.
  • NAT traversal, peer discovery, overlay networks.
  • Cross-device identity (running the same URL from multiple machines is possible by copying keys.json but is not designed-for).
  • OS keychain integration; encryption-at-rest for keys. v1 is plaintext at 0600; threat model documented in CONTEXT.md.
  • OS notifications (no --notify in v1).
  • Localhost submission API for non-Go web apps. Web apps integrate by signing and POSTing per the spec.
  • Algorithms other than Ed25519.
  • Telemetry, metrics endpoints, health endpoints.

Further Notes

The order matters for keeping spec-first discipline honest:

  1. envelope + signing + actor-doc parsing (pure modules, zero I/O).
  2. testdata/vectors/ — start with sign-and-verify, tampered-body, and recipient-mismatch cases.
  3. verifier + sender (compose the above; vectors run green).
  4. actor_cache, store, http, outbox (the daemon parts).
  5. examples/pythondo this before the TUI. If a Python client takes more than ~100 lines, the spec is too complex; fix the spec before building UI on top of it.
  6. tls + config + cli (operator surface).
  7. tui (presentation).
  8. examples/javascript + examples/bash (round out cross-language coverage).

License

Deferred to maintainer; README placeholder.

## Problem Statement The wire protocol and design rationale are documented in `SPEC.md`, `CONTEXT.md`, `README.md`, and `CLAUDE.md`, but no implementation exists. Users cannot send or receive messages; the protocol is paper, not running software. Anyone wanting to integrate URL-to-URL messaging with cryptographic origin authentication has nothing to install, run, or import. There are also no test vectors yet, so the spec's normative conformance contract — declared in `SPEC.md` §14 — is not enforceable. ## Solution Build the v1 reference implementation as a Go single binary with a public package for embedding (`pkg/msg`), an HTTPS daemon (`msg serve`), a bubbletea TUI (`msg`) sharing SQLite state with the daemon, and a normative test-vector suite under `testdata/vectors/`. Ship example clients in Python, JavaScript, and Bash to prove the spec is implementable in <100 lines elsewhere. After this PRD is delivered, a user can: - Run `msg init` to set up a URL identity and TLS configuration. - Run `msg serve` to start receiving at their URL. - Run `msg` to read inbox and compose messages from a TUI. - Pipe a payload to `msg send <url>` for shell automation. - Implement the protocol in any language using `SPEC.md` and the test vectors. - Embed the protocol in a Go program via `pkg/msg`. ## User Stories ### Operator / first-time setup 1. As a first-time user, I want to set up a working URL identity with `msg init`, so I don't have to manually generate keys, write a config, or pick a TLS mode. 2. As a first-time user, I want auto-TLS via Let's Encrypt to be the default, so I get a working HTTPS endpoint without manual cert management. 3. As an operator behind a reverse proxy, I want to disable built-in TLS and listen on loopback, so I can let Caddy or nginx terminate. 4. As an operator with existing certs, I want to point the daemon at PEM files, so I can keep my existing cert workflow. 5. As an operator, I want the daemon to verify on startup that its configured URL matches what it can serve, so misconfiguration fails loudly. 6. As an operator, I want sensible XDG-compliant default paths for config, db, and keys, so I don't have to figure out where things go. 7. As an operator, I want flag, env var, and config-file overrides in a clear precedence order (flag > env > config > default), so I can configure the daemon however my deploy expects. ### Sender 8. As a sender, I want to compose a message in the TUI and have it cryptographically signed and delivered without thinking about keys, so messaging feels as simple as email. 9. As a sender, I want to see the live status of my outgoing message (queued / delivered / receipt-confirmed / failed), so I know whether the recipient got it. 10. As a sender, I want to optionally request a signed receipt, so I can prove delivery later. 11. As a sender, I want failed messages to retry automatically with exponential backoff, so a transient blip doesn't drop my message. 12. As a sender, I want to see the specific error code on failure (`bad-signature`, `wrong-recipient`, `stale-timestamp`, `unknown-key`, `duplicate-id`, `malformed-envelope`, `unsupported-version`), so I can debug. 13. As a sender, I want to retry a failed message manually with one keystroke in the TUI, so I don't have to recompose. 14. As a sender, I want to send to a URL via a shell pipeline (`echo … | msg send <url>`), so I can automate notifications from scripts. 15. As a sender, I want to reply to a message in the TUI and have `inReplyTo` set automatically, so threads work without ceremony. 16. As a sender, I want my outbound to be queued before delivery so I can compose offline, and the daemon flushes when reachability returns. ### Receiver 17. As a receiver, I want messages from any URL whose signature verifies to arrive in my inbox automatically, so I don't have to whitelist senders. 18. As a receiver, I want every message verified for origin authenticity per the §7 verification procedure before it is persisted, so I can trust who sent what. 19. As a receiver, I want messages that fail verification rejected with the specific stable error code from `SPEC.md` §9, so senders can debug. 20. As a receiver, I want my daemon to keep receiving even when the TUI is closed, so I am reachable while away from my terminal. 21. As a receiver, I want the daemon to re-fetch the sender's actor doc once when an unknown `keyId` arrives, so key rotation works transparently. 22. As a receiver, I want every accepted message durably persisted before the 2xx response is returned, so my acknowledgments mean something across restarts. 23. As a receiver, I want to optionally restrict inbound to URLs in my contacts table (`--acl=contacts`), so I can run a stricter policy. 24. As a receiver, I want a per-sender rate limit by default, so a misbehaving peer can't flood my inbox. 25. As a receiver, I want a `(sender, id)` duplicate to be rejected as `duplicate-id`, so replays are cleanly visible in error responses. ### TUI 26. As a TUI user, I want a two-pane contacts/thread layout, so I can navigate conversations with one keystroke per move. 27. As a TUI user, I want vim-style and arrow-key navigation, so I can use it without learning a new mental model. 28. As a TUI user, I want full-text search across all my messages, so I can find a phrase from last month. 29. As a TUI user, I want to add a contact by pasting a URL and confirming with the actor's display name, so adding people is one step. 30. As a TUI user, I want clear status indicators for queued/delivered/receipt-confirmed/failed messages, so I can scan the inbox at a glance. 31. As a TUI user, I want a help screen, so I don't have to remember key bindings. 32. As a TUI user, I want unread messages visually distinguished, so I notice new arrivals. 33. As a TUI user, I want the TUI to surface the `name` from a peer's actor doc as a label alongside their URL, never as a substitute for the URL, so I can't be fooled by a spoofed display name. ### Identity and key management 34. As a user, I want my private keys stored at mode `0600` in my config dir, so they're protected by filesystem permissions. 35. As a user, I want to rotate my key with one command (`msg keys rotate`), so I can recover from suspected compromise. 36. As a user, I want the old key to remain valid in my actor doc during a configurable retain window, so in-flight messages aren't lost during rotation. 37. As a user, I want to prune retired keys past their retain window with one command (`msg keys prune`), so my actor doc stays clean. 38. As a user, I want the daemon to generate a fresh keypair on first run if none exists, so I don't have to remember an init step. ### Embedder / protocol implementer 39. As a Go developer, I want a public package (`pkg/msg`) for embedding sign / verify / actor logic, so I can integrate URL-to-URL messaging into my Go service without running a sidecar. 40. As an implementer of another language, I want a 1-page wire spec and a directory of normative test vectors, so I can build a conformant client in under 100 lines. 41. As an implementer, I want every observable wire behavior captured by a vector, so my tests can prove parity with the reference implementation. 42. As a web developer, I want to send messages from my web app by signing and POSTing directly per the spec, so I don't need a Go sidecar. 43. As an implementer of a third-party client, I want example clients in Python, JavaScript, and Bash that round-trip with the Go binary, so I have working reference code in a familiar language. ### Maintainer 44. As a contributor, I want CLAUDE.md's spec-first discipline (vectors first, code second) enforced in PR review, so wire behavior doesn't quietly diverge. 45. As a contributor, I want CONTEXT.md to surface the design rationale for non-obvious choices, so I can make informed changes. 46. As a contributor, I want every vector exercised in `go test ./...`, so a regression in protocol behavior fails CI. ### Operations 47. As an operator, I want structured logging (slog) by default, so I can debug without parsing free-form output. 48. As an operator, I want signature failures, retries, and rate-limit hits logged at appropriate levels, so I can spot trouble. 49. As an operator, I want message bodies excluded from logs by default, so private content does not end up in log aggregators. ## Implementation Decisions ### Architecture - Go single binary, module rooted at `code.bas.es/arne/msg`. Public package at `pkg/msg`; daemon, TUI, and store internals under `internal/`. - SQLite via `modernc.org/sqlite` (pure Go) — keeps the binary cgo-free for clean cross-compilation. - Two binary modes share state via SQLite only: `msg serve` (daemon) and `msg` (TUI). No IPC, no Unix sockets, no localhost HTTP between them. - TUI watches for new rows by polling SQLite at ~250ms; daemon polls the outbox at the same cadence. ### Deep modules These encapsulate behavior behind a small, stable interface and carry the protocol's logic. They are individually unit-testable and composable. - **`envelope`** — envelope and actor-doc schema, JSON encode/decode, structural validation. Pure. - **`signing`** — Ed25519 sign and verify over arbitrary byte sequences, plus key encoding. Pure crypto. - **`verifier`** — composes envelope + signing + actor cache + clock + dedup-set into the full `SPEC.md` §7 verification procedure. Returns a structured outcome with stable `SPEC.md` §9 error codes. Single entry point: takes raw body bytes, signature header, claimed recipient URL, and dependencies. This is the protocol's central rejection-logic module — every vector tests it. - **`sender`** — symmetric: builds an envelope, signs the raw body bytes, POSTs to the recipient, parses the response (status, error code, optional receipt). Receipt verification reuses the same verifier path. - **`actor_cache`** — TTL-based cache of actor docs keyed by URL, encapsulating the `SPEC.md` §7.3 "refetch once on unknown keyId" rule. Hides fetch + cache + refetch policy. - **`keys`** — load / save `keys.json`, generate, rotate (add new + retire old with retain window), prune. File-mode 0600 enforcement. - **`outbox`** — send-loop state machine: pending → sending → delivered | failed-permanent | failed-pending-user. Backoff schedule 5s, 30s, 5m, 30m. Hard cap: 4 attempts or 24h elapsed. ### Shallow / glue modules - **`store`** — SQLite migrations and typed DAO methods over the schema (unified `messages` table with `direction`, plus `actor_cache`, `contacts`, `receipts`, FTS5 virtual table `messages_fts`). - **`http`** — `net/http` handler wiring the verifier into request handling; serves the actor doc on `GET <url>`; handles per-sender rate limiting and the `--acl=contacts` mode. - **`tls`** — config helper for the three TLS modes (auto via certmagic / autocert; cert+key files; HTTP loopback for reverse-proxy fronting). - **`config`** — TOML loading at `${XDG_CONFIG_HOME:-~/.config}/msg/config.toml`, with override stack flag > env (`MSG_*`) > config > default. - **`tui`** — bubbletea views, lipgloss styles, key bindings; address-book add flow; `r` for retry; `/` for FTS search; `?` for help. - **`cli`** — cobra command tree for `serve`, `init`, `send`, `inbox list`, `keys init|rotate|prune`, `version`. - **`examples`** — Python, JavaScript, and Bash reference clients exercising the same vectors against the Go binary. ### Wire protocol behavior - All wire-observable behavior is captured by vectors under `testdata/vectors/`. Vectors are normative: a behavior change updates the vector first, the code second. - Receiver returns 2xx only after the verified message is durably committed. - Receipts are opt-in per-message via `Msg-Receipt: required`. Receivers may decline (return 204); senders tolerate the absence. - Ed25519 only in v1; the `algorithm` field on each key in the actor doc is the source of truth for what verifies a given `keyId`. - `Msg-Signature` carries plain padded-base64 (RFC 4648 §4), no algorithm prefix. - Error code strings (`bad-signature`, `stale-timestamp`, `unknown-key`, `duplicate-id`, `wrong-recipient`, `malformed-envelope`, `unsupported-version`) are stable. Renaming is breaking. ### Defaults and policy - Timestamp skew window: ±5 minutes. Configurable. - Per-sender rate limit: 60 messages / minute. Configurable. - Key retain window on rotation: 30 days. - Outbound retry: 5s, 30s, 5m, 30m; hard cap 4 attempts or 24h. - Listen address default `:443` when TLS mode is `auto` or `files`; `127.0.0.1:8088` when `none`. ### Storage schema (high-level) - `messages` — unified inbox + outbox. `direction` column. Outbound rows additionally carry `send_status`, `attempts`, `next_attempt_at`, `error_code`. Raw signed envelope bytes stored on every row for re-verification, audit-grade export, and migration. - `actor_cache` — URL → cached actor doc + `fetched_at`. - `contacts` — URL → user-defined `display_name`, pinned status. - `receipts` — signed delivery acks for outbound messages, FK to `messages.rowid`. - `messages_fts` — FTS5 virtual table over `payload` and `peer_url` for TUI search. ### Logging - `slog` JSON output by default. Levels: error, warn, info, debug. - Message payloads excluded from logs at info level. Debug level may include them. ## Testing Decisions A good test asserts external behavior at the public boundary, not implementation details. For msg, this means: - **Vector tests** against `verifier` and `sender`. Every file under `testdata/vectors/` is a black-box case (inputs + expected status code, error code, or expected signature bytes). Vectors are the conformance contract; a vector change *is* a behavior change. - **Unit tests** on the deep modules: `envelope`, `signing`, `actor_cache`, `keys`, `outbox`. Each has a small, pure-ish interface that is comfortable to test in isolation. - **One end-to-end integration test** that boots two daemons in-process, has one POST to the other, verifies receipt round-trip, and exercises persistence, the real net/http stack, and signature verification end-to-end. - **Not unit-tested in v1**: `tui`, `cli`, `tls`, `config`. Covered transitively by the integration test or are too presentation-heavy to test in isolation cheaply. - **Examples directory CI**: each example client (Python, JavaScript, Bash) is exercised against the Go binary using the same vectors. Cross-language interop is enforced in CI, not assumed. Prior art: this is a greenfield Go repo; no in-repo prior art exists. Reference patterns are the Go standard library's `*_test.go` table-driven test convention, the `testdata/` directory convention, and black-box testing via `pkg_test` packages. ## Out of Scope The following are explicitly out of scope for v1, per `CONTEXT.md`: - **Group messaging / multi-recipient envelopes.** Future direction is boards-as-URLs (a board is just another URL whose daemon re-broadcasts received posts to subscriber URLs) — a deployment pattern, not a protocol extension. - **End-to-end payload encryption.** Origin authentication only. - **NAT traversal, peer discovery, overlay networks.** - **Cross-device identity** (running the same URL from multiple machines is possible by copying `keys.json` but is not designed-for). - **OS keychain integration; encryption-at-rest for keys.** v1 is plaintext at `0600`; threat model documented in `CONTEXT.md`. - **OS notifications** (no `--notify` in v1). - **Localhost submission API** for non-Go web apps. Web apps integrate by signing and POSTing per the spec. - **Algorithms other than Ed25519.** - **Telemetry, metrics endpoints, health endpoints.** ## Further Notes ### Recommended order of work The order matters for keeping spec-first discipline honest: 1. `envelope` + `signing` + actor-doc parsing (pure modules, zero I/O). 2. `testdata/vectors/` — start with sign-and-verify, tampered-body, and recipient-mismatch cases. 3. `verifier` + `sender` (compose the above; vectors run green). 4. `actor_cache`, `store`, `http`, `outbox` (the daemon parts). 5. `examples/python` — *do this before the TUI*. If a Python client takes more than ~100 lines, the spec is too complex; fix the spec before building UI on top of it. 6. `tls` + `config` + `cli` (operator surface). 7. `tui` (presentation). 8. `examples/javascript` + `examples/bash` (round out cross-language coverage). ### License Deferred to maintainer; README placeholder.
Sign in to join this conversation.
No labels
needs-triage
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
arne/msg#1
No description provided.