Skip to content

Commit 5029ff5

Browse files
Address copilot pass 10 on #2
The generator was hard-coding VM controller to the WebID, but the validator (in pass 4) was already comparing each VM controller against the profile's declared `controller` (with @id fallback). For delegated-control profiles those differ, so the snippet would fail my own VM-controller check. - lib/multikey.js: buildNostrVerificationMethod accepts an explicit `controller` arg, defaulting to webId. Self-control behavior is unchanged; delegated-control callers can now pass the profile's declared controller through. - lib/lws-cid.js: export normalizeControllers so callers outside the validator can derive a canonical controller IRI from JSON-LD's string/object/array shapes. - doctor.js: capture the profile's controller during runAll using normalizeControllers (first IRI, fallback to canonical WebID), thread it through state and into buildNostrVerificationMethod. Verified end-to-end: in both self-controlled and delegated-controlled profiles, the generated VM is accepted by runLwsCidChecks when merged back into the source profile.
1 parent 39d932f commit 5029ff5

3 files changed

Lines changed: 35 additions & 15 deletions

File tree

doctor.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* no auth — just a read. The output is a green/yellow/red checklist.
99
*/
1010

11-
import { runLwsCidChecks } from './lib/lws-cid.js';
11+
import { runLwsCidChecks, normalizeControllers } from './lib/lws-cid.js';
1212
import { buildNostrVerificationMethod } from './lib/multikey.js';
1313

1414
const form = document.getElementById('check-form');
@@ -31,6 +31,7 @@ const copyStatus = document.getElementById('copy-status');
3131

3232
let lastWebId = null;
3333
let lastDocUrl = null;
34+
let lastController = null;
3435

