| … | … | @@ -74,7 +74,24 @@ const FAVELA_PORT = Number(process.env.FAVELA_PORT) || 4041 |
| 74 | 74 | // worklet's second one used to be built. |
| 75 | 75 | const state = { hpp: null, ready: false, favelaUrl: null, favelaSession: null } |
| 76 | 76 | |
| 77 | // BOOT TIMING. "The app takes a very long time to start" has been a standing complaint with no number |
| 78 | // attached, and every guess about the cause (bundle size? swarm join? replication?) is unfalsifiable |
| 79 | // without one. Each stage logs its own duration and the cumulative total, so the next boot on a real |
| 80 | // handset says where the time actually goes instead of inviting another guess. |
| 81 | // |
| 82 | // Deliberately measured with Date.now() deltas around the stages that ALREADY log — no new checkpoints |
| 83 | // invented, so the timings line up with the existing narrative in the log. |
| 84 | let _bootT0 = 0 |
| 85 | let _stageT = 0 |
| 86 | function stage (name, extra = {}) { |
| 87 | const now = Date.now() |
| 88 | const ms = _stageT ? now - _stageT : 0 |
| 89 | _stageT = now |
| 90 | log('boot stage: ' + name, { ms, totalMs: now - _bootT0, ...extra }) |
| 91 | } |
| 92 | |
| 77 | 93 | async function boot () { |
| 94 | _bootT0 = _stageT = Date.now() |
| 78 | 95 | prepareEnv() |
| 79 | 96 | log('booting', { storage }) |
| 80 | 97 | |
| … | … | @@ -131,6 +148,7 @@ async function boot () { |
| 131 | 148 | const configured = (cfg.dids || []).length |
| 132 | 149 | if (up === 0 && configured > 0) log('hpp booted NO DIDs — read-only despite ' + configured + ' configured', { dids: 0, configured }) |
| 133 | 150 | else log('hpp up — signing enabled', { dids: up, configured }) |
| 151 | stage('hpp up') |
| 134 | 152 | } catch (e) { |
| 135 | 153 | // A node that cannot sign still reads; a node that cannot boot is useless. Degrade, loudly. |
| 136 | 154 | try { console.error('[favela] hpp boot FAILED — read-only:', (e && e.stack) || String(e)) } catch {} |
| … | … | @@ -213,6 +231,7 @@ async function boot () { |
| 213 | 231 | const roster = rosterEnv({ hosted: hostedDids(), tracked: readTracked() }) |
| 214 | 232 | if (roster) process.env.FAVELA_ROSTER = roster |
| 215 | 233 | log('favela-web roster', { dids: roster ? roster.split(',').length : 0 }) |
| 234 | stage('favela-web starting') |
| 216 | 235 | fs.mkdirSync(process.env.FAVELA_STORE_ROOT, { recursive: true }) |
| 217 | 236 | await import('favela-web/bin/favela-web.js') |
| 218 | 237 | state.favelaUrl = 'http://127.0.0.1:' + FAVELA_PORT |
| … | … | @@ -221,6 +240,7 @@ async function boot () { |
| 221 | 240 | const fetchImpl = (await import('bare-fetch')).default |
| 222 | 241 | const res = await fetchImpl(state.favelaUrl + '/status') |
| 223 | 242 | log('favela-web in-process', { url: state.favelaUrl, status: res.status }) |
| 243 | stage('favela-web serving') |
| 224 | 244 | |
| 225 | 245 | // SIGN IN to our own favela-web. Without this every UI fetch is ANONYMOUS: viewerOf(req) returns |
| 226 | 246 | // null, so liked/reposted state and per-viewer mutes silently vanish from every row and every |
| … | … | @@ -228,28 +248,8 @@ async function boot () { |
| 228 | 248 | // the kind of quiet degradation this port exists to remove, so it is logged either way. |
| 229 | 249 | // |
| 230 | 250 | // Non-fatal: a phone that cannot sign in still reads (anonymously). Reporting beats guessing. |
| 231 | | const entry = state.hpp && state.hpp.nodes && state.hpp.nodes[0] |
| 232 | | if (entry) { |
| 233 | | try { |
| 234 | | const { loadOrCreateSubKey, establishSession, sessionNodeOf } = await import('./session.js') |
| 235 | | // hpp hands back a WRAPPER, not the SwarmDevice — see sessionNodeOf. Passing `entry` straight |
| 236 | | // through is what made every boot sign-in fail with "an hpp node with delegate() required". |
| 237 | | const node = sessionNodeOf(entry) |
| 238 | | if (!node) throw new Error('hpp entry exposes no delegate() — cannot sign in') |
| 239 | | const key = loadOrCreateSubKey(path.join(dirs.root, 'ui-subkey.json'), fs, path.dirname) |
| 240 | | if (key.created) log('minted a UI sub-key for this device (delegated by our own DID)') |
| 241 | | const s = await establishSession({ |
| 242 | | favelaUrl: state.favelaUrl, node, privHex: key.privHex, pubHex: key.pubHex, fetchImpl |
| 243 | | }) |
| 244 | | state.favelaSession = s.token |
| 245 | | log('signed in to favela-web', { did: s.did.slice(0, 20) + '…', handle: s.handle }) |
| 246 | | } catch (e) { |
| 247 | | state.favelaSession = null |
| 248 | | log('favela-web sign-in FAILED — the UI will read anonymously (no liked/reposted state, no writes)', { error: e.message }) |
| 249 | | } |
| 250 | | } else { |
| 251 | | log('no DID yet — favela-web stays anonymous until this device is paired') |
| 252 | | } |
| 251 | await signInToFavela() |
| 252 | stage('signed in') |
| 253 | 253 | } catch (e) { log('favela-web started but did not answer /status', { error: e.message }) } |
| 254 | 254 | } catch (e) { |
| 255 | 255 | state.favelaUrl = null |
| … | … | @@ -325,7 +325,57 @@ function readTracked () { try { return JSON.parse(fs.readFileSync(TRACKED(), 'ut |
| 325 | 325 | function hostedDids () { return ((state.hpp && state.hpp.nodes) || []).map((n) => n && n.did).filter(Boolean) } |
| 326 | 326 | function writeTracked (list) { try { fs.writeFileSync(TRACKED(), JSON.stringify(list, null, 2) + '\n') } catch {} } |
| 327 | 327 | |
| 328 | // SIGN IN to our own favela-web. Without this every UI fetch is ANONYMOUS: viewerOf(req) returns null, |
| 329 | // so liked/reposted state and per-viewer mutes silently vanish from every row and every write route 401s. |
| 330 | // The failure mode is invisible — posts just render un-liked — so it is logged either way. |
| 331 | // |
| 332 | // ⛔ CALLABLE AGAIN, not just at boot. favela-web's session store is an in-memory Map, so it is emptied |
| 333 | // by every restart of that process while this side still holds the old token. The WebView then hits |
| 334 | // /auth/handoff, gets "unknown or expired token", and RENDERS THE REFUSAL — a recoverable condition |
| 335 | // presented as a dead end, with force-stopping the app as the only way out. Nothing about the |
| 336 | // credentials changed: this side can mint a fresh session whenever asked, and now does. |
| 337 | // |
| 338 | // Non-fatal by design: a phone that cannot sign in still reads, anonymously. Reporting beats guessing. |
| 339 | async function signInToFavela () { |
| 340 | const entry = state.hpp && state.hpp.nodes && state.hpp.nodes[0] |
| 341 | if (!entry) { |
| 342 | log('no DID yet — favela-web stays anonymous until this device is paired') |
| 343 | return { ok: false, reason: 'no DID' } |
| 344 | } |
| 345 | try { |
| 346 | const fetchImpl = (await import('bare-fetch')).default |
| 347 | const { loadOrCreateSubKey, establishSession, sessionNodeOf } = await import('./session.js') |
| 348 | // hpp hands back a WRAPPER, not the SwarmDevice — see sessionNodeOf. Passing `entry` straight |
| 349 | // through is what made every boot sign-in fail with "an hpp node with delegate() required". |
| 350 | const node = sessionNodeOf(entry) |
| 351 | if (!node) throw new Error('hpp entry exposes no delegate() — cannot sign in') |
| 352 | const key = loadOrCreateSubKey(path.join(dirs.root, 'ui-subkey.json'), fs, path.dirname) |
| 353 | if (key.created) log('minted a UI sub-key for this device (delegated by our own DID)') |
| 354 | const s = await establishSession({ |
| 355 | favelaUrl: state.favelaUrl, node, privHex: key.privHex, pubHex: key.pubHex, fetchImpl |
| 356 | }) |
| 357 | state.favelaSession = s.token |
| 358 | log('signed in to favela-web', { did: s.did.slice(0, 20) + '…', handle: s.handle }) |
| 359 | return { ok: true, token: s.token } |
| 360 | } catch (e) { |
| 361 | state.favelaSession = null |
| 362 | log('favela-web sign-in FAILED — the UI will read anonymously (no liked/reposted state, no writes)', { error: e.message }) |
| 363 | return { ok: false, reason: e.message } |
| 364 | } |
| 365 | } |
| 366 | |
| 328 | 367 | const handlers = { |
| 368 | // Mint a FRESH favela-web session and hand back the token. |
| 369 | // |
| 370 | // Exists because favela-web's session store is in memory: when that process restarts, this side's |
| 371 | // token is instantly unknown and /auth/handoff answers 403. The WebView used to render that refusal |
| 372 | // and stop, which reads as "signed out" and is only escapable by force-stopping the app. It is a |
| 373 | // recoverable condition — nothing about the credentials changed — so the UI now asks for a new |
| 374 | // session instead of dead-ending. See App.jsx's onHttpError. |
| 375 | async resession () { |
| 376 | const r = await signInToFavela() |
| 377 | return { ok: !!r.ok, favelaSession: state.favelaSession, favelaUrl: state.favelaUrl, reason: r.reason || null } |
| 378 | }, |
| 329 | 379 | async status () { |
| 330 | 380 | // favelaSession is the session COOKIE VALUE, handed to the UI so its fetches can carry |
| 331 | 381 | // `Cookie: favela_session=…`. HttpOnly is a browser protection against document-script access; there |