clone

a self-hosted sync node for lofi apps: jazz sync over iroh, ticket-gated, with no static ip or cloud needed

docs: finish the contract-accuracy pass and the operator hardening docs

Dami committed 1b14a99
Contract accuracy (lofi#122 §4): the last three places stating tickets
are stored passkey-encrypted as a blanket fact — README, the mod.ts
example, and app-ticket.md's enrollment flow — now state the shipped
custody: sealed at rest under a device-bound key, provision capability
split via the scope-down exchange and passkey-gated, boot ceremony-free.

Operator docs (lofi#122 §5): beyond-the-lan gains a proxy-log redaction
section with concrete Caddy and nginx configs (default access logs
collect /t/<secret> paths); tickets-explained records why the secret
rides the URL path (the browser WebSocket API cannot set headers) and
names the logging cost that choice carries; hosting-lofi-apps documents
the shell weight and precompression guidance for serving the app.

Part of FelineStateMachine/lofi#123 (D) / lofi#122 §4-§5.
commit
1b14a9961dff4eceabe2cc88bb1b6f80b9a80e81
parent
92023eabbc5267ba7f2a6f6547a2800a4c10b80a
author
Dami <dami.etoile@gmail.com>
6 files changed, +70 -13
MREADME.md5++--
Mdocs/app-ticket.md15+++++++-----
Mdocs/hosting-lofi-apps.md10+++++++-
Mdocs/site/beyond-the-lan.md40++++++++++++++++++++++++++++++
Mdocs/site/tickets-explained.md11++++++--
Mmod.ts2+-
M README.md +3 -2
M docs/app-ticket.md +9 -6
@@ -120,12 +120,15 @@ setups hold the admin secret and can query Jazz directly.
120120 121121 1. User pastes/scans the ticket string; app parses it (`decodeAppTicket` in `@nzip/lofi-node`122122 mirrors the validation: prefix, `v: 1`, http(s) URL with a `/t/<43-char-secret>` path).123-2. App encrypts the ticket with the passkey-derived at-rest key (lofi `package/runtime/auth.ts`:124- `derivePrfSecret` → `deriveAtRestKey` → `encryptAtRest`) and stores the blob in localStorage125- (suggested key: `lofi:sync-location:<appId>`).126-3. On boot, after the user unlocks with their passkey: decrypt, use `ticket.url` as the runtime127- `serverUrl` override. The decrypted local data therefore reveals _location + access_ for the128- user's lofi data.123+2. A `provision`-scoped ticket is split before anything persists: the app calls the scope-down124+ exchange (above) and declares the derived sync ticket as its sink; the provision original is held125+ in memory and, on PRF-capable devices, sealed behind the user's passkey — otherwise the user's126+ password manager keeps the durable copy. Against a node without the exchange, the ticket enrolls127+ as pasted.128+3. The declared sink persists only as a sealed envelope under a device-bound key (localStorage key129+ `lofi:data-sink:<appId>`; nothing bearer-shaped is stored in cleartext). Boot opens it silently —130+ no ceremony — and uses `ticket.url` as the runtime `serverUrl`. Unlocking sealed provision131+ capability for an admin operation is a user-verifying passkey ceremony.129132 4. `ticket.appId` should match the app's own id; refuse enrollment otherwise.130133 131134 ## Revocation semantics
M docs/hosting-lofi-apps.md +9 -1
M docs/site/beyond-the-lan.md +40 -0
@@ -29,6 +29,46 @@ Note what TLS does **not** change: the ticket still gates access, revocation sti
2929 and the WebSocket upgrade still rides the same URL — the browser client derives its endpoints from3030 the http(s) base.3131 32+## Redact ticket secrets from proxy logs33+34+Every gated request carries `/t/<secret>` in its path, and a reverse proxy's default access log35+writes request paths — so a stock logging config quietly collects bearer credentials on disk. Redact36+the path before it is logged (or disable access logging for the gate's host).37+38+Caddy, with its log field filter:39+40+```caddyfile41+node.example.com {42+ reverse_proxy 127.0.0.1:480243+ log {44+ format filter {45+ wrap console46+ fields {47+ request>uri regexp /t/[A-Za-z0-9_-]{43} /t/<redacted>48+ }49+ }50+ }51+}52+```53+54+nginx, by logging a scrubbed copy of the URI:55+56+```nginx57+map $request_uri $redacted_uri {58+ "~^(?<pre>.*/t/)[A-Za-z0-9_-]{43}(?<post>.*)$" "${pre}<redacted>${post}";59+ default $request_uri;60+}61+log_format redacted '$remote_addr - [$time_local] '62+ '"$request_method $redacted_uri $server_protocol" '63+ '$status $body_bytes_sent';64+access_log /var/log/nginx/access.log redacted;65+```66+67+The same concern applies to anything else that records URLs on the request path — tracing68+middleware, error reporters, CDN logs. The node itself compares digests and logs ticket ids, never69+secrets; keep the perimeter to the same standard. If a secret does land in a log, revoke that ticket70+(`lofi-node ticket revoke <id>`) — reissuing is one command and live sockets close within seconds.71+3272 ## Bring your own relay3373 3474 Paired nodes hole-punch direct connections wherever possible; an
M docs/site/tickets-explained.md +8 -3
M mod.ts +1 -1