| … | … | @@ -77,9 +77,21 @@ export async function serveDeviceUds (device, { uds, blobs = null, onRelease = n |
| 77 | 77 | if (!uri || !val) throw new Error('label-intent: uri and val required') |
| 78 | 78 | return l.ensure(uri, val, { neg: !!neg }) |
| 79 | 79 | }) |
| 80 | // label-list — also the labeler's DIAGNOSTIC (2026-10-14: member labels never re-signed and nothing said why). | |
| 81 | // `holds` is the exact gate the label tick asks (LabelRoom.holds → device.isSigner(), election WITH the network | |
| 82 | // head — get-state's holdsSigner asks without it and can disagree). `labels` is the signed log itself, `ver` | |
| 83 | // included, so "is it signed correctly" is a read, not an inference. Read-only. | |
| 80 | 84 | svc.handle('label-list', async () => { |
| 81 | 85 | const l = needLabels() |
| 82 | return { intents: await l.base.intents(), signed: (await l.signedLog()).length } | |
| 86 | const log = await l.signedLog() | |
| 87 | let holds = null | |
| 88 | try { holds = l.holds ? !!(await l.holds()) : null } catch { holds = null } | |
| 89 | return { | |
| 90 | intents: await l.base.intents(), | |
| 91 | signed: log.length, | |
| 92 | holds, | |
| 93 | labels: log.map((x) => ({ uri: x.uri, val: x.val, cts: x.cts, neg: !!x.neg, ver: x.ver === undefined ? null : x.ver })) | |
| 94 | } | |
| 83 | 95 | }) |
| 84 | 96 | // get-record: read ONE record back. get-state lists {collection, rkey} and no values, so a client |
| 85 | 97 | // could see THAT a record exists and never what it says — which makes "is what I would write |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "hyper-proto-peer", |
| 3 | "version": "0.3.130", | |
| 3 | "version": "0.3.131", | |
| 4 | 4 | "description": "Self-custody, P2P-native atproto peer \u2014 holds the user's key + content as Hypercores and authors+signs its own repo. The source of truth in the peer/bridge split (Model B); atproto-bridge projects it to bsky.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "lib/peer.js", |
| … | … | @@ -62,6 +62,26 @@ test('★ label-intent is idempotent; label-list shows intents and how many are |
| 62 | 62 | t.is(g.changed, true, 'a negation is a change') |
| 63 | 63 | }) |
| 64 | 64 | |
| 65 | // ⛔ DIAGNOSE WHAT THE GATE SEES. 2026-10-14: loser-club's member labels were never re-signed with `ver`, both | |
| 66 | // co-owners logged nothing, and get-state's holdsSigner (asked WITHOUT the network head) said stortron while the | |
| 67 | // label gate asks WITH it. label-list now answers both questions on the device itself: would this device's label | |
| 68 | // tick sign right now (the same holds() the tick calls), and what is actually in the signed log, ver included. | |
| 69 | test('label-list reports the label gate and each signed label with its ver', async (t) => { | |
| 70 | const sb = mockSb() | |
| 71 | let gate = true | |
| 72 | const room = new LabelRoom(sb, { src: SRC, privateKey: KEY, holds: () => gate, ns: '__labelintent__' }) | |
| 73 | const c = await serve(t, room) | |
| 74 | await c.call('label-intent', { uri: WJ, val: 'loser-club-member' }) | |
| 75 | await room.tick() | |
| 76 | const l = await c.call('label-list', {}) | |
| 77 | t.is(l.holds, true, 'holds: the gate the tick asks') | |
| 78 | t.is(l.labels.length, 1) | |
| 79 | t.alike({ uri: l.labels[0].uri, val: l.labels[0].val, ver: l.labels[0].ver, neg: l.labels[0].neg }, { uri: WJ, val: 'loser-club-member', ver: 1, neg: false }) | |
| 80 | t.ok(typeof l.labels[0].cts === 'string', 'with its cts') | |
| 81 | gate = false | |
| 82 | t.is((await c.call('label-list', {})).holds, false, 'and it follows the election') | |
| 83 | }) | |
| 84 | ||
| 65 | 85 | test('label-intent checks its input', async (t) => { |
| 66 | 86 | const c = await serve(t, new LabelRoom(mockSb(), { src: SRC, privateKey: KEY, holds: () => true, ns: '__labelintent__' })) |
| 67 | 87 | await t.exception(() => c.call('label-intent', { uri: WJ }), /val/) |