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>
| M | README.md | 5 | ++-- |
| M | docs/ | 15 | +++++++----- |
| M | docs/ | 10 | +++++++- |
| M | docs/ | 40 | ++++++++++++++++++++++++++++++ |
| M | docs/ | 11 | ++++++-- |
| M | mod.ts | 2 | +- |
@@ -5,8 +5,9 @@
55 **Self-host the sync backend for [lofi](https://github.com/FelineStateMachine/lofi) apps: one daemon66 embedding a Jazz sync server, iroh peer-to-peer transport, and ticket-gated access.**77 8-Issue a ticket carrying location + secret; the app stores it passkey-encrypted and syncs against9-your node. Pair homes by ticket — no static IPs, no cloud — and choose where your data lives.8+Issue a ticket carrying location + secret; the app seals it at rest — admin capability behind the9+user's passkey — and syncs against your node. Pair homes by ticket — no static IPs, no cloud — and10+choose where your data lives.1011 1112 [Documentation](https://lofi.host/node) · [API reference](https://lofi.host/node/api) ·1213 [JSR package](https://jsr.io/@nzip/lofi-node) · [MIT License](LICENSE)@@ -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@@ -85,7 +85,15 @@ deno task preview
8585 8686 `JAZZ_SERVER_URL` must be http(s) — lofi's preflight rejects ws URLs; the browser client derives the8787 WebSocket endpoint itself (`…/apps/<uuid>/ws`, preserving the ticket's `/t/<secret>` base path). On88-an open-mode node use the node URL directly. Beyond a trusted LAN, front the gate with TLS;88+an open-mode node use the node URL directly.89+90+Budget for the app shell's weight when you serve it: the Jazz WASM engine is ~9.8 MB uncompressed91+(~3 MB gzip), and lofi's build prints the measured shell total on every run. Serve the static app92+with compression — precompress at deploy time (`brotli -k`, `gzip -k9` over `dist/`) and let the93+file server prefer the precompressed variants (Caddy `file_server` with `precompressed br gzip`,94+nginx `gzip_static on;`/`brotli_static on;`), or at minimum enable on-the-fly compression for95+`.wasm` and `.js`. The service worker precaches the shell once per app version, so the cost is a96+cold install and each update, not every visit. Beyond a trusted LAN, front the gate with TLS;8997 installed PWAs require a secure origin. (Once lofi ships its runtime serverUrl override, the9098 build-time env becomes unnecessary — the app enrolls the ticket at runtime instead; see9199 [app-ticket.md](app-ticket.md).)@@ -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@@ -17,9 +17,14 @@ lofi side implements against.
1717 1818 ## Why the secret lives in the URL path1919 20-Jazz clients preserve a base path in their server URL and reject query parameters. By making the21-ticket's URL `http(s)://host:port/t/<secret>`, every request the client ever makes — sync WebSocket22-connects, catalogue reads, admin calls — carries the secret with **zero client changes**. The gate20+A header was the obvious alternative and it does not survive contact with the platform: the21+browser's WebSocket API cannot set request headers, so a header-borne credential would need a22+different authentication path for exactly the connection that matters most. Jazz clients preserve a23+base path in their server URL and reject query parameters. By making the ticket's URL24+`http(s)://host:port/t/<secret>`, every request the client ever makes — sync WebSocket connects,25+catalogue reads, admin calls — carries the secret with **zero client changes**. The cost of the26+choice is that the secret rides in a URL, which is why proxy access logs need27+[redaction](beyond-the-lan.md) and why the gate compares digests rather than logging paths. The gate2328 validates the secret (timing-safe, digest against digest), strips the prefix, and proxies to the2429 loopback-only Jazz server. The lofi app uses the ticket URL verbatim as its sync server; that is the2530 whole integration.@@ -16,7 +16,7 @@
1616 * access: "ticket",1717 * });1818 * const { ticket } = await node.issueTicket({ label: "phone" });19- * // the app stores `ticket` passkey-encrypted and uses its URL as serverUrl19+ * // the app seals `ticket` at rest and uses its URL as serverUrl2020 * ```2121 *2222 * The CLI lives at the `./cli` export (`dx -A jsr:/lofi-node/cli`), and