Skip to content

Commit f71ea62

Browse files
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.
1 parent 8fd2726 commit f71ea62

5 files changed

Lines changed: 352 additions & 5 deletions

File tree

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ 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 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:
1212

1313
- Profile fetches as `application/ld+json`?
1414
- `@context` declares the CID v1 vocabulary (controller, verificationMethod, authentication, …)?
@@ -22,12 +22,14 @@ A diagnostic tool for [Solid](https://solidproject.org/) pods and the surroundin
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: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,122 @@ 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:disabled {
258+
opacity: 0.5;
259+
cursor: not-allowed;
260+
}
261+
.add-key button#copy-snippet {
262+
background: #475569;
263+
margin-top: 8px;
264+
}
265+
.add-key button#copy-snippet:hover:not(:disabled) { background: #334155; }
266+
.add-key .copy-status {
267+
margin-left: 10px;
268+
font-size: 12px;
269+
color: var(--pass);
270+
}
271+
272+
.kv {
273+
display: grid;
274+
grid-template-columns: max-content 1fr;
275+
gap: 4px 16px;
276+
margin: 16px 0;
277+
font-size: 13px;
278+
}
279+
.kv dt { font-weight: 600; }
280+
.kv dd {
281+
margin: 0;
282+
word-break: break-all;
283+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
284+
font-size: 12px;
285+
}
286+
.kv dd code {
287+
display: inline;
288+
background: transparent;
289+
padding: 0;
290+
}
291+
292+
#snippet {
293+
display: block;
294+
background: #0f172a;
295+
color: #cbd5e1;
296+
padding: 14px;
297+
border-radius: 8px;
298+
font-size: 12px;
299+
overflow-x: auto;
300+
white-space: pre;
301+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
302+
}
303+
#snippet pre, .add-key pre {
304+
margin: 0;
305+
}

doctor.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
import { runLwsCidChecks } from './lib/lws-cid.js';
12+
import { buildNostrVerificationMethod } from './lib/multikey.js';
1213

1314
const form = document.getElementById('check-form');
1415
const input = document.getElementById('webid');
@@ -17,6 +18,19 @@ const results = document.getElementById('results');
1718
const checksEl = document.getElementById('checks');
1819
const rawEl = document.getElementById('raw-body');
1920

