OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay commits / 66b1e11

fix: emit capability-doc `operator` only when declared
My previous commit added `operator` unconditionally, so every doc carried `operator: null`. canonicalSignablePayload() covers EVERY key except `signature`, which meant the signed bytes of every relay in the fleet changed — including relays with nothing to declare — and the checked-in capability-doc conformance vector, which pins the default signable shape, failed with "capability doc signableHash mismatch". Conditional inclusion instead, matching the idiom already used for privacyTransports and indexRoom ("added below only when non-empty, preserving the signable shape"). The default doc is byte-identical again, and the field appears only where it carries information. Caught by test/unit/profile-vector-verifier.test.js, which is exactly the guard that should catch it — a frozen conformance vector external implementations verify against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66b1e11505d9518f70510bd7f0748899b5ac9b71
by bigdestiny2 <33146784+bigdestiny2@users.noreply.github.com> · 2026-07-28T14:28:50.000Z

Changed files (2) +13 -3

packages/core/core/capability-doc.js +7 -1
……@@ -352,7 +352,6 @@ export function buildCapabilityDoc (opts = {}) {
352352 version: opts.version || null,
353353 runtime,
354354 region,
355 operator,
356355 contact: opts.contact || config.contact || null,
357356 terms_of_service: opts.termsOfService || config.termsOfService || null,
358357 supported_transports: transports,
……@@ -393,6 +392,13 @@ export function buildCapabilityDoc (opts = {}) {
393392 // advertisements (hiverelay.onion/1). Omitted unless verified ready
394393 // (added below only when non-empty, preserving the signable shape).
395394 ...(privacyTransports.length > 0 ? { privacyTransports } : {}),
395 // operator — present only when declared, for the same reason: the canonical
396 // signer covers every key, so emitting `operator: null` everywhere would
397 // change the signed bytes of every relay in the fleet, including the ones
398 // that have nothing to say. Conditional inclusion keeps the default
399 // signable shape byte-identical (the checked-in capability-doc conformance
400 // vector pins it) and changes the doc only where it carries information.
401 ...(operator ? { operator } : {}),
396402 // indexRoom — z32 link to this relay's schema-sheets index room, if a
397403 // sidecar has published one. Additive: clients that don't understand it
398404 // ignore it and fall back to catalogBeeKey / /catalog.json. schemaVersion
test/unit/capability-doc.test.js +6 -2
……@@ -81,8 +81,12 @@ test('publishes operator identity so quorum diversity can be counted honestly',
8181 // own operator — so five boxes in one datacenter under one owner satisfied a
8282 // minOperators floor of five. The CLI accepted --operator all along; it was
8383 // simply never published, so no client could see the concentration.
84 // Absent, not null: the canonical signer covers every key present, so
85 // emitting `operator: null` everywhere would change the signed bytes of every
86 // relay — including those with nothing to declare — and break the checked-in
87 // capability-doc conformance vector that pins the default signable shape.
8488 const unset = buildCapabilityDoc({ relay: { config: {} } })
85 t.is(unset.operator, null, 'absent when the operator has not declared one')
89 t.absent('operator' in unset, 'key is omitted entirely when undeclared')
8690
8791 const declared = buildCapabilityDoc({
8892 relay: { config: { operator: 'hive-foundation-utah', regions: ['NA'] } }
……@@ -90,7 +94,7 @@ test('publishes operator identity so quorum diversity can be counted honestly',
9094 t.is(declared.operator, 'hive-foundation-utah')
9195 t.is(declared.region, 'NA')
9296
93 t.is(buildCapabilityDoc({ relay: { config: { operator: ' ' } } }).operator, null,
97 t.absent('operator' in buildCapabilityDoc({ relay: { config: { operator: ' ' } } }),
9498 'whitespace is not an operator identity')
9599})
96100