OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface-android commits / b669de8

0.0.20: hyper-proto-peer in the worklet — this is the full-stack app
⛔ THE BOUNDARY IS REVERSED DELIBERATELY. `test/boundary.test.js` forbade the whole atproto stack, on the reading that this repo existed to be free of it. That reading is wrong: favela-android was the PROTOTYPE and this is the full-stack app, so hyper-proto-peer belongs here — the phone holds its own DID key and signs its own repo, which is what a full node means. ⛔ WHAT THE TEST STILL FORBIDS IS `favela-web`. It is the prototype's renderer and favela-surface replaces it; importing it would mean two UIs over one node, which is the duplication this repo actually exists to end (#45 measured the alternative at 1,771 lines of render.js plus 715 lines of React Native built and reversed inside four days). The rule was narrowed, not deleted. ⛔ THE ATPROTO OPS LIVE IN THE WORKLET, NOT IN `dispatch`. favela-peer's own boundary forbids the daemon importing anything `atproto-`, and hpp's mint reaches atproto-identity — so they cannot go there. `inProcessClient` handles them and delegates everything else, which is exactly where the two halves are already one process. Neither package learns about the other, and the surface calls `did-mint` the same way it calls anything. ⚠ `bare-globals` FIRST. The atproto stack reaches @noble, which uses TextEncoder at module scope, and Bare has no such global — a shim loaded afterwards is loaded too late. favela-android does the same, one line up. ⚠ Minting is not submitting: `did-mint` never touches the network, because submitting the genesis is "a PERMANENT, PUBLIC, IRREVERSIBLE append to a global log". And the root mnemonic is RETURNED, never stored — hpp's mint is explicit that minting is the one moment the recovery root exists on the device. MEASURED cost: bundle 15 -> 31 MB, addons 43 -> 58 (58/58 have arm64 prebuilds, which stage-addons fails fast on), APK 126 -> 154 MB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b669de8bf853527281d3b967da9ae18712d7c371
by plankatron <fleeky@gmail.com> · 2026-09-10T22:39:02.000Z

Changed files (4) +69 -14

lib/node.js +48 -2
……@@ -23,6 +23,10 @@
2323// ⚠ The HTTP server below is a genuine boundary: a WebView is a browser and speaks HTTP. It binds
2424// loopback only, and serves the same pages a desktop gets.
2525
26// ⛔ FIRST, before anything that reaches @noble via the atproto stack. Those modules use TextEncoder
27// at module scope and Bare has no such global, so the import fails with "TextEncoder is not defined"
28// — a shim loaded after the fact is loaded too late. favela-android does the same thing one line up.
29import 'hyper-proto-peer/lib/bare-globals.js'
2630import fs from 'bare-fs'
2731import process from 'bare-process'
2832import { join } from 'bare-path'
……@@ -68,7 +72,40 @@ let lastOtaNote = null
6872 * socket server itself calls.
6973 */
7074export function inProcessClient (node) {
71 return { call: (cmd, args = {}) => dispatch(node, cmd, args) }
75 return {
76 call: async (cmd, args = {}) => {
77 // ⛔ THE WORKLET OWNS THE COMPOSITION, and it has to: favela-peer's own boundary test forbids
78 // the daemon importing anything `atproto-`, and hyper-proto-peer's mint reaches
79 // atproto-identity. So the atproto ops cannot live in `dispatch` — but they can live HERE,
80 // where the two halves are already in one process, and the surface calls them the same way it
81 // calls everything else. Neither package learns about the other.
82 if (ATPROTO_OPS[cmd]) return ATPROTO_OPS[cmd](node, args)
83 return dispatch(node, cmd, args)
84 }
85 }
86}
87
88// ⛔ MINTING IS NOT SUBMITTING. `mintIdentity` never touches the network — submitting the genesis to
89// the PLC directory is "a PERMANENT, PUBLIC, IRREVERSIBLE append to a global log" and is a separate
90// call for that reason. A mint that cannot reach the directory still yields a working P2P identity,
91// and the op can be re-derived from the mnemonic and submitted later.
92//
93// ⛔ AND IT IS P2P-ONLY: no handle, no PDS. hyper-proto-peer's mint records why — "creating an
94// identity and joining someone's infrastructure are different decisions, and requiring both at once
95// is what made minting undecidable". The handle and service endpoint arrive later as a PLC update.
96const ATPROTO_OPS = {
97 'did-mint': async (node, a) => {
98 const { mintIdentity } = await import('hyper-proto-peer/lib/mint.js')
99 const r = await mintIdentity({ rootMnemonic: a.mnemonic || null, onStep: () => {} })
100 // ⚠ The root mnemonic comes back and is NOT stored here. hpp's mint is explicit: minting on the
101 // device is the one moment the recovery root exists there, so it is RETURNED for the caller to
102 // show once and nothing persists it in the config. Its own test asserts that.
103 return { did: r.did, mnemonic: r.rootMnemonic, device: r.devicePublicKey }
104 },
105 'did-submit': async (node, a) => {
106 const { submitMintedGenesis } = await import('hyper-proto-peer/lib/mint.js')
107 return submitMintedGenesis({ did: a.did })
108 }
72109}
73110
74111/**
……@@ -271,7 +308,15 @@ export async function boot (storage) {
271308 // truth for the same three fields — favela-android's note, and the reason this is a file.
272309 const writeState = () => {
273310 let staged = null
274 try { staged = (JSON.parse(fs.readFileSync(join(storage, 'bundles', 'current.json'), 'utf8')) || {}).treeHash } catch {}
311 let stagedBuiltAt = null
312 try {
313 const cur = JSON.parse(fs.readFileSync(join(storage, 'bundles', 'current.json'), 'utf8')) || {}
314 staged = cur.treeHash
315 // ⛔ THE STAGED BUNDLE'S BUILD TIME, because the page cannot decide without it. chooseBundle
316 // prefers the ASSET when the app is newer, so a staged-but-older bundle will never be
317 // applied — and a banner offering to restart for it is a button that does nothing, for ever.
318 stagedBuiltAt = cur.builtAt || null
319 } catch {}
275320 // ⛔ THE VERSION IS THE HALF A PERSON CAN ACT ON. Two tree hashes tell you something changed;
276321 // they do not tell you WHICH build you are running, so "is my update installed?" stayed
277322 // unanswerable from the device. `version` is what a release page shows.
……@@ -280,6 +325,7 @@ export async function boot (storage) {
280325 available: !!ota,
281326 version: VERSION,
282327 builtAt: BUILT_AT,
328 stagedBuiltAt,
283329 note: lastOtaNote
284330 }
285331 try { fs.writeFileSync(join(storage, STATE_FILE), JSON.stringify(st)) } catch { /* status only */ }
lib/version.js +2 -2
……@@ -1,4 +1,4 @@
11// GENERATED by scripts/gen-version.mjs — do not edit.
2export const VERSION = "0.0.17"
3export const BUILT_AT = 1789066113076
2export const VERSION = "0.0.20"
3export const BUILT_AT = 1789079828686
44export default { VERSION, BUILT_AT }
package.json +3 -2
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-surface-android",
3 "version": "0.0.17",
3 "version": "0.0.20",
44 "description": "favela-surface-android \u2014 a React Native shell over a bare-kit worklet running a full favela-peer node (markdown records, no atproto) with favela-surface rendering its pages in a WebView.",
55 "type": "module",
66 "private": true,
……@@ -40,7 +40,8 @@
4040 "react-native": "^0.86.2",
4141 "react-native-bare-kit": "^0.14.5",
4242 "react-native-fs": "^2.20.0",
43 "react-native-webview": "^14.0.1"
43 "react-native-webview": "^14.0.1",
44 "hyper-proto-peer": "file:../hyper-proto-peer"
4445 },
4546 "devDependencies": {
4647 "@react-native-community/cli": "^20.2.0",
test/boundary.test.js +16 -8
……@@ -1,17 +1,25 @@
1// The worklet must not reach the atproto stack.
1// The worklet must not reach the PROTOTYPE'S UI.
22//
3// ⛔ This repo exists because favela-android is tied to it. The tie was never in a single import —
4// `lib/node.js` there pulls hyper-proto-peer, favela-web, atproto-store and atproto-identity through
5// NINE dynamic imports, which a static reading of the file misses entirely. So this checks both
6// forms, and it checks IMPORTS rather than mentions: the prose in these files discusses favela-web
7// and hyper-proto-peer constantly, and should.
3// ⛔ REVERSED DELIBERATELY, 2026-09-10. This file used to forbid the whole atproto stack, on the
4// reading that this repo existed to be free of it. That reading is wrong: favela-android was the
5// PROTOTYPE and this is the full-stack app, so hyper-proto-peer belongs here — the phone holds its
6// own DID key and signs its own repo, which is the entire point of a full node.
7//
8// ⛔ WHAT STILL MUST NOT COME BACK IS `favela-web`. It is the prototype's renderer and favela-surface
9// replaces it; importing it would mean two UIs over one node, which is the duplication this repo was
10// actually created to end (#45 measured the alternative at 1,771 lines of render.js plus 715 lines
11// of React Native built and reversed in four days).
12//
13// ⚠ Checks IMPORTS rather than mentions, static AND dynamic — favela-android's coupling took the
14// dynamic form through nine `import()` calls, which a static reading misses entirely. The prose in
15// these files discusses favela-web constantly and should.
816
917import test from 'brittle'
1018import fs from 'bare-fs'
1119import { join } from 'bare-path'
1220
1321const ROOT = new URL('..', import.meta.url).pathname
14const FORBIDDEN = /(hyper-proto-peer|favela-web|atproto-[a-z]+|ap-bridge)/
22const FORBIDDEN = /(favela-web)/
1523
1624const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
1725 e.name === 'node_modules' ? []
……@@ -23,7 +31,7 @@ const importsOf = (src) => [
2331 ...src.matchAll(/(?:from|import)\s*\(?\s*['"]([^'"]+)['"]/g)
2432].map((m) => m[1])
2533
26test('★ no source file imports the atproto stack — static OR dynamic', (t) => {
34test('★ no source file imports the prototype UI — static OR dynamic', (t) => {
2735 for (const f of [...walk(join(ROOT, 'lib')), ...walk(join(ROOT, 'app'))]) {
2836 for (const spec of importsOf(fs.readFileSync(f, 'utf8'))) {
2937 t.absent(FORBIDDEN.test(spec), f.slice(ROOT.length) + ' imports ' + spec)