| … | … | @@ -1483,12 +1483,27 @@ export function reachSettings (reach) { |
| 1483 | 1483 | (on.has(name) ? ' checked' : '') + '> ' + esc(label) + '</label>' + |
| 1484 | 1484 | '<br><span class="who">' + esc(why) + '</span></p>' |
| 1485 | 1485 | |
| 1486 | const cred = reach.credential | |
| 1487 | ? '<p class="who">Publishing as <code>' + esc(reach.credential) + '</code></p>' | |
| 1488 | // identity.md §5.1 — with no designated credential the projection is withheld rather than | |
| 1489 | // published under a guess. The page has to say that, or an enabled network looks like it works. | |
| 1490 | : '<p class="warn">No identifier is designated, so nothing will be published even where a ' + | |
| 1491 | 'network is ticked. Link one on the machine running the daemon.</p>' | |
| 1486 | // ⛔ identity.md §5.1 — with no designated credential the projection is WITHHELD rather than | |
| 1487 | // published under a guess: "Several without a designation → refuse to project. Silently picking | |
| 1488 | // one would publish under an identity the author did not choose." The page has to say so, or an | |
| 1489 | // enabled network looks like it works. | |
| 1490 | // | |
| 1491 | // ⛔ AND IT HAS TO BE SETTABLE HERE. This said "Link one on the machine running the daemon" and | |
| 1492 | // that mechanism did not exist: `setReach` accepts a credential, this rendered one, and NOTHING | |
| 1493 | // anywhere passed one — no route, no CLI verb. An instruction pointing at a door nobody built is | |
| 1494 | // worse than no instruction, because it reads like operator error. Same shape as `ota.js` being | |
| 1495 | // copied and never imported. | |
| 1496 | // | |
| 1497 | // ⚠ It is the ATPROTO credential specifically (§5.1: "designated: 'atproto'"), which is why it | |
| 1498 | // says DID rather than "identifier". The fediverse equivalent is not a credential at all — §6 | |
| 1499 | // makes a bridge a RESOURCE you hold a grant on, obtained by registering with one. | |
| 1500 | const cred = '<p><label>Bluesky identity (DID)<br>' + | |
| 1501 | '<input type="text" name="credential" value="' + esc(reach.credential || '') + '" ' + | |
| 1502 | 'placeholder="did:plc:…" autocomplete="off"></label></p>' + | |
| 1503 | (reach.credential | |
| 1504 | ? '<p class="who">Publishing to Bluesky as <code>' + esc(reach.credential) + '</code></p>' | |
| 1505 | : '<p class="warn">No Bluesky identity is designated, so nothing is published there even ' + | |
| 1506 | 'with the box ticked. Paste a DID this node holds, or leave it empty to keep it off.</p>') | |
| 1492 | 1507 | |
| 1493 | 1508 | return '<h2>Publishing</h2>' + |
| 1494 | 1509 | (on.size === 0 |
| … | … | @@ -847,8 +847,16 @@ export async function route (req, res, ctx) { |
| 847 | 847 | // ⚠ One field per network — see reachSettings(). An unchecked box sends nothing at all, so |
| 848 | 848 | // absence is the correct reading of "off" here. |
| 849 | 849 | const networks = ['bsky', 'fediverse'].filter((n) => form['reach-' + n]) |
| 850 | // ⛔ SENT, because it never was. The field has been rendered and accepted by the daemon the | |
| 851 | // whole time with nothing in between — so the page told people to designate an identity | |
| 852 | // somewhere else, and there was no somewhere else. | |
| 853 | // | |
| 854 | // ⚠ An empty box CLEARS it, deliberately: `setReach` treats a falsy credential as null, and | |
| 855 | // withholding is the fail-closed state. Leaving it unsendable would make "stop publishing under | |
| 856 | // this identity" impossible from the only surface that can say it. | |
| 857 | const credential = String(form.credential || '').trim() || null | |
| 850 | 858 | try { |
| 851 | await client.call('set-reach', { networks }) | |
| 859 | await client.call('set-reach', { networks, credential }) | |
| 852 | 860 | } catch (err) { |
| 853 | 861 | return seeOther(res, '/settings?m=' + encodeURIComponent(err.message)) |
| 854 | 862 | } |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-surface", |
| 3 | "version": "0.0.56", | |
| 3 | "version": "0.0.57", | |
| 4 | 4 | "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -1291,6 +1291,38 @@ test('a LINKED node passes straight through, and an old daemon is not locked out |
| 1291 | 1291 | }) |
| 1292 | 1292 | |
| 1293 | 1293 | |
| 1294 | // ── the designated credential ──────────────────────────────────────────────────────────────────── | |
| 1295 | // | |
| 1296 | // ⛔ `setReach` has accepted a credential and `reachSettings` has rendered one since they were | |
| 1297 | // written, with NOTHING in between — no route, no CLI verb. So the page told people to designate an | |
| 1298 | // identity "on the machine running the daemon" and there was no such mechanism anywhere. These pin | |
| 1299 | // the wire that was missing. | |
| 1300 | test('★ POST /reach sends the credential — the half that was never wired', async (t) => { | |
| 1301 | answers.whoami = { feed: 'ab', device: 'cd', linked: true } | |
| 1302 | answers['set-reach'] = { networks: ['bsky'], credential: 'did:plc:abc' } | |
| 1303 | calls.length = 0 | |
| 1304 | await request('POST', '/reach', { | |
| 1305 | headers: { cookie, 'content-type': 'application/x-www-form-urlencoded' }, | |
| 1306 | body: 'reach-bsky=on&credential=did%3Aplc%3Aabc' | |
| 1307 | }) | |
| 1308 | const set = calls.find((c) => c.cmd === 'set-reach') | |
| 1309 | t.alike(set.args.networks, ['bsky']) | |
| 1310 | t.is(set.args.credential, 'did:plc:abc', 'the DID reaches the daemon') | |
| 1311 | }) | |
| 1312 | ||
| 1313 | // ⚠ Clearing must be possible from the only surface that can say it — withholding is the | |
| 1314 | // fail-closed state, so "stop publishing under this identity" has to be expressible. | |
| 1315 | test('an empty credential box CLEARS the designation rather than being ignored', async (t) => { | |
| 1316 | answers.whoami = { feed: 'ab', device: 'cd', linked: true } | |
| 1317 | answers['set-reach'] = { networks: [], credential: null } | |
| 1318 | calls.length = 0 | |
| 1319 | await request('POST', '/reach', { | |
| 1320 | headers: { cookie, 'content-type': 'application/x-www-form-urlencoded' }, | |
| 1321 | body: 'credential=' | |
| 1322 | }) | |
| 1323 | t.is(calls.find((c) => c.cmd === 'set-reach').args.credential, null) | |
| 1324 | }) | |
| 1325 | ||
| 1294 | 1326 | test('shut down', async (t) => { |
| 1295 | 1327 | await new Promise((r) => server.close(r)) |
| 1296 | 1328 | await new Promise((r) => daemon.close(r)) |