Skip to content

Commit cc29dac

Browse files
Address copilot review on #2
- Multikey docstring: "e70102" → "e701" (constant was already correct; comment was stale from earlier fix). - nostrPubkeyToMultikey: validate that input is actually hex, not just 64 chars long. - doctor.js: only reveal add-key section when the profile fetched successfully; runAll now returns { checks, profileFetched, docUrl } so callers can distinguish "diagnostics ran" from "diagnostics found a real profile to root a snippet against." - doctor.js: clear stale signer output on connection error so users can't accidentally copy an outdated snippet after a failed reconnect. Also corrected the verificationMethod controller while in the file: the spec / JSS Phase A profile shape uses the WebID-with-fragment as controller (matching the profile's outer `controller` predicate), not the bare document URL. Updated lws-cid.js's VM-controller check to compare against the profile's @id with fragment so the validator and generator agree.
1 parent f71ea62 commit cc29dac

3 files changed

Lines changed: 52 additions & 22 deletions

File tree

doctor.js

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,21 @@ form.addEventListener('submit', async (e) => {
5353
results.hidden = false;
5454

5555
try {
56-
const checks = await runAll(url);
56+
const { checks, profileFetched, docUrl } = await runAll(url);
5757
renderChecks(checks);
58-
lastWebId = url;
59-
revealAddKeySection();
58+
if (profileFetched && docUrl) {
59+
lastWebId = docUrl;
60+
revealAddKeySection();
61+
} else {
62+
hideAddKeySection();
63+
}
6064
} catch (err) {
6165
renderChecks([{
6266
status: 'fail',
6367
label: 'Diagnostics crashed',
6468
detail: String(err?.message || err),
6569
}]);
70+
hideAddKeySection();
6671
} finally {
6772
button.disabled = false;
6873
button.textContent = 'Run diagnostics';
@@ -71,6 +76,7 @@ form.addEventListener('submit', async (e) => {
7176

7277
async function runAll(webIdUrl) {
7378
const checks = [];
79+
const result = { checks, profileFetched: false, docUrl: null };
7480

7581
// 1. Resolve the document URL — strip the fragment.
7682
let docUrl;
@@ -79,22 +85,22 @@ async function runAll(webIdUrl) {
7985
docUrl.hash = '';
8086
} catch (err) {
8187
checks.push({ status: 'fail', label: 'WebID is a valid URL', detail: err.message });
82-
return checks;
88+
return result;
8389
}
8490
checks.push({ status: 'pass', label: 'WebID is a valid URL', detail: docUrl.toString() });
8591

8692
// 2. Fetch as JSON-LD. We avoid Accept: text/turtle so the conneg layer
8793
// doesn't transform the document — we want to validate the JSON-LD
8894
// representation directly.
89-
let res, body, contentType;
95+
let res, contentType;
9096
try {
9197
res = await fetch(docUrl.toString(), {
9298
headers: { 'Accept': 'application/ld+json' },
9399
});
94100
contentType = (res.headers.get('content-type') || '').toLowerCase();
95101
} catch (err) {
96102
checks.push({ status: 'fail', label: 'Profile is reachable', detail: err.message });
97-
return checks;
103+
return result;
98104
}
99105

100106
if (!res.ok) {
@@ -103,7 +109,7 @@ async function runAll(webIdUrl) {
103109
label: 'Profile is reachable',
104110
detail: `HTTP ${res.status} from ${docUrl}`,
105111
});
106-
return checks;
112+
return result;
107113
}
108114
checks.push({
109115
status: 'pass',
@@ -137,16 +143,20 @@ async function runAll(webIdUrl) {
137143
label: 'Profile parses as JSON',
138144
detail: err.message,
139145
});
140-
return checks;
146+
return result;
141147
}
142148
checks.push({ status: 'pass', label: 'Profile parses as JSON' });
143149

150+
// Profile was fetched and parsed — safe to root a snippet against this URL.
151+
result.profileFetched = true;
152+
result.docUrl = docUrl.toString();
153+
144154
// 4. Run LWS-CID structural checks.
145155
for (const c of runLwsCidChecks(profile, { webIdUrl, docUrl: docUrl.toString() })) {
146156
checks.push(c);
147157
}
148158

149-
return checks;
159+
return result;
150160
}
151161

152162
// --- B.2: connect Nostr signer & compute Multikey VM -----------------
@@ -156,6 +166,24 @@ function revealAddKeySection() {
156166
detectSigner();
157167
}
158168

169+
function hideAddKeySection() {
170+
addKeySection.hidden = true;
171+
signerOutput.hidden = true;
172+
pubkeyHexEl.textContent = '';
173+
pubkeyMbEl.textContent = '';
174+
snippetEl.textContent = '';
175+
snippetTarget.textContent = '';
176+
lastWebId = null;
177+
}
178+
179+
function clearSignerOutput() {
180+
signerOutput.hidden = true;
181+
pubkeyHexEl.textContent = '';
182+
pubkeyMbEl.textContent = '';
183+
snippetEl.textContent = '';
184+
snippetTarget.textContent = '';
185+
}
186+
159187
function detectSigner() {
160188
if (typeof window.nostr?.getPublicKey === 'function') {
161189
setSignerStatus('ready', 'NIP-07 signer detected (window.nostr).');
@@ -185,6 +213,7 @@ connectButton.addEventListener('click', async () => {
185213
signerOutput.hidden = false;
186214
setSignerStatus('ready', 'Connected. The snippet below is ready to paste into your profile.');
187215
} catch (err) {
216+
clearSignerOutput();
188217
setSignerStatus('error', `Could not read pubkey: ${err.message || err}`);
189218
} finally {
190219
connectButton.textContent = 'Reconnect signer';

lib/lws-cid.js

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,15 @@ export function runLwsCidChecks(profile, { webIdUrl }) {
127127
label: `verificationMethod has ${vms.length} entr${vms.length === 1 ? 'y' : 'ies'}`,
128128
});
129129

130-
// Entry-level checks.
130+
// Entry-level checks. VM controllers are compared against the
131+
// profile's @id (with fragment if present) — that matches the CID
132+
// v1 self-control pattern where the outer profile.controller is
133+
// also the WebID itself, not the bare document URL.
134+
const profileIdAbs = absolutize(id, requestedNoHash);
131135
const seenIds = new Set();
132136
let allOk = true;
133137
for (const [i, vm] of vms.entries()) {
134-
const probs = validateVerificationMethod(vm, requestedNoHash, idAbs(profile, requestedNoHash));
138+
const probs = validateVerificationMethod(vm, requestedNoHash, profileIdAbs);
135139
if (probs.length === 0) continue;
136140
allOk = false;
137141
out.push({
@@ -352,12 +356,3 @@ function absolutize(u, base) {
352356
return u;
353357
}
354358
}
355-
356-
// Helper to compute the absolute, fragmentless form of the profile's
357-
// own @id — used to check that verificationMethod controllers point at
358-
// the WebID's document URL.
359-
function idAbs(profile, base) {
360-
const id = profile['@id'] || profile.id;
361-
if (!id) return null;
362-
return stripHash(absolutize(id, base));
363-
}

lib/multikey.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* 3. Prepend the multicodec varint for secp256k1-pub: 0xe7, 0x01
1010
* 4. Multibase-encode with base16-lower — prefix "f"
1111
*
12-
* Result: "f" + "e70102" + <parity-hex> + <pubkey-hex>
12+
* Result: "f" + "e701" + <parity-hex> + <pubkey-hex>
1313
*/
1414

1515
// Multicodec varint for secp256k1-pub: code 0xe7 → varint bytes 0xe7, 0x01
@@ -28,6 +28,9 @@ export function nostrPubkeyToMultikey(xOnlyHex, opts = {}) {
2828
if (hex.length !== 64) {
2929
throw new Error(`Nostr pubkey must be 32 bytes (64 hex chars); got ${hex.length}`);
3030
}
31+
if (!/^[0-9a-f]{64}$/.test(hex)) {
32+
throw new Error('Nostr pubkey must be lowercase hex (0-9, a-f)');
33+
}
3134

3235
let parity = opts.parity ?? 0x02;
3336
if (typeof parity === 'string') parity = parseInt(parity, 16);
@@ -79,11 +82,14 @@ export function buildNostrVerificationMethod({ webId, xOnlyHex, fragment = 'nost
7982
// The VM id is a fragment URI rooted at the WebID's document. We strip
8083
// any existing fragment so paths like ".../card.jsonld#me" yield
8184
// ".../card.jsonld#nostr-key-1".
85+
// The controller is the WebID itself (with its fragment) — this matches
86+
// CID v1 self-control where the profile's outer controller is also the
87+
// WebID, so all controller predicates inside the document agree.
8288
const docUrl = stripHash(webId);
8389
return {
8490
id: `${docUrl}#${fragment}`,
8591
type: 'Multikey',
86-
controller: docUrl,
92+
controller: webId,
8793
publicKeyMultibase: mb,
8894
};
8995
}

0 commit comments

Comments
 (0)