21+
const addKeySection = document.getElementById('add-key');
22+
const signerStatus = document.getElementById('signer-status');
23+
const connectButton = document.getElementById('connect-signer');
24+
const signerOutput = document.getElementById('signer-output');
25+
const pubkeyHexEl = document.getElementById('pubkey-hex');
26+
const pubkeyMbEl = document.getElementById('pubkey-multibase');
27+
const snippetEl = document.getElementById('snippet');
28+
const snippetTarget = document.getElementById('snippet-target');
29+
const copyButton = document.getElementById('copy-snippet');
30+
const copyStatus = document.getElementById('copy-status');
31+
32+
let lastWebId = null;
33+
2034
// Allow ?webid=… in the URL to pre-fill (handy for sharing / bookmarks).
2135
const params = new URLSearchParams(window.location.search);
2236
if (params.has('webid')) {
@@ -41,6 +55,8 @@ form.addEventListener('submit', async (e) => {
4155
try {
4256
const checks = await runAll(url);
4357
renderChecks(checks);
58+
lastWebId = url;
59+
revealAddKeySection();
4460
} catch (err) {
4561
renderChecks([{
4662
status: 'fail',
@@ -133,6 +149,77 @@ async function runAll(webIdUrl) {
133149
return checks;
134150
}
135151

152+
// --- B.2: connect Nostr signer & compute Multikey VM -----------------
153+
154+
function revealAddKeySection() {
155+
addKeySection.hidden = false;
156+
detectSigner();
157+
}
158+
159+
function detectSigner() {
160+
if (typeof window.nostr?.getPublicKey === 'function') {
161+
setSignerStatus('ready', 'NIP-07 signer detected (window.nostr).');
162+
connectButton.disabled = false;
163+
} else {
164+
setSignerStatus('absent',
165+
'No NIP-07 signer found. Install xlogin or another window.nostr provider, then reload.');
166+
connectButton.disabled = true;
167+
}
168+
}
169+
170+
function setSignerStatus(state, text) {
171+
signerStatus.className = `signer-status ${state}`;
172+
signerStatus.querySelector('.text').textContent = text;
173+
}
174+
175+
connectButton.addEventListener('click', async () => {
176+
if (!lastWebId) return;
177+
connectButton.disabled = true;
178+
connectButton.textContent = 'Connecting…';
179+
try {
180+
const xOnlyHex = await window.nostr.getPublicKey();
181+
if (!/^[0-9a-f]{64}$/i.test(xOnlyHex)) {
182+
throw new Error(`Signer returned an unexpected pubkey: ${xOnlyHex}`);
183+
}
184+
renderSnippet(xOnlyHex, lastWebId);
185+
signerOutput.hidden = false;
186+
setSignerStatus('ready', 'Connected. The snippet below is ready to paste into your profile.');
187+
} catch (err) {
188+
setSignerStatus('error', `Could not read pubkey: ${err.message || err}`);
189+
} finally {
190+
connectButton.textContent = 'Reconnect signer';
191+
connectButton.disabled = false;
192+
}
193+
});
194+
195+
function renderSnippet(xOnlyHex, webId) {
196+
const vm = buildNostrVerificationMethod({ webId, xOnlyHex });
197+
pubkeyHexEl.textContent = xOnlyHex;
198+
pubkeyMbEl.textContent = vm.publicKeyMultibase;
199+
snippetTarget.textContent = vm.controller;
200+
201+
// Show the three additions a CID v1 profile needs together: the
202+
// verificationMethod itself, plus authentication / assertionMethod
203+
// arrays referencing it. JSON-LD doesn't have "patch" syntax, so we
204+
// present it as a partial document the user can merge manually.
205+
const partial = {
206+
verificationMethod: [vm],
207+
authentication: [vm.id],
208+
assertionMethod: [vm.id],
209+
};
210+
snippetEl.textContent = JSON.stringify(partial, null, 2);
211+
}
212+
213+
copyButton.addEventListener('click', async () => {
214+
try {
215+
await navigator.clipboard.writeText(snippetEl.textContent);
216+
copyStatus.textContent = 'Copied.';
217+
} catch (err) {
218+
copyStatus.textContent = `Couldn't copy: ${err.message || err}`;
219+
}
220+
setTimeout(() => { copyStatus.textContent = ''; }, 2500);
221+
});
222+
136223
function renderChecks(checks) {
137224
checksEl.innerHTML = '';
138225
for (const c of checks) {

index.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,41 @@ <h3>Results</h3>
3939
</div>
4040
</section>
4141

42+
<section class="add-key" id="add-key" hidden>
43+
<h2>Add a Nostr verification method</h2>
44+
<p>Read your Nostr public key from a <a href="https://github.com/nostr-protocol/nips/blob/master/07.md" target="_blank" rel="noopener">NIP-07</a>
45+
signer (e.g. <a href="https://xlogin.solid.social/" target="_blank" rel="noopener">xlogin</a>) and produce the
46+
<code>verificationMethod</code> JSON to add to your profile. Encoding follows
47+
<a href="https://nostrcg.github.io/did-nostr/" target="_blank" rel="noopener">did:nostr</a>'s Multikey recipe.
48+
No keys are sent anywhere &mdash; this is a pure client-side computation.</p>
49+
50+
<div class="signer-status" id="signer-status">
51+
<span class="dot"></span>
52+
<span class="text">Checking for signer…</span>
53+
</div>
54+
55+
<button type="button" id="connect-signer" disabled>Connect signer</button>
56+
57+
<div id="signer-output" hidden>
58+
<dl class="kv">
59+
<dt>Nostr pubkey (hex)</dt>
60+
<dd><code id="pubkey-hex"></code></dd>
61+
<dt>publicKeyMultibase</dt>
62+
<dd><code id="pubkey-multibase"></code></dd>
63+
</dl>
64+
65+
<h3>Patch to apply</h3>
66+
<p class="hint">Add these triples to your <code id="snippet-target"></code>. Field
67+
<code>id</code> uses the <code>#nostr-key-1</code> fragment by convention &mdash; pick another
68+
if it collides.</p>
69+
<pre><code id="snippet"></code></pre>
70+
<button type="button" id="copy-snippet">Copy snippet</button>
71+
<span class="copy-status" id="copy-status" aria-live="polite"></span>
72+
73+
<p class="hint future">B.3 will close the loop with an in-app PATCH so you don't need to copy/paste.</p>
74+
</div>
75+
</section>
76+
4277
<footer>
4378
<p>
4479
<a href="https://github.com/JavaScriptSolidServer/doctor" target="_blank" rel="noopener">source</a>

0 commit comments

Comments
 (0)