OverviewFilesCommitsIssuesDocsReleasesPeers

favela-android commits / 6162368

worklet: sign in to our own favela-web at boot, and stop hardcoding its port
Wires lib/session.js into boot(). After favela-web answers /status, the worklet mints (once, persisted) a secp256k1 sub-key, has its own hpp node delegate it, and logs in — capturing the session cookie into state.favelaSession, which `status` now returns so the UI can send `Cookie: favela_session=…` on its fetches. Without this every UI fetch is anonymous: viewerOf(req) is null, so liked/reposted state and per-viewer mutes disappear from every row and every write route 401s. Nothing announces that — posts just render un-liked — so both the success and the failure are logged explicitly rather than left to be inferred from empty state. Non-fatal by design: a phone that cannot sign in still reads anonymously, and an unpaired phone says so instead of failing. On exposing the cookie to the UI: HttpOnly protects a document from script access. There is no document here, and the UI shares this process's trust boundary — the worklet already holds the DID signing key, which is strictly more sensitive than a 30-day session token. Also fixes a PRE-EXISTING test defect surfaced while verifying this (confirmed by stashing: it fails identically without these changes). FAVELA_PORT was a hardcoded constant, so test/ipc.test.mjs — which boots this file for real — died with EADDRINUSE on any machine already running a favela-web. It now honours the env, so the suite can bind an ephemeral port; the phone still gets 4041. Verified: full suite green (adopt + ipc + session) with FAVELA_PORT=4188. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6162368b81896938bce18525fea7ec1c7bbd5915
by plankatron <fleeky@gmail.com> · 2026-08-11T17:46:45.000Z

Changed files (1) +44 -3

lib/node.js +44 -3
……@@ -65,8 +65,11 @@ function prepareEnv () {
6565 process.env.FAVELA_RETENTION_BUDGET = process.env.FAVELA_RETENTION_BUDGET || String(64 * 1024 * 1024)
6666}
6767
68const FAVELA_PORT = 4041
69const state = { appview: null, hpp: null, ready: false, favelaUrl: null }
68// Env-overridable so the suite can bind an ephemeral port. Hardcoding it meant test/ipc.test.mjs —
69// which boots this file for real — died with EADDRINUSE whenever a favela-web was already running on
70// this machine, i.e. on any developer box that had actually deployed one. The phone always gets 4041.
71const FAVELA_PORT = Number(process.env.FAVELA_PORT) || 4041
72const state = { appview: null, hpp: null, ready: false, favelaUrl: null, favelaSession: null }
7073
7174async function boot () {
7275 prepareEnv()
……@@ -199,6 +202,31 @@ async function boot () {
199202 const fetchImpl = (await import('bare-fetch')).default
200203 const res = await fetchImpl(state.favelaUrl + '/status')
201204 log('favela-web in-process', { url: state.favelaUrl, status: res.status })
205
206 // SIGN IN to our own favela-web. Without this every UI fetch is ANONYMOUS: viewerOf(req) returns
207 // null, so liked/reposted state and per-viewer mutes silently vanish from every row and every
208 // write route 401s. The failure mode is invisible — posts just render un-liked — which is exactly
209 // the kind of quiet degradation this port exists to remove, so it is logged either way.
210 //
211 // Non-fatal: a phone that cannot sign in still reads (anonymously). Reporting beats guessing.
212 const node = state.hpp && state.hpp.nodes && state.hpp.nodes[0]
213 if (node) {
214 try {
215 const { loadOrCreateSubKey, establishSession } = await import('./session.js')
216 const key = loadOrCreateSubKey(path.join(dirs.root, 'ui-subkey.json'), fs, path.dirname)
217 if (key.created) log('minted a UI sub-key for this device (delegated by our own DID)')
218 const s = await establishSession({
219 favelaUrl: state.favelaUrl, node, privHex: key.privHex, pubHex: key.pubHex, fetchImpl
220 })
221 state.favelaSession = s.token
222 log('signed in to favela-web', { did: s.did.slice(0, 20) + '…', handle: s.handle })
223 } catch (e) {
224 state.favelaSession = null
225 log('favela-web sign-in FAILED — the UI will read anonymously (no liked/reposted state, no writes)', { error: e.message })
226 }
227 } else {
228 log('no DID yet — favela-web stays anonymous until this device is paired')
229 }
202230 } catch (e) { log('favela-web started but did not answer /status', { error: e.message }) }
203231 } catch (e) {
204232 state.favelaUrl = null
……@@ -254,7 +282,20 @@ function writeTracked (list) { try { fs.writeFileSync(TRACKED(), JSON.stringify(
254282
255283const handlers = {
256284 async status () {
257 return { ready: state.ready, signing: !!state.hpp, storage: dirs.root, favelaUrl: state.favelaUrl }
285 // favelaSession is the session COOKIE VALUE, handed to the UI so its fetches can carry
286 // `Cookie: favela_session=…`. HttpOnly is a browser protection against document-script access; there
287 // is no document here, and the UI shares this process's trust boundary — the worklet already holds
288 // the DID signing key, which is strictly more sensitive than a 30-day session token.
289 //
290 // Null means the UI is anonymous, and it must SAY so rather than render as if signed in: no
291 // liked/reposted state, no per-viewer mutes, and every write will 401.
292 return {
293 ready: state.ready,
294 signing: !!state.hpp,
295 storage: dirs.root,
296 favelaUrl: state.favelaUrl,
297 favelaSession: state.favelaSession
298 }
258299 },
259300 // HOME IS WHO YOU FOLLOW, not what happens to be indexed. Unscoped, getTimeline returns everything in
260301 // the local index — and this node indexes only the DIDs it hosts, so "the timeline" was your own posts