OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface commits / 84ad65a

0.0.68: serve static/ from text imports, so the phone's bundle carries it
The router filled /static/ by reading static/ off disk at boot. The phone runs the surface from a bare-pack bundle, which packs only imports, so the read failed silently and every /static/*.js was a 404 there: composer.js never ran, photos went up with no level pyramid, and a 2,645,504-byte photo had no level under bsky's 2 MB ceiling, so bsky got the post without its image. lib/static-files.js imports each file with { type: 'text' }; the test fails if the router reads the directory or a file in static/ is not imported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84ad65a8636c9a3fe1f04568f5a1dcda40c2686e
by plankatron <fleeky@gmail.com> · 2026-10-15T00:00:57.000Z

Changed files (5) +70 -9

lib/router.js +2 -8
……@@ -18,6 +18,7 @@ import * as pages from './pages.js'
1818import { embeds } from 'favela-markdown/lib/render.js'
1919import { recordUri } from 'favela-markdown/lib/uri.js'
2020import { parse as parseMultipart } from './multipart.js'
21import { STATIC } from './static-files.js'
2122import b4a from 'b4a'
2223
2324// ⚠ Both spellings: bare-tcp reports IPv4 loopback as `::ffff:127.0.0.1` when the listener is dual
……@@ -27,14 +28,7 @@ const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1'])
2728// ⛔ VENDORED in static/, not taken as a dependency. Depending on hyper-identity to serve four
2829// browser files pulled a whole multi-writer log engine into this renderer's published
2930// closure — in a process whose entire boundary is "no storage, no keys, no swarm". See
30// static/README.md.
31const STATIC_DIR = new URL('../static/', import.meta.url).pathname
32const STATIC = new Map()
33// ⚠ Read the DIRECTORY. The list was hand-maintained and a file added to static/ without being
34// named here is served as a 404 — which for a module script is a page that silently does nothing.
35for (const f of (() => { try { return fs.readdirSync(STATIC_DIR).filter((x) => x.endsWith('.js')) } catch { return [] } })()) {
36 try { STATIC.set('/static/' + f, fs.readFileSync(STATIC_DIR + f, 'utf8')) } catch {}
37}
31// static/README.md. IMPORTED as text (lib/static-files.js), so a bundle carries them too.
3832
3933/**
4034 * ⛔ Per-RESPONSE, keyed by the response object itself. A module-level "current theme" is request
lib/static-files.js +31 -0
……@@ -0,0 +1,31 @@
1// The browser files served at /static/, as TEXT MODULES.
2//
3// ⛔ IMPORTED, NOT READ FROM DISK. The router used to readdirSync static/ at boot. favela-surface-android
4// runs this from a bare-pack bundle, and bare-pack packs only what the module graph imports — so on the
5// phone the read failed, the catch made it an empty list, and every /static/*.js was a 404. composer.js
6// never ran, photos went up with no level pyramid, and bsky got those posts without their images.
7//
8// ⚠ A file added to static/ must be added HERE. test/static-files.test.js fails until it is (the drift
9// the directory read was guarding against), and favela-surface-android's CI checks the built bundle.
10
11import s0 from '../static/anproto-client.js' with { type: 'text' }
12import s1 from '../static/composer.js' with { type: 'text' }
13import s2 from '../static/explore.js' with { type: 'text' }
14import s3 from '../static/levels.js' with { type: 'text' }
15import s4 from '../static/noble-crypto.js' with { type: 'text' }
16import s5 from '../static/noble-ed25519.js' with { type: 'text' }
17import s6 from '../static/noble-sha512.js' with { type: 'text' }
18import s7 from '../static/three.core.min.js' with { type: 'text' }
19import s8 from '../static/three.module.min.js' with { type: 'text' }
20
21export const STATIC = new Map([
22 ['/static/anproto-client.js', s0],
23 ['/static/composer.js', s1],
24 ['/static/explore.js', s2],
25 ['/static/levels.js', s3],
26 ['/static/noble-crypto.js', s4],
27 ['/static/noble-ed25519.js', s5],
28 ['/static/noble-sha512.js', s6],
29 ['/static/three.core.min.js', s7],
30 ['/static/three.module.min.js', s8]
31])
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-surface",
3 "version": "0.0.67",
3 "version": "0.0.68",
44 "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.",
55 "type": "module",
66 "main": "index.js",
static/README.md +4 -0
……@@ -2,6 +2,10 @@
22
33Browser assets, served at `/static/…`.
44
5⛔ **A new file here must be imported in `lib/static-files.js`.** They are served from text imports,
6not read off disk, because the phone runs the surface from a bare-pack bundle that holds only what is
7imported. `test/static-files.test.js` fails until a new file is listed.
8
59⛔ **Vendored, not fetched.** A page that loaded its signer from someone else's server would let
610that server mint signatures with the user's key.
711
test/static-files.test.js +32 -0
……@@ -0,0 +1,32 @@
1// ⛔ static/ must reach the page from INSIDE A BUNDLE.
2//
3// The router filled /static/ by reading this directory at boot. favela-surface-android runs the
4// surface from a bare-pack bundle, which packs only what the module graph imports — so on the phone the
5// directory read failed, the catch made it an empty list, and every /static/*.js was a 404. composer.js
6// never ran, photos went up with no level pyramid, and bsky got the post without its image.
7//
8// So the files are IMPORTED (as text), and this checks both halves: nothing reads the directory at
9// runtime, and the imported set is exactly what is in static/ — the drift the directory read existed to
10// prevent (a file added here and never named is a page that silently does nothing).
11
12import test from 'brittle'
13import fs from '#node:fs'
14import { join } from '#node:path'
15
16const ROOT = new URL('..', import.meta.url).pathname
17
18test('★ the router does not read static/ from disk at runtime', (t) => {
19 const src = fs.readFileSync(join(ROOT, 'lib/router.js'), 'utf8')
20 t.absent(/readdirSync/.test(src), 'no directory read')
21 t.absent(/STATIC_DIR/.test(src), 'no path into static/ built at runtime')
22})
23
24test('★ every static/*.js is served, byte for byte, and nothing else', async (t) => {
25 const { STATIC } = await import('../lib/static-files.js')
26 const onDisk = fs.readdirSync(join(ROOT, 'static')).filter((f) => f.endsWith('.js')).sort()
27 t.ok(onDisk.length > 0, 'static/ has files')
28 t.alike([...STATIC.keys()].sort(), onDisk.map((f) => '/static/' + f), 'the served set IS the directory')
29 for (const f of onDisk) {
30 t.is(STATIC.get('/static/' + f), fs.readFileSync(join(ROOT, 'static', f), 'utf8'), f + ' is the file')
31 }
32})