Skip to content

Commit c69182e

Browse files
B.0: read-only LWS / CID v1 profile-shape diagnostic
Vanilla static site with one diagnostic: paste a WebID URL, get a green/yellow/red checklist of what an LWS-CID verifier would find. No build step. Deploys from gh-pages. Checks (all structural, no DID-doc resolution yet): - profile fetches as JSON-LD - @context declares CID v1 vocabulary - @id present and self-resolving - controller === @id (self-controlled) - verificationMethod entries well-formed and ids unique - authentication / assertionMethod entries point at real VMs - alsoKnownAs entries are DID URIs Refs JSS#386 (overall convergence tracker for profile-to-key linking).
0 parents  commit c69182e

6 files changed

Lines changed: 1469 additions & 0 deletions

File tree

LICENSE

Lines changed: 662 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# doctor
2+
3+
A diagnostic tool for [Solid](https://solidproject.org/) pods and the surrounding decentralized-web stack.
4+
5+
> **Status:** alpha. First diagnostic ships as the LWS / W3C Controlled Identifiers v1.0 profile-shape check. More will accrete; well-defined ones may extract into focused tools.
6+
7+
**Live:** https://javascriptsolidserver.github.io/doctor/
8+
9+
## What it checks today
10+
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:
12+
13+
- Profile fetches as `application/ld+json`?
14+
- `@context` declares the CID v1 vocabulary (controller, verificationMethod, authentication, …)?
15+
- `controller === @id` (CID v1 self-control contract)?
16+
- `verificationMethod` populated?
17+
- Each entry has `id`, `type`, `controller`, and either `publicKeyJwk` or `publicKeyMultibase`?
18+
- `controller` of each method matches the WebID?
19+
- `id` values unique?
20+
- `authentication` entries point at real verificationMethods?
21+
- `alsoKnownAs` entries are DID URIs?
22+
23+
Read-only — no auth, no mutations, no server roundtrip beyond the GETs.
24+
25+
## Roadmap (rough)
26+
27+
- **B.0** — Read-only LWS-CID profile validator (this commit)
28+
- **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
31+
- **B.4** — did:key + WebAuthn passkey verification methods
32+
- **B.5** — More diagnostics: ACL inheritance, type-index integrity, OIDC discovery, ActivityPub actor doc, …
33+
34+
## Why a separate repo
35+
36+
This is intentionally not coupled to JSS or any specific Solid server. It runs as a pure browser app against any pod that can serve JSON-LD profile docs.
37+
38+
## Stack
39+
40+
Vanilla JS. No build step. Single HTML entry point + a couple of modules. Deploys as a static site to GitHub Pages from the `gh-pages` branch.
41+
42+
## Refs
43+
44+
- [JSS#386](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/386) — overall convergence tracker for Solid profile-to-key linking
45+
- [JSS#388](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/388) — Phase A (server-side): pod profiles became CID-document-shaped
46+
- [W3C Controlled Identifiers v1.0](https://www.w3.org/TR/cid-1.0/)
47+
- [LWS 1.0 SSI via CID (FPWD 2026-04-23)](https://www.w3.org/TR/2026/WD-lws10-authn-ssi-cid-20260423/)
48+
- [did:nostr](https://nostrcg.github.io/did-nostr/)
49+
50+
## License
51+
52+
[AGPL-3.0-only](./LICENSE) — matches JSS.

doctor.css

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/* doctor — minimal vanilla styling, mobile-friendly */
2+
:root {
3+
--bg: #f5f7fa;
4+
--panel: #fff;
5+
--ink: #0f172a;
6+
--muted: #64748b;
7+
--border: #e2e8f0;
8+
--accent: #2563eb;
9+
--pass: #16a34a;
10+
--warn: #ca8a04;
11+
--fail: #dc2626;
12+
--skip: #94a3b8;
13+
}
14+
15+
* { box-sizing: border-box; }
16+
17+
body {
18+
font: 15px/1.55 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
19+
background: var(--bg);
20+
color: var(--ink);
21+
margin: 0;
22+
padding: 32px 16px 64px;
23+
}
24+
25+
main {
26+
max-width: 760px;
27+
margin: 0 auto;
28+
}
29+
30+
header h1 {
31+
margin: 0 0 4px;
32+
font-size: 28px;
33+
letter-spacing: -0.02em;
34+
}
35+
header .tagline {
36+
margin: 0 0 28px;
37+
color: var(--muted);
38+
font-size: 14px;
39+
}
40+
41+
.checker {
42+
background: var(--panel);
43+
border: 1px solid var(--border);
44+
border-radius: 12px;
45+
padding: 24px;
46+
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
47+
}
48+
49+
.checker h2 {
50+
margin: 0 0 6px;
51+
font-size: 18px;
52+
}
53+
.checker > p {
54+
margin: 0 0 18px;
55+
color: var(--muted);
56+
font-size: 13px;
57+
}
58+
.checker code {
59+
background: #eef2f7;
60+
padding: 1px 5px;
61+
border-radius: 4px;
62+
font-size: 12px;
63+
}
64+
65+
form label {
66+
display: block;
67+
font-size: 13px;
68+
font-weight: 600;
69+
margin-bottom: 6px;
70+
}
71+
72+
form input[type="url"] {
73+
width: 100%;
74+
padding: 10px 12px;
75+
border: 1px solid var(--border);
76+
border-radius: 8px;
77+
font: inherit;
78+
margin-bottom: 12px;
79+
background: #fff;
80+
}
81+
form input[type="url"]:focus {
82+
outline: none;
83+
border-color: var(--accent);
84+
}
85+
86+
form button {
87+
background: var(--accent);
88+
color: #fff;
89+
border: 0;
90+
padding: 10px 18px;
91+
border-radius: 8px;
92+
font: inherit;
93+
font-weight: 600;
94+
cursor: pointer;
95+
}
96+
form button:hover { background: #1d4ed8; }
97+
form button:disabled {
98+
opacity: 0.6;
99+
cursor: progress;
100+
}
101+
102+
#results {
103+
margin-top: 24px;
104+
padding-top: 20px;
105+
border-top: 1px solid var(--border);
106+
}
107+
#results h3 {
108+
margin: 0 0 12px;
109+
font-size: 16px;
110+
}
111+
112+
.checks {
113+
list-style: none;
114+
padding: 0;
115+
margin: 0;
116+
}
117+
.check {
118+
padding: 10px 12px;
119+
margin-bottom: 6px;
120+
border-radius: 8px;
121+
border: 1px solid var(--border);
122+
background: #fff;
123+
display: flex;
124+
align-items: flex-start;
125+
gap: 10px;
126+
font-size: 14px;
127+
}
128+
.check .icon {
129+
flex-shrink: 0;
130+
width: 20px;
131+
height: 20px;
132+
border-radius: 50%;
133+
display: inline-flex;
134+
align-items: center;
135+
justify-content: center;
136+
font-size: 12px;
137+
font-weight: 700;
138+
color: #fff;
139+
margin-top: 1px;
140+
}
141+
.check.pass .icon { background: var(--pass); }
142+
.check.warn .icon { background: var(--warn); }
143+
.check.fail .icon { background: var(--fail); }
144+
.check.skip .icon { background: var(--skip); }
145+
.check .body { flex: 1; }
146+
.check .label { font-weight: 600; }
147+
.check .detail {
148+
font-size: 12px;
149+
color: var(--muted);
150+
margin-top: 3px;
151+
word-break: break-word;
152+
}
153+
154+
#raw {
155+
margin-top: 18px;
156+
}
157+
#raw summary {
158+
cursor: pointer;
159+
font-size: 13px;
160+
color: var(--muted);
161+
padding: 4px 0;
162+
}
163+
#raw pre {
164+
background: #0f172a;
165+
color: #cbd5e1;
166+
padding: 14px;
167+
border-radius: 8px;
168+
font-size: 12px;
169+
overflow-x: auto;
170+
margin: 8px 0 0;
171+
white-space: pre-wrap;
172+
word-break: break-word;
173+
}
174+
175+
footer {
176+
margin-top: 28px;
177+
text-align: center;
178+
font-size: 12px;
179+
color: var(--muted);
180+
}
181+
footer a {
182+
color: inherit;
183+
text-decoration: underline;
184+
}
185+
186+
a { color: var(--accent); }

doctor.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* doctor — Solid pod diagnostics
3+
*
4+
* B.0: Read-only LWS / W3C Controlled Identifiers v1.0 profile-shape check.
5+
*
6+
* Fetches a WebID URL with Accept: application/ld+json and runs a series
7+
* of structural checks against the parsed JSON-LD profile. No mutations,
8+
* no auth — just a read. The output is a green/yellow/red checklist.
9+
*/
10+
11+
import { runLwsCidChecks } from './lib/lws-cid.js';
12+
13+
const form = document.getElementById('check-form');
14+
const input = document.getElementById('webid');
15+
const button = form.querySelector('button[type="submit"]');
16+
const results = document.getElementById('results');
17+
const checksEl = document.getElementById('checks');
18+
const rawEl = document.getElementById('raw-body');
19+
20+
// Allow ?webid=… in the URL to pre-fill (handy for sharing / bookmarks).
21+
const params = new URLSearchParams(window.location.search);
22+
if (params.has('webid')) {
23+
input.value = params.get('webid');
24+
}
25+
26+
form.addEventListener('submit', async (e) => {
27+
e.preventDefault();
28+
const url = input.value.trim();
29+
if (!url) return;
30+
31+
// Reflect the current target in the URL so refreshes / bookmarks survive.
32+
const newParams = new URLSearchParams({ webid: url });
33+
history.replaceState(null, '', `${window.location.pathname}?${newParams.toString()}`);
34+
35+
button.disabled = true;
36+
button.textContent = 'Running…';
37+
checksEl.innerHTML = '';
38+
rawEl.textContent = '';
39+
results.hidden = false;
40+
41+
try {
42+
const checks = await runAll(url);
43+
renderChecks(checks);
44+
} catch (err) {
45+
renderChecks([{
46+
status: 'fail',
47+
label: 'Diagnostics crashed',
48+
detail: String(err?.message || err),
49+
}]);
50+
} finally {
51+
button.disabled = false;
52+
button.textContent = 'Run diagnostics';
53+
}
54+
});
55+
56+
async function runAll(webIdUrl) {
57+
const checks = [];
58+
59+
// 1. Resolve the document URL — strip the fragment.
60+
let docUrl;
61+
try {
62+
docUrl = new URL(webIdUrl);
63+
docUrl.hash = '';
64+
} catch (err) {
65+
checks.push({ status: 'fail', label: 'WebID is a valid URL', detail: err.message });
66+
return checks;
67+
}
68+
checks.push({ status: 'pass', label: 'WebID is a valid URL', detail: docUrl.toString() });
69+
70+
// 2. Fetch as JSON-LD. We avoid Accept: text/turtle so the conneg layer
71+
// doesn't transform the document — we want to validate the JSON-LD
72+
// representation directly.
73+
let res, body, contentType;
74+
try {
75+
res = await fetch(docUrl.toString(), {
76+
headers: { 'Accept': 'application/ld+json' },
77+
});
78+
contentType = (res.headers.get('content-type') || '').toLowerCase();
79+
} catch (err) {
80+
checks.push({ status: 'fail', label: 'Profile is reachable', detail: err.message });
81+
return checks;
82+
}
83+
84+
if (!res.ok) {
85+
checks.push({
86+
status: 'fail',
87+
label: 'Profile is reachable',
88+
detail: `HTTP ${res.status} from ${docUrl}`,
89+
});
90+
return checks;
91+
}
92+
checks.push({
93+
status: 'pass',
94+
label: 'Profile is reachable',
95+
detail: `${res.status} ${res.statusText}`,
96+
});
97+
98+
if (!contentType.includes('json')) {
99+
checks.push({
100+
status: 'warn',
101+
label: 'Content-Type is JSON-LD',
102+
detail: `Got "${contentType || '(none)'}" — wanted application/ld+json. Server may not honor Accept; we'll try parsing anyway.`,
103+
});
104+
} else {
105+
checks.push({
106+
status: 'pass',
107+
label: 'Content-Type is JSON-LD',
108+
detail: contentType,
109+
});
110+
}
111+
112+
// 3. Parse JSON.
113+
const text = await res.text();
114+
rawEl.textContent = text;
115+
let profile;
116+
try {
117+
profile = JSON.parse(text);
118+
} catch (err) {
119+
checks.push({
120+
status: 'fail',
121+
label: 'Profile parses as JSON',
122+
detail: err.message,
123+
});
124+
return checks;
125+
}
126+
checks.push({ status: 'pass', label: 'Profile parses as JSON' });
127+
128+
// 4. Run LWS-CID structural checks.
129+
for (const c of runLwsCidChecks(profile, { webIdUrl, docUrl: docUrl.toString() })) {
130+
checks.push(c);
131+
}
132+
133+
return checks;
134+
}
135+
136+
function renderChecks(checks) {
137+
checksEl.innerHTML = '';
138+
for (const c of checks) {
139+
const li = document.createElement('li');
140+
li.className = `check ${c.status}`;
141+
const symbols = { pass: '✓', warn: '!', fail: '✗', skip: '·' };
142+
li.innerHTML = `
143+
<span class="icon">${symbols[c.status] ?? '?'}</span>
144+
<div class="body">
145+
<div class="label"></div>
146+
${c.detail ? `<div class="detail"></div>` : ''}
147+
</div>
148+
`;
149+
li.querySelector('.label').textContent = c.label;
150+
if (c.detail) li.querySelector('.detail').textContent = c.detail;
151+
checksEl.appendChild(li);
152+
}
153+
}

0 commit comments

Comments
 (0)