tinyrelaycommit

Commit 565130580365

browse files at this commit | history

1diff --git a/README.md b/README.md
2index 7b5091c..0b2825b 100644
3--- a/README.md
4+++ b/README.md
5@@ -79,8 +79,6 @@ See [Wiki](docs/wiki.md) for pages, versions and forks, merge requests and redir
6
7 See [Custom views](docs/views.md) for rendering fenced code blocks through a transform you run and keeping the result as signed artifacts.
8
9-See [Architecture](docs/architecture.md) for service responsibilities, event acceptance and dependency checks.
10-
11 **Manage > Health** shows the relay and script versions and whether the browser's WebMCP tools are registered. See [WebMCP tools](docs/webmcp.md) for browser-agent integration and authorization behavior.
12
13 Agents outside the browser connect to `/mcp`, a stateless Model Context Protocol endpoint authenticated with NIP-98, and read `/llms.txt` for a summary of the relay's machine surface. See [MCP](docs/mcp.md). An agent acts under a grant the owner signs, and the owner reviews what it proposes:
14@@ -130,6 +128,20 @@ flowchart LR
15
16 See [Personal relay deployment](docs/personal-relay.md) for the reference installation.
17
18+## Explore the code
19+
20+Start with [`cmd/tiny`](cmd/tiny/doc.go) for commands and process lifetime, or [`daemon`](internal/daemon/doc.go) for tenant assembly and event flow. Each internal package has a `doc.go` overview. Comments on interfaces, constructors and transaction hooks explain ownership, inputs and failure behavior where those contracts are declared. Tests live beside the code they exercise.
21+
22+Use editor symbol help or read the same comments from the command line:
23+
24+```sh
25+go doc ./internal/daemon
26+go doc ./internal/storage SaveOptions
27+go doc ./internal/work Handler
28+```
29+
30+See [Architecture](docs/architecture.md) for the package map, event acceptance and dependency checks.
31+
32 ## Test and diagnose
33
34 ```sh
35diff --git a/cmd/tiny/doc.go b/cmd/tiny/doc.go
36new file mode 100644
37index 0000000..2a43f93
38--- /dev/null
39+++ b/cmd/tiny/doc.go
40@@ -0,0 +1,9 @@
41+// Command tiny runs and administers a multitenant Nostr relay.
42+//
43+// main creates a context cancelled by interrupt or SIGTERM and reports the
44+// error returned by run. serve binds the HTTP listeners, starts daemon.App,
45+// runs optional peer monitoring and joins server shutdown before closing the
46+// application. Tenant subcommands open an App for catalog operations. The
47+// git-token subcommand produces signed Git authorization material from an
48+// operator-selected key source.
49+package main
50diff --git a/cmd/tiny/main.go b/cmd/tiny/main.go
51index 91ff9df..1098a29 100644
52--- a/cmd/tiny/main.go
53+++ b/cmd/tiny/main.go
54@@ -33,6 +33,8 @@ func main() {
55 }
56 }
57
58+// run dispatches arguments after the executable name and writes command output
59+// to out. Returning errors to main keeps process exit at the command boundary.
60 func run(ctx context.Context, args []string, out io.Writer) error {
61 if len(args) == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h" {
62 _, err := fmt.Fprintln(out, "tiny serve [--data-dir PATH] [--listen :7447]\ntiny tenant create --name NAME --owner PUBKEY [--template default]\ntiny tenant list|enable|disable|host [options]\ntiny git-token --repo URL [--key-env TINY_AGENT_KEY] [--format header|value|git]\ntiny templates\ntiny version")
63@@ -215,6 +217,8 @@ func hexOwner(value string) bool {
64 return true
65 }
66
67+// serve owns listeners and peer monitoring around the App lifecycle. Shutdown
68+// drains the servers and joins peer monitoring before deferred App cleanup.
69 func serve(ctx context.Context, args []string, out io.Writer) error {
70 opts, err := parseServe(args)
71 if err != nil {
72@@ -297,6 +301,9 @@ func serve(ctx context.Context, args []string, out io.Writer) error {
73 return serverErr
74 }
75
76+// awaitServers starts shutdown when ctx ends or a server returns. Each server
77+// must report once to results; all reports are drained before returning the
78+// joined shutdown and serving errors.
79 func awaitServers(ctx context.Context, servers []*http.Server, results <-chan error) error {
80 var first error
81 received := 0
82diff --git a/docs/architecture.md b/docs/architecture.md
83index 19a53b8..45c921a 100644
84--- a/docs/architecture.md
85+++ b/docs/architecture.md
86@@ -1,31 +1,47 @@
87 # Architecture
88
89-Each tenant has a durable store and a set of services. The daemon constructs those services, admits operations and manages their lifetimes.
90+Each tenant has a SQLite store, a policy snapshot and a set of services. The daemon constructs those services, connects their contracts and manages their lifetimes. HTTP, WebSocket and management adapters establish caller identity and enter the tenant's operation gate before calling services.
91+
92+## Find your starting point
93+
94+[`cmd/tiny`](../cmd/tiny/doc.go) owns command dispatch, listeners and process shutdown. [`daemon`](../internal/daemon/doc.go) describes tenant assembly and event flow. Every internal package has a `doc.go` overview, and interface, constructor and transaction comments describe the contracts at their declarations. These comments are also available through `go doc` and editor symbol help.
95
96 ## Responsibilities
97
98-| Area | Responsibility |
99+| Packages | Responsibility |
100 | --- | --- |
101-| `event` and `policy` | Event formats and shared policy decisions. |
102-| `storage` | Event persistence, queries and transaction boundaries. |
103-| `community` | Membership, moderation, rooms and their projections. |
104-| `records` | Relay identity, signed records and notification records. Community data arrives through a required read interface. |
105-| `blob` | File storage, upload rules and file HTTP endpoints. |
106-| `gitrelay` | Repository admission, storage, synchronization, repair and Git HTTP endpoints. |
107-| `replication` | Replication policy, jobs and delivery handlers. |
108-| `work` | Durable queue claims, retries and worker lifetimes. The daemon combines handlers from the services. |
109-| `webui` | Pages, typed room reads and browser interactions. |
110-
111-Keep schema changes and data access with the service that owns the data. Share a policy function or a small read contract when several services need the same rule or projection. Avoid introducing a general utility package for unrelated behavior.
112+| [`event`](../internal/event/doc.go), [`policy`](../internal/policy/doc.go) | Event and filter values, signing, validation and shared policy decisions. |
113+| [`auth`](../internal/auth/doc.go), [`gates`](../internal/gates/doc.go) | Signed request proofs, event admission and visibility checks. |
114+| [`storage`](../internal/storage/doc.go), [`work`](../internal/work/doc.go) | Event persistence, queries, transaction hooks and durable queue claims. |
115+| [`community`](../internal/community/doc.go), [`communityread`](../internal/communityread/doc.go) | Membership, moderation, rooms, agents and the read models consumed by records. |
116+| [`records`](../internal/records/doc.go) | Relay identity, signed protocol records, projections and notifications. |
117+| [`blob`](../internal/blob/doc.go), [`sites`](../internal/sites/doc.go) | Content-addressed files, upload rules and sites backed by signed manifests. |
118+| [`gitrelay`](../internal/gitrelay/doc.go) | Repository admission, object storage, synchronization, repair and Git HTTP endpoints. |
119+| [`replication`](../internal/replication/doc.go), [`syncprotocol`](../internal/syncprotocol/doc.go) | Relay synchronization plans and transports, count sketches and reconciliation sessions. |
120+| [`relay`](../internal/relay/doc.go), [`mcp`](../internal/mcp/doc.go), [`webui`](../internal/webui/doc.go) | WebSocket sessions, MCP requests, HTML pages and browser interactions. |
121+| [`views`](../internal/views/doc.go), [`wiki`](../internal/wiki/doc.go), [`webpush`](../internal/webpush/doc.go) | Fenced-block parsing, article rendering and encrypted browser push delivery. |
122+| [`catalog`](../internal/catalog/doc.go), [`domains`](../internal/domains/doc.go) | Tenant lifecycle records, filesystem paths and host mappings. |
123+| [`templates`](../internal/templates/doc.go), [`configport`](../internal/configport/doc.go) | Built-in policy templates and configuration import, export and application. |
124+| [`telemetry`](../internal/telemetry/doc.go), [`daemon`](../internal/daemon/doc.go) | Observability, service assembly, request routing and worker ownership. |
125+
126+## Ownership and contracts
127+
128+`App` owns the catalog, telemetry and active tenants. Each `Tenant` owns its store and service lifetimes. Services borrow the store and policy reader supplied at construction. Policy reads return a shared snapshot; management writes persist and replace that snapshot. Shutdown stops admissions, joins background work and drains active operations before closing storage.
129+
130+Services own the schema and data access for their features. `storage.Save` and `storage.SaveTx` accept transaction hooks so event data, projections and work intents can commit together. APIs that receive `*sql.Tx` use the caller's transaction. Community event handlers that accept a persistence callback open the transaction and pass it into the callback.
131+
132+Consumers declare interfaces around the operations they need. `records.CommunityReader` reads membership and moderation projections through `communityread` values. `relay.Backend` supplies event operations to the WebSocket router. `webui.Backend` supplies management results, and `webui.RoomsReader` supplies typed room pages. The daemon implements these adapters and assembles the feature handlers used by `work.Worker`.
133
134 ## Event acceptance
135
136-Client publication and imported events share the persistence and projection path. Each entry point retains its own admission rules and protocol actions. Event projections and durable work records commit together.
137+Client publication and imported events use the shared event persistence and projection path. Each entry point applies its admission rules and protocol actions. Signed repository state stays pending until its referenced Git objects are available; promotion makes it visible and plans its optional follow-up work.
138
139-Callbacks and custom views are optional. Local planning can recover after acceptance, with bounded retries that recheck the source and captured registrations. Completed work keeps its identity and is not reset by planning recovery. Remote delivery and transforms retain their own retry limits. See [callbacks](agents.md#retries-and-pauses) and [custom views](views.md#failures).
140+Callbacks and custom views are optional. A planning intent commits with an accepted event and captures its eligible registrations or failed local lookups. Recovery rechecks the source and current registrations, with at most three worker attempts and a 15-minute age limit. Stable child intent IDs preserve completed work. Remote delivery and transforms have their own retry policies. See [callbacks](agents.md#retries-and-pauses) and [custom views](views.md#failures).
141
142 ## Browser boundaries
143
144-Pages render without JavaScript. The page template owns the shell; page templates own their content sections. Generic components live in `components.js`, room interactions live in `rooms.js`, and signed publication uses the shared `tiny.signing` contract. The verifier checks signed events before publication.
145+Pages render without JavaScript. `page.html` owns the shell and panel dispatch; each page template owns one content section. Markup uses elements and IDs for styling in `style.css`. Shared form and data elements live in `components.js`, room interactions live in `rooms.js`, and signed publication uses `tiny.signing`. The signing contract verifies returned events before publication. Browser modules describe their entry points and shared services in their source headers.
146+
147+## Dependencies and checks
148
149 Go module versions are declared in `go.mod`, with dependency checksums in `go.sum`. Browser dependencies are locked in `package-lock.json`. `make verify` checks the Go and JavaScript code and verifies generated browser bundles against their sources. See [Testing](testing.md) for local and Linux checks.
150diff --git a/internal/auth/auth.go b/internal/auth/auth.go
151index 169a968..32bd14c 100644
152--- a/internal/auth/auth.go
153+++ b/internal/auth/auth.go
154@@ -1,5 +1,3 @@
155-// Package auth identifies Nostr callers at HTTP and websocket boundaries.
156-// Access policy belongs to the caller; this package only verifies proofs.
157 package auth
158
159 import (
160@@ -27,6 +25,8 @@ type Validator struct {
161 seen map[string]time.Time
162 }
163
164+// NewValidator uses now to check proof freshness and track replay windows.
165+// A nil clock selects the system clock.
166 func NewValidator(now func() time.Time) *Validator {
167 if now == nil {
168 now = time.Now
169@@ -34,6 +34,9 @@ func NewValidator(now func() time.Time) *Validator {
170 return &Validator{now: now, seen: make(map[string]time.Time)}
171 }
172
173+// VerifyNIP98 validates a kind 27235 request proof against the exact URL,
174+// method and body supplied by the caller. Successfully verified event IDs are
175+// retained for the one-minute replay window.
176 func (v *Validator) VerifyNIP98(header, rawURL, method, body string) (event.Event, error) {
177 e, err := decodeToken(header, "NIP-98")
178 if err != nil {
179@@ -64,13 +67,15 @@ func (v *Validator) VerifyNIP98(header, rawURL, method, body string) (event.Even
180 return e, nil
181 }
182
183+// VerifyBlossom validates a BUD-11 authorization for action. It accepts the
184+// protocol's reusable authorization form and checks its action and expiry.
185 func (v *Validator) VerifyBlossom(header, action string) (event.Event, error) {
186 return v.verifyBlossom(header, action, "", "", false)
187 }
188
189-// VerifyBlossomRequest verifies a BUD-11 request, including optional server
190-// domain and blob-hash scope. It is separate from VerifyBlossom to preserve
191-// callers that use the older, server-agnostic helper.
192+// VerifyBlossomRequest validates a BUD-11 authorization with optional server
193+// and blob-hash scope. Upload, delete, mirror and media actions require an x
194+// tag when this scoped form is used.
195 func (v *Validator) VerifyBlossomRequest(header, action, server, blobHash string) (event.Event, error) {
196 return v.verifyBlossom(header, action, server, blobHash, true)
197 }
198@@ -215,6 +220,8 @@ type ChallengeManager struct {
199 active map[string]time.Time
200 }
201
202+// NewChallengeManager creates a NIP-42 challenge store for relayURL. Issued
203+// challenges are single-use and expire with the validator replay window.
204 func NewChallengeManager(relayURL string, now func() time.Time) *ChallengeManager {
205 if now == nil {
206 now = time.Now
207@@ -222,6 +229,7 @@ func NewChallengeManager(relayURL string, now func() time.Time) *ChallengeManage
208 return &ChallengeManager{relayURL: relayURL, now: now, active: make(map[string]time.Time)}
209 }
210
211+// Issue creates and stores a cryptographically random NIP-42 challenge.
212 func (m *ChallengeManager) Issue() (string, error) {
213 raw := make([]byte, 32)
214 if _, err := rand.Read(raw); err != nil {
215@@ -236,6 +244,7 @@ func (m *ChallengeManager) Issue() (string, error) {
216 return challenge, nil
217 }
218
219+// Verify validates and consumes a NIP-42 AUTH event for the configured relay.
220 func (m *ChallengeManager) Verify(e event.Event) error {
221 if err := event.Validate(e); err != nil {
222 return authError(err.Error())
223diff --git a/internal/auth/doc.go b/internal/auth/doc.go
224new file mode 100644
225index 0000000..a3bd9cc
226--- /dev/null
227+++ b/internal/auth/doc.go
228@@ -0,0 +1,6 @@
229+// Package auth verifies the signed proofs used at HTTP and WebSocket
230+// boundaries. It handles NIP-98 request signatures, Blossom authorization,
231+// GRASP-08 repository proofs and NIP-42 challenges. Callers use the returned
232+// public key to apply relay policy and keep authorization state outside this
233+// package.
234+package auth
235diff --git a/internal/auth/grasp08.go b/internal/auth/grasp08.go
236index 3d98b26..39ee850 100644
237--- a/internal/auth/grasp08.go
238+++ b/internal/auth/grasp08.go
239@@ -8,16 +8,13 @@ import (
240 "github.com/FelineStateMachine/tinyrelay/internal/event"
241 )
242
243-// ErrGRASP08Unauthorized is intentionally detail-free. GRASP-08 uses an
244-// empty 401 challenge so a caller cannot distinguish an unknown repository
245-// from an unauthorized one.
246+// ErrGRASP08Unauthorized is the detail-free GRASP-08 authentication error
247+// returned for every failed repository proof.
248 var ErrGRASP08Unauthorized = errors.New("auth-required:")
249
250-// VerifyGRASP08 verifies the reusable repository-root proof used by the
251-// GRASP-08 private-service profile. The proof is a kind 27235 event naming
252-// the repository root and GET; it is valid for 60 seconds and is reusable
253-// across the repository's Smart HTTP requests. Unlike NIP-98, payload tags
254-// are intentionally ignored and the proof is not replay-tracked.
255+// VerifyGRASP08 validates a reusable kind 27235 GET proof for the repository
256+// root named by a GRASP-08 Smart HTTP request. The proof remains valid for
257+// one minute across the repository's supported Git requests.
258 func (v *Validator) VerifyGRASP08(header, rawURL string) (event.Event, error) {
259 e, err := decodeToken(header, "GRASP-08")
260 if err != nil {
261@@ -40,8 +37,8 @@ func (v *Validator) VerifyGRASP08(header, rawURL string) (event.Event, error) {
262 return e, nil
263 }
264
265-// GRASP08RepositoryRoot returns the canonical repository URL for a Smart HTTP
266-// request beneath a .git path.
267+// GRASP08RepositoryRoot returns the canonical repository URL for a supported
268+// Smart HTTP request beneath a .git path.
269 func GRASP08RepositoryRoot(raw string) (string, bool) {
270 u, err := url.Parse(raw)
271 if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.User != nil {
272diff --git a/internal/blob/doc.go b/internal/blob/doc.go
273index c0ee9d9..7632e42 100644
274--- a/internal/blob/doc.go
275+++ b/internal/blob/doc.go
276@@ -1,2 +1,26 @@
277-// Package blob implements the Blossom and NIP-96 file doors for a local relay.
278+// Package blob implements the local relay's Blossom and NIP-96 file endpoints.
279+//
280+// Service stores immutable blob bytes under Config.Root/blobs, addressed by
281+// their lowercase SHA-256 digest. Metadata, uploader claims, moderation
282+// state, tombstones and multipart ranges live in the supplied storage.Store.
283+// New creates the metadata tables, reconciles files left by an interrupted
284+// upload and removes stale multipart state before returning a ready service.
285+// The caller drives service operations and mounts its Handler in the HTTP
286+// router. Startup and request work run within those calls.
287+//
288+// Put validates and hashes the input before making a new object visible. Its
289+// optional Commit callback runs in the metadata transaction, which lets a
290+// caller record related ownership or application state atomically with the
291+// blob claim. Get returns an open file only after metadata and moderation
292+// checks pass. Delete removes metadata transactionally and then removes the
293+// content file; DeleteForUploader removes one claim and removes shared bytes
294+// only when the final claim is gone.
295+//
296+// HTTP authorization is supplied by Config.Authorize. The callback returns
297+// the authenticated pubkey used for ownership and quota checks. Config's
298+// CanRead, IsOwner, ValidateUpload and RecordReport hooks provide the host
299+// policy and moderation boundaries without making this package own identity
300+// or event storage. Multipart uploads are reserved in the metadata database,
301+// staged under the blob root and finalized only after all declared ranges
302+// are present.
303 package blob
304diff --git a/internal/blob/service.go b/internal/blob/service.go
305index 871a16a..e973b46 100644
306--- a/internal/blob/service.go
307+++ b/internal/blob/service.go
308@@ -16,6 +16,7 @@ import (
309 "sync"
310 )
311
312+// Action identifies the file-door operation being authorized.
313 type Action string
314
315 const (
316@@ -27,11 +28,22 @@ const (
317 ActionReport Action = "report"
318 )
319
320-type Authorize func(*http.Request, Action) (string, error)
321-type CanRead func(context.Context, string, []string) bool
322-type ResolveIP func(context.Context, string) ([]net.IP, error)
323-type ReportRecorder func(context.Context, *sql.Tx, string, string, string, string, string) error
324+// Authorize authenticates an HTTP request for an action and returns the
325+// caller's pubkey.
326+type Authorize func(request *http.Request, action Action) (pubkey string, err error)
327
328+// CanRead reports whether any supplied reader pubkey may read the blob.
329+type CanRead func(ctx context.Context, blobSHA string, readerPubkeys []string) bool
330+
331+// ResolveIP resolves a host for mirror URL policy checks.
332+type ResolveIP func(ctx context.Context, host string) ([]net.IP, error)
333+
334+// ReportRecorder records a report in the host moderation transaction. The
335+// targetKind and reportType values describe the reported object and report;
336+// blob reports currently pass "blob" for both.
337+type ReportRecorder func(ctx context.Context, tx *sql.Tx, reporter string, targetBlob string, targetKind string, reportType string, content string) error
338+
339+// Config supplies durable storage, HTTP policy and host integration hooks.
340 type Config struct {
341 Root string
342 PublicURL string
343@@ -57,6 +69,7 @@ type Config struct {
344 UploadTerms func(context.Context, string) (UploadTerms, error)
345 }
346
347+// Limits are applied when each upload starts. Zero leaves that limit open.
348 type Limits struct {
349 MaxFileBytes int64
350 UserStorageBytes int64
351@@ -76,6 +89,7 @@ type UploadTerms struct {
352 // image, video, audio or document is plain.
353 var encryptedTypes = map[string]bool{"application/octet-stream": true, "application/vnd.blossom.directory+msgpack": true}
354
355+// Blob is the metadata for one content-addressed object.
356 type Blob struct {
357 SHA256 string `json:"sha256"`
358 Size int64 `json:"size"`
359@@ -84,17 +98,20 @@ type Blob struct {
360 Uploaded int64 `json:"uploaded"`
361 }
362
363+// PutOptions describes a blob upload and its optional metadata transaction.
364 type PutOptions struct {
365 Reader io.Reader
366 Type string
367 Uploader string
368 Hash string
369- // Commit records ownership metadata in the blob transaction. The final
370- // argument reports whether the blob is new. An error rolls back all claims.
371+ // Commit records ownership metadata in the blob transaction. Its isNew
372+ // argument reports whether the blob was newly installed. An error rolls
373+ // back all claims.
374 // New bytes remain unavailable until their ownership metadata is durable.
375- Commit func(context.Context, *sql.Tx, Blob, bool) error
376+ Commit func(ctx context.Context, tx *sql.Tx, blob Blob, isNew bool) error
377 }
378
379+// Service owns blob files and their metadata operations.
380 type Service struct {
381 config Config
382 root string
383diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go
384index 9035212..e9b89f8 100644
385--- a/internal/catalog/catalog.go
386+++ b/internal/catalog/catalog.go
387@@ -1,4 +1,3 @@
388-// Package catalog owns the durable registry of self-hosted relay tenants.
389 package catalog
390
391 import (
392@@ -20,9 +19,15 @@ import (
393 type Status string
394
395 const (
396+ // StatusCreating identifies a tenant whose directories or services are
397+ // still being initialized.
398 StatusCreating Status = "creating"
399- StatusReady Status = "ready"
400+ // StatusReady identifies a tenant available for normal service.
401+ StatusReady Status = "ready"
402+ // StatusDisabled identifies a tenant stopped by the operator and eligible
403+ // for reactivation or deletion.
404 StatusDisabled Status = "disabled"
405+ // StatusDeleting identifies a tenant whose deletion intent is recorded.
406 StatusDeleting Status = "deleting"
407 )
408
409@@ -36,12 +41,17 @@ var (
410 )
411
412 type CreateOptions struct {
413- Name string
414- Owner string
415+ // Name is the operator-visible unique tenant name.
416+ Name string
417+ // Owner is the tenant owner's public key.
418+ Owner string
419+ // Template identifies the configuration template used during setup.
420 Template string
421- Source string
422+ // Source records where the tenant configuration came from.
423+ Source string
424 }
425
426+// Paths contains the private filesystem locations assigned to a tenant.
427 type Paths struct {
428 Root string
429 Database string
430diff --git a/internal/catalog/doc.go b/internal/catalog/doc.go
431new file mode 100644
432index 0000000..350f62d
433--- /dev/null
434+++ b/internal/catalog/doc.go
435@@ -0,0 +1,11 @@
436+// Package catalog owns the durable registry and filesystem paths for
437+// self-hosted relay tenants.
438+//
439+// Open creates or opens the catalog database, configures SQLite durability,
440+// prepares the tenant tables and resumes recorded deletions. Create records a
441+// tenant in StatusCreating and creates its private directories; callers move
442+// it to StatusReady after tenant initialization succeeds. Disabled tenants
443+// can be enabled again or permanently removed with Delete. Delete records its
444+// intent, stages the tenant directory, removes the staged data, then removes
445+// the catalog rows, allowing Open to resume an interrupted deletion.
446+package catalog
447diff --git a/internal/community/community.go b/internal/community/community.go
448index 01cfa75..0f174b4 100644
449--- a/internal/community/community.go
450+++ b/internal/community/community.go
451@@ -1,4 +1,3 @@
452-// Package community owns tenant membership, moderation, invitations and audit.
453 package community
454
455 import (
456diff --git a/internal/community/config.go b/internal/community/config.go
457index 919f646..89a9d69 100644
458--- a/internal/community/config.go
459+++ b/internal/community/config.go
460@@ -175,8 +175,10 @@ func readConfigRows(ctx context.Context, db *sql.DB, query string, scan func(*sq
461 return rows.Err()
462 }
463
464-// ApplyConfigTx replaces selected community configuration sections inside a
465-// caller-owned transaction. Empty sections are meaningful replacements.
466+// ApplyConfigTx replaces each selected community configuration section inside
467+// the caller-owned transaction. A selected empty section clears that section;
468+// an unselected section is left unchanged. The caller commits or rolls back
469+// the transaction after all related configuration writes succeed.
470 func ApplyConfigTx(ctx context.Context, tx *sql.Tx, c ConfigSnapshot, members, bans, addresses, eventBans, kinds, retention bool) error {
471 now := time.Now().Unix()
472 if members {
473diff --git a/internal/community/doc.go b/internal/community/doc.go
474new file mode 100644
475index 0000000..e155c33
476--- /dev/null
477+++ b/internal/community/doc.go
478@@ -0,0 +1,16 @@
479+// Package community owns the tenant's membership, moderation, invitations,
480+// rooms, agents and audit records.
481+//
482+// A Service stores community state in the tenant database and exposes direct
483+// operations plus transaction hooks for event ingestion. Methods whose names
484+// end in Tx and accept *sql.Tx use a transaction owned by the caller and leave
485+// commit or rollback to that caller. Event handlers whose callback accepts
486+// *sql.Tx open and close the transaction internally; the callback stores the
487+// accepted event in that transaction, so event, community state and
488+// projection work commit or roll back together.
489+//
490+// Community policy is read through the function supplied to ConfigurePolicy.
491+// Records consumes the narrow read model implemented by Members,
492+// MemberStatus and ModerationCounts, which keeps records independent of the
493+// community tables.
494+package community
495diff --git a/internal/community/membership.go b/internal/community/membership.go
496index 8e0f675..fd15539 100644
497--- a/internal/community/membership.go
498+++ b/internal/community/membership.go
499@@ -71,11 +71,10 @@ type MembershipResult struct {
500 NewRequest bool
501 }
502
503-// HandleMembershipEventTx applies join/leave admission and invokes persist in
504-// the same SQLite transaction. The callback must store the accepted event (or
505-// return an error); membership and projection work then roll back together.
506-// Management events that need records-owned state should use the root's
507-// transaction hooks and EnqueueProjectionTx directly.
508+// HandleMembershipEventTx applies join/leave admission in a transaction owned
509+// by the service and invokes persist with that transaction. persist owns
510+// accepted-event storage; returning an error rolls back membership, the
511+// accepted event and queued projection work together.
512 func (s *Service) HandleMembershipEventTx(ctx context.Context, ev event.Event, persist func(*sql.Tx) error) (MembershipResult, error) {
513 if persist == nil {
514 return MembershipResult{}, errors.New("community: nil event persistence callback")
515@@ -281,9 +280,8 @@ func (s *Service) HandleModerationEventTx(ctx context.Context, ev event.Event, p
516 return out, err
517 }
518
519-// OnMembershipApplied is retained as a source compatibility shim. Projection
520-// refresh is now driven by durable records-projection work intents; invoking a
521-// relay callback here can deadlock a publish fence.
522+// OnMembershipApplied ignores fn. Projection refresh uses durable
523+// records-projection work intents.
524 func (s *Service) OnMembershipApplied(fn func(event.Event)) {
525 _ = fn
526 }
527diff --git a/internal/community/records_reader.go b/internal/community/records_reader.go
528index 6edda93..8b26732 100644
529--- a/internal/community/records_reader.go
530+++ b/internal/community/records_reader.go
531@@ -31,7 +31,7 @@ func (s *Service) Members(ctx context.Context) ([]communityread.Member, error) {
532 return out, rows.Err()
533 }
534
535-// MemberStatus reports heir membership and the current member count in one
536+// MemberStatus reports the key's membership and the current member count in one
537 // call so succession checks use a consistent community snapshot.
538 func (s *Service) MemberStatus(ctx context.Context, pubkey string) (bool, int, error) {
539 var count int
540diff --git a/internal/communityread/communityread.go b/internal/communityread/communityread.go
541index 22c18d1..29c24f8 100644
542--- a/internal/communityread/communityread.go
543+++ b/internal/communityread/communityread.go
544@@ -1,13 +1,16 @@
545-// Package communityread contains the small read models shared by community
546-// and records. It has no storage or service dependencies.
547 package communityread
548
549 type Member struct {
550+ // PubKey is the member's 32-byte public key in lowercase hexadecimal form.
551 PubKey string
552- Name string
553- Role string
554+ // Name is the member's optional NIP-05-style local name.
555+ Name string
556+ // Role is the current community role.
557+ Role string
558 }
559
560+// ModerationCounts contains moderation activity counted from a caller-supplied
561+// Unix timestamp.
562 type ModerationCounts struct {
563 Bans, Reports, Resolved, Hidden, BlockedAddresses int
564 }
565diff --git a/internal/communityread/doc.go b/internal/communityread/doc.go
566new file mode 100644
567index 0000000..1cc8330
568--- /dev/null
569+++ b/internal/communityread/doc.go
570@@ -0,0 +1,6 @@
571+// Package communityread defines the read models shared by community and
572+// records.
573+//
574+// These structs let records consume community data through its CommunityReader
575+// interface. The community service supplies the underlying queries.
576+package communityread
577diff --git a/internal/configport/configport.go b/internal/configport/configport.go
578index 4fa9eb4..c2936be 100644
579--- a/internal/configport/configport.go
580+++ b/internal/configport/configport.go
581@@ -1,4 +1,3 @@
582-// Package configport imports, exports, plans and applies bind.ws relay files.
583 package configport
584
585 import (
586@@ -21,16 +20,23 @@ import (
587
588 const Format = "bind.ws/relay-config/2"
589
590+// Member is a community member in an exported configuration.
591 type Member struct {
592 PubKey string `json:"pubkey"`
593 Name string `json:"name,omitempty"`
594 Note string `json:"note,omitempty"`
595 Role string `json:"role"`
596 }
597+
598+// Rule is a retention rule. A nil Kind applies to the tenant default.
599 type Rule struct {
600 Kind *int `json:"kind"`
601 Days int `json:"days"`
602 }
603+
604+// Config is the JSON representation accepted by Parse and produced by
605+// Export. Sections records which keys were present in the input so Apply can
606+// distinguish omission from an explicit empty replacement.
607 type Config struct {
608 Format string `json:"format"`
609 ExportedAt int64 `json:"exported_at,omitempty"`
610@@ -51,6 +57,8 @@ type Config struct {
611 Sections []string `json:"-"`
612 Warnings []string `json:"-"`
613 }
614+
615+// Changes describes the policy and community sections changed by an apply.
616 type Changes struct {
617 Policy []string `json:"policy,omitempty"`
618 Members []string `json:"members,omitempty"`
619@@ -58,6 +66,9 @@ type Changes struct {
620 Warnings []string `json:"warnings,omitempty"`
621 FinalPolicy policy.Policy `json:"-"`
622 }
623+
624+// ConfigStore coordinates configuration with the tenant store, community
625+// service and policy callbacks.
626 type ConfigStore struct {
627 Store *storage.Store
628 Community *community.Service
629@@ -70,6 +81,7 @@ type ConfigStore struct {
630 ValidatePolicy func(policy.Policy) error
631 }
632
633+// ApplyOptions controls an import or preset application.
634 type ApplyOptions struct {
635 DryRun bool
636 MigrationOwner string
637diff --git a/internal/configport/doc.go b/internal/configport/doc.go
638new file mode 100644
639index 0000000..e8ce03f
640--- /dev/null
641+++ b/internal/configport/doc.go
642@@ -0,0 +1,10 @@
643+// Package configport imports, exports, plans and applies bind.ws relay
644+// configuration files.
645+//
646+// Parse accepts the current relay configuration format and records which
647+// sections were present. ApplyWithOptions merges policy values, replaces each
648+// supplied community section, and writes policy, community data, connections
649+// and jobs in one storage transaction. An omitted section is left unchanged;
650+// an included empty section clears that section. OnApplied runs only after a
651+// successful commit, while ValidatePolicy runs before the transaction.
652+package configport
653diff --git a/internal/daemon/callbacks.go b/internal/daemon/callbacks.go
654index cfc5580..f6cf681 100644
655--- a/internal/daemon/callbacks.go
656+++ b/internal/daemon/callbacks.go
657@@ -604,8 +604,8 @@ func (s *callbackService) callbackOwnerCanSee(ctx context.Context, owner string,
658 return s.gate.CanSee(ctx, e, relay.Session{PubKeys: []string{owner}, RelayURL: s.relayURL}, nil)
659 }
660
661-// Prepare preserves the original planning API for callers that do not yet
662-// persist captured candidate IDs alongside the event.
663+// Prepare matches current callback registrations and rechecks their access
664+// before returning delivery intents. The caller persists the returned work.
665 func (s *callbackService) Prepare(ctx context.Context, e event.Event) ([]storage.Intent, error) {
666 ids, err := s.CandidateIDs(ctx, e)
667 if err != nil {
668@@ -614,8 +614,9 @@ func (s *callbackService) Prepare(ctx context.Context, e event.Event) ([]storage
669 return s.PrepareFor(ctx, e, ids)
670 }
671
672-// notifyCallbacks runs after an event is stored. It preserves the legacy
673-// best-effort queueing path for callers that do not share the event tx.
674+// notifyCallbacks plans and queues delivery in its own transaction, logging
675+// planning or storage failures. Ephemeral publication uses this path because
676+// its event is carried by the delivery payload.
677 func (s *callbackService) notifyCallbacks(ctx context.Context, e event.Event) {
678 intents, err := s.Prepare(ctx, e)
679 if err != nil {
680diff --git a/internal/daemon/callbacks_service.go b/internal/daemon/callbacks_service.go
681index 041de27..752c5c3 100644
682--- a/internal/daemon/callbacks_service.go
683+++ b/internal/daemon/callbacks_service.go
684@@ -13,7 +13,8 @@ import (
685 )
686
687 // callbackService owns callback registration, matching and durable delivery.
688-// Its dependencies are explicit so callback state does not enlarge Tenant.
689+// It borrows the tenant store, community and gate. Per-registration locks
690+// serialize remote delivery; the daemon owns worker startup and shutdown.
691 type callbackService struct {
692 store *storage.Store
693 community *community.Service
694@@ -30,6 +31,9 @@ type callbackService struct {
695 callbackClient *http.Client
696 }
697
698+// callbackServiceConfig binds storage, current policy and visibility checks.
699+// PublicURL identifies the HTTP relay in deliveries; RelayURL supplies the
700+// WebSocket address used by event visibility checks.
701 type callbackServiceConfig struct {
702 Store *storage.Store
703 Community *community.Service
704diff --git a/internal/daemon/custom_views.go b/internal/daemon/custom_views.go
705index b6c63ea..ee88ea9 100644
706--- a/internal/daemon/custom_views.go
707+++ b/internal/daemon/custom_views.go
708@@ -31,8 +31,9 @@ import (
709 )
710
711 // customViewService owns custom view definitions, transform execution and
712-// artifact storage. Tenant supplies the narrow lifecycle callbacks when it
713-// constructs the service.
714+// artifact storage. It borrows tenant dependencies and serializes transforms
715+// per view. Git is bound after repository initialization and before workers
716+// start; Records supplies the signing identity for generated artifacts.
717 type customViewService struct {
718 store *storage.Store
719 community *community.Service
720diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go
721index 260fcac..4ad2a73 100644
722--- a/internal/daemon/daemon.go
723+++ b/internal/daemon/daemon.go
724@@ -1,4 +1,3 @@
725-// Package daemon composes the self-hosted services and owns their lifetimes.
726 package daemon
727
728 import (
729@@ -21,6 +20,10 @@ import (
730 "github.com/FelineStateMachine/tinyrelay/internal/templates"
731 )
732
733+// Config supplies process-level paths, public addresses, transport limits and
734+// operator permissions. Tenant policy supplies each relay's access rules.
735+// DataDir is required; DefaultTenant defaults to "main". PublicURL, when set,
736+// is an absolute HTTP or HTTPS address used for externally visible URLs.
737 type Config struct {
738 DataDir string
739 PublicURL string
740@@ -39,13 +42,15 @@ type Config struct {
741 // host-wide push registrations. Tenant policy PushCallbacks is a separate
742 // owner-controlled allowlist; both approvals are required.
743 PushCallbackOrigins []string
744- // PeerMonitor configures optional NIP-66 liveness probes for explicitly
745- // configured peers. It never discovers or crawls public relays.
746+ // PeerMonitor configures NIP-66 liveness probes for its listed peers.
747 PeerMonitor *PeerMonitorConfig
748 }
749
750 type CreateOptions = catalog.CreateOptions
751
752+// App owns the catalog, active tenants and process-wide service lifecycle.
753+// New constructs it, Start starts serving work, and Close releases resources.
754+// An HTTP server uses App as its handler.
755 type App struct {
756 cfg Config
757 catalog *catalog.Catalog
758@@ -57,6 +62,9 @@ type App struct {
759 lifecycle lifecycleState
760 }
761
762+// New opens catalog and telemetry resources and attempts to resume incomplete
763+// tenant creation. The caller owns the returned App and calls Close to release
764+// it. Start begins the serving lifecycle.
765 func New(ctx context.Context, cfg Config) (*App, error) {
766 if cfg.DataDir == "" {
767 return nil, errors.New("data directory is required")
768@@ -143,6 +151,10 @@ func (a *App) PeerMonitorRun(ctx context.Context) error {
769 return a.peerMonitor.Run(ctx)
770 }
771
772+// Create provisions a tenant from a template, initializes its durable state
773+// and marks it ready. A running App also starts its services. The returned
774+// metadata identifies the creation record even if a later setup step fails;
775+// RecoverCreating resumes incomplete creation from that durable record.
776 func (a *App) Create(ctx context.Context, opts CreateOptions) (catalog.Tenant, error) {
777 if template, ok := templates.Find(opts.Template); ok && template.Source == "required" && strings.TrimSpace(opts.Source) == "" {
778 return catalog.Tenant{}, fmt.Errorf("template %q requires a source relay", opts.Template)
779@@ -271,6 +283,9 @@ func (a *App) tenant(ctx context.Context, meta catalog.Tenant, publicURL string)
780 return t, nil
781 }
782
783+// Close stops catalog reconciliation and tenant services, then closes shared
784+// resources and releases the process lock. It joins cleanup errors and returns
785+// nil on subsequent calls. The caller shuts down its HTTP servers separately.
786 func (a *App) Close(ctx context.Context) error {
787 a.mu.Lock()
788 if a.closed {
789@@ -294,6 +309,8 @@ func (a *App) Close(ctx context.Context) error {
790 return errors.Join(errs...)
791 }
792
793+// ServeHTTP resolves the request's tenant or hosted site and delegates to its
794+// handler. Health and relay discovery routes are served at the process level.
795 func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
796 if r.URL.Path == "/relays" && a.ServeLanding(w, r) {
797 return
798diff --git a/internal/daemon/doc.go b/internal/daemon/doc.go
799new file mode 100644
800index 0000000..0b5c12f
801--- /dev/null
802+++ b/internal/daemon/doc.go
803@@ -0,0 +1,52 @@
804+// Package daemon assembles the services of a multitenant Nostr relay.
805+// App owns the tenant catalog, telemetry and active Tenant instances. Each
806+// Tenant owns one store, its policy snapshot, protocol handlers and background
807+// services. HTTP, WebSocket and management requests reach domain services
808+// through these adapters.
809+//
810+// # Process and tenant lifetimes
811+//
812+// New opens the catalog and telemetry and attempts to resume incomplete tenant
813+// creation.
814+// Start acquires the serving process lock, opens ready tenants and starts
815+// catalog reconciliation. The caller serves App as an HTTP handler and calls
816+// Close after stopping its HTTP listeners. Close stops reconciliation, closes
817+// tenants, releases shared resources and releases the process lock. The
818+// optional peer monitor runs under the context passed to PeerMonitorRun.
819+//
820+// A tenant initializes community state and its admission gate before building
821+// the protocol and feature services. Custom-view storage is ready when Git
822+// initialization can promote repository events; the Git service is then bound
823+// to the view service. Handler registration completes before worker goroutines
824+// start. Tenant.Close stops new operations, cancels and joins background work,
825+// closes live connections and drains active operations before closing storage.
826+//
827+// # Admission and persistence
828+//
829+// Tenant implements the relay backend and the typed room reader used by the
830+// web UI. Execute dispatches management methods under the supplied actor's
831+// current authority. HTTP and WebSocket adapters establish that actor through
832+// request authentication. Policy returns a read-only snapshot; policy changes
833+// are serialized and replace the snapshot after persistence succeeds.
834+//
835+// Client publication and replication ingestion use eventCommitter to compose
836+// event storage, domain projections and durable intents. Client entry points
837+// also process membership, moderation and room actions. The relay router owns
838+// live event delivery. A repository state stays pending until Git promotion
839+// makes its referenced objects available.
840+//
841+// # Optional work and maintenance
842+//
843+// callbackService owns webhook registrations and delivery. customViewService
844+// owns transform definitions, requests and signed artifacts. eventFollowups
845+// captures matching registrations and persists planning work with accepted
846+// events. Planning checks current sources and registrations, uses stable work
847+// identities and retries within bounded attempts and age. Each feature's
848+// handler owns its remote delivery or transform policy.
849+//
850+// The maintenance gate admits ordinary operations while exclusive maintenance
851+// is idle. A maintenance operation blocks new admissions and drains active
852+// operations. Nested calls carry the admission in their context, and callers
853+// release it with the returned completion function. Workers use the same gate
854+// while operating on the tenant's services and store.
855+package daemon
856diff --git a/internal/daemon/event_followups.go b/internal/daemon/event_followups.go
857index 65804e4..477d397 100644
858--- a/internal/daemon/event_followups.go
859+++ b/internal/daemon/event_followups.go
860@@ -20,9 +20,9 @@ const (
861 followupMaxAttempts = 3
862 )
863
864-// eventFollowups plans optional work without depending on a remote service.
865-// Its outbox record commits with the source event; delivery and transforms
866-// retain their own bounded retry policies and deterministic intent IDs.
867+// eventFollowups turns accepted events into optional callback and transform
868+// intents using local state. Its planning record commits with the source
869+// event; feature handlers perform remote work using their own retry policies.
870 type eventFollowups struct {
871 store *storage.Store
872 callbacks *callbackService
873@@ -31,6 +31,10 @@ type eventFollowups struct {
874 push func(context.Context, replicationPushFollowupPayload) ([]storage.Intent, error)
875 }
876
877+// eventFollowupPayload captures the signed event and matching registrations.
878+// Discovery flags retain failed local lookups for retry. Released identifies
879+// repository state whose objects are available, and RecoverPush requests the
880+// client registration snapshot saved before Git promotion.
881 type eventFollowupPayload struct {
882 Event event.Event `json:"event"`
883 AcceptedAt int64 `json:"accepted_at"`
884@@ -47,6 +51,10 @@ func (f *eventFollowups) prepare(ctx context.Context, e event.Event, now int64,
885 return f.prepareWithPush(ctx, e, now, released, nil)
886 }
887
888+// prepareWithPush captures eligible targets or lookup failures in an intent
889+// for the event transaction. It returns nil for ephemeral events and events
890+// with no eligible work. Repository webhook and view discovery starts when
891+// released is true.
892 func (f *eventFollowups) prepareWithPush(ctx context.Context, e event.Event, now int64, released bool, push *replicationPushFollowupPayload) *storage.Intent {
893 if event.IsEphemeral(e.Kind) {
894 return nil
895@@ -80,9 +88,9 @@ func (f *eventFollowups) prepareWithPush(ctx context.Context, e event.Event, now
896 return &storage.Intent{Kind: eventFollowupKind, EventID: e.ID, Target: target, Payload: string(raw)}
897 }
898
899-// try runs only local planning after commit. A failure leaves the outbox
900-// pending for the worker; success marks planning complete with its derived
901-// intents, so a later retry never replays already planned remote work.
902+// try runs local planning after commit. A planning failure leaves the parent
903+// pending for the worker. Success commits its derived intents and parent
904+// completion together. Stable child IDs preserve existing job states on retry.
905 func (f *eventFollowups) try(ctx context.Context, intent *storage.Intent) {
906 if intent == nil {
907 return
908@@ -93,6 +101,8 @@ func (f *eventFollowups) try(ctx context.Context, intent *storage.Intent) {
909 }
910 }
911
912+// handle is the queue adapter. It makes exhausted planning failures terminal;
913+// the worker owns completion, retry scheduling and claim-token checks.
914 func (f *eventFollowups) handle(ctx context.Context, job work.Intent) error {
915 err := f.plan(ctx, job, false)
916 if err != nil && job.Attempts >= followupMaxAttempts {
917@@ -101,6 +111,10 @@ func (f *eventFollowups) handle(ctx context.Context, job work.Intent) error {
918 return err
919 }
920
921+// plan rechecks the source, then persists the work each available feature can
922+// prepare. A feature error keeps the parent retryable while successful child
923+// intents retain their identity. pending allows the synchronous path to mark
924+// an unclaimed parent complete; a running worker completes its own claim.
925 func (f *eventFollowups) plan(ctx context.Context, job work.Intent, pending bool) error {
926 var payload eventFollowupPayload
927 if err := json.Unmarshal([]byte(job.Payload), &payload); err != nil || payload.Event.ID != job.EventID {
928@@ -171,8 +185,9 @@ func (f *eventFollowups) releasedPush(ctx context.Context, eventID string) (*rep
929 return payload.Push, nil
930 }
931
932-// Discovery is retried only when the original local lookup failed. The
933-// acceptance cutoff prevents a new registration from receiving old events.
934+// callbackIntents rechecks captured registrations and retries failed discovery.
935+// Recovery requires a creation time strictly before acceptance. Both times
936+// have second precision, so uncertain same-second registrations are skipped.
937 func (f *eventFollowups) callbackIntents(ctx context.Context, payload eventFollowupPayload) ([]storage.Intent, error) {
938 if payload.DiscoverCallbacks {
939 ids, err := f.callbacks.CandidateIDs(ctx, payload.Event)
940@@ -192,6 +207,8 @@ func (f *eventFollowups) callbackIntents(ctx context.Context, payload eventFollo
941 return f.callbacks.PrepareFor(ctx, payload.Event, payload.Callbacks)
942 }
943
944+// viewIntents rechecks registration generations and source relevance. Failed
945+// discovery uses the same strict acceptance cutoff as callbackIntents.
946 func (f *eventFollowups) viewIntents(ctx context.Context, payload eventFollowupPayload) ([]storage.Intent, error) {
947 if payload.DiscoverViews {
948 targets, err := f.views.CandidateTargets(ctx, payload.Event)
949diff --git a/internal/daemon/http.go b/internal/daemon/http.go
950index 02bde41..386d0a4 100644
951--- a/internal/daemon/http.go
952+++ b/internal/daemon/http.go
953@@ -500,6 +500,10 @@ func (t *Tenant) manageHTTP(w http.ResponseWriter, r *http.Request) {
954 writeJSON(w, 200, map[string]any{"result": result})
955 }
956
957+// Execute dispatches a management method with positional JSON parameters.
958+// actor is the authenticated public key, or empty for an anonymous caller.
959+// The transport establishes actor identity; method handlers apply current
960+// membership and resource permissions. Results retain each method's wire shape.
961 func (t *Tenant) Execute(ctx context.Context, actor, method string, params []json.RawMessage) (any, error) {
962 ctx, done, admissionErr := t.beginOperation(ctx)
963 if admissionErr != nil {
964diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go
965index bb6ef8b..6d0383d 100644
966--- a/internal/daemon/lifecycle.go
967+++ b/internal/daemon/lifecycle.go
968@@ -27,6 +27,9 @@ type lifecycleState struct {
969 done chan struct{}
970 }
971
972+// AcquireProcessLock exclusively locks dataDir for a serving process. Lock
973+// acquisition fails immediately when another process holds it. The caller
974+// retains the lock until every tenant using the directory has closed.
975 func AcquireProcessLock(dataDir string) (*ProcessLock, error) {
976 if dataDir == "" {
977 return nil, errors.New("daemon: data directory is required")
978@@ -45,6 +48,7 @@ func AcquireProcessLock(dataDir string) (*ProcessLock, error) {
979 return &ProcessLock{file: file}, nil
980 }
981
982+// Close releases the process lock and closes its file handle.
983 func (l *ProcessLock) Close() error {
984 if l == nil || l.file == nil {
985 return nil
986@@ -52,8 +56,9 @@ func (l *ProcessLock) Close() error {
987 return errors.Join(syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN), l.file.Close())
988 }
989
990-// Start eagerly opens permanent ready tenants, so jobs resume after restart
991-// without waiting for the first browser or websocket request.
992+// Start acquires the process lock, opens ready tenants and starts catalog
993+// reconciliation. baseURL supplies their public address. ctx bounds startup;
994+// App.Close ends the running lifecycle. Calls on a started App return nil.
995 func (a *App) Start(ctx context.Context, baseURL string) error {
996 a.lifecycle.mu.Lock()
997 defer a.lifecycle.mu.Unlock()
998diff --git a/internal/daemon/maintenance.go b/internal/daemon/maintenance.go
999index e607310..b55beed 100644
1000--- a/internal/daemon/maintenance.go
1001+++ b/internal/daemon/maintenance.go
1002@@ -6,12 +6,17 @@ import (
1003 "sync"
1004 )
1005
1006+// Admission errors distinguish exclusive maintenance from tenant shutdown.
1007+// Callers may retry maintenance refusals after the active operation finishes.
1008 var (
1009 ErrMaintenance = errors.New("retryable: tenant maintenance in progress")
1010 ErrMaintenanceBusy = errors.New("retryable: tenant maintenance is already active")
1011 ErrTenantClosing = errors.New("tenant is closing")
1012 )
1013
1014+// maintenanceGate counts admitted operations and grants exclusive maintenance
1015+// after those operations drain. Context markers carry admission across nested
1016+// service calls. A returned completion function releases each admission once.
1017 type maintenanceGate struct {
1018 mu sync.Mutex
1019 active int
1020@@ -27,9 +32,14 @@ type maintenanceContext struct {
1021 maintenance bool
1022 }
1023
1024+// beginOperation admits ordinary tenant work. The caller passes the returned
1025+// context to nested operations and defers the completion function on success.
1026 func (t *Tenant) beginOperation(ctx context.Context) (context.Context, func(), error) {
1027 return t.maintenance.beginOperation(ctx)
1028 }
1029+
1030+// beginMaintenance grants exclusive tenant access after active work drains.
1031+// The returned context admits maintenance's nested calls until completion.
1032 func (t *Tenant) beginMaintenance(ctx context.Context) (context.Context, func(), error) {
1033 return t.maintenance.beginMaintenance(ctx)
1034 }
1035diff --git a/internal/daemon/private_grasp.go b/internal/daemon/private_grasp.go
1036index d3ad27b..99b0814 100644
1037--- a/internal/daemon/private_grasp.go
1038+++ b/internal/daemon/private_grasp.go
1039@@ -23,10 +23,9 @@ func privateGitProof(r *http.Request) (event.Event, bool) {
1040 return proof, ok
1041 }
1042
1043-// PrivateServiceProfile is the protocol-facing description of a GRASP-08
1044-// tenant. It intentionally contains no tenant name, description, tags, or
1045-// repository coordinates: those are private data until the caller has
1046-// authenticated and passed the membership check.
1047+// PrivateServiceProfile describes a GRASP-08 tenant's configured protocol,
1048+// authentication methods, read policy and owner identity. Protocol adapters
1049+// choose the fields appropriate to the request's authenticated audience.
1050 type PrivateServiceProfile struct {
1051 Enabled bool
1052 Protocol string
1053@@ -35,9 +34,8 @@ type PrivateServiceProfile struct {
1054 Owner string
1055 }
1056
1057-// PrivateAccess is evaluated against the durable community table for every
1058-// request. Repository maintainers are deliberately absent from this type;
1059-// maintaining a repository does not grant tenant membership.
1060+// PrivateAccess records tenant-level authority resolved from current community
1061+// membership. Owner, member and moderator roles grant private-service access.
1062 type PrivateAccess struct {
1063 PubKey string
1064 Role string
1065@@ -68,8 +66,8 @@ func (t *Tenant) PrivateProfile() PrivateServiceProfile {
1066 }
1067 }
1068
1069-// PrivateAccess resolves current membership. It deliberately does not cache
1070-// the result, so removals and bans take effect on the next request.
1071+// PrivateAccess reads current membership and bans for each request, so access
1072+// changes apply to the next authorization check.
1073 func (t *Tenant) PrivateAccess(ctx context.Context, pubkey string) (PrivateAccess, error) {
1074 access := PrivateAccess{PubKey: pubkey}
1075 if t == nil || !t.PrivateServiceEnabled() {
1076diff --git a/internal/daemon/services.go b/internal/daemon/services.go
1077index a10f24f..3d80178 100644
1078--- a/internal/daemon/services.go
1079+++ b/internal/daemon/services.go
1080@@ -33,9 +33,9 @@ import (
1081 "github.com/FelineStateMachine/tinyrelay/internal/work"
1082 )
1083
1084-// initServices constructs the doors around a tenant's single durable store.
1085-// Each service receives the same policy snapshot function so policy changes
1086-// take effect without reopening the tenant.
1087+// initServices constructs feature services around the tenant store and policy
1088+// reader. Custom-view storage is initialized before Git can promote events;
1089+// the resulting Git service is bound before handlers and workers start.
1090 func (t *Tenant) initServices(ctx context.Context) error {
1091 var err error
1092 if err := t.initSessions(ctx); err != nil {
1093@@ -224,6 +224,9 @@ func (b backend) ReadAllowed(ctx context.Context, actor string) error {
1094 return b.tenant.requirePrivateAccess(ctx, actor)
1095 }
1096
1097+// closeServices cancels and joins the tenant's worker, scheduler and Git
1098+// loops, then returns the worker's terminal queue error, if any. Store and
1099+// relay connection cleanup belong to Tenant.Close.
1100 func (t *Tenant) closeServices(ctx context.Context) error {
1101 if t.workCancel != nil {
1102 t.workCancel()
1103@@ -614,6 +617,10 @@ func (t *Tenant) workHandlers() map[string]work.Handler {
1104 return handlers
1105 }
1106
1107+// releaseGit publishes a repository state after Git verifies its objects. It
1108+// removes the pending marker and persists optional planning in one transaction
1109+// under the router's publication lock. Push planning retains the registrations
1110+// captured at client acceptance; webhook and view matching occurs at release.
1111 func (t *Tenant) releaseGit(ctx context.Context, id string) error {
1112 var followup *storage.Intent
1113 if t.followups != nil {
1114@@ -741,6 +748,10 @@ func (t *Tenant) ingest(ctx context.Context, e event.Event, _ replication.Origin
1115 return err
1116 }
1117
1118+// commitImported applies import admission and commits the shared projections
1119+// and optional planning for an event. It returns persistence errors to the
1120+// caller, which decides how to handle duplicate and superseded imports.
1121+// origin supplies the replication source for planning.
1122 func (t *Tenant) commitImported(ctx context.Context, e event.Event, origin replication.Origin) error {
1123 if err := t.gate.Import(ctx, e, time.Now().Unix()); err != nil {
1124 return err
1125diff --git a/internal/daemon/tenant.go b/internal/daemon/tenant.go
1126index 2aef1d6..8ce2e04 100644
1127--- a/internal/daemon/tenant.go
1128+++ b/internal/daemon/tenant.go
1129@@ -40,6 +40,9 @@ type tenantConfig struct {
1130 publicURL string
1131 }
1132
1133+// Tenant composes one relay's policy, storage, protocol adapters and services.
1134+// App creates and owns tenants. Request handlers and workers enter the tenant's
1135+// maintenance gate before accessing shared services.
1136 type Tenant struct {
1137 app *App
1138 meta catalog.Tenant
1139@@ -146,6 +149,9 @@ func (t *Tenant) authenticated(ctx context.Context, s relay.Session) error {
1140 return nil
1141 }
1142
1143+// Policy returns the current read-only policy snapshot. Its maps and slices
1144+// share the snapshot's backing data; callers preserve them and submit changes
1145+// through the policy management path.
1146 func (t *Tenant) Policy() policy.Policy {
1147 t.mu.RLock()
1148 defer t.mu.RUnlock()
1149@@ -153,6 +159,10 @@ func (t *Tenant) Policy() policy.Policy {
1150 return t.policy
1151 }
1152 func (t *Tenant) RelayURL() string { return strings.Replace(t.publicURL, "http", "ws", 1) }
1153+
1154+// Close stops new operations, joins background services, closes live relay
1155+// connections and waits for active operations before closing the store. If
1156+// that wait exceeds ctx, the store stays open for the remaining operations.
1157 func (t *Tenant) Close(ctx context.Context) error {
1158 t.maintenance.markClosing()
1159 servicesErr := t.closeServices(ctx)
1160@@ -163,6 +173,12 @@ func (t *Tenant) Close(ctx context.Context) error {
1161 return errors.Join(servicesErr, routerErr, t.store.Close())
1162 }
1163
1164+// Publish applies write admission and the event kind's persistence or control
1165+// action for session s. Its string result is the relay OK message; duplicate
1166+// events produce a duplicate message with a nil error. Durable event writes
1167+// include projections and follow-up planning intents in their transaction.
1168+// Git metadata staging runs after that commit and before a successful reply.
1169+// The relay router handles live fan-out of accepted events.
1170 func (t *Tenant) Publish(ctx context.Context, e event.Event, s relay.Session) (string, error) {
1171 ctx, done, admissionErr := t.beginOperation(ctx)
1172 if admissionErr != nil {
1173@@ -234,8 +250,8 @@ func (t *Tenant) Publish(ctx context.Context, e event.Event, s relay.Session) (s
1174 return reason, nil
1175 }
1176
1177-// roomScope names the room an event is addressed to when that room is not
1178-// the tenant's own group, which keeps its established handling.
1179+// roomScope returns the named room for a room event. An empty result selects
1180+// the tenant-wide group path, including Marmot group events.
1181 func (t *Tenant) roomScope(e event.Event) string {
1182 if e.Kind == event.KIND_MARMOT_GROUP {
1183 return ""
1184@@ -289,6 +305,10 @@ func hexLower(value string) bool {
1185 return true
1186 }
1187
1188+// Query returns the authorized union of the filters for session s. It applies
1189+// per-filter limits, removes duplicate IDs and sorts by descending creation
1190+// time, then ascending ID. Matching NIP-43 invitations are generated for the
1191+// requesting session alongside the stored results.
1192 func (t *Tenant) Query(ctx context.Context, filters []event.Filter, s relay.Session) ([]event.Event, error) {
1193 ctx, done, admissionErr := t.beginOperation(ctx)
1194 if admissionErr != nil {
1195@@ -333,6 +353,9 @@ func (t *Tenant) Query(ctx context.Context, filters []event.Filter, s relay.Sess
1196 return result, nil
1197 }
1198
1199+// Count returns a NIP-45 result with the size of Query's authorized union,
1200+// using filters with their limits cleared. A supported single-filter request
1201+// also receives an HLL sketch. The tenant's count feature must be enabled.
1202 func (t *Tenant) Count(ctx context.Context, filters []event.Filter, s relay.Session) (any, error) {
1203 ctx, done, admissionErr := t.beginOperation(ctx)
1204 if admissionErr != nil {
1205@@ -367,6 +390,8 @@ func (t *Tenant) Count(ctx context.Context, filters []event.Filter, s relay.Sess
1206 return result, nil
1207 }
1208
1209+// Sync supplies visible event IDs and timestamps for a reconciliation session.
1210+// It clears the filter limit and requires the tenant's sync feature.
1211 func (t *Tenant) Sync(ctx context.Context, f event.Filter, s relay.Session) ([]syncprotocol.Item, error) {
1212 ctx, done, admissionErr := t.beginOperation(ctx)
1213 if admissionErr != nil {
1214@@ -388,14 +413,20 @@ func (t *Tenant) Sync(ctx context.Context, f event.Filter, s relay.Session) ([]s
1215 return items, nil
1216 }
1217
1218+// CanRead checks an event against current policy and session authority for
1219+// live delivery, including pending state, moderation and room visibility.
1220 func (t *Tenant) CanRead(e event.Event, s relay.Session) bool {
1221 return t.gate.CanSee(context.Background(), e, s, nil)
1222 }
1223
1224+// CanReadFilter checks live visibility with the subscription filter's context,
1225+// including addressed signer messages available during authentication.
1226 func (t *Tenant) CanReadFilter(e event.Event, s relay.Session, f *event.Filter) bool {
1227 return t.gate.CanSee(context.Background(), e, s, f)
1228 }
1229
1230+// QueryHints returns Query's events and relay completion hints: "finish" or
1231+// "more", plus "auth" when authentication can affect the query's visibility.
1232 func (t *Tenant) QueryHints(ctx context.Context, filters []event.Filter, s relay.Session) ([]event.Event, []string, error) {
1233 ctx, done, admissionErr := t.beginOperation(ctx)
1234 if admissionErr != nil {
1235diff --git a/internal/domains/doc.go b/internal/domains/doc.go
1236new file mode 100644
1237index 0000000..c72bfa2
1238--- /dev/null
1239+++ b/internal/domains/doc.go
1240@@ -0,0 +1,11 @@
1241+// Package domains manages operator-configured host mappings for one tenant.
1242+//
1243+// Service writes mappings through catalog and validates host names and site
1244+// labels before doing so. Management methods require the configured owner
1245+// check; list access is available to every caller of Execute. Check resolves
1246+// the mapping and, when configured, performs DNS lookup for the host.
1247+//
1248+// WebAddressHandler serves the NIP-05A web-address discovery endpoint. It
1249+// validates the requested path, applies the configured authorization and read
1250+// checks, and asks the host for the storage-backed address filter.
1251+package domains
1252diff --git a/internal/domains/domains.go b/internal/domains/domains.go
1253index 5b69feb..b6492ec 100644
1254--- a/internal/domains/domains.go
1255+++ b/internal/domains/domains.go
1256@@ -1,6 +1,3 @@
1257-// Package domains implements operator-managed local host mappings. A mapping
1258-// is explicit configuration; it never provisions DNS, certificates, or a
1259-// wildcard route on the operator's behalf.
1260 package domains
1261
1262 import (
1263@@ -20,11 +17,18 @@ import (
1264 type ResolveDNS func(context.Context, string) ([]net.IP, error)
1265
1266 type Config struct {
1267- Catalog *catalog.Catalog
1268- TenantID string
1269- BaseHost string
1270- RelayURL string
1271- Owner func(string) bool
1272+ // Catalog stores the tenant's primary and secondary host mappings.
1273+ Catalog *catalog.Catalog
1274+ // TenantID scopes all writes and reads performed by Service.
1275+ TenantID string
1276+ // BaseHost is the relay's canonical host and may be empty for a tenant
1277+ // without a primary host.
1278+ BaseHost string
1279+ // RelayURL is returned in discovery responses.
1280+ RelayURL string
1281+ // Owner authorizes management calls made through Execute.
1282+ Owner func(string) bool
1283+ // ResolveDNS checks the configured host's DNS records when Check runs.
1284 ResolveDNS ResolveDNS
1285 }
1286
1287diff --git a/internal/event/doc.go b/internal/event/doc.go
1288new file mode 100644
1289index 0000000..763a8d4
1290--- /dev/null
1291+++ b/internal/event/doc.go
1292@@ -0,0 +1,5 @@
1293+// Package event defines the relay's Nostr event and filter values. It owns
1294+// wire parsing, canonical serialization, Schnorr signing and signature
1295+// validation, plus the common kind and tag helpers used by storage and
1296+// protocol gates.
1297+package event
1298diff --git a/internal/event/event.go b/internal/event/event.go
1299index 9a6d8ba..0257c8b 100644
1300--- a/internal/event/event.go
1301+++ b/internal/event/event.go
1302@@ -27,6 +27,8 @@ type Event struct {
1303 Sig string `json:"sig"`
1304 }
1305
1306+// Validate checks the wire shape, canonical ID and Schnorr signature of e.
1307+// Kind-specific checks are applied by the protocol gate that owns the kind.
1308 func Validate(e Event) error {
1309 if !hex64(e.ID) {
1310 return errors.New("invalid: bad id")
1311@@ -62,6 +64,7 @@ func Validate(e Event) error {
1312 return nil
1313 }
1314
1315+// Parse decodes and validates one complete JSON event object.
1316 func Parse(raw []byte) (Event, error) {
1317 if err := requireFields(raw); err != nil {
1318 return Event{}, err
1319@@ -76,6 +79,7 @@ func Parse(raw []byte) (Event, error) {
1320 return e, nil
1321 }
1322
1323+// Canonical encodes e as the JSON representation used on relay boundaries.
1324 func Canonical(e Event) ([]byte, error) {
1325 var out bytes.Buffer
1326 encoder := json.NewEncoder(&out)
1327@@ -86,6 +90,8 @@ func Canonical(e Event) ([]byte, error) {
1328 return bytes.TrimSuffix(out.Bytes(), []byte{'\n'}), nil
1329 }
1330
1331+// Tag returns the first value for name, or an empty string when the tag is
1332+// absent or has no value.
1333 func Tag(e Event, name string) string {
1334 for _, tag := range e.Tags {
1335 if len(tag) > 0 && tag[0] == name && len(tag) > 1 {
1336@@ -95,6 +101,7 @@ func Tag(e Event, name string) string {
1337 return ""
1338 }
1339
1340+// TagValues returns every first value belonging to tags named name.
1341 func TagValues(e Event, name string) []string {
1342 values := make([]string, 0)
1343 for _, tag := range e.Tags {
1344@@ -105,6 +112,8 @@ func TagValues(e Event, name string) []string {
1345 return values
1346 }
1347
1348+// Expiration returns the positive Unix timestamp in the expiration tag.
1349+// Malformed, missing and nonpositive values return zero.
1350 func Expiration(e Event) int64 {
1351 v, err := strconv.ParseInt(Tag(e, "expiration"), 10, 64)
1352 if err != nil || v <= 0 {
1353@@ -113,6 +122,7 @@ func Expiration(e Event) int64 {
1354 return v
1355 }
1356
1357+// Difficulty returns the NIP-13 proof-of-work difficulty encoded by id.
1358 func Difficulty(id string) int {
1359 parsed, err := nostr.IDFromHex(id)
1360 if err != nil {
1361@@ -121,6 +131,7 @@ func Difficulty(id string) int {
1362 return nip13.Difficulty(parsed)
1363 }
1364
1365+// CommittedDifficulty returns the target in a nonce tag's third field.
1366 func CommittedDifficulty(e Event) int {
1367 for _, tag := range e.Tags {
1368 if len(tag) > 2 && tag[0] == "nonce" {
1369@@ -131,6 +142,8 @@ func CommittedDifficulty(e Event) int {
1370 return 0
1371 }
1372
1373+// Sign derives the public key, ID and Schnorr signature for e from secretHex.
1374+// A nil tag slice is encoded as an empty tag list before signing.
1375 func Sign(e *Event, secretHex string) error {
1376 secret, err := decodeSecret(secretHex)
1377 if err != nil {
1378@@ -155,6 +168,7 @@ func Sign(e *Event, secretHex string) error {
1379 return nil
1380 }
1381
1382+// GenerateKey creates a random nonzero 32-byte private key in hexadecimal.
1383 func GenerateKey() (string, error) {
1384 for {
1385 key := make([]byte, 32)
1386@@ -167,6 +181,7 @@ func GenerateKey() (string, error) {
1387 }
1388 }
1389
1390+// PublicKey derives the x-only Schnorr public key for secretHex.
1391 func PublicKey(secretHex string) (string, error) {
1392 secret, err := decodeSecret(secretHex)
1393 if err != nil {
1394@@ -176,9 +191,16 @@ func PublicKey(secretHex string) (string, error) {
1395 return hex.EncodeToString(schnorr.SerializePubKey(pubkey)), nil
1396 }
1397
1398-func IsEphemeral(k int) bool { return k >= 20000 && k < 30000 }
1399+// IsEphemeral reports whether k is in Nostr's ephemeral kind range.
1400+func IsEphemeral(k int) bool { return k >= 20000 && k < 30000 }
1401+
1402+// IsReplaceable reports whether k is a replaceable event kind.
1403 func IsReplaceable(k int) bool { return k == 0 || k == 3 || k >= 10000 && k < 20000 }
1404+
1405+// IsAddressable reports whether k is an addressable event kind.
1406 func IsAddressable(k int) bool { return k >= 30000 && k < 40000 }
1407+
1408+// IsPrivate reports whether k carries recipient-scoped content.
1409 func IsPrivate(k int) bool {
1410 return k == 4 || k == 13 || k == KIND_WRAP || k == 21059 || k == KIND_NOSTR_CONNECT || k == KIND_PUSH_REGISTRATION
1411 }
1412diff --git a/internal/event/filter.go b/internal/event/filter.go
1413index 0cb01f7..8f804d2 100644
1414--- a/internal/event/filter.go
1415+++ b/internal/event/filter.go
1416@@ -6,6 +6,12 @@ import (
1417 "strings"
1418 )
1419
1420+// Filter describes the event fields and tags a subscription or query accepts.
1421+// Fields combine with AND; values within a list are alternatives. Nil IDs,
1422+// Authors and Kinds leave that field unrestricted, while an empty non-nil list
1423+// matches nothing. Tags uses names without the wire prefix #. Since and Until
1424+// are inclusive Unix timestamps. Limit controls a historical result batch;
1425+// Matches evaluates each event independently of that limit.
1426 type Filter struct {
1427 IDs []string `json:"ids,omitempty"`
1428 Authors []string `json:"authors,omitempty"`
1429@@ -17,6 +23,8 @@ type Filter struct {
1430 Search string `json:"search,omitempty"`
1431 }
1432
1433+// ParseFilter decodes supported Nostr filter fields, including #tag keys, and
1434+// checks their JSON types.
1435 func ParseFilter(raw []byte) (Filter, error) {
1436 var values map[string]json.RawMessage
1437 if err := json.Unmarshal(raw, &values); err != nil {
1438@@ -79,6 +87,7 @@ func ParseFilter(raw []byte) (Filter, error) {
1439 return f, nil
1440 }
1441
1442+// MarshalJSON encodes tag constraints with their Nostr #name wire keys.
1443 func (f Filter) MarshalJSON() ([]byte, error) {
1444 values := make(map[string]json.RawMessage, 7+len(f.Tags))
1445 if f.IDs != nil {
1446@@ -108,6 +117,8 @@ func (f Filter) MarshalJSON() ([]byte, error) {
1447 return json.Marshal(values)
1448 }
1449
1450+// Matches reports whether e satisfies every populated constraint in f.
1451+// Search terms are matched against public content and exclude private kinds.
1452 func Matches(f Filter, e Event) bool {
1453 if f.IDs != nil && !contains(f.IDs, e.ID) || f.Authors != nil && !contains(f.Authors, e.PubKey) || f.Kinds != nil && !containsInt(f.Kinds, e.Kind) {
1454 return false
1455@@ -136,6 +147,8 @@ func Matches(f Filter, e Event) bool {
1456 return true
1457 }
1458
1459+// SearchTerms splits a search query into terms and drops protocol-style
1460+// name:value tokens.
1461 func SearchTerms(query string) []string { return slicesFilter(strings.Fields(query)) }
1462
1463 func slicesFilter(words []string) []string {
1464diff --git a/internal/event/kinds.go b/internal/event/kinds.go
1465index 67ad632..da02910 100644
1466--- a/internal/event/kinds.go
1467+++ b/internal/event/kinds.go
1468@@ -80,16 +80,19 @@ var JobFeedbackStatuses = []string{"payment-required", "processing", "error", "s
1469 // JobInputTypes is the NIP-90 vocabulary of the i tag's input type.
1470 var JobInputTypes = []string{"url", "event", "job", "text"}
1471
1472+// IsJobRequest reports whether kind is a NIP-90 request kind.
1473 func IsJobRequest(kind int) bool { return kind >= KIND_JOB_REQUEST_MIN && kind <= KIND_JOB_REQUEST_MAX }
1474-func IsJobResult(kind int) bool { return kind >= KIND_JOB_RESULT_MIN && kind <= KIND_JOB_RESULT_MAX }
1475+
1476+// IsJobResult reports whether kind is a NIP-90 result kind.
1477+func IsJobResult(kind int) bool { return kind >= KIND_JOB_RESULT_MIN && kind <= KIND_JOB_RESULT_MAX }
1478
1479 // IsJobKind reports whether the kind is a job request, result or feedback.
1480 func IsJobKind(kind int) bool {
1481 return IsJobRequest(kind) || IsJobResult(kind) || kind == KIND_JOB_FEEDBACK
1482 }
1483
1484-// JobResultKind returns the result kind for a request kind, or 0 when the
1485-// kind is not a job request.
1486+// JobResultKind returns the result kind for a request kind, or 0 for other
1487+// kinds.
1488 func JobResultKind(request int) int {
1489 if !IsJobRequest(request) {
1490 return 0
1491@@ -97,6 +100,8 @@ func JobResultKind(request int) int {
1492 return request + 1000
1493 }
1494
1495+// IsJobFeedbackStatus reports whether status is in the NIP-90 feedback
1496+// vocabulary.
1497 func IsJobFeedbackStatus(status string) bool {
1498 for _, known := range JobFeedbackStatuses {
1499 if known == status {
1500@@ -106,6 +111,7 @@ func IsJobFeedbackStatus(status string) bool {
1501 return false
1502 }
1503
1504+// IsJobInputType reports whether kind is a supported NIP-90 input type.
1505 func IsJobInputType(kind string) bool {
1506 for _, known := range JobInputTypes {
1507 if known == kind {
1508diff --git a/internal/gates/doc.go b/internal/gates/doc.go
1509new file mode 100644
1510index 0000000..cdc74c2
1511--- /dev/null
1512+++ b/internal/gates/doc.go
1513@@ -0,0 +1,4 @@
1514+// Package gates applies tenant policy and protocol admission rules to relay
1515+// events. Gate checks are shared by client writes, host imports, queries and
1516+// live delivery, while membership and storage services supply durable state.
1517+package gates
1518diff --git a/internal/gates/gates.go b/internal/gates/gates.go
1519index 2ed14c0..416d234 100644
1520--- a/internal/gates/gates.go
1521+++ b/internal/gates/gates.go
1522@@ -1,4 +1,3 @@
1523-// Package gates implements policy checks shared by ingest, query and live fan-out.
1524 package gates
1525
1526 import (
1527@@ -16,6 +15,8 @@ import (
1528 "github.com/dlclark/regexp2"
1529 )
1530
1531+// Config supplies the policy and optional durable services used by Gate.
1532+// Store and Community enable checks that depend on persisted relay state.
1533 type Config struct {
1534 Store *storage.Store
1535 Community *community.Service
1536@@ -23,6 +24,7 @@ type Config struct {
1537 Slug string
1538 }
1539
1540+// Gate evaluates event admission and visibility for one tenant configuration.
1541 type Gate struct {
1542 cfg Config
1543 agents agentLimiter
1544@@ -116,6 +118,8 @@ func New(cfg Config) (*Gate, error) {
1545 return &Gate{cfg: cfg}, nil
1546 }
1547
1548+// Write validates a client-published event against protocol shape, tenant
1549+// policy, membership, bans, room access, retention and proof-of-work rules.
1550 func (g *Gate) Write(ctx context.Context, e event.Event, s relay.Session, now int64) error {
1551 p := g.cfg.Policy()
1552 if e.Kind == event.KIND_AUTH {
1553@@ -286,9 +290,9 @@ func inboxAdmission(p policy.Policy, e event.Event) bool {
1554 return false
1555 }
1556
1557-// Import applies only the common event gate used by host-side pulls. It does
1558-// not apply client admission rules, kind allowlists, retention, PoW, or create
1559-// delivery fan-out; the imported event's origin must be recorded by storage.
1560+// Import validates an event received from another host using protocol shape,
1561+// safety, ban, agent and room existence checks. The caller records its origin
1562+// in storage after this common gate succeeds.
1563 func (g *Gate) Import(ctx context.Context, e event.Event, now int64) error {
1564 p := g.cfg.Policy()
1565 if err := event.Validate(e); err != nil {
1566@@ -466,6 +470,8 @@ func (g *Gate) kindRules(ctx context.Context, p policy.Policy, kind int) (bool,
1567 return allowedAny, blocked, nil
1568 }
1569
1570+// RetentionDays returns the configured retention period for kind, using the
1571+// wildcard rule when no exact rule exists.
1572 func (g *Gate) RetentionDays(ctx context.Context, kind int) int {
1573 if g.cfg.Community == nil {
1574 wildcard := 0
1575@@ -606,6 +612,8 @@ func (g *Gate) guestPass(ctx context.Context, p policy.Policy, e event.Event, a
1576 return false
1577 }
1578
1579+// Read validates a subscription request and returns an AUTH hint when a
1580+// subscriber may receive private or members-only results after authenticating.
1581 func (g *Gate) Read(ctx context.Context, filters []event.Filter, s relay.Session) (bool, error) {
1582 p := g.cfg.Policy()
1583 if p.Features.Signer && len(filters) > 0 {
1584@@ -662,6 +670,8 @@ func (g *Gate) Read(ctx context.Context, filters []event.Filter, s relay.Session
1585 return authHint, nil
1586 }
1587
1588+// CanSee reports whether session s may receive e. It applies hidden-event,
1589+// ban, repository privacy, policy, room and recipient checks.
1590 func (g *Gate) CanSee(ctx context.Context, e event.Event, s relay.Session, f *event.Filter) bool {
1591 p := g.cfg.Policy()
1592 if g.cfg.Store != nil {
1593diff --git a/internal/gitrelay/backup_import.go b/internal/gitrelay/backup_import.go
1594index d5c3acb..8a515b1 100644
1595--- a/internal/gitrelay/backup_import.go
1596+++ b/internal/gitrelay/backup_import.go
1597@@ -19,6 +19,7 @@ import (
1598 "strings"
1599 )
1600
1601+// BackupObject is one validated Git object and its compressed content chunks.
1602 type BackupObject struct {
1603 OID string `json:"oid"`
1604 Type string `json:"type"`
1605@@ -26,6 +27,7 @@ type BackupObject struct {
1606 Chunks []string `json:"chunks"`
1607 }
1608
1609+// BackupRepository contains the objects and refs for one imported repository.
1610 type BackupRepository struct {
1611 Key string `json:"key"`
1612 Owner string `json:"owner"`
1613@@ -35,11 +37,13 @@ type BackupRepository struct {
1614 Refs map[string]string `json:"refs"`
1615 }
1616
1617+// GitBackup is the complete bind.ws/git-sqlite/1 import document.
1618 type GitBackup struct {
1619 Format string `json:"format"`
1620 Repositories []BackupRepository `json:"repositories"`
1621 }
1622
1623+// ValidateGitBackup validates an import document without writing Git state.
1624 func ValidateGitBackup(input []byte) (GitBackup, error) {
1625 var backup GitBackup
1626 if err := json.Unmarshal(input, &backup); err != nil {
1627diff --git a/internal/gitrelay/doc.go b/internal/gitrelay/doc.go
1628new file mode 100644
1629index 0000000..f16166c
1630--- /dev/null
1631+++ b/internal/gitrelay/doc.go
1632@@ -0,0 +1,24 @@
1633+// Package gitrelay coordinates signed Nostr repository events with native
1634+// Git object storage and smart HTTP. Signed events establish repository refs,
1635+// metadata and maintainer authority.
1636+//
1637+// GitRelay stores bare repositories under Config.Root. Journal files under
1638+// .tinyrelay/journal make event-to-ref transitions recoverable, while pending
1639+// state files keep a signed ref announcement out of the visible repository
1640+// until every advertised tip object is present. New creates the root,
1641+// recovers journals and reloads repository metadata from the event store.
1642+// Synchronization runs through Service.Tick or transport callbacks invoked by
1643+// the caller.
1644+//
1645+// Validate checks event and maintainer admission. After the caller durably
1646+// stores the event, CommitAfterStore stages the corresponding Git transition.
1647+// CommitAfterStoreNoNotify serves callers already holding the host publication
1648+// fence. PromotePending completes a receive-pack repair and invokes OnPromote
1649+// after state becomes visible.
1650+//
1651+// ServeHTTP exposes Git smart HTTP for public and private repositories.
1652+// Private requests pass through AuthorizeHTTP. GRASP-08 private peer traffic
1653+// is limited to Config.PrivatePeers and is signed by HTTPAuth. GitSync and
1654+// EventSync supply synchronization transports; the daemon supplies the relay
1655+// client and WebSocket lifecycle.
1656+package gitrelay
1657diff --git a/internal/gitrelay/gitrelay.go b/internal/gitrelay/gitrelay.go
1658index 310d6b9..e030113 100644
1659--- a/internal/gitrelay/gitrelay.go
1660+++ b/internal/gitrelay/gitrelay.go
1661@@ -1,6 +1,3 @@
1662-// Package gitrelay provides the self-hosted GRASP smart-HTTP boundary. Git
1663-// own receive-pack/upload-pack remain the object and pack implementation;
1664-// signed Nostr repository events remain the authority for refs.
1665 package gitrelay
1666
1667 import (
1668@@ -17,6 +14,7 @@ import (
1669 "github.com/FelineStateMachine/tinyrelay/internal/storage"
1670 )
1671
1672+// Repository is the resolved authority and Git state for one hosted project.
1673 type Repository struct {
1674 Owner string
1675 Identifier string
1676@@ -48,6 +46,7 @@ type MaintainerSource interface {
1677 IsMaintainer(ctx context.Context, r Repository, pubkey string) bool
1678 }
1679
1680+// Config supplies the event store, Git root, authorization and transport hooks.
1681 type Config struct {
1682 Store *storage.Store
1683 Root string
1684@@ -63,15 +62,15 @@ type Config struct {
1685 // advertised clone URLs. It is optional for an embedded relay.
1686 ServiceURL string
1687 // PublicURL is the canonical HTTPS URL used in repository advertisements;
1688- // ServiceURL remains accepted as a compatibility alias.
1689+ // ServiceURL is used when PublicURL is empty.
1690 PublicURL string
1691 // EnableGRASP06 enables the alternative PR repository surface. It is kept
1692 // opt-in because publishing a capability creates an interoperability
1693 // promise beyond the base GRASP-01 HTTP endpoint.
1694 EnableGRASP06 bool
1695- // AllowMissingObjects is retained for configuration compatibility. Missing
1696- // objects always enter the durable pending path; this flag no longer makes
1697- // incomplete state visible.
1698+ // AllowMissingObjects is ignored.
1699+ // Missing objects enter the durable pending path and stay hidden until
1700+ // receive-pack or repair supplies every advertised tip.
1701 AllowMissingObjects bool
1702 // AllowPrivateRelays permits operator-controlled private DNS targets for
1703 // self-hosted networks. Public deployments should leave it disabled.
1704@@ -92,6 +91,7 @@ type Config struct {
1705 OnPromote func(context.Context, string, Repository) error
1706 }
1707
1708+// GitRelay coordinates signed repository events with native bare repositories.
1709 type GitRelay struct {
1710 store *storage.Store
1711 root string
1712diff --git a/internal/mcp/doc.go b/internal/mcp/doc.go
1713new file mode 100644
1714index 0000000..9ec13a1
1715--- /dev/null
1716+++ b/internal/mcp/doc.go
1717@@ -0,0 +1,11 @@
1718+// Package mcp serves the Model Context Protocol revision 2026-07-28 through
1719+// its stateless Streamable HTTP binding.
1720+//
1721+// Server accepts one JSON-RPC request or notification per POST. Protocol
1722+// version, method, tool name and selected resource name are mirrored in HTTP
1723+// headers and checked against the JSON body. Registry keeps tools in
1724+// registration order, validates their JSON Schema subset, and invokes each
1725+// Handler with an authenticated actor and request context. Tool failures are
1726+// returned as execution results; transport and validation failures use
1727+// JSON-RPC errors.
1728+package mcp
1729diff --git a/internal/mcp/mcp.go b/internal/mcp/mcp.go
1730index 086cc4d..cdbadde 100644
1731--- a/internal/mcp/mcp.go
1732+++ b/internal/mcp/mcp.go
1733@@ -1,7 +1,3 @@
1734-// Package mcp implements the Model Context Protocol, revision 2026-07-28, in
1735-// the stateless form of its Streamable HTTP binding: one endpoint that
1736-// accepts POST, no sessions, no initialize handshake, and every request
1737-// carrying its protocol version in both the headers and the body.
1738 package mcp
1739
1740 import (
1741@@ -14,9 +10,7 @@ import (
1742 // Version is the protocol revision this package implements.
1743 const Version = "2026-07-28"
1744
1745-// SupportedVersions lists the protocol revisions the server accepts. Older
1746-// revisions negotiated a session with an initialize handshake and are not
1747-// served.
1748+// SupportedVersions lists the protocol revisions the server accepts.
1749 var SupportedVersions = []string{Version}
1750
1751 // Header names mirrored from the JSON-RPC body so intermediaries can route
1752diff --git a/internal/mcp/server.go b/internal/mcp/server.go
1753index f65cddc..f82efd3 100644
1754--- a/internal/mcp/server.go
1755+++ b/internal/mcp/server.go
1756@@ -62,8 +62,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.Handle(w,
1757 func (s *Server) Handle(w http.ResponseWriter, r *http.Request) Observation {
1758 w.Header().Set("Cache-Control", "no-store")
1759 if r.Method != http.MethodPost {
1760- // GET opened a server stream and DELETE ended a session in earlier
1761- // revisions. Neither exists in this one.
1762 w.Header().Set("Allow", "POST")
1763 http.Error(w, "method not allowed: the MCP endpoint accepts POST", http.StatusMethodNotAllowed)
1764 return Observation{Outcome: OutcomeInvalid, Status: http.StatusMethodNotAllowed}
1765diff --git a/internal/policy/access.go b/internal/policy/access.go
1766index afa16cf..96f7b0d 100644
1767--- a/internal/policy/access.go
1768+++ b/internal/policy/access.go
1769@@ -13,6 +13,8 @@ type Access struct {
1770 Owner bool
1771 }
1772
1773+// CanRead applies the read rule, kind blocks, private repository rules and
1774+// recipient addressing to one event.
1775 func CanRead(p Policy, e event.Event, a Access) bool {
1776 if containsInt(p.BlockedKinds, e.Kind) {
1777 return false
1778@@ -44,6 +46,8 @@ func IsPrivateRepository(e event.Event) bool {
1779 return false
1780 }
1781
1782+// CanWrite applies kind blocks, allowlists and the tenant write rule to one
1783+// event.
1784 func CanWrite(p Policy, e event.Event, a Access) bool {
1785 if containsInt(p.BlockedKinds, e.Kind) {
1786 return false
1787diff --git a/internal/policy/doc.go b/internal/policy/doc.go
1788new file mode 100644
1789index 0000000..43693b4
1790--- /dev/null
1791+++ b/internal/policy/doc.go
1792@@ -0,0 +1,4 @@
1793+// Package policy defines the durable tenant policy and the access decisions
1794+// derived from it. Policies are JSON values validated at their boundary;
1795+// access checks receive the caller's authenticated keys and membership facts.
1796+package policy
1797diff --git a/internal/policy/policy.go b/internal/policy/policy.go
1798index 40ac263..07142b6 100644
1799--- a/internal/policy/policy.go
1800+++ b/internal/policy/policy.go
1801@@ -1,4 +1,3 @@
1802-// Package policy contains the durable, host-independent policy for a relay.
1803 package policy
1804
1805 import (
1806@@ -10,11 +9,13 @@ import (
1807 "strings"
1808 )
1809
1810+// Succession describes the account handoff after a period of inactivity.
1811 type Succession struct {
1812 Heir string `json:"heir"`
1813 AfterDays int `json:"afterDays"`
1814 }
1815
1816+// Notify selects tenant notification categories.
1817 type Notify struct {
1818 Reports bool `json:"reports"`
1819 Jobs bool `json:"jobs"`
1820@@ -22,16 +23,19 @@ type Notify struct {
1821 Digest bool `json:"digest"`
1822 }
1823
1824+// MemberInvites controls invitation depth and quota.
1825 type MemberInvites struct {
1826 Depth int `json:"depth"`
1827 Quota int `json:"quota"`
1828 }
1829
1830+// Sites controls hosted site publication and mirroring.
1831 type Sites struct {
1832 Enabled bool `json:"enabled"`
1833 Mirror bool `json:"mirror"`
1834 }
1835
1836+// Features selects relay protocols and tenant capabilities.
1837 type Features struct {
1838 Search string `json:"search"`
1839 Sync bool `json:"sync"`
1840@@ -57,10 +61,12 @@ type Features struct {
1841 Jobs bool `json:"jobs"`
1842 }
1843
1844+// Delivery controls event delivery services.
1845 type Delivery struct {
1846 Enabled bool `json:"enabled"`
1847 }
1848
1849+// Inbox controls targeted inbox admission.
1850 type Inbox struct {
1851 Targeted bool `json:"targeted"`
1852 }
1853@@ -71,6 +77,7 @@ type FileLimits struct {
1854 UserStorageBytes int64 `json:"userStorageBytes"`
1855 }
1856
1857+// CustomHost records a tenant-managed host and its provisioning state.
1858 type CustomHost struct {
1859 Host string `json:"host"`
1860 ID string `json:"id"`
1861@@ -80,13 +87,15 @@ type CustomHost struct {
1862 SSLStatus string `json:"sslStatus"`
1863 }
1864
1865+// RetentionRule assigns a retention period to one kind; negative kinds are
1866+// used as the wildcard rule by the gate.
1867 type RetentionRule struct {
1868 Kind int `json:"kind"`
1869 Days int `json:"days"`
1870 }
1871
1872-// Policy is the tenant policy. Host capacity and scheduling controls are
1873-// intentionally outside this type; they are not tenant entitlements.
1874+// Policy is the durable policy for one tenant. It combines identity and
1875+// presentation settings, access rules, feature switches and storage limits.
1876 type Policy struct {
1877 Owner string `json:"owner"`
1878 CustomHosts []CustomHost `json:"customHosts"`
1879@@ -142,9 +151,8 @@ const DefaultRooms = 64
1880 // name one.
1881 const DefaultCallbacks = 4
1882
1883-// UnmarshalJSON keeps the room and callback allowances and the long task
1884-// switch at their defaults when a stored policy predates the fields, so
1885-// existing tenants do not lose room creation, callbacks or job traffic.
1886+// UnmarshalJSON applies the current defaults for room, callback and job
1887+// settings before decoding a stored policy.
1888 func (p *Policy) UnmarshalJSON(data []byte) error {
1889 type plain Policy
1890 decoded := plain{Rooms: DefaultRooms, Callbacks: DefaultCallbacks}
1891@@ -162,6 +170,8 @@ func Defaults(owner string) Policy {
1892 Notify: Notify{}, Views: map[string]string{}, Tags: []string{}, LanguageTags: []string{}, RelayCountries: []string{}, OpenKinds: []int{}, BlockedWords: []string{}, PushCallbacks: []string{}, Delivery: Delivery{}}
1893 }
1894
1895+// Validate checks policy enum values, URL fields, feature dependencies and
1896+// numeric bounds.
1897 func Validate(p Policy) error {
1898 if p.FileLimits.MaxFileBytes < 0 || p.FileLimits.UserStorageBytes < 0 {
1899 return errors.New("fileLimits: byte allowances cannot be negative")
1900diff --git a/internal/policy/privacy.go b/internal/policy/privacy.go
1901index 714ef0e..3851745 100644
1902--- a/internal/policy/privacy.go
1903+++ b/internal/policy/privacy.go
1904@@ -12,13 +12,11 @@ import (
1905 // replaceable repository state events. It keeps the policy package independent
1906 // from the storage implementation.
1907 type PrivateRepositoryLookup interface {
1908- IsPrivateRepository(context.Context, string, string) (bool, error)
1909+ IsPrivateRepository(ctx context.Context, pubkey, identifier string) (bool, error)
1910 }
1911
1912-// PrivateServiceEnabled reports whether the tenant can serve the complete
1913-// GRASP-08 private service contract. Valid policies already enforce these
1914-// implications, but keeping the predicate here gives callers one definition
1915-// when inspecting policy values during startup or migration.
1916+// PrivateServiceEnabled reports whether the tenant has the complete GRASP-08
1917+// private service contract enabled.
1918 func (p Policy) PrivateServiceEnabled() bool {
1919 return p.Features.Grasp && p.Features.Grasp08 && p.Reads == "members"
1920 }
1921diff --git a/internal/records/doc.go b/internal/records/doc.go
1922new file mode 100644
1923index 0000000..30bc75a
1924--- /dev/null
1925+++ b/internal/records/doc.go
1926@@ -0,0 +1,15 @@
1927+// Package records owns durable relay identity, signed protocol records,
1928+// projections, views, notifications and succession state.
1929+//
1930+// New initializes the records schema and loads the relay signing key from the
1931+// tenant settings. Config.Community is required because membership and
1932+// moderation projections are part of the records service's read contract.
1933+//
1934+// Signed records that advance the durable records clock run under the service
1935+// signing lock and persist the clock before invoking Config.OnGenerated.
1936+// OnGenerated runs after that transaction commits and commonly persists or
1937+// publishes the returned event. Its error is returned to the caller; the clock
1938+// remains durable even when the callback fails.
1939+// GenerateEphemeral signs a response without advancing the durable clock or
1940+// invoking OnGenerated.
1941+package records
1942diff --git a/internal/records/records.go b/internal/records/records.go
1943index aab41bd..1aed113 100644
1944--- a/internal/records/records.go
1945+++ b/internal/records/records.go
1946@@ -1,6 +1,3 @@
1947-// Package records owns the durable records that a relay signs about itself.
1948-// It deliberately keeps the relay secret inside the tenant settings table and
1949-// exposes only public keys and generated events to callers.
1950 package records
1951
1952 import (
1953@@ -18,21 +15,26 @@ import (
1954 )
1955
1956 type Config struct {
1957- Store *storage.Store
1958- Policy func() policy.Policy
1959- SetPolicy func(policy.Policy) error
1960- RelayURL string
1961- GroupID string
1962- OnGenerated func(context.Context, event.Event) error
1963+ // Store holds the tenant database and records settings.
1964+ Store *storage.Store
1965+ // Policy returns the current durable tenant policy when records evaluates
1966+ // access or builds a projection.
1967+ Policy func() policy.Policy
1968+ // SetPolicy persists a policy change requested by records management.
1969+ SetPolicy func(policy.Policy) error
1970+ RelayURL string
1971+ GroupID string
1972+ // OnGenerated receives a signed event after its records clock transaction
1973+ // commits. The callback commonly persists, publishes or queues the event.
1974+ OnGenerated func(context.Context, event.Event) error
1975+ // DeliverNotification delivers a sealed notification event to its target.
1976 DeliverNotification func(context.Context, event.Event, string) error
1977 // PushNotification receives a plaintext summary of each notification for
1978 // device delivery. The gift wrap itself is opaque to the relay once sealed.
1979 PushNotification func(context.Context, string, string, string, string) error
1980 OnTransfer func(context.Context, string, string) error
1981- // Community supplies membership and moderation projections. Keeping these
1982- // reads behind callbacks prevents records from depending on community's
1983- // table layout. A nil reader is reserved for isolated records use; hosts
1984- // serving community projections must provide it.
1985+ // Community supplies membership and moderation projections through the
1986+ // CommunityReader boundary.
1987 Community CommunityReader
1988 }
1989
1990@@ -40,9 +42,9 @@ type Config struct {
1991 // Implementations own the community schema and may use a transaction-backed
1992 // snapshot when the host is publishing a projection.
1993 type CommunityReader interface {
1994- Members(context.Context) ([]communityread.Member, error)
1995- ModerationCounts(context.Context, int64) (communityread.ModerationCounts, error)
1996- MemberStatus(context.Context, string) (member bool, memberCount int, err error)
1997+ Members(ctx context.Context) ([]communityread.Member, error)
1998+ ModerationCounts(ctx context.Context, since int64) (communityread.ModerationCounts, error)
1999+ MemberStatus(ctx context.Context, pubkey string) (member bool, memberCount int, err error)
2000 }
2001
2002 type Member = communityread.Member
2003diff --git a/internal/relay/doc.go b/internal/relay/doc.go
2004new file mode 100644
2005index 0000000..dad3541
2006--- /dev/null
2007+++ b/internal/relay/doc.go
2008@@ -0,0 +1,10 @@
2009+// Package relay is the WebSocket transport boundary for the relay.
2010+//
2011+// Router owns connection lifecycle, authentication messages, subscriptions,
2012+// request and count queries, event publication and NIP-77 negotiation. A
2013+// Backend supplies durable publication and query behavior; optional interfaces
2014+// add reader privacy, query hints, policy invalidation and sync snapshots.
2015+// Router serializes publication with subscription registration and snapshots,
2016+// so a live event cannot pass a historical query boundary. Close stops every
2017+// connection and waits for its reader and writer goroutines to finish.
2018+package relay
2019diff --git a/internal/relay/relay.go b/internal/relay/relay.go
2020index 4716205..1965073 100644
2021--- a/internal/relay/relay.go
2022+++ b/internal/relay/relay.go
2023@@ -1,6 +1,3 @@
2024-// Package relay owns the websocket protocol boundary. It deliberately keeps
2025-// storage and policy behind Backend so the wire protocol cannot accidentally
2026-// become the tenant's data model.
2027 package relay
2028
2029 import (
2030@@ -23,56 +20,80 @@ import (
2031 )
2032
2033 type Session struct {
2034- PubKeys []string
2035+ // PubKeys contains public keys established by the connection's AUTH flow.
2036+ PubKeys []string
2037+ // RelayURL is the relay identity used when authenticating and publishing.
2038 RelayURL string
2039+ // RemoteIP is the peer address observed by the HTTP server.
2040 RemoteIP string
2041 }
2042
2043-// Backend publishes while the Router's write gate is held. Publish must make
2044-// the event and any delivery intent durable before returning nil.
2045+// Backend publishes while the Router's write gate is held. Publish performs
2046+// the backend's admission, persistence, duplicate handling or control action
2047+// before returning. A nil error allows Router to fan out the event unless the
2048+// returned reason identifies a duplicate. Persistence and delivery planning
2049+// remain backend responsibilities for event kinds that require them.
2050 type Backend interface {
2051- Publish(context.Context, event.Event, Session) (string, error)
2052- Query(context.Context, []event.Filter, Session) ([]event.Event, error)
2053- Count(context.Context, []event.Filter, Session) (any, error)
2054+ Publish(ctx context.Context, event event.Event, session Session) (reason string, err error)
2055+ Query(ctx context.Context, filters []event.Filter, session Session) ([]event.Event, error)
2056+ Count(ctx context.Context, filters []event.Filter, session Session) (any, error)
2057 }
2058
2059 // Reader-side privacy can be stricter than the query filter. It is checked for
2060 // both historical and live delivery, including encrypted signer traffic.
2061 type Reader interface {
2062- CanRead(event.Event, Session) bool
2063+ CanRead(event event.Event, session Session) bool
2064 }
2065
2066+// FilterReader applies reader privacy with the subscription filter available
2067+// to the policy decision.
2068 type FilterReader interface {
2069- CanReadFilter(event.Event, Session, *event.Filter) bool
2070+ CanReadFilter(event event.Event, session Session, filter *event.Filter) bool
2071 }
2072
2073+// QueryHints can return query results together with completion hints. Router
2074+// places these values in the EOSE response; the relay backend uses values such
2075+// as "finish", "more" and "auth".
2076 type QueryHints interface {
2077- QueryHints(context.Context, []event.Filter, Session) ([]event.Event, []string, error)
2078+ QueryHints(ctx context.Context, filters []event.Filter, session Session) ([]event.Event, []string, error)
2079 }
2080
2081-// Policy changes can invalidate subscriptions. The Router removes them from
2082-// clients immediately; the optional callback lets storage invalidate caches.
2083-type PolicyChanges interface{ CloseSubscriptions(Session) }
2084+// PolicyChanges can invalidate subscriptions in storage after an access
2085+// policy revision. Router removes client state before invoking the callback.
2086+type PolicyChanges interface {
2087+ CloseSubscriptions(session Session)
2088+}
2089
2090 // SyncBackend supplies the authorized event snapshot for a NIP-77 session.
2091 // The Router holds its read gate while calling Sync, so a concurrent Publish
2092 // cannot create an opening between snapshot selection and NEG-OPEN.
2093 type SyncBackend interface {
2094- Sync(context.Context, event.Filter, Session) ([]syncprotocol.Item, error)
2095+ Sync(ctx context.Context, filter event.Filter, session Session) ([]syncprotocol.Item, error)
2096 }
2097
2098 type Config struct {
2099- RelayURL string
2100+ // RelayURL is the default relay identity included in sessions.
2101+ RelayURL string
2102+ // RequestRelayURL optionally derives the relay identity from each request.
2103 RequestRelayURL func(*http.Request) string
2104- OnAuthenticate func(context.Context, Session) error
2105+ // OnAuthenticate checks the authenticated session candidate and may persist
2106+ // authentication bookkeeping. The Session argument is a value snapshot;
2107+ // Router records the newly authenticated public key after this callback.
2108+ OnAuthenticate func(context.Context, Session) error
2109+ // MaxMessageBytes limits websocket text messages. Zero leaves the limit
2110+ // unlimited; a negative value selects the 512 KiB default.
2111 MaxMessageBytes int64
2112+ // MaxPendingBytes bounds queued output per connection.
2113 MaxPendingBytes int
2114- ReadLimit time.Duration
2115- WriteLimit time.Duration
2116- PingInterval time.Duration
2117- OriginPatterns []string
2118- OnConnection func(int)
2119- OnSubscription func(int)
2120+ // ReadLimit, WriteLimit and PingInterval control websocket liveness.
2121+ ReadLimit time.Duration
2122+ WriteLimit time.Duration
2123+ PingInterval time.Duration
2124+ // OriginPatterns is passed to the websocket origin check.
2125+ OriginPatterns []string
2126+ // OnConnection and OnSubscription receive +1 and -1 lifecycle deltas.
2127+ OnConnection func(int)
2128+ OnSubscription func(int)
2129 }
2130
2131 func (c Config) withDefaults() Config {
2132diff --git a/internal/replication/backup.go b/internal/replication/backup.go
2133index 53aadc3..be2b6d8 100644
2134--- a/internal/replication/backup.go
2135+++ b/internal/replication/backup.go
2136@@ -18,8 +18,10 @@ import (
2137 "github.com/FelineStateMachine/tinyrelay/internal/storage"
2138 )
2139
2140+// BackupFormat identifies the JSON archive format produced by this package.
2141 const BackupFormat = "tinyrelay/backup/1"
2142
2143+// BackupArchive is a sealed event and application-state export.
2144 type BackupArchive struct {
2145 Format string `json:"format"`
2146 Events []event.Event `json:"events"`
2147@@ -28,6 +30,7 @@ type BackupArchive struct {
2148 Created int64 `json:"created_at"`
2149 }
2150
2151+// BackupState contains application-owned state and exported objects.
2152 type BackupState struct {
2153 Config json.RawMessage `json:"config,omitempty"`
2154 Community json.RawMessage `json:"community,omitempty"`
2155@@ -39,12 +42,14 @@ type BackupState struct {
2156 Database []BackupTable `json:"database,omitempty"`
2157 }
2158
2159+// BackupTable is a named table snapshot represented as JSON rows.
2160 type BackupTable struct {
2161 Name string `json:"name"`
2162 Columns []string `json:"columns"`
2163 Rows [][]json.RawMessage `json:"rows"`
2164 }
2165
2166+// BackupObject is an exported named file or blob with an integrity digest.
2167 type BackupObject struct {
2168 Name string `json:"name"`
2169 SHA256 string `json:"sha256"`
2170@@ -54,18 +59,23 @@ type BackupObject struct {
2171 Uploaded int64 `json:"uploaded,omitempty"`
2172 }
2173
2174+// BackupProvider snapshots and restores application state associated with a
2175+// relay database. The provider owns its external resources.
2176 type BackupProvider interface {
2177- Snapshot(context.Context) (BackupState, error)
2178- Restore(context.Context, BackupState) error
2179+ Snapshot(ctx context.Context) (BackupState, error)
2180+ Restore(ctx context.Context, state BackupState) error
2181 }
2182
2183+// FullBackupProvider can produce a complete archive using its own snapshot
2184+// coordination.
2185 type FullBackupProvider interface {
2186 BackupProvider
2187- SnapshotArchive(context.Context, int64) (BackupArchive, error)
2188+ SnapshotArchive(ctx context.Context, created int64) (BackupArchive, error)
2189 }
2190
2191+// RestoreFinalizer runs application cleanup after a successful restore.
2192 type RestoreFinalizer interface {
2193- FinalizeRestore(context.Context) error
2194+ FinalizeRestore(ctx context.Context) error
2195 }
2196
2197 func CreateBackup(ctx context.Context, store *storage.Store, now int64) (BackupArchive, error) {
2198diff --git a/internal/replication/doc.go b/internal/replication/doc.go
2199new file mode 100644
2200index 0000000..2904766
2201--- /dev/null
2202+++ b/internal/replication/doc.go
2203@@ -0,0 +1,19 @@
2204+// Package replication plans and executes durable relay synchronization.
2205+//
2206+// Planning is separate from transport. RelayDirectory and Discovery provide
2207+// routing inputs; PrepareIntents turns them into idempotent storage.Intent
2208+// values that can be committed with the event that caused them. Delivery,
2209+// PullTransport and PushTransport perform network operations and are owned by
2210+// the application that constructs the Service or calls the standalone sync
2211+// functions. Those callers retain ownership of their transports and stores.
2212+//
2213+// Service owns replication's durable work queue and job state. Handlers can be
2214+// composed with other work handlers and run by work.RunPool. A handler error
2215+// is retried by the work package with backoff; a terminal error can be marked
2216+// with work.Stop. Successful recurring jobs enqueue a new intent, preserving a
2217+// completed record for the prior run.
2218+//
2219+// WebSocket transports validate relay URLs and public address resolution before
2220+// dialing. Backup and artifact helpers write complete files through temporary
2221+// files and same-directory rename.
2222+package replication
2223diff --git a/internal/replication/jobs.go b/internal/replication/jobs.go
2224index caf5b65..cde513a 100644
2225--- a/internal/replication/jobs.go
2226+++ b/internal/replication/jobs.go
2227@@ -13,6 +13,7 @@ import (
2228 "github.com/FelineStateMachine/tinyrelay/internal/work"
2229 )
2230
2231+// JobKind selects the durable replication operation.
2232 type JobKind string
2233
2234 const (
2235@@ -24,6 +25,8 @@ const (
2236 JobBackup JobKind = "backup"
2237 )
2238
2239+// JobSpec describes one immediate or recurring replication job. Every is an
2240+// interval in hours; zero runs once.
2241 type JobSpec struct {
2242 ID string `json:"id"`
2243 Kind JobKind `json:"kind"`
2244@@ -33,10 +36,12 @@ type JobSpec struct {
2245 DiscoverPubKey string `json:"discoverPubKey,omitempty"`
2246 }
2247
2248+// JobRunner executes a validated job specification.
2249 type JobRunner interface {
2250 Run(ctx context.Context, spec JobSpec) error
2251 }
2252
2253+// JobHandler decodes and validates durable job intents before delegating them.
2254 type JobHandler struct {
2255 runner JobRunner
2256 }
2257diff --git a/internal/replication/replication.go b/internal/replication/replication.go
2258index 56c6c83..61fc385 100644
2259--- a/internal/replication/replication.go
2260+++ b/internal/replication/replication.go
2261@@ -1,7 +1,3 @@
2262-// Package replication contains durable self-hosted relay fanout and job
2263-// planning. Network I/O is deliberately behind small interfaces so a relay
2264-// can use local test transports, a normal websocket client, or an operator's
2265-// proxy without changing queue semantics.
2266 package replication
2267
2268 import (
2269@@ -16,6 +12,8 @@ import (
2270 "github.com/FelineStateMachine/tinyrelay/internal/storage"
2271 )
2272
2273+// Origin identifies how an event entered the relay and controls whether it is
2274+// eligible for outbound fanout.
2275 type Origin string
2276
2277 const (
2278@@ -26,8 +24,8 @@ const (
2279 OriginBackupRestore Origin = "backup_restore"
2280 )
2281
2282-// Policy contains only routing decisions. It has no lease, fuel, or product
2283-// quota fields; the host controls process capacity independently.
2284+// Policy supplies current fanout decisions and relay identity. The host
2285+// controls process capacity independently.
2286 type Policy struct {
2287 Enabled bool
2288 ReadMembersOnly bool
2289@@ -35,26 +33,31 @@ type Policy struct {
2290 SelfRelayURL string
2291 }
2292
2293+// RelayDirectory returns write relays for an author and read relays for a
2294+// recipient. Returned URLs are filtered by PrepareIntents.
2295 type RelayDirectory interface {
2296 WriteRelays(pubkey string) []string
2297 ReadRelays(pubkey string) []string
2298 }
2299
2300-// RelayListDiscovery preserves the source order of all NIP-65 relay tags for
2301+// RelayListDiscovery returns the source-ordered NIP-65 relay list used by
2302 // owner backfill. RelayDirectory's directional methods remain sufficient for
2303 // delivery planning.
2304 type RelayListDiscovery interface {
2305 RelayList(pubkey string) []string
2306 }
2307
2308+// Discovery resolves write relays for an author.
2309 type Discovery interface {
2310 DiscoverRelays(ctx context.Context, pubkey string) ([]string, error)
2311 }
2312
2313+// ReadDiscovery resolves read relays for a recipient.
2314 type ReadDiscovery interface {
2315 DiscoverReadRelays(ctx context.Context, pubkey string) ([]string, error)
2316 }
2317
2318+// IntentPayload records the event origin in a delivery intent.
2319 type IntentPayload struct {
2320 Origin Origin `json:"origin"`
2321 }
2322@@ -173,10 +176,14 @@ func canonicalRelayURL(raw string) string {
2323 return scheme + "://" + host + path + "?" + u.RawQuery
2324 }
2325
2326+// DeliveryTransport sends one event to a relay target. The implementation
2327+// owns connection reuse, request deadlines and transport shutdown.
2328 type DeliveryTransport interface {
2329 Send(ctx context.Context, target string, e event.Event) (DeliveryResult, error)
2330 }
2331
2332+// DeliveryResult reports the relay's acceptance response. Duplicate
2333+// acknowledgments are treated as successful delivery by the handlers.
2334 type DeliveryResult struct {
2335 Accepted bool
2336 Message string
2337diff --git a/internal/replication/service.go b/internal/replication/service.go
2338index 81ab8fb..352d3c2 100644
2339--- a/internal/replication/service.go
2340+++ b/internal/replication/service.go
2341@@ -16,6 +16,10 @@ import (
2342 "github.com/FelineStateMachine/tinyrelay/internal/work"
2343 )
2344
2345+// Config supplies a Service's store, routing policy, transports and optional
2346+// application hooks. Store is required. DataDir defaults to the store's
2347+// directory; Workers defaults to work.RunPool's worker count when Run is used.
2348+// The caller retains ownership of the supplied transports and store.
2349 type Config struct {
2350 Store *storage.Store
2351 Policy Policy
2352@@ -36,12 +40,14 @@ type Config struct {
2353 Workers int
2354 }
2355
2356+// Service owns replication job state and the durable queue built on Store.
2357 type Service struct {
2358 store *storage.Store
2359 queue *work.Queue
2360 config Config
2361 }
2362
2363+// NewService initializes replication job tables and returns a service.
2364 func NewService(config Config) (*Service, error) {
2365 if config.Store == nil {
2366 return nil, errors.New("replication: store is required")
2367@@ -55,8 +61,10 @@ func NewService(config Config) (*Service, error) {
2368 return &Service{store: config.Store, queue: work.New(config.Store), config: config}, nil
2369 }
2370
2371+// Store returns the service's supplied store.
2372 func (s *Service) Store() *storage.Store { return s.store }
2373
2374+// Queue returns the service's durable replication queue.
2375 func (s *Service) Queue() *work.Queue { return s.queue }
2376
2377 func (s *Service) Prepare(e event.Event, origin Origin) []storage.Intent {
2378diff --git a/internal/replication/sync.go b/internal/replication/sync.go
2379index 2829817..234be00 100644
2380--- a/internal/replication/sync.go
2381+++ b/internal/replication/sync.go
2382@@ -9,14 +9,19 @@ import (
2383 "github.com/FelineStateMachine/tinyrelay/internal/storage"
2384 )
2385
2386+// PullTransport reads events from target. The transport owns network clients
2387+// and their lifecycle; PullOnce owns validation and storage.
2388 type PullTransport interface {
2389 Query(ctx context.Context, target string, filter event.Filter) ([]event.Event, error)
2390 }
2391
2392+// PushTransport sends one event to target. PushAfter advances its cursor only
2393+// after Send returns, including a relay duplicate acknowledgment.
2394 type PushTransport interface {
2395 Send(ctx context.Context, target string, e event.Event) (DeliveryResult, error)
2396 }
2397
2398+// SyncStats summarizes one pull or push pass.
2399 type SyncStats struct {
2400 Stored int
2401 Duplicates int
2402diff --git a/internal/replication/websocket.go b/internal/replication/websocket.go
2403index d1425f9..4201611 100644
2404--- a/internal/replication/websocket.go
2405+++ b/internal/replication/websocket.go
2406@@ -18,20 +18,28 @@ import (
2407 "github.com/coder/websocket"
2408 )
2409
2410+// Socket is the message-oriented connection used by NostrTransport. The
2411+// caller that receives a Socket owns it and closes it when the exchange ends.
2412 type Socket interface {
2413- Read(context.Context) ([]byte, error)
2414- Write(context.Context, []byte) error
2415- Close(error) error
2416+ Read(ctx context.Context) ([]byte, error)
2417+ Write(ctx context.Context, message []byte) error
2418+ Close(cause error) error
2419 }
2420
2421+// Dialer opens a Socket for one relay target and returns its HTTP handshake.
2422 type Dialer interface {
2423- Dial(context.Context, string) (Socket, *http.Response, error)
2424+ Dial(ctx context.Context, target string) (Socket, *http.Response, error)
2425 }
2426
2427+// IPResolver resolves relay hostnames for address-policy checks and dialing.
2428 type IPResolver interface {
2429- LookupIPAddr(context.Context, string) ([]net.IPAddr, error)
2430+ LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error)
2431 }
2432
2433+// WebsocketDialer validates websocket targets, blocks private addresses unless
2434+// AllowPrivate is set and optionally answers NIP-42 authentication challenges.
2435+// HTTPClient and Resolver are caller-supplied dependencies and remain owned by
2436+// the caller.
2437 type WebsocketDialer struct {
2438 AllowPrivate bool
2439 MaxMessageBytes int64
2440@@ -250,6 +258,9 @@ func (s coderSocket) Close(err error) error {
2441 return s.conn.Close(code, "replication complete")
2442 }
2443
2444+// NostrTransport implements PullTransport and PushTransport over Nostr
2445+// websocket protocols. Dialer defaults to WebsocketDialer when unset, and
2446+// Timeout defaults to 20 seconds.
2447 type NostrTransport struct {
2448 Dialer Dialer
2449 Timeout time.Duration
2450diff --git a/internal/sites/doc.go b/internal/sites/doc.go
2451new file mode 100644
2452index 0000000..ee61705
2453--- /dev/null
2454+++ b/internal/sites/doc.go
2455@@ -0,0 +1,21 @@
2456+// Package sites implements NIP-5A site manifests and serves their content.
2457+//
2458+// A manifest event maps URL paths to blob hashes. SiteLabel and ParseSite
2459+// translate the three supported labels: an owner's npub, a named site, and a
2460+// snapshot. ValidateManifest checks path entries, aggregate hashes and
2461+// lineage before a manifest is indexed.
2462+//
2463+// Service keeps the label-to-event index in the site_manifests table of the
2464+// supplied storage.Store. New creates that table and its deletion trigger.
2465+// SaveOptions supplies the storage transaction hook that updates the index
2466+// with an accepted event and, when mirroring is enabled by policy, queues a
2467+// site-mirror intent. ApplyTx and DeleteTx are the corresponding transaction
2468+// hooks for callers that already own the event transaction.
2469+//
2470+// Mirror reads missing files from the servers advertised by a manifest,
2471+// verifies each response against its declared hash and stores it through the
2472+// configured BlobPut callback. RunMirrorIntent resolves the event captured by
2473+// storage and retries that idempotent operation. Handler serves only GET and
2474+// HEAD content after host, policy and optional ReadAccess checks; the manifest
2475+// remains the authority for path selection and discovery responses.
2476+package sites
2477diff --git a/internal/sites/service.go b/internal/sites/service.go
2478index 1d7aedf..92f1fe0 100644
2479--- a/internal/sites/service.go
2480+++ b/internal/sites/service.go
2481@@ -23,19 +23,34 @@ import (
2482 "github.com/FelineStateMachine/tinyrelay/internal/storage"
2483 )
2484
2485+// Blob is a fetched site file and its declared content metadata.
2486 type Blob struct {
2487 Body io.ReadCloser
2488 Type string
2489 Size int64
2490 }
2491
2492-type BlobGet func(context.Context, string) (Blob, error)
2493-type BlobPut func(context.Context, string, string, io.Reader) error
2494-type Fetch func(context.Context, *http.Request) (*http.Response, error)
2495-type ResolveIP func(context.Context, string) ([]net.IP, error)
2496-type ResolveHost func(context.Context, string) (string, error)
2497-type ReadAccess func(*http.Request) bool
2498+// BlobGet opens a local blob by its SHA-256 hash. The caller closes Blob.Body.
2499+type BlobGet func(ctx context.Context, blobSHA string) (Blob, error)
2500
2501+// BlobPut stores bytes under blobSHA with the supplied content type. The
2502+// callback consumes the reader before returning.
2503+type BlobPut func(ctx context.Context, blobSHA string, contentType string, body io.Reader) error
2504+
2505+// Fetch performs an outbound site file request. The caller closes the
2506+// returned response body.
2507+type Fetch func(ctx context.Context, request *http.Request) (*http.Response, error)
2508+
2509+// ResolveIP resolves a host before an outbound site request is allowed.
2510+type ResolveIP func(ctx context.Context, host string) ([]net.IP, error)
2511+
2512+// ResolveHost resolves a request host to a site label.
2513+type ResolveHost func(ctx context.Context, host string) (label string, err error)
2514+
2515+// ReadAccess authorizes a request for site content.
2516+type ReadAccess func(request *http.Request) bool
2517+
2518+// Config supplies storage, blob, network and request policy dependencies.
2519 type Config struct {
2520 Store *storage.Store
2521 GetBlob BlobGet
2522diff --git a/internal/sites/sites.go b/internal/sites/sites.go
2523index 4cfa6f3..0a6531d 100644
2524--- a/internal/sites/sites.go
2525+++ b/internal/sites/sites.go
2526@@ -28,6 +28,7 @@ var (
2527 pathPattern = regexp.MustCompile(`^/(?:[^/]+/)*[^/.]+\.[^/.]+$`)
2528 )
2529
2530+// Site identifies a manifest family and its addressable key.
2531 type Site struct {
2532 Kind int
2533 PubKey string
2534@@ -35,6 +36,7 @@ type Site struct {
2535 ID string
2536 }
2537
2538+// Base36 encodes a lowercase 32-byte hexadecimal key as a fixed-width label.
2539 func Base36(value string) (string, error) {
2540 if !hexPattern.MatchString(value) {
2541 return "", errors.New("expected lowercase 32-byte hex")
2542@@ -47,6 +49,7 @@ func Base36(value string) (string, error) {
2543 return strings.Repeat("0", 50-len(value36)) + value36, nil
2544 }
2545
2546+// Unbase36 decodes a fixed-width site label into lowercase hexadecimal.
2547 func Unbase36(value string) (string, bool) {
2548 if len(value) != 50 || !regexp.MustCompile(`^[0-9a-z]+$`).MatchString(value) {
2549 return "", false
2550@@ -58,6 +61,7 @@ func Unbase36(value string) (string, bool) {
2551 return fmt.Sprintf("%064x", n), true
2552 }
2553
2554+// ParseSite parses an npub, named-site or snapshot label.
2555 func ParseSite(label string) (Site, bool) {
2556 if strings.HasPrefix(label, "npub1") {
2557 key, ok := decodeNpub(label)
2558@@ -81,6 +85,7 @@ func ParseSite(label string) (Site, bool) {
2559 return Site{Kind: KindNamedSite, PubKey: key, D: label[50:]}, true
2560 }
2561
2562+// SiteLabel returns the host label represented by a manifest event.
2563 func SiteLabel(e event.Event) string {
2564 switch e.Kind {
2565 case KindSite:
2566@@ -106,6 +111,7 @@ func SiteLabel(e event.Event) string {
2567 }
2568 }
2569
2570+// SitePaths returns the path tags in manifest order.
2571 func SitePaths(e event.Event) [][]string {
2572 paths := make([][]string, 0)
2573 for _, tag := range e.Tags {
2574@@ -116,6 +122,7 @@ func SitePaths(e event.Event) [][]string {
2575 return paths
2576 }
2577
2578+// Aggregate hashes the canonical path and blob mappings in a manifest.
2579 func Aggregate(paths [][]string) string {
2580 lines := make([]string, 0, len(paths))
2581 for _, tag := range paths {
2582@@ -128,6 +135,7 @@ func Aggregate(paths [][]string) string {
2583 return hex.EncodeToString(sum[:])
2584 }
2585
2586+// ValidateManifest checks a site manifest's paths, aggregate and lineage.
2587 func ValidateManifest(e event.Event) error {
2588 if e.Kind != KindSite && e.Kind != KindNamedSite && e.Kind != KindSiteSnapshot {
2589 return nil
2590diff --git a/internal/storage/doc.go b/internal/storage/doc.go
2591new file mode 100644
2592index 0000000..f15e209
2593--- /dev/null
2594+++ b/internal/storage/doc.go
2595@@ -0,0 +1,22 @@
2596+// Package storage owns a tenant's SQLite database and the transaction boundary
2597+// for event data, projections and durable work intents.
2598+//
2599+// Open creates the database and schema with SQLite WAL mode, full synchronous
2600+// writes, foreign-key enforcement and one database connection. Each method
2601+// uses its context for database work.
2602+//
2603+// Save applies duplicate, deletion and replacement rules, stores searchable
2604+// indexes and inserts durable work intents in one transaction. Callers validate
2605+// events before passing them to storage.
2606+// SaveTx performs the same operation inside a transaction supplied by the
2607+// caller. The caller owns that transaction and commits it after SaveTx
2608+// succeeds. WithTx provides the matching begin, commit and rollback boundary;
2609+// its callback uses the supplied *sql.Tx for all database operations.
2610+//
2611+// Query excludes expired, hidden and pending events and applies recipient
2612+// checks through QueryOptions.Access. It returns events in created-at
2613+// descending order with ID ascending as the stable tie breaker. The tenant
2614+// gate applies the remaining policy and resource visibility rules. After
2615+// reads events by their increasing insertion sequence for cursored
2616+// replication; removed events leave gaps in that sequence.
2617+package storage
2618diff --git a/internal/storage/history.go b/internal/storage/history.go
2619index 7f18ffd..b699803 100644
2620--- a/internal/storage/history.go
2621+++ b/internal/storage/history.go
2622@@ -8,6 +8,7 @@ import (
2623 "github.com/FelineStateMachine/tinyrelay/internal/event"
2624 )
2625
2626+// HistoryRow identifies one retained version of a replaceable list.
2627 type HistoryRow struct {
2628 Kind int `json:"kind"`
2629 D string `json:"d"`
2630@@ -33,21 +34,31 @@ func (s *Store) ListHistory(ctx context.Context, owner string, now int64) ([]His
2631 return result, rows.Err()
2632 }
2633
2634+// RestoreResult contains a prior list event draft and its difference from the
2635+// current list.
2636 type RestoreResult struct {
2637 Draft Draft `json:"draft"`
2638 Diff Diff `json:"diff"`
2639 }
2640+
2641+// Draft is the event body needed to publish a restored list version.
2642 type Draft struct {
2643 Kind int `json:"kind"`
2644 CreatedAt int64 `json:"created_at"`
2645 Tags [][]string `json:"tags"`
2646 Content string `json:"content"`
2647 }
2648+
2649+// Diff describes content and tag changes between the retained and current
2650+// versions.
2651 type Diff struct {
2652 AddedTags [][]string `json:"addedTags"`
2653 RemovedTags [][]string `json:"removedTags"`
2654 ContentChanged bool `json:"contentChanged"`
2655 }
2656+
2657+// RestoreOptions identifies the retained list event and the query time used
2658+// for expiration checks.
2659 type RestoreOptions struct {
2660 Owner string
2661 EventID string
2662diff --git a/internal/storage/maintenance.go b/internal/storage/maintenance.go
2663index 324405c..53742e4 100644
2664--- a/internal/storage/maintenance.go
2665+++ b/internal/storage/maintenance.go
2666@@ -10,6 +10,7 @@ import (
2667 "github.com/FelineStateMachine/tinyrelay/internal/event"
2668 )
2669
2670+// Stats reports event counts, database size and the current WAL size.
2671 type Stats struct {
2672 Events int64 `json:"events"`
2673 Bytes int64 `json:"bytes"`
2674@@ -40,6 +41,8 @@ func (s *Store) Stats(ctx context.Context) (Stats, error) {
2675 return result, nil
2676 }
2677
2678+// Vanish records a deletion horizon and removes the author's events and
2679+// related history through until, inclusive.
2680 func (s *Store) Vanish(ctx context.Context, pubkey string, until int64) error {
2681 return s.WithTx(ctx, func(tx *sql.Tx) error {
2682 statements := []struct {
2683@@ -61,6 +64,8 @@ func (s *Store) Vanish(ctx context.Context, pubkey string, until int64) error {
2684 })
2685 }
2686
2687+// SweepExpired removes expired events and history and returns the next event
2688+// expiration timestamp, or zero when none remain.
2689 func (s *Store) SweepExpired(ctx context.Context, now int64) (int64, error) {
2690 var next int64
2691 err := s.WithTx(ctx, func(tx *sql.Tx) error {
2692@@ -74,6 +79,7 @@ func (s *Store) SweepExpired(ctx context.Context, now int64) (int64, error) {
2693 return next, err
2694 }
2695
2696+// DeleteEvent removes one event by ID and reports whether a row was removed.
2697 func (s *Store) DeleteEvent(ctx context.Context, id string) (bool, error) {
2698 result, err := s.db.ExecContext(ctx, "DELETE FROM events WHERE id=?", id)
2699 if err != nil {
2700@@ -83,6 +89,7 @@ func (s *Store) DeleteEvent(ctx context.Context, id string) (bool, error) {
2701 return n > 0, err
2702 }
2703
2704+// EraseAuthor removes an author's events and retained list and wiki history.
2705 func (s *Store) EraseAuthor(ctx context.Context, pubkey string) (int64, error) {
2706 var n int64
2707 err := s.WithTx(ctx, func(tx *sql.Tx) error {
2708@@ -103,6 +110,7 @@ func (s *Store) EraseAuthor(ctx context.Context, pubkey string) (int64, error) {
2709 return n, err
2710 }
2711
2712+// GetSetting decodes a JSON setting into dest.
2713 func (s *Store) GetSetting(ctx context.Context, key string, dest any) error {
2714 var raw string
2715 if err := s.db.QueryRowContext(ctx, "SELECT value FROM settings WHERE key=?", key).Scan(&raw); err != nil {
2716@@ -114,6 +122,7 @@ func (s *Store) GetSetting(ctx context.Context, key string, dest any) error {
2717 return nil
2718 }
2719
2720+// PutSetting encodes value as JSON and upserts it under key.
2721 func (s *Store) PutSetting(ctx context.Context, key string, value any) error {
2722 return s.WithTx(ctx, func(tx *sql.Tx) error { return PutSetting(ctx, tx, key, value) })
2723 }
2724@@ -154,6 +163,7 @@ func (s *Store) PruneSettings(ctx context.Context, prefix string, keep map[strin
2725 return nil
2726 }
2727
2728+// PutSetting writes one setting using a caller-owned transaction.
2729 func PutSetting(ctx context.Context, tx *sql.Tx, key string, value any) error {
2730 raw, err := json.Marshal(value)
2731 if err != nil {
2732diff --git a/internal/storage/query.go b/internal/storage/query.go
2733index bd71504..5569b80 100644
2734--- a/internal/storage/query.go
2735+++ b/internal/storage/query.go
2736@@ -11,10 +11,16 @@ import (
2737 "github.com/FelineStateMachine/tinyrelay/internal/event"
2738 )
2739
2740+// Access controls which private and callback events a query may return. All
2741+// bypasses recipient filtering but still excludes hidden, pending and expired
2742+// rows.
2743 type Access struct {
2744 PubKeys []string
2745 All bool
2746 }
2747+
2748+// QueryOptions supplies query time, access principals, an optional result
2749+// limit and a cursor from a prior Query result.
2750 type QueryOptions struct {
2751 Now int64
2752 Access Access
2753@@ -27,6 +33,9 @@ type EventCursor struct {
2754 CreatedAt int64
2755 ID string
2756 }
2757+
2758+// QueryResult contains the matching events and whether the limit hid more
2759+// rows.
2760 type QueryResult struct {
2761 Events []event.Event
2762 More bool
2763diff --git a/internal/storage/retention.go b/internal/storage/retention.go
2764index b3c2c95..ee3db71 100644
2765--- a/internal/storage/retention.go
2766+++ b/internal/storage/retention.go
2767@@ -7,14 +7,21 @@ import (
2768 "fmt"
2769 )
2770
2771+// RetentionRule removes non-identity events older than Days. A negative Kind
2772+// applies the catch-all rule to kinds eligible for retention.
2773 type RetentionRule struct {
2774 Kind int
2775 Days int
2776 }
2777+
2778+// MemberRetention removes eligible events and list history for one author
2779+// older than Days.
2780 type MemberRetention struct {
2781 PubKey string
2782 Days int
2783 }
2784+
2785+// RetentionOptions supplies the retention time, relay identity and rules.
2786 type RetentionOptions struct {
2787 Now int64
2788 Identity string
2789@@ -86,6 +93,7 @@ func deleteCount(ctx context.Context, tx *sql.Tx, query string, args ...any) (in
2790 return result.RowsAffected()
2791 }
2792
2793+// KindStats reports count, size and time bounds for one stored event kind.
2794 type KindStats struct {
2795 Kind int `json:"kind"`
2796 N int64 `json:"n"`
2797diff --git a/internal/storage/save.go b/internal/storage/save.go
2798index 5907b85..89cb0ea 100644
2799--- a/internal/storage/save.go
2800+++ b/internal/storage/save.go
2801@@ -13,6 +13,9 @@ import (
2802 "github.com/FelineStateMachine/tinyrelay/internal/event"
2803 )
2804
2805+// Intent describes durable work to insert with an event save. The tuple of
2806+// Kind, EventID and Target determines its stable ID; Payload is retained for
2807+// the worker handling that kind.
2808 type Intent struct {
2809 Kind string `json:"kind"`
2810 EventID string `json:"event_id"`
2811@@ -20,6 +23,12 @@ type Intent struct {
2812 Payload string `json:"payload"`
2813 }
2814
2815+// SaveOptions controls event persistence. Now is the Unix time used for
2816+// history and intent timestamps. SearchMode is empty for the default indexed
2817+// kinds, "full" for all public content and "off" to skip content indexing.
2818+// For stored events, Intents and BeforeCommit run in the event transaction.
2819+// Ephemeral events return before those hooks. A BeforeCommit error rolls back
2820+// the transaction owned by Save; SaveTx returns it to the transaction's owner.
2821 type SaveOptions struct {
2822 Now int64
2823 SearchMode string
2824@@ -27,11 +36,17 @@ type SaveOptions struct {
2825 BeforeCommit func(context.Context, *sql.Tx) error
2826 }
2827
2828+// SaveResult reports the event sequence. Ephemeral events return without a
2829+// database row and set Ephemeral.
2830 type SaveResult struct {
2831 Sequence int64
2832 Ephemeral bool
2833 }
2834
2835+// Save commits an event, its indexes and the supplied transaction hooks
2836+// together. Callers validate the event before saving. Duplicate, replaced,
2837+// deleted and vanished events return a Rejection. Ephemeral events return an
2838+// Ephemeral result without writing rows or running the hooks.
2839 func (s *Store) Save(ctx context.Context, e event.Event, opts SaveOptions) (SaveResult, error) {
2840 var result SaveResult
2841 err := s.WithTx(ctx, func(tx *sql.Tx) error {
2842diff --git a/internal/storage/store.go b/internal/storage/store.go
2843index 9cd9c5e..70b7e2d 100644
2844--- a/internal/storage/store.go
2845+++ b/internal/storage/store.go
2846@@ -1,4 +1,3 @@
2847-// Package storage owns each tenant's local SQLite database and transaction boundary.
2848 package storage
2849
2850 import (
2851@@ -15,6 +14,7 @@ import (
2852 _ "modernc.org/sqlite"
2853 )
2854
2855+// Rejection is a stable Save error that callers can match with errors.Is.
2856 type Rejection string
2857
2858 func (e Rejection) Error() string { return string(e) }
2859@@ -26,10 +26,14 @@ const (
2860 ErrVanished Rejection = "blocked: this pubkey asked for its events to be deleted"
2861 )
2862
2863+// Observer receives operation name, outcome and duration for instrumented
2864+// Store operations.
2865 type Observer interface {
2866- ObserveStorage(string, string, time.Duration)
2867+ ObserveStorage(operation, outcome string, duration time.Duration)
2868 }
2869
2870+// Store is a tenant's SQLite database. A Store must be closed when its owner
2871+// is done with it.
2872 type Store struct {
2873 db *sql.DB
2874 path string
2875@@ -37,6 +41,9 @@ type Store struct {
2876 observer Observer
2877 }
2878
2879+// Open creates or opens path, initializes the tenant schema and returns a
2880+// Store. The parent directory is created with mode 0700 and the database file
2881+// with mode 0600.
2882 func Open(ctx context.Context, path string) (*Store, error) {
2883 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
2884 return nil, fmt.Errorf("create database directory: %w", err)
2885@@ -58,8 +65,8 @@ func Open(ctx context.Context, path string) (*Store, error) {
2886 if err != nil {
2887 return nil, fmt.Errorf("open database: %w", err)
2888 }
2889- // One connection is the initial serialization boundary. Reader pools can be
2890- // introduced after measurements without changing transaction semantics.
2891+ // One connection serializes tenant database operations. Transaction hooks
2892+ // use their supplied transaction while it holds this connection.
2893 db.SetMaxOpenConns(1)
2894 db.SetMaxIdleConns(1)
2895 s := &Store{db: db, path: path}
2896@@ -69,9 +76,18 @@ func Open(ctx context.Context, path string) (*Store, error) {
2897 return s, nil
2898 }
2899
2900-func (s *Store) DB() *sql.DB { return s.db }
2901+// DB returns the database used by tenant services for their schemas, queries
2902+// and transactions. Event persistence uses Save or SaveTx to apply storage's
2903+// replacement, deletion and indexing rules.
2904+func (s *Store) DB() *sql.DB { return s.db }
2905+
2906+// Path returns the database file path supplied to Open.
2907 func (s *Store) Path() string { return s.path }
2908+
2909+// Close releases the database connection.
2910 func (s *Store) Close() error { return s.db.Close() }
2911+
2912+// SetObserver replaces the observer used for operation timing and outcomes.
2913 func (s *Store) SetObserver(o Observer) {
2914 s.observerMu.Lock()
2915 defer s.observerMu.Unlock()
2916@@ -91,8 +107,9 @@ func (s *Store) observe(operation string, started time.Time, err error) {
2917 s.observer.ObserveStorage(operation, outcome, time.Since(started))
2918 }
2919
2920-// WithTx must not call another Store method: the callback owns the connection.
2921-// A successful return guarantees all domain changes and work intents committed.
2922+// WithTx opens a transaction, calls fn and commits after fn succeeds. It rolls
2923+// back on failure. The callback uses the supplied transaction for database
2924+// work because it holds the store's sole connection until completion.
2925 func (s *Store) WithTx(ctx context.Context, fn func(*sql.Tx) error) (err error) {
2926 started := time.Now()
2927 defer func() { s.observe("commit", started, err) }()
2928diff --git a/internal/syncprotocol/doc.go b/internal/syncprotocol/doc.go
2929new file mode 100644
2930index 0000000..4e7936c
2931--- /dev/null
2932+++ b/internal/syncprotocol/doc.go
2933@@ -0,0 +1,4 @@
2934+// Package syncprotocol contains the relay's NIP-45 count sketch and NIP-77
2935+// negentropy session wrappers. The wrappers exchange protocol strings while
2936+// exposing relay-friendly event IDs and reconciliation results.
2937+package syncprotocol
2938diff --git a/internal/syncprotocol/hll.go b/internal/syncprotocol/hll.go
2939index dcabb2c..6118b08 100644
2940--- a/internal/syncprotocol/hll.go
2941+++ b/internal/syncprotocol/hll.go
2942@@ -25,6 +25,9 @@ func NewHLL(offset int) *HLL {
2943 return &HLL{offset: offset}
2944 }
2945
2946+// HLLFilterOffset derives the register byte offset from the single tag value
2947+// used by the relay's count filter. Hashed values and address-style values are
2948+// both accepted.
2949 func HLLFilterOffset(f event.Filter) (int, bool) {
2950 if len(f.Tags) != 1 {
2951 return 0, false
2952@@ -51,6 +54,7 @@ func HLLFilterOffset(f event.Filter) (int, bool) {
2953 return 0, false
2954 }
2955
2956+// Add incorporates one 32-byte hexadecimal public key into the sketch.
2957 func (h *HLL) Add(pubkeyHex string) error {
2958 if len(pubkeyHex) != 64 || !isHexCharacters(pubkeyHex) {
2959 return fmt.Errorf("invalid pubkey: expected 32-byte hex")
2960@@ -75,8 +79,10 @@ func (h *HLL) Add(pubkeyHex string) error {
2961 return nil
2962 }
2963
2964+// Hex returns the 256 registers in their wire hexadecimal form.
2965 func (h *HLL) Hex() string { return hex.EncodeToString(h.regs[:]) }
2966
2967+// MergeHex unions another 256-register sketch into h.
2968 func (h *HLL) MergeHex(encoded string) error {
2969 if len(encoded) != hllRegisters*2 {
2970 return fmt.Errorf("invalid HLL: expected %d hex characters", hllRegisters*2)
2971diff --git a/internal/syncprotocol/negentropy.go b/internal/syncprotocol/negentropy.go
2972index e7b1d13..e6789d3 100644
2973--- a/internal/syncprotocol/negentropy.go
2974+++ b/internal/syncprotocol/negentropy.go
2975@@ -8,11 +8,14 @@ import (
2976 "fiatjaf.com/nostr/nip77/negentropy/storage/vector"
2977 )
2978
2979+// Item identifies one event in a reconciliation index.
2980 type Item struct {
2981 ID string
2982 Timestamp int64
2983 }
2984
2985+// Result contains one negentropy response and the IDs discovered by the
2986+// initiator when reconciliation completes.
2987 type Result struct {
2988 Response string
2989 Have []string
2990@@ -20,11 +23,16 @@ type Result struct {
2991 Done bool
2992 }
2993
2994+// Session holds one stateful negentropy exchange. A session is used by one
2995+// initiator or responder for the lifetime of that exchange.
2996 type Session struct {
2997 neg *negentropy.Negentropy
2998 initiator bool
2999 }
3000
3001+// NewSession builds a sealed negentropy index from the relay's event IDs and
3002+// timestamps. The initiator flag selects the side that starts the exchange
3003+// and receives Have and Need IDs.
3004 func NewSession(items []Item, initiator bool) (*Session, error) {
3005 vec := vector.New()
3006 for _, item := range items {
3007@@ -38,6 +46,7 @@ func NewSession(items []Item, initiator bool) (*Session, error) {
3008 return &Session{neg: negentropy.New(vec, 60_000, initiator, initiator), initiator: initiator}, nil
3009 }
3010
3011+// Start begins an initiator session and returns its protocol message.
3012 func (s *Session) Start() (string, error) {
3013 if !s.initiator {
3014 return "", fmt.Errorf("only the initiator can start a negentropy session")
3015@@ -45,6 +54,8 @@ func (s *Session) Start() (string, error) {
3016 return s.neg.Start(), nil
3017 }
3018
3019+// Reconcile consumes one peer message and returns the next response. An
3020+// initiator receives IDs present on each side once the exchange is complete.
3021 func (s *Session) Reconcile(message string) (Result, error) {
3022 if message == "" {
3023 return Result{}, fmt.Errorf("empty negentropy message")
3024diff --git a/internal/telemetry/doc.go b/internal/telemetry/doc.go
3025new file mode 100644
3026index 0000000..257e474
3027--- /dev/null
3028+++ b/internal/telemetry/doc.go
3029@@ -0,0 +1,9 @@
3030+// Package telemetry is the relay's process-local observability boundary.
3031+//
3032+// Telemetry owns an isolated Prometheus registry, OpenTelemetry tracer
3033+// provider, structured logger and authenticated diagnostics handlers. Metrics
3034+// use bounded operation and outcome labels. New configures sampling and an
3035+// optional OTLP HTTP exporter; Close flushes and shuts down the tracer
3036+// provider. HTTP exposes metrics and, when configured, token-protected
3037+// diagnostics without registering process-wide handlers.
3038+package telemetry
3039diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go
3040index 26c7a8f..54bcb0c 100644
3041--- a/internal/telemetry/telemetry.go
3042+++ b/internal/telemetry/telemetry.go
3043@@ -1,4 +1,3 @@
3044-// Package telemetry contains the relay's low-cardinality observability boundary.
3045 package telemetry
3046
3047 import (
3048diff --git a/internal/templates/doc.go b/internal/templates/doc.go
3049new file mode 100644
3050index 0000000..32540be
3051--- /dev/null
3052+++ b/internal/templates/doc.go
3053@@ -0,0 +1,10 @@
3054+// Package templates contains the built-in relay policy and connection catalog.
3055+//
3056+// Templates describe a named starting policy, event kind limits, retention,
3057+// source settings and recommended connections. Connections describe the app,
3058+// visibility, inputs, links and copyable values shown by the relay UI. The
3059+// catalog functions return copies so callers can customize results without
3060+// changing the built-in definitions. ApplyTemplate starts with policy defaults
3061+// for an owner, applies the template patch, then applies kind and retention
3062+// limits.
3063+package templates
3064diff --git a/internal/templates/templates.go b/internal/templates/templates.go
3065index e1d7365..7459e7c 100644
3066--- a/internal/templates/templates.go
3067+++ b/internal/templates/templates.go
3068@@ -1,4 +1,3 @@
3069-// Package templates contains the built-in declarative relay and connection catalog.
3070 package templates
3071
3072 import (
3073@@ -8,6 +7,7 @@ import (
3074 "github.com/FelineStateMachine/tinyrelay/internal/policy"
3075 )
3076
3077+// Template describes a built-in relay policy and its recommended connections.
3078 type Template struct {
3079 Name string
3080 Title string
3081@@ -21,6 +21,7 @@ type Template struct {
3082 Connections []string
3083 }
3084
3085+// Connection describes one client integration displayed for a relay.
3086 type Connection struct {
3087 Name string
3088 Title string
3089@@ -35,7 +36,10 @@ type Connection struct {
3090 Links []Link
3091 }
3092
3093+// Input describes a value a connection asks the user to supply.
3094 type Input struct{ Name, Label, Placeholder, Default, Pattern string }
3095+
3096+// Link describes an external URL or copyable value for a connection.
3097 type Link struct{ Label, Href, Copy string }
3098
3099 var templateCatalog = []Template{
3100@@ -78,6 +82,8 @@ func Names() []string {
3101 }
3102 return out
3103 }
3104+
3105+// Connections returns the built-in connection catalog in display order.
3106 func Connections() []Connection {
3107 out := make([]Connection, len(connectionCatalog))
3108 for i, c := range connectionCatalog {
3109@@ -85,6 +91,8 @@ func Connections() []Connection {
3110 }
3111 return out
3112 }
3113+
3114+// Find returns a named template and a boolean indicating whether it exists.
3115 func Find(name string) (Template, bool) {
3116 for _, t := range templateCatalog {
3117 if t.Name == name {
3118diff --git a/internal/views/doc.go b/internal/views/doc.go
3119new file mode 100644
3120index 0000000..29d1b6c
3121--- /dev/null
3122+++ b/internal/views/doc.go
3123@@ -0,0 +1,9 @@
3124+// Package views parses fenced code blocks and connects them to custom view
3125+// renderers.
3126+//
3127+// Blocks preserves source order and normalizes CRLF line endings. Hash names
3128+// an artifact by its language and source, while Path gives its relay URL.
3129+// Figure emits accessible markup with the rendered image and an inline source
3130+// fallback. Renderer maps each lowercased language to the first registered
3131+// view that supports it.
3132+package views
3133diff --git a/internal/views/views.go b/internal/views/views.go
3134index f77ca8e..46e0e34 100644
3135--- a/internal/views/views.go
3136+++ b/internal/views/views.go
3137@@ -1,7 +1,3 @@
3138-// Package views holds the parts of custom views that the renderers and the
3139-// relay share: finding fenced code blocks in text, naming a block by its
3140-// hash and the markup that shows an artifact in place of a block. It knows
3141-// nothing about any particular transform.
3142 package views
3143
3144 import (
3145diff --git a/internal/webpush/doc.go b/internal/webpush/doc.go
3146new file mode 100644
3147index 0000000..dcfb285
3148--- /dev/null
3149+++ b/internal/webpush/doc.go
3150@@ -0,0 +1,9 @@
3151+// Package webpush implements browser push delivery for the relay.
3152+//
3153+// Subscription validates an HTTPS endpoint, an uncompressed P-256 subscriber
3154+// key and its 16-byte authentication secret. Keys stores the relay's P-256
3155+// VAPID key and serializes it as unpadded base64url PKCS#8. Encrypt implements
3156+// RFC 8291 aes128gcm payload encryption. Send builds the RFC 8292 VAPID
3157+// authorization, posts the encrypted record, and reports whether the service
3158+// accepted or retired the subscription.
3159+package webpush
3160diff --git a/internal/webpush/webpush.go b/internal/webpush/webpush.go
3161index 056bdd4..bafaa61 100644
3162--- a/internal/webpush/webpush.go
3163+++ b/internal/webpush/webpush.go
3164@@ -1,6 +1,3 @@
3165-// Package webpush sends Web Push messages: RFC 8291 payload encryption with
3166-// the aes128gcm content encoding and RFC 8292 VAPID authorization. It uses
3167-// only the standard library so the relay keeps its dependency footprint.
3168 package webpush
3169
3170 import (
3171diff --git a/internal/webui/blossom-encryption.js b/internal/webui/blossom-encryption.js
3172index 2f62933..6b389a6 100644
3173--- a/internal/webui/blossom-encryption.js
3174+++ b/internal/webui/blossom-encryption.js
3175@@ -1,8 +1,9 @@
3176-// BUD-15 draft (June 2026), BUD-15: Client-side CHK encrypted blobs.
3177-// https://github.com/hzrd149/blossom/pull/104
3178-// Keys are client-only values. Callers must not include k in requests to a server.
3179+// BUD-15 client-side CHK encrypted blobs. Keys remain in the browser page and
3180+// are passed to the caller for sharing or later decryption.
3181 (() => {
3182 "use strict";
3183+ // Browser encryption for Blossom content. The public API stores and checks
3184+ // content-addressed ciphertext while keeping keys in the caller's page.
3185
3186 const encoder = new TextEncoder();
3187 const zeroNonce = new Uint8Array(12);
3188diff --git a/internal/webui/blossom-manifests.js b/internal/webui/blossom-manifests.js
3189index 7496ce2..ea03065 100644
3190--- a/internal/webui/blossom-manifests.js
3191+++ b/internal/webui/blossom-manifests.js
3192@@ -1,7 +1,10 @@
3193-// BUD-16 and BUD-17 MessagePack manifests. This is maintained browser source,
3194-// embedded directly by the Go templates; it is intentionally not bundled.
3195+// BUD-16 and BUD-17 MessagePack manifests. Go templates embed this browser
3196+// source directly so the same codec serves pages and file controls.
3197 (() => {
3198 "use strict";
3199+ // Manifest codec and tree reader. The API encodes and validates file and
3200+ // directory manifests, builds chunked trees and reads them through caller
3201+ // supplied fetch functions.
3202 const CHUNK_SIZE = 2 * 1024 * 1024;
3203 const MAX_LINKS = 174;
3204 const MAX_DEPTH = 64;
3205diff --git a/internal/webui/blossom-upload.js b/internal/webui/blossom-upload.js
3206index ba1c180..20d5602 100644
3207--- a/internal/webui/blossom-upload.js
3208+++ b/internal/webui/blossom-upload.js
3209@@ -1,9 +1,9 @@
3210-// BUD-14 draft multipart uploads.
3211-// https://github.com/hzrd149/blossom/pull/102
3212-// Upload state is deliberately held by the caller. It does not survive a page
3213-// navigation because BUD-14 has no offset discovery protocol.
3214+// BUD-14 multipart uploads. Upload state belongs to the caller and supports
3215+// retry, progress and cancellation for the active page.
3216 (() => {
3217 "use strict";
3218+ // Upload transport. plan describes signed requests for a payload; upload
3219+ // executes that plan with retry, progress and cancellation support.
3220
3221 const {sha256, hex} = globalThis.tiny.util;
3222 const asBytes = async value => {
3223diff --git a/internal/webui/bridge.js b/internal/webui/bridge.js
3224index 225c3b5..09d0325 100644
3225--- a/internal/webui/bridge.js
3226+++ b/internal/webui/bridge.js
3227@@ -1,6 +1,7 @@
3228 // Signer bridge: session sign-in and sign-out, NIP-07 and NIP-46 signers,
3229-// Nostr Connect, and the NIP-98 signed fetch that every management call uses.
3230-// The browser never receives a relay private key; it only asks the signer.
3231+// Nostr Connect and the NIP-98 signed fetch used by authenticated calls.
3232+// It exposes tiny.localPath, tiny.authorization and tiny.signedFetch, and
3233+// announces a connected signer with tiny:signer.
3234 (() => {
3235 "use strict";
3236
3237diff --git a/internal/webui/doc.go b/internal/webui/doc.go
3238new file mode 100644
3239index 0000000..8623e9e
3240--- /dev/null
3241+++ b/internal/webui/doc.go
3242@@ -0,0 +1,13 @@
3243+// Package webui renders the relay's HTML pages and browser support modules.
3244+//
3245+// App owns the HTTP surface, templates, page data and browser assets. It asks
3246+// Backend for relay identity, policy and method results, and uses ActorResolver
3247+// to associate a request with the authenticated public key. The daemon owns
3248+// relay policy and relay writes.
3249+//
3250+// Pages use a shared shell in page.html. The shell supplies the navigation rail,
3251+// content column, contextual panel and command footer. Each page template
3252+// supplies one content section and remains readable when scripts are disabled.
3253+// JavaScript modules progressively add signing, navigation, forms, components,
3254+// room streams, file transfers and WebMCP controls.
3255+package webui
3256diff --git a/internal/webui/file-messages.js b/internal/webui/file-messages.js
3257index d9b33e1..28eb6df 100644
3258--- a/internal/webui/file-messages.js
3259+++ b/internal/webui/file-messages.js
3260@@ -1,4 +1,5 @@
3261-/* NIP-17 file messages. This module never publishes to ordinary relays. */
3262+/* NIP-17 file messages. The module builds encrypted rumors and gift-wrap
3263+ events, discovers inbox relays and sends bounded deliveries. */
3264 (() => {
3265 "use strict";
3266 const {relayURL} = globalThis.tiny.util;
3267diff --git a/internal/webui/file-workspace.js b/internal/webui/file-workspace.js
3268index 06886fe..3af6ef4 100644
3269--- a/internal/webui/file-workspace.js
3270+++ b/internal/webui/file-workspace.js
3271@@ -1,4 +1,5 @@
3272-// Draft BUD-16/17 trees. Every object written here is encrypted with BUD-15.
3273+// File workspace controls. Selected files and folders become encrypted
3274+// Blossom chunks and manifests; references are read back in the browser.
3275 (() => {
3276 "use strict";
3277
3278diff --git a/internal/webui/page.html b/internal/webui/page.html
3279index 5946017..56db928 100644
3280--- a/internal/webui/page.html
3281+++ b/internal/webui/page.html
3282@@ -1,3 +1,7 @@
3283+{{/* The page shell owns the rail, content column, contextual panel and
3284+footer. Page templates render one section selected by .Tab. The body works
3285+before scripts load; tiny.js and bridge.js establish shared browser services,
3286+then feature modules are loaded for the current page. */}}
3287 {{define "page"}}<!doctype html>
3288 <html lang="en" data-js="no" data-scripts="{{scriptsVersion}}">
3289 <head>
3290diff --git a/internal/webui/private-services.js b/internal/webui/private-services.js
3291index ef3b280..9a0e8e4 100644
3292--- a/internal/webui/private-services.js
3293+++ b/internal/webui/private-services.js
3294@@ -1,5 +1,5 @@
3295-// PrivateServices edits the encrypted GRASP-08 kind 10318 list. The relay
3296-// only sees the signed event; NIP-44 encryption stays with the user's signer.
3297+// PrivateServices edits the encrypted kind 10318 relay list. NIP-44 encryption
3298+// and event signing stay with the user's signer.
3299 (() => {
3300 "use strict";
3301
3302diff --git a/internal/webui/read_contracts.go b/internal/webui/read_contracts.go
3303index afbe140..54a712d 100644
3304--- a/internal/webui/read_contracts.go
3305+++ b/internal/webui/read_contracts.go
3306@@ -8,20 +8,37 @@ import (
3307 "github.com/FelineStateMachine/tinyrelay/internal/event"
3308 )
3309
3310-// RoomsReader is the presentation-owned read contract for room pages. The
3311-// legacy Backend.Query method remains supported for adapters that have not
3312-// adopted this interface yet.
3313+// RoomsReader supplies the typed reads used by room pages. actor is the
3314+// authenticated public key, or an empty string for an anonymous request.
3315+// cursor is empty for the newest page and otherwise is the value returned in
3316+// NextCursor. limit requests at most that many rows. The daemon adapter uses
3317+// 100 when the requested limit falls outside 1 through 100.
3318+//
3319+// ListRooms returns visible room summaries ordered by room ID and a cursor for
3320+// the next page. ReadRoom returns the room summary, visible members, newest
3321+// room messages, message edits and a cursor for older messages. ReadThread
3322+// returns the room summary, one root event, its replies, edits and a cursor for
3323+// older replies. Reads enforce tenant and room access and return errors for
3324+// missing rooms, invalid cursors or an inaccessible room. ReadThread also
3325+// returns an error when rootID is not a lower-case hexadecimal event ID or does
3326+// not identify a message in roomID.
3327 type RoomsReader interface {
3328- ListRooms(context.Context, string, string, int) (RoomList, error)
3329- ReadRoom(context.Context, string, string, string, int) (RoomPage, error)
3330- ReadThread(context.Context, string, string, string, string, int) (RoomPage, error)
3331+ // ListRooms returns up to limit visible room summaries after cursor.
3332+ ListRooms(ctx context.Context, actor, cursor string, limit int) (RoomList, error)
3333+ // ReadRoom returns one visible room, its members, messages and edits after cursor.
3334+ ReadRoom(ctx context.Context, actor, roomID, cursor string, limit int) (RoomPage, error)
3335+ // ReadThread returns one room thread root, replies and edits after cursor.
3336+ ReadThread(ctx context.Context, actor, roomID, rootID, cursor string, limit int) (RoomPage, error)
3337 }
3338
3339 type RoomList struct {
3340- Rooms []RoomSummary `json:"rooms"`
3341- NextCursor string `json:"next_cursor,omitempty"`
3342+ // Rooms contains visible room summaries in ascending ID order.
3343+ Rooms []RoomSummary `json:"rooms"`
3344+ // NextCursor is the last room ID when another page is available.
3345+ NextCursor string `json:"next_cursor,omitempty"`
3346 }
3347
3348+// RoomSummary is the room metadata shown in the rail and room header.
3349 type RoomSummary struct {
3350 ID string `json:"id"`
3351 Name string `json:"name,omitempty"`
3352@@ -46,6 +63,9 @@ type RoomMember struct {
3353
3354 type RoomMessage = event.Event
3355
3356+// RoomPage is the typed result for a room or thread read. A room read fills
3357+// Members and Messages; a thread read fills Root and Replies. Edits contains
3358+// the newest visible edit for the returned targets.
3359 type RoomPage struct {
3360 Room RoomSummary `json:"room"`
3361 Members []RoomMember `json:"members,omitempty"`
3362@@ -56,9 +76,8 @@ type RoomPage struct {
3363 NextCursor string `json:"next_cursor,omitempty"`
3364 }
3365
3366-// rowsReader adapts the legacy method-and-JSON backend to the read shapes
3367-// used by page renderers. It keeps wire compatibility while making list
3368-// queries consistently return rows at the presentation boundary.
3369+// rowsReader converts method-and-JSON query results into the row slices used
3370+// by page renderers.
3371 type rowsReader struct{ backend Backend }
3372
3373 func (r rowsReader) rows(ctx context.Context, method string, params []json.RawMessage, actor string) ([]any, error) {
3374diff --git a/internal/webui/rooms.html b/internal/webui/rooms.html
3375index ebca938..6b45829 100644
3376--- a/internal/webui/rooms.html
3377+++ b/internal/webui/rooms.html
3378@@ -1,4 +1,6 @@
3379-{{/* Rooms: the list, one room's timeline with its compose bar, and one thread. */}}
3380+{{/* Rooms renders the list, one room's timeline with its compose bar and one
3381+thread. room-live receives the room stream after the server-rendered page
3382+establishes the initial state. */}}
3383 {{define "rooms"}}<section id="rooms">
3384 <h1>Rooms</h1>
3385 {{if .Error}}{{template "alert" .Error}}
3386diff --git a/internal/webui/rooms.js b/internal/webui/rooms.js
3387index 9e1fb9b..5eb86d3 100644
3388--- a/internal/webui/rooms.js
3389+++ b/internal/webui/rooms.js
3390@@ -2,6 +2,10 @@
3391 // runs after components.js has published the shared UI helper contract.
3392 (() => {
3393 "use strict";
3394+ // Room elements own composition, room management actions and live updates.
3395+ // room-compose signs kind 9 and 12 events, room-create signs room metadata,
3396+ // room-action handles membership controls, and room-live consumes the room
3397+ // event stream.
3398 const tiny = window.tiny;
3399 const {FormElement, el, isHex64} = tiny.ui;
3400 // Rooms: the compose bar, room creation, room administration and the live
3401diff --git a/internal/webui/tiny.js b/internal/webui/tiny.js
3402index 9b8e8b7..5a7def6 100644
3403--- a/internal/webui/tiny.js
3404+++ b/internal/webui/tiny.js
3405@@ -1,5 +1,7 @@
3406-// Shared browser helpers. This file is loaded before every feature module
3407-// and the signer bridge, so each helper exists exactly once.
3408+// Shared browser helpers. This file is loaded before every feature module and
3409+// the signer bridge. It creates tiny.util for byte, URL, encoding and wiki
3410+// name operations, tiny.signing for verified event signing and publication,
3411+// and tiny.require for loading page-specific bundles once.
3412 (() => {
3413 "use strict";
3414 const tiny = globalThis.tiny || {};
3415diff --git a/internal/webui/webmcp.js b/internal/webui/webmcp.js
3416index 073817b..0e54821 100644
3417--- a/internal/webui/webmcp.js
3418+++ b/internal/webui/webmcp.js
3419@@ -1,5 +1,9 @@
3420 (function () {
3421 "use strict";
3422+ // WebMCP registers read, write and navigation tools for the browser model
3423+ // context. Read tools call the relay query surface, write tools use the
3424+ // signed management or event boundary, and navigation tools open relay
3425+ // pages in the current tab.
3426
3427 const context = document.modelContext || navigator.modelContext;
3428 const state = window.tinyWebMCP = {
3429diff --git a/internal/webui/webui.go b/internal/webui/webui.go
3430index ba16c70..c024b02 100644
3431--- a/internal/webui/webui.go
3432+++ b/internal/webui/webui.go
3433@@ -1,6 +1,3 @@
3434-// Package webui contains the deliberately plain HTML surface for a hosted
3435-// relay. It owns presentation and form routing; relay policy and mutations
3436-// remain in the daemon supplied Backend.
3437 package webui
3438
3439 import (
3440@@ -34,8 +31,12 @@ var signerJS []byte
3441 //go:embed fixi.js
3442 var fixiJS []byte
3443
3444+// Backend supplies relay identity and the method calls used by page handlers.
3445+// Query receives the request context, method name, JSON arguments and actor
3446+// public key, and returns the method's JSON-shaped result. Policy, URL, slug
3447+// and identity describe the relay rendered into each page.
3448 type Backend interface {
3449- Query(context.Context, string, []json.RawMessage, string) (any, error)
3450+ Query(ctx context.Context, method string, params []json.RawMessage, actor string) (any, error)
3451 Policy() policy.Policy
3452 URL() string
3453 Slug() string
3454@@ -48,8 +49,12 @@ type CustomViewSource interface {
3455 CustomViews() []views.View
3456 }
3457
3458-type ActorResolver func(*http.Request) (string, error)
3459-type QRProvider func(string) ([]byte, error)
3460+// ActorResolver returns the authenticated public key associated with request.
3461+// An empty key represents an anonymous browser request.
3462+type ActorResolver func(request *http.Request) (string, error)
3463+
3464+// QRProvider encodes text as an image for pages that show a connection QR.
3465+type QRProvider func(text string) ([]byte, error)
3466
3467 type App struct {
3468 backend Backend
3469diff --git a/internal/wiki/doc.go b/internal/wiki/doc.go
3470new file mode 100644
3471index 0000000..aca8864
3472--- /dev/null
3473+++ b/internal/wiki/doc.go
3474@@ -0,0 +1,8 @@
3475+// Package wiki implements storage-independent NIP-54 article behavior.
3476+//
3477+// Normalize converts a title to an article d tag. Links discovers wikilinks,
3478+// reference links and nostr: targets in Djot content. RenderHTML supports the
3479+// article syntax used by the relay, escapes text and unsafe link targets, and
3480+// returns template HTML for server-side insertion. RenderHTMLWith accepts a
3481+// fenced-block callback so custom views can replace selected code blocks.
3482+package wiki
3483diff --git a/internal/wiki/wiki.go b/internal/wiki/wiki.go
3484index 5c6b692..68a9011 100644
3485--- a/internal/wiki/wiki.go
3486+++ b/internal/wiki/wiki.go
3487@@ -1,6 +1,3 @@
3488-// Package wiki implements the NIP-54 rules that do not depend on storage:
3489-// article name normalization, link discovery and a conservative Djot
3490-// renderer for article content.
3491 package wiki
3492
3493 import (
3494diff --git a/internal/work/doc.go b/internal/work/doc.go
3495new file mode 100644
3496index 0000000..c631c5b
3497--- /dev/null
3498+++ b/internal/work/doc.go
3499@@ -0,0 +1,15 @@
3500+// Package work provides durable, fenced work intents backed by a storage.Store.
3501+//
3502+// Queue.Enqueue is idempotent by intent ID. Claim selects the oldest due
3503+// pending intent, or a running intent whose lease expired, and assigns a
3504+// random claim token. Renew, Complete, Retry and CancelClaim update an intent
3505+// only while that token owns the claim, fencing updates from earlier attempts.
3506+//
3507+// Worker invokes the handler for a claim while renewing its lease. A nil or
3508+// missing handler is an ordinary failure and is retried. Handler errors are
3509+// retried with deterministic jittered exponential backoff; Stop marks the
3510+// intent cancelled and records the underlying error. If lease renewal fails,
3511+// the handler context is cancelled and its result is discarded as fenced.
3512+// RunPool starts the requested number of workers, defaults to four, and stops
3513+// the pool when its context is cancelled or a queue operation fails.
3514+package work
3515diff --git a/internal/work/work.go b/internal/work/work.go
3516index f1dc58a..60ed70f 100644
3517--- a/internal/work/work.go
3518+++ b/internal/work/work.go
3519@@ -1,4 +1,3 @@
3520-// Package work provides durable, fenced work intents backed by SQLite.
3521 package work
3522
3523 import (
3524@@ -20,6 +19,7 @@ import (
3525
3526 const defaultLease = 30 * time.Second
3527
3528+// ErrClaimLost denotes loss of worker lease ownership.
3529 var ErrClaimLost = errors.New("work: claim lost")
3530
3531 type stopError struct{ err error }
3532@@ -37,7 +37,9 @@ func (e *stopError) Unwrap() error { return e.err }
3533 // instead of retrying it, while retaining the underlying error for operators.
3534 func Stop(err error) error { return &stopError{err: err} }
3535
3536-// Intent is a durable unit of work. ClaimToken is only valid until ClaimUntil.
3537+// Intent is a durable unit of work. ClaimToken identifies the current claim;
3538+// ClaimUntil determines when another worker can claim an expired lease.
3539+// State is pending, running, completed or cancelled.
3540 type Intent struct {
3541 ID string
3542 Kind string
3543@@ -322,17 +324,24 @@ func metricKind(kind string) string {
3544 return "other"
3545 }
3546
3547-// Handler processes one claimed intent.
3548-type Handler func(context.Context, Intent) error
3549+// Handler processes one claimed intent while the worker renews its lease.
3550+// A nil error completes the claim; an error schedules a retry. Stop marks a
3551+// terminal failure. The handler observes ctx cancellation so the worker can
3552+// finish shutdown or release an attempt whose claim was lost. A retry can
3553+// repeat an external effect after an interrupted acknowledgment; handlers own
3554+// deduplication of those effects.
3555+type Handler func(ctx context.Context, intent Intent) error
3556
3557 // WorkerOptions controls claim leases and optional handler timing observation.
3558+// Lease and Renew default to 30 seconds and one third of the lease.
3559 type WorkerOptions struct {
3560 Lease time.Duration
3561 Renew time.Duration
3562 Observe func(kind, outcome string, duration time.Duration)
3563 }
3564
3565-// PoolOptions controls a pool of durable workers.
3566+// PoolOptions controls a pool of durable workers. Workers less than or equal
3567+// to zero use the default of four.
3568 type PoolOptions struct {
3569 Workers int
3570 Worker WorkerOptions

About

ownerc01503e8035f
visibilitypublic
head565130580365

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs