OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface commits / a82ea7f

README: "/login is the one page with JavaScript" was false, and the file read like a spec
Three pages emit a script tag, not one. /login does (the browser signs a challenge with an Ed25519 key it keeps in localStorage — there is no form post that produces a signature, so this page genuinely cannot degrade). /explore does, and it is where 733K of vendored three.js goes. And the timeline carries composer.js, the @-picker. The composer is the interesting one and the README had no room for it, because the file was organised around a boundary rule rather than around a reader. It posts a real multipart form; the script only saves typing, inserts text a person could have typed, and cannot produce a mention the daemon would not have resolved anyway. That is the pattern anything else here should copy, and it is worth naming. So the JavaScript story is now a table that says, per page, whether it works with JavaScript off — which is the question someone actually has — instead of a claim that had drifted two pages out of date. Reordered around use: what it is, how to run it, how to sign in, what needs JavaScript, and the no-storage boundary last. The FAVELA_SOCK trap moved up next to "Running", since "every page says the daemon is down" is the first thing that happens to anyone who starts it by hand and it was buried at the bottom. Sizes and page routes verified on disk rather than recalled: signer chain 22474 bytes, three.js 750938, composer 2651, and composer() renders inside the timeline rather than at a route of its own. 175/175 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a82ea7fda75d1fc48316332340261caec44388fa
by plankatron <fleeky@gmail.com> · 2026-09-09T17:56:54.000Z

Changed files (1) +52 -27

README.md +52 -27
……@@ -1,16 +1,9 @@
11# favela-surface
22
3The favela renderer. Server-side HTML, no JavaScript required to read.
3**Read your favela in a browser.** `favela-peer` owns the data; this turns it into pages.
44
5## The boundary
6
7⛔ **No storage, no keys, no swarm.** It holds a socket to `favela-peer` and nothing else. A surface
8that grows a Corestore has become a second peer implementation, which is the fork risk
9`favela-markdown/spec/library.md` §1 names for the codec and which applies here identically.
10
11Everything it knows, it asked the daemon. Everything it renders, `favela-markdown` rendered.
12
13A test enforces both: no `atproto-*`/`ap-*` imports, and no `corestore`/`hypercore`/`autobee`.
5It is a renderer and only a renderer. Everything it knows it asked the daemon over a unix socket,
6and everything it shows, `favela-markdown` rendered. Close it and you lose nothing but the window.
147
158## Running
169
……@@ -19,6 +12,14 @@ favela-peer & # the daemon owns the data
1912favela-surface # http://127.0.0.1:4042
2013```
2114
15⚠ **If every page says the daemon is down, it is almost always the socket.** `FAVELA_SOCK` has to
16point at the *daemon's* storage, not the surface's. The two apps have separate defaults, so a
17surface left on its own looks for a socket nobody created. This package's pgh defaults set it to
18`${HOME}/.config/favela-peer/favela.sock`; set it yourself if you run the daemon elsewhere.
19
20`FAVELA_SURFACE_PORT` and `FAVELA_SURFACE_HOST` move the listener. It binds `127.0.0.1` — this is
21your own daemon's data with no authentication in front of it, so exposing it is a decision.
22
2223## Signing in
2324
2425Every route is gated except `/login`. A browser gets in with a **one-time invite**, minted on the
……@@ -30,24 +31,48 @@ favela browsers # who is admitted
3031favela revoke <pubkey> # retire one
3132```
3233
33The browser generates an Ed25519 keypair, keeps it in `localStorage`, and answers a challenge with
34it. There is **no expiry and no password** — the key is the credential, and revocation is the only
35way out. ⛔ A revoke lands on the browser's *next request*, not when a session lapses: the daemon is
36asked whether the key is still allowed on every page load.
34Your browser makes an Ed25519 keypair, keeps it in `localStorage`, and answers a challenge with it.
35There is **no expiry and no password** — the key is the credential, so revoking is the only way out.
36
37Two consequences worth knowing before they surprise you:
38
39- A revoke lands on that browser's **next request**, not whenever a session would have lapsed. The
40 daemon is asked whether the key is still allowed on every page load.
41- Clearing site data means asking for a new invite. Nothing on the server remembers you.
42- Restarting the surface signs everyone out, because sessions live in memory. That is a restart,
43 not an expiry, and it follows from the surface holding no storage.
3744
38⚠ **`/login` is the one page with JavaScript.** Reading needs none; signing does — a form post
39cannot produce an Ed25519 signature. The signer is served from this process (`/static/…`), never a
40CDN: a page that loaded its signer from someone else's server would let that server mint signatures
41with your key.
45## JavaScript
46
47Reading needs none. Timelines, profiles, posts, search, settings, the graph — all server-rendered
48HTML, and they work with JavaScript switched off.
49
50Three pages load a script, for three different reasons — and only two of them need one:
51
52| page | what it loads | without JavaScript |
53|---|---|---|
54| `/login` | an inline module + the vendored signer (21K) | **doesn't work** — a form post cannot produce an Ed25519 signature |
55| `/` (the timeline) | `composer.js` (2.6K), the `@`-picker | **works** — it posts a real multipart form; the picker only saves you typing |
56| `/explore` | `explore.js` + three.js (~730K) | **doesn't work** — it is a rotating 3D view |
57
58The composer is the pattern to copy. It inserts `@alice` — ordinary text you could have typed —
59and the daemon resolves mentions on the write path either way. With the script missing the composer
60still works; with it present it cannot produce a mention the server would not have produced anyway.
61
62`/explore` is the one place a browser pays for a megabyte, and only that page loads it. Its legend
63is server-rendered on purpose — an explanation of what the dots mean is no use if it arrives after
64the WebGL.
65
66⛔ **Everything under `/static/` is vendored, never a CDN.** A page that loaded its signer from
67someone else's server would let that server mint signatures with your key. See `static/README.md`.
68
69## The boundary
4270
43⚠ Sessions live in memory, so restarting the surface signs everyone out. That is a restart, not an
44expiry, and it follows from the surface holding no storage.
71⛔ **No storage, no keys, no swarm.** This process holds a socket and nothing else. A surface that
72grows a Corestore has quietly become a second peer implementation — the fork risk
73`favela-markdown/spec/library.md` §1 names for the codec, which applies here identically.
4574
46⚠ `FAVELA_SOCK` must point at the DAEMON's socket, which is `favela-peer`'s storage — declared in
47this package's pgh defaults as `${HOME}/.config/favela-peer/favela.sock`. The two apps have separate
48storage defaults, so a surface left on its own default looks up a socket that does not exist and
49every page reports the daemon as down.
75`test/boundary.test.js` enforces it: no source file and no dependency may name `corestore`,
76`hypercore`, `autobee`, `autobase`, `hyperswarm`, `hyper-overlay`, `atproto-*` or `ap-bridge`.
5077
51`FAVELA_SOCK` points at the daemon; `FAVELA_SURFACE_PORT` and `FAVELA_SURFACE_HOST` at the listener.
52It binds `127.0.0.1` by default — this is your own daemon's data with no authentication in front of
53it, so exposing it is a decision, not a default.
78`npm test` — 175 tests, 565 asserts, all passing.