| … | … | @@ -65,8 +65,11 @@ function prepareEnv () { |
| 65 | 65 | process.env.FAVELA_RETENTION_BUDGET = process.env.FAVELA_RETENTION_BUDGET || String(64 * 1024 * 1024) |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | | const FAVELA_PORT = 4041 |
| 69 | | const 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. |
| 71 | const FAVELA_PORT = Number(process.env.FAVELA_PORT) || 4041 |
| 72 | const state = { appview: null, hpp: null, ready: false, favelaUrl: null, favelaSession: null } |
| 70 | 73 | |
| 71 | 74 | async function boot () { |
| 72 | 75 | prepareEnv() |
| … | … | @@ -199,6 +202,31 @@ async function boot () { |
| 199 | 202 | const fetchImpl = (await import('bare-fetch')).default |
| 200 | 203 | const res = await fetchImpl(state.favelaUrl + '/status') |
| 201 | 204 | 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 | } |
| 202 | 230 | } catch (e) { log('favela-web started but did not answer /status', { error: e.message }) } |
| 203 | 231 | } catch (e) { |
| 204 | 232 | state.favelaUrl = null |
| … | … | @@ -254,7 +282,20 @@ function writeTracked (list) { try { fs.writeFileSync(TRACKED(), JSON.stringify( |
| 254 | 282 | |
| 255 | 283 | const handlers = { |
| 256 | 284 | 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 | } |
| 258 | 299 | }, |
| 259 | 300 | // HOME IS WHO YOU FOLLOW, not what happens to be indexed. Unscoped, getTimeline returns everything in |
| 260 | 301 | // the local index — and this node indexes only the DIDs it hosts, so "the timeline" was your own posts |