| … | … | @@ -18,6 +18,7 @@ import * as pages from './pages.js' |
| 18 | 18 | import { embeds } from 'favela-markdown/lib/render.js' |
| 19 | 19 | import { recordUri } from 'favela-markdown/lib/uri.js' |
| 20 | 20 | import { parse as parseMultipart } from './multipart.js' |
| 21 | import { STATIC } from './static-files.js' | |
| 21 | 22 | import b4a from 'b4a' |
| 22 | 23 | |
| 23 | 24 | // ⚠ 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']) |
| 27 | 28 | // ⛔ VENDORED in static/, not taken as a dependency. Depending on hyper-identity to serve four |
| 28 | 29 | // browser files pulled a whole multi-writer log engine into this renderer's published |
| 29 | 30 | // closure — in a process whose entire boundary is "no storage, no keys, no swarm". See |
| 30 | // static/README.md. | |
| 31 | const STATIC_DIR = new URL('../static/', import.meta.url).pathname | |
| 32 | const 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. | |
| 35 | for (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. | |
| 38 | 32 | |
| 39 | 33 | /** |
| 40 | 34 | * ⛔ Per-RESPONSE, keyed by the response object itself. A module-level "current theme" is request |
| … | … | @@ -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 | ||
| 11 | import s0 from '../static/anproto-client.js' with { type: 'text' } | |
| 12 | import s1 from '../static/composer.js' with { type: 'text' } | |
| 13 | import s2 from '../static/explore.js' with { type: 'text' } | |
| 14 | import s3 from '../static/levels.js' with { type: 'text' } | |
| 15 | import s4 from '../static/noble-crypto.js' with { type: 'text' } | |
| 16 | import s5 from '../static/noble-ed25519.js' with { type: 'text' } | |
| 17 | import s6 from '../static/noble-sha512.js' with { type: 'text' } | |
| 18 | import s7 from '../static/three.core.min.js' with { type: 'text' } | |
| 19 | import s8 from '../static/three.module.min.js' with { type: 'text' } | |
| 20 | ||
| 21 | export 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 | ]) |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-surface", |
| 3 | "version": "0.0.67", | |
| 3 | "version": "0.0.68", | |
| 4 | 4 | "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -2,6 +2,10 @@ |
| 2 | 2 | |
| 3 | 3 | Browser assets, served at `/static/…`. |
| 4 | 4 | |
| 5 | ⛔ **A new file here must be imported in `lib/static-files.js`.** They are served from text imports, | |
| 6 | not read off disk, because the phone runs the surface from a bare-pack bundle that holds only what is | |
| 7 | imported. `test/static-files.test.js` fails until a new file is listed. | |
| 8 | ||
| 5 | 9 | ⛔ **Vendored, not fetched.** A page that loaded its signer from someone else's server would let |
| 6 | 10 | that server mint signatures with the user's key. |
| 7 | 11 |
| … | … | @@ -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 | ||
| 12 | import test from 'brittle' | |
| 13 | import fs from '#node:fs' | |
| 14 | import { join } from '#node:path' | |
| 15 | ||
| 16 | const ROOT = new URL('..', import.meta.url).pathname | |
| 17 | ||
| 18 | test('★ 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 | ||
| 24 | test('★ 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 | }) |