| … | … | @@ -2153,6 +2153,34 @@ const server = http.createServer(async (req, res) => { |
| 2153 | 2153 | // |
| 2154 | 2154 | // Accepts JSON *or* form-urlencoded so it works with scripting off (same dual shape as /api/post): |
| 2155 | 2155 | // 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 | } |
| 2156 | 2184 | if (req.method === 'POST' && path === '/api/profile') { |
| 2157 | 2185 | const isForm = !(req.headers['content-type'] || '').includes('application/json') |
| 2158 | 2186 | const fail = (code, msg) => isForm ? redirect(res, '/settings') : json(res, code, JSON.stringify({ error: msg })) |
| … | … | @@ -2818,6 +2846,11 @@ try { |
| 2818 | 2846 | const seen = new Map(ledger.map((e) => [e.collection, e])) |
| 2819 | 2847 | return { |
| 2820 | 2848 | 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, |
| 2821 | 2854 | tracked: [...Appview.TRACKED.entries()].map(([collection, meta]) => ({ collection, view: meta.view, bytes: !!meta.bytes })), |
| 2822 | 2855 | ignored: [...IGNORED_COLLECTIONS.entries()].map(([collection, reason]) => ({ |
| 2823 | 2856 | collection, |