3536
// Allow ?webid=… in the URL to pre-fill (handy for sharing / bookmarks).
3637
const params = new URLSearchParams(window.location.search);
@@ -58,11 +59,12 @@ form.addEventListener('submit', async (e) => {
5859
hideAddKeySection();
5960

6061
try {
61-
const { checks, profileFetched, webId, docUrl } = await runAll(url);
62+
const { checks, profileFetched, webId, docUrl, controller } = await runAll(url);
6263
renderChecks(checks);
6364
if (profileFetched && webId) {
6465
lastWebId = webId;
6566
lastDocUrl = docUrl;
67+
lastController = controller;
6668
revealAddKeySection();
6769
} else {
6870
hideAddKeySection();
@@ -82,7 +84,7 @@ form.addEventListener('submit', async (e) => {
8284

8385
async function runAll(webIdUrl) {
8486
const checks = [];
85-
const result = { checks, profileFetched: false, docUrl: null, webId: null };
87+
const result = { checks, profileFetched: false, docUrl: null, webId: null, controller: null };
8688

8789
// 1. Resolve the document URL — strip the fragment.
8890
let docUrl;
@@ -174,9 +176,18 @@ async function runAll(webIdUrl) {
174176
// malformed @id; fall through to user-supplied URL
175177
}
176178
}
179+
// Derive the controller IRI from the profile's declared `controller`
180+
// (handling all four JSON-LD shapes), falling back to the canonical
181+
// WebID when controller is absent. Generated VMs use this so that on
182+
// delegated-control profiles the snippet matches the profile's own
183+
// controller predicate (and passes the validator).
184+
const declaredCtrls = normalizeControllers(profile.controller, docUrl.toString());
185+
const controllerIri = declaredCtrls[0] ?? canonicalWebId;
186+
177187
result.profileFetched = true;
178188
result.docUrl = docUrl.toString();
179189
result.webId = canonicalWebId;
190+
result.controller = controllerIri;
180191

181192
// 4. Run LWS-CID structural checks.
182193
for (const c of runLwsCidChecks(profile, { webIdUrl })) {
@@ -202,6 +213,7 @@ function hideAddKeySection() {
202213
clearSignerOutput();
203214
lastWebId = null;
204215
lastDocUrl = null;
216+
lastController = null;
205217
}
206218

207219
function clearSignerOutput() {
@@ -250,7 +262,7 @@ connectButton.addEventListener('click', async () => {
250262
if (!/^[0-9a-f]{64}$/i.test(xOnlyHex)) {
251263
throw new Error(`Signer returned an unexpected pubkey: ${xOnlyHex}`);
252264
}
253-
renderSnippet(xOnlyHex, lastWebId, lastDocUrl);
265+
renderSnippet(xOnlyHex, lastWebId, lastDocUrl, lastController);
254266
signerOutput.hidden = false;
255267
setSignerStatus('ready', 'Connected. The snippet below is ready to paste into your profile.');
256268
connectButton.textContent = 'Reconnect signer';
@@ -262,8 +274,8 @@ connectButton.addEventListener('click', async () => {
262274
}
263275
});
264276

265-
function renderSnippet(xOnlyHex, webId, docUrl) {
266-
const vm = buildNostrVerificationMethod({ webId, xOnlyHex });
277+
function renderSnippet(xOnlyHex, webId, docUrl, controller) {
278+
const vm = buildNostrVerificationMethod({ webId, xOnlyHex, controller });
267279
pubkeyHexEl.textContent = xOnlyHex;
268280
pubkeyMbEl.textContent = vm.publicKeyMultibase;
269281
// Write target is the document URL (no fragment) — you can't PUT/PATCH

lib/lws-cid.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ function validateVerificationMethod(vm, baseUrl, expectedCtrls) {
364364
* Normalize a JSON-LD controller value (string | { '@id' | 'id': string }
365365
* | array of either) to a list of absolutized IRI strings.
366366
*/
367-
function normalizeControllers(value, baseUrl) {
367+
export function normalizeControllers(value, baseUrl) {
368368
if (value === undefined || value === null) return [];
369369
const list = Array.isArray(value) ? value : [value];
370370
const out = [];

lib/multikey.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,28 +68,36 @@ export function multikeyToNostrPubkey(mb) {
6868
}
6969

7070
/**
71-
* Build a CID-v1 verificationMethod entry for a Nostr key controlled by
72-
* the given WebID.
71+
* Build a CID-v1 verificationMethod entry for a Nostr key.
7372
*
7473
* @param {object} args
75-
* @param {string} args.webId - The full WebID URI (with or without #me)
74+
* @param {string} args.webId - The full WebID URI (with or without #me).
75+
* Used to derive the VM `id` (fragment URI rooted at the document).
7676
* @param {string} args.xOnlyHex - 32-byte x-only Nostr pubkey, hex
77+
* @param {string} [args.controller=webId] - The CID document that
78+
* controls this verificationMethod. Defaults to the WebID itself
79+
* (the common self-controlled case). For delegated-control profiles,
80+
* pass the profile's declared `controller` so the VM agrees with the
81+
* outer controller predicate.
7782
* @param {string} [args.fragment='nostr-key-1'] - Fragment id for the VM
7883
* @param {0x02|0x03} [args.parity=0x02]
7984
*/
80-
export function buildNostrVerificationMethod({ webId, xOnlyHex, fragment = 'nostr-key-1', parity = 0x02 }) {
85+
export function buildNostrVerificationMethod({
86+
webId,
87+
xOnlyHex,
88+
controller,
89+
fragment = 'nostr-key-1',
90+
parity = 0x02,
91+
}) {
8192
const mb = nostrPubkeyToMultikey(xOnlyHex, { parity });
8293
// The VM id is a fragment URI rooted at the WebID's document. We strip
8394
// any existing fragment so paths like ".../card.jsonld#me" yield
8495
// ".../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.
8896
const docUrl = stripHash(webId);
8997
return {
9098
id: `${docUrl}#${fragment}`,
9199
type: 'Multikey',
92-
controller: webId,
100+
controller: controller ?? webId,
93101
publicKeyMultibase: mb,
94102
};
95103
}

0 commit comments

Comments
 (0)