| … | … | @@ -16,6 +16,7 @@ import fs from '#node:fs' |
| 16 | 16 | import { sessionIdFrom, setCookie, clearCookie } from './session.js' |
| 17 | 17 | import * as pages from './pages.js' |
| 18 | 18 | import { embeds } from 'favela-markdown/lib/render.js' |
| 19 | import { recordUri } from 'favela-markdown/lib/uri.js' | |
| 19 | 20 | import { parse as parseMultipart } from './multipart.js' |
| 20 | 21 | import b4a from 'b4a' |
| 21 | 22 | |
| … | … | @@ -288,7 +289,21 @@ export async function route (req, res, ctx) { |
| 288 | 289 | // ⛔ `?quote=` is a COMPOSER SEED, not a mode. It becomes editable text in the box and nothing |
| 289 | 290 | // downstream knows a quote happened — which is what "quotes are just links" means in practice. |
| 290 | 291 | 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 | |
| 292 | 307 | // ⛔ `[@name](favela://<root>)` — the canonical form. Built here rather than left to the author, |
| 293 | 308 | // because nobody is going to type a 64-character identity root. |
| 294 | 309 | const mr = url.searchParams.get('mention') |
| … | … | @@ -1,7 +1,7 @@ |
| 1 | 1 | { |
| 2 | 2 | "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.", | |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| 7 | 7 | "bin": { |
| … | … | @@ -658,18 +658,36 @@ test('★ /repost and /unrepost reach the daemon', async (t) => { |
| 658 | 658 | t.is(String(r.headers.location), '/u/' + 'ab'.repeat(32), 'and returns you where you were') |
| 659 | 659 | }) |
| 660 | 660 | |
| 661 | test('⛔ ?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. | |
| 665 | test('⛔ ?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' | |
| 665 | 671 | const r = await request('GET', '/?quote=' + encodeURIComponent(target), { headers: { cookie } }) |
| 666 | 672 | 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') | |
| 668 | 675 | // "quotes are just links": nothing is written until the author posts, and what they post is an |
| 669 | 676 | // ordinary post. A quote that created a record on page load would be a different design. |
| 670 | 677 | t.is(calls.filter((c) => c.cmd === 'post').length, 0, 'nothing was written') |
| 671 | 678 | }) |
| 672 | 679 | |
| 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. | |
| 682 | test('⛔ 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 | ||
| 673 | 691 | test('⚠ a malformed ?quote= seeds nothing rather than garbage', async (t) => { |
| 674 | 692 | reset() |
| 675 | 693 | answers = { timeline: [], unresolved: [], profiles: {} } |