OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface commits / 88b1705

0.0.47: a quote names the author's identity root, not their feed key
The composer seed built `favela://<feed>/<collection>/<name>`. A mention carries an identity ROOT, and both are 64 hex — so the same position in the same scheme meant two different things depending on which line built it (hyper-social #61 §12 #1). favela-markdown 0.1.6 settles it as the root; this is the mint site that was putting a locator there. `profiles` is already fetched two lines above and carries feed → root for every readable feed, so resolving costs nothing. ⚠ When the root is unknown we now emit NOTHING rather than falling back to the feed key. A URI goes into a record's bytes and SOCIAL-MODEL §8 forbids rewriting a record you did not author, so a wrong address written once is permanent; an empty composer is recoverable. ⛔ The existing test asserted the bug — it seeded with a feed key and checked for it. Rewritten to assert the root form and to assert the feed key is ABSENT, plus a second test pinning the unresolvable case to "seeds nothing". favela-surface 175/175 tests, 565/565 asserts (was 173/561) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88b17057a7ef56f813f308540f9b6116e9cb5483
by plankatron <fleeky@gmail.com> · 2026-09-08T14:33:37.000Z

Changed files (3) +41 -8

lib/router.js +16 -1
……@@ -16,6 +16,7 @@ import fs from '#node:fs'
1616import { sessionIdFrom, setCookie, clearCookie } from './session.js'
1717import * as pages from './pages.js'
1818import { embeds } from 'favela-markdown/lib/render.js'
19import { recordUri } from 'favela-markdown/lib/uri.js'
1920import { parse as parseMultipart } from './multipart.js'
2021import b4a from 'b4a'
2122
……@@ -288,7 +289,21 @@ export async function route (req, res, ctx) {
288289 // ⛔ `?quote=` is a COMPOSER SEED, not a mode. It becomes editable text in the box and nothing
289290 // downstream knows a quote happened — which is what "quotes are just links" means in practice.
290291 const q = url.searchParams.get('quote')
291 const quote = q && parseTarget(q) ? 'favela://' + q : null
292 // ⛔ THE AUTHORITY IS THE IDENTITY ROOT, not the feed key (hyper-social #61 §12 #1). This built
293 // `'favela://' + feed + '/' + collection + '/' + name` — a FEED key in the position where a
294 // mention carries a ROOT. Both are 64 hex, so nothing downstream could tell which it had, and
295 // `isMention` (bare `^favela://`) then classified the whole thing as a mention of a user.
296 //
297 // A feed key is a LOCATOR; the root is the identity, and it survives device changes and feed
298 // migration. `profiles` is already fetched above and carries feed → root for every readable
299 // feed, so the resolution costs nothing here.
300 //
301 // ⚠ If the root is unknown we emit NOTHING rather than falling back to the feed key. A URI goes
302 // into a record's bytes and SOCIAL-MODEL §8 forbids rewriting a record you did not author — a
303 // wrong address written once is permanent, and an empty composer is recoverable.
304 const qt = q ? parseTarget(q) : null
305 const qroot = qt && profiles && profiles[qt.feed] ? profiles[qt.feed].root : null
306 const quote = qt && qroot ? recordUri({ root: qroot, collection: qt.collection, name: qt.name }) : null
292307 // ⛔ `[@name](favela://<root>)` — the canonical form. Built here rather than left to the author,
293308 // because nobody is going to type a 64-character identity root.
294309 const mr = url.searchParams.get('mention')
package.json +2 -2
……@@ -1,7 +1,7 @@
11{
22 "name": "favela-surface",
3 "version": "0.0.46",
4 "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.",
3 "version": "0.0.47",
4 "description": "The favela renderer — server-side HTML over the daemon's socket. No storage, no keys, no swarm.",
55 "type": "module",
66 "main": "index.js",
77 "bin": {
test/routes.test.js +23 -5
……@@ -658,18 +658,36 @@ test('★ /repost and /unrepost reach the daemon', async (t) => {
658658 t.is(String(r.headers.location), '/u/' + 'ab'.repeat(32), 'and returns you where you were')
659659})
660660
661test('⛔ ?quote= seeds the composer and creates NO record', async (t) => {
662 reset()
663 answers = { timeline: [], unresolved: [], profiles: {} }
664 const target = 'ab'.repeat(32) + '/post/p1'
661// ⛔ THIS TEST USED TO ASSERT THE BUG. It seeded the composer with `favela://<FEED KEY>/…` and
662// checked for it. A mention carries an identity ROOT (dialect.js:37) and both are 64 hex, so the
663// authority meant two different things depending on which code path built it — hyper-social
664// #61 §12 #1. The authority is the ROOT; a feed key is a locator.
665test('⛔ ?quote= seeds the composer with a ROOT-authority URI, and creates NO record', async (t) => {
666 reset()
667 const feed = 'ab'.repeat(32)
668 const root = 'cd'.repeat(32)
669 answers = { timeline: [], unresolved: [], profiles: { [feed]: { root } } }
670 const target = feed + '/post/p1'
665671 const r = await request('GET', '/?quote=' + encodeURIComponent(target), { headers: { cookie } })
666672 t.is(r.status, 200)
667 t.ok(r.body.includes('favela://' + 'ab'.repeat(32)), 'the link is in the box')
673 t.ok(r.body.includes('favela://' + root + '/post/p1'), 'the authority is the identity root')
674 t.absent(r.body.includes('favela://' + feed), 'never the feed key')
668675 // "quotes are just links": nothing is written until the author posts, and what they post is an
669676 // ordinary post. A quote that created a record on page load would be a different design.
670677 t.is(calls.filter((c) => c.cmd === 'post').length, 0, 'nothing was written')
671678})
672679
680// ⚠ A URI goes into a record's bytes, and SOCIAL-MODEL §8 forbids rewriting a record you did not
681// author — so a wrong address written once is permanent. An empty composer is recoverable.
682test('⛔ a quote whose root cannot be resolved seeds NOTHING, never a feed-key address', async (t) => {
683 reset()
684 const feed = 'ab'.repeat(32)
685 answers = { timeline: [], unresolved: [], profiles: {} }
686 const r = await request('GET', '/?quote=' + encodeURIComponent(feed + '/post/p1'), { headers: { cookie } })
687 t.is(r.status, 200)
688 t.absent(r.body.includes('favela://' + feed), 'no fallback to the locator')
689})
690
673691test('⚠ a malformed ?quote= seeds nothing rather than garbage', async (t) => {
674692 reset()
675693 answers = { timeline: [], unresolved: [], profiles: {} }