Skip to content

Commit 768d250

Browse files
B.2: connect NIP-07 signer and emit Multikey verificationMethod (#2)
* B.2: connect NIP-07 signer and emit Multikey verificationMethod Adds a second section to the doctor: after diagnostics run, the user can connect their Nostr signer (xlogin or any window.nostr provider) and get back a copyable JSON snippet — verificationMethod plus authentication / assertionMethod entries — to paste into their profile. Multikey encoding follows did:nostr's recipe: "f" + e701 (multicodec varint for secp256k1-pub) + parity byte (0x02 default) + 32-byte x-only Nostr pubkey verified against the spec example. No keys leave the browser; B.3 will close the loop with an in-app PATCH via Solid-OIDC. Closes #1. * 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. * Address copilot pass 2 on #2 - doctor.js: thread the canonical WebID (with fragment) through to the Multikey builder. runAll now returns the absolutized profile @id as `webId`, and the connect handler uses that — not the fragmentless docUrl — when calling buildNostrVerificationMethod. Without this the earlier controller-with-fragment fix was undone at the callsite. - doctor.js: drop the unused `docUrl` arg from runLwsCidChecks call. - lib/multikey.js: error message no longer says "lowercase hex" since normalizeHex lowercases first; uppercase input is accepted. * Address copilot pass 3 on #2 - doctor.js: snippet target shown to the user is the document URL (fragmentless) — that's the actual PUT/PATCH target. The VM's controller still uses the WebID-with-fragment. Threaded docUrl through renderSnippet alongside webId. - doctor.js: clearSignerOutput now resets the connect-button label and copy status, so a re-run of diagnostics doesn't leave stale "Reconnect signer" / copy text behind. - doctor.css: dropped dead "#snippet pre" rule (snippet IS the code inside the pre, so the descendant selector matches nothing). * Address copilot pass 4 on #2 - lib/lws-cid.js: VM controllers are now compared against the profile's declared `controller` (absolutized), with fallback to `@id`. The previous code used `@id` unconditionally, which would incorrectly fail otherwise-valid delegated-control profiles. Also fixes the converse: a delegated profile with VMs still pointing at the original WebID now correctly fails (verified with a mismatched test case). - doctor.css + doctor.js: copy-status no longer renders error text in the success color. Added `.success` and `.error` modifier classes with a neutral default; click handler sets the appropriate one. * Address copilot pass 5 on #2 JSON-LD lets `controller` be a string, object with @id/id, OR an array of either form. The previous code assumed string|@id-object, so an array silently produced an undefined expected controller and disabled the mismatch check entirely. Added normalizeControllers() helper that handles all four shapes (string, object-with-@id, object-with-id-shorthand, array) and returns a list of absolutized IRIs. Both the profile's outer controller and each VM's controller are normalized through it; a VM passes if any of its controllers is in the profile's expected controller set. Verified against five edge cases: array containing the WebID (passes), object-with-id shorthand (passes), VM matching second array entry (passes), VM matching neither (correctly fails), and VM with array-valued controller (passes when intersection is non-empty). * Address copilot pass 6 on #2 - doctor.js: harden canonical-WebID derivation. Wrap URL parsing in try/catch so a malformed profile @id can't crash diagnostics; only adopt the profile @id when its fragmentless form matches the fetched docUrl, otherwise fall back to the user-supplied URL. This prevents a snippet whose VM id is rooted at one document while the UI tells the user to PATCH a different one. - lib/lws-cid.js: route the self-control check through normalizeControllers too, so all four JSON-LD shapes (string, object-with-@id, object-with-id, array) yield consistent results. Previously, controller={id: ...} or array forms produced an undefined comparison and a misleading warn. Empty/unresolvable controller now reports "does not resolve to a usable IRI" rather than silently degrading. * Address copilot pass 7 on #2 - README: "green/red checklist" → "pass/warn/fail/skip checklist". The actual UI has four states; binary phrasing was misleading. - lib/lws-cid.js: skip the self-control comparison when @id is missing rather than emitting "@id=null" in a delegated-control warning. The earlier "Profile has @id" check has already failed, and "controller === @id" isn't meaningful without an @id to compare against. * Address copilot pass 8 on #2 - doctor.js: hideAddKeySection() at the start of the submit handler (before awaiting runAll). Without this, a stale snippet from a previous run remained visible — and clickable — during a new run, so a user could copy or apply a snippet rooted at the wrong WebID. - doctor.js: clearSignerOutput() when revealing the add-key section. Without this, after diagnostics complete on a NEW WebID, the old pubkey/snippet from the previous run was still visible until the user clicked reconnect — same accidental-copy hazard. * Address copilot pass 9 on #2 - index.html: signer-status gets role="status" + aria-live="polite" so screen readers announce state changes (signer detected / connected / error). Decorative dot marked aria-hidden. - doctor.js: re-check window.nostr presence at click time. The provider can be uninstalled or disabled between detect-on-reveal and the click, which would have thrown a raw TypeError. Now surfaces the same "install xlogin and reload" message and disables the button. - doctor.css: :focus-visible outline on .add-key buttons so keyboard navigation has a visible focus indicator. - README: VM-controller wording updated to reflect the validator's actual behavior (matches profile's declared controller, with fallback to @id), since the previous wording implied a stricter always-the-WebID rule. * 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. * Address copilot pass 11 on #2 - doctor.js: bail with a clean fail-check when JSON.parse returns null, a primitive, or an array. Previously these would crash downstream on profile['@id'] / profile.controller access and surface as a generic "Diagnostics crashed". - lib/multikey.js: validate that webId is a non-empty string at the buildNostrVerificationMethod entry point. Also harden stripHash to not throw on non-string input — defensive since the helper is exported for standalone use.
1 parent 8fd2726 commit 768d250

6 files changed

Lines changed: 551 additions & 40 deletions

File tree

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,30 @@ A diagnostic tool for [Solid](https://solidproject.org/) pods and the surroundin
66
77
**Live:** https://jss.live/doctor/
88

9-
## What it checks today
9+
## What it does today
1010

11-
**LWS / CID v1 profile shape** — drop in a WebID URL, get a green/red checklist of what's structurally there and what's missing for [LWS 1.0](https://www.w3.org/TR/2026/WD-lws10-authn-ssi-cid-20260423/) auth conformance:
11+
**1. LWS / CID v1 profile shape check** — drop in a WebID URL, get a pass/warn/fail/skip checklist of what's structurally there and what's missing for [LWS 1.0](https://www.w3.org/TR/2026/WD-lws10-authn-ssi-cid-20260423/) auth conformance:
1212

1313
- Profile fetches as `application/ld+json`?
1414
- `@context` declares the CID v1 vocabulary (controller, verificationMethod, authentication, …)?
1515
- `controller === @id` (CID v1 self-control contract)?
1616
- `verificationMethod` populated?
1717
- Each entry has `id`, `type`, `controller`, and either `publicKeyJwk` or `publicKeyMultibase`?
18-
- `controller` of each method matches the WebID?
18+
- Each method's `controller` matches the profile's declared `controller` (with fallback to `@id` when `controller` is absent), so delegated-control profiles validate correctly?
1919
- `id` values unique?
2020
- `authentication` entries point at real verificationMethods?
2121
- `alsoKnownAs` entries are DID URIs?
2222

2323
Read-only — no auth, no mutations, no server roundtrip beyond the GETs.
2424

25+
**2. Nostr verification-method generator** — reads your Nostr pubkey from a [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md) signer (e.g. [xlogin](https://xlogin.solid.social/)), encodes it per [did:nostr](https://nostrcg.github.io/did-nostr/)'s Multikey recipe, and emits a copyable JSON snippet to add to your profile. No keys leave your browser.
26+
2527
## Roadmap (rough)
2628

27-
- **B.0** — Read-only LWS-CID profile validator (this commit)
29+
- ~~**B.0**~~ — Read-only LWS-CID profile validator ✅
30+
- ~~**B.2**~~ — Read pubkey from NIP-07 signer; emit Multikey verificationMethod snippet ✅
2831
- **B.1** — Bidirectional `alsoKnownAs` ↔ DID-doc check (resolve `did:nostr:…` and verify the DID points back at this WebID)
29-
- **B.2** — xlogin / NIP-07 sign-in to act as a WebID owner
30-
- **B.3** — PATCH `verificationMethod` (Multikey for Nostr secp256k1) into the signed-in user's profile
32+
- **B.3** — In-app PATCH of the snippet via Solid-OIDC sign-in (closes the loop end-to-end)
3133
- **B.4** — did:key + WebAuthn passkey verification methods
3234
- **B.5** — More diagnostics: ACL inheritance, type-index integrity, OIDC discovery, ActivityPub actor doc, …
3335

doctor.css

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,128 @@ footer a {
184184
}
185185

186186
a { color: var(--accent); }
187+
188+
/* --- B.2: add-key section ----------------------------------------- */
189+
190+
.add-key {
191+
background: var(--panel);
192+
border: 1px solid var(--border);
193+
border-radius: 12px;
194+
padding: 24px;
195+
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
196+
margin-top: 20px;
197+
}
198+
.add-key h2 {
199+
margin: 0 0 6px;
200+
font-size: 18px;
201+
}
202+
.add-key > p {
203+
margin: 0 0 18px;
204+
color: var(--muted);
205+
font-size: 13px;
206+
}
207+
.add-key code {
208+
background: #eef2f7;
209+
padding: 1px 5px;
210+
border-radius: 4px;
211+
font-size: 12px;
212+
}
213+
.add-key h3 {
214+
margin: 18px 0 8px;
215+
font-size: 14px;
216+
}
217+
.add-key p.hint {
218+
font-size: 12px;
219+
color: var(--muted);
220+
margin: 0 0 8px;
221+
}
222+
.add-key p.hint.future {
223+
font-style: italic;
224+
margin-top: 16px;
225+
}
226+
227+
.signer-status {
228+
display: flex;
229+
align-items: center;
230+
gap: 8px;
231+
font-size: 13px;
232+
color: var(--muted);
233+
margin-bottom: 12px;
234+
}
235+
.signer-status .dot {
236+
width: 10px;
237+
height: 10px;
238+
border-radius: 50%;
239+
background: var(--skip);
240+
flex-shrink: 0;
241+
}
242+
.signer-status.ready .dot { background: var(--pass); }
243+
.signer-status.absent .dot { background: var(--fail); }
244+
.signer-status.error .dot { background: var(--fail); }
245+
246+
.add-key button {
247+
background: var(--accent);
248+
color: #fff;
249+
border: 0;
250+
padding: 9px 16px;
251+
border-radius: 8px;
252+
font: inherit;
253+
font-weight: 600;
254+
cursor: pointer;
255+
}
256+
.add-key button:hover:not(:disabled) { background: #1d4ed8; }
257+
.add-key button:focus-visible {
258+
outline: 2px solid var(--accent);
259+
outline-offset: 2px;
260+
}
261+
.add-key button:disabled {
262+
opacity: 0.5;
263+
cursor: not-allowed;
264+
}
265+
.add-key button#copy-snippet {
266+
background: #475569;
267+
margin-top: 8px;
268+
}
269+
.add-key button#copy-snippet:hover:not(:disabled) { background: #334155; }
270+
.add-key .copy-status {
271+
margin-left: 10px;
272+
font-size: 12px;
273+
color: var(--muted);
274+
}
275+
.add-key .copy-status.success { color: var(--pass); }
276+
.add-key .copy-status.error { color: var(--fail); }
277+
278+
.kv {
279+
display: grid;
280+
grid-template-columns: max-content 1fr;
281+
gap: 4px 16px;
282+
margin: 16px 0;
283+
font-size: 13px;
284+
}
285+
.kv dt { font-weight: 600; }
286+
.kv dd {
287+
margin: 0;
288+
word-break: break-all;
289+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
290+
font-size: 12px;
291+
}
292+
.kv dd code {
293+
display: inline;
294+
background: transparent;
295+
padding: 0;
296+
}
297+
298+
#snippet {
299+
display: block;
300+
background: #0f172a;
301+
color: #cbd5e1;
302+
padding: 14px;
303+
border-radius: 8px;
304+
font-size: 12px;
305+
overflow-x: auto;
306+
white-space: pre;
307+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
308+
}
309+
.add-key pre {
310+
margin: 0;
311+
}

0 commit comments

Comments
 (0)