OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web commits / 9dfcf0b

0.3.63: serve the two reads a co-located node has no list to ride on
GET /api/profile did not exist. `FavelaApi.profile({did})` has called it since it was written, so that client has always 404'd — invisibly, because every in-tree caller reads profiles through a LIST route that hydrates them inline (/api/follows?hydrate=1, /api/people). Verified against the live daemon: GET /api/profile 404 while /api/follows and /api/author return 200. A phone asking about one tracked DID has no list to ride on. It returns `held` alongside the fields, because a DID we hold no profile record for is normal — a network follower found through a backlink — and "absent" must not render as "someone with no name". The `collections` control verb now also returns the raw `ledger` it already computed. The curated tracked/ignored/unknown split is for a human reading a diagnostic; a programmatic caller wants the entries, and rebuilding them from `unknown` would be a subset masquerading as the whole. Additive. Both exist so favela-android can delete its second appview: the phone was answering these from an index of its own, which no screen read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9dfcf0b7194f7c455cd7e4d66dbe48bf683ceb2c
by plankatron <fleeky@gmail.com> · 2026-08-18T23:28:01.000Z

Changed files (2) +34 -1

bin/favela-web.js +33 -0
……@@ -2153,6 +2153,34 @@ const server = http.createServer(async (req, res) => {
21532153 //
21542154 // Accepts JSON *or* form-urlencoded so it works with scripting off (same dual shape as /api/post):
21552155 // the no-JS path posts a plain <form> and gets a redirect back to /settings.
2156 // One person, as JSON. `FavelaApi.profile({did})` has called GET /api/profile since it was written and
2157 // this server only ever handled the POST — so the client has always 404'd here, silently, because
2158 // every in-tree caller happened to read profiles through a LIST route that hydrates them inline
2159 // (/api/follows?hydrate=1, /api/people). A phone asking about one tracked DID has no list to ride on.
2160 //
2161 // ⛔ `held` IS THE POINT. A DID we hold no profile record for is normal — a network follower
2162 // discovered through a backlink, or an account indexed before it ever wrote app.bsky.actor.profile —
2163 // and it must not render as "someone with no name". Absent and nameless are different facts, and the
2164 // caller can only tell them apart if we say which one this is.
2165 if (req.method === 'GET' && path === '/api/profile') {
2166 const did = url.searchParams.get('did')
2167 if (!did || !did.startsWith('did:')) return json(res, 400, JSON.stringify({ error: 'did required' }))
2168 const prof = await av.getProfile(did).catch(() => null)
2169 // Raced against a deadline, like every other handle hydration here: a handle that misses it renders
2170 // as a short DID, which is the honest fallback. The profile page learned this the slow way — it
2171 // resolved serially and hung for >90s.
2172 let h = null
2173 try { h = await Promise.race([handleResolver.resolve(did), delay(2500).then(() => null)]) } catch {}
2174 return json(res, 200, JSON.stringify({
2175 did,
2176 held: !!prof,
2177 displayName: (prof && prof.displayName) || null,
2178 description: (prof && prof.description) || null,
2179 avatar: (prof && prof.avatar) || null,
2180 handle: h && h.verified ? h.handle : null,
2181 verified: !!(h && h.verified)
2182 }))
2183 }
21562184 if (req.method === 'POST' && path === '/api/profile') {
21572185 const isForm = !(req.headers['content-type'] || '').includes('application/json')
21582186 const fail = (code, msg) => isForm ? redirect(res, '/settings') : json(res, code, JSON.stringify({ error: msg }))
……@@ -2818,6 +2846,11 @@ try {
28182846 const seen = new Map(ledger.map((e) => [e.collection, e]))
28192847 return {
28202848 ok: true,
2849 // The RAW ledger, alongside the curated classes below. The curation exists for a human reading a
2850 // diagnostic; a programmatic caller (favela-android's `collections` IPC verb) wants the entries
2851 // themselves, and synthesising them back out of `unknown` would be a subset masquerading as the
2852 // whole. Additive — nothing that reads the curated fields changes.
2853 ledger,
28212854 tracked: [...Appview.TRACKED.entries()].map(([collection, meta]) => ({ collection, view: meta.view, bytes: !!meta.bytes })),
28222855 ignored: [...IGNORED_COLLECTIONS.entries()].map(([collection, reason]) => ({
28232856 collection,
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-web",
3 "version": "0.3.62",
3 "version": "0.3.63",
44 "description": "favela-web — the web/SSR surface of Favela, the native P2P-local, community-owned social client + appview over the self-custody atproto stack (hyper-proto-peer). One of the Favela surfaces (alongside favela-cli and favela-native). bsky is an optional POSSE export, not the home. See DESIGN.md.",
55 "type": "module",
66 "main": "bin/favela-web.js",