Tracker for converging the existing profile/key-linking issues (#42, #6, #4, #39, #86, #319) onto the W3C Linked Web Storage 1.0 Authentication Suite FPWDs published 2026-04-23 — and on the substrate they delegate to, W3C Controlled Identifiers v1.0.
This issue does not propose a new design from scratch. It captures the design that fell out of reading the LWS FPWDs against JSS's current state, so future PRs can reference one place.
Key insight
LWS auth FPWDs delegate identity-document semantics to W3C Controlled Identifiers 1.0. A WebID becomes LWS-conformant for self-signed auth (LWS10-CID, LWS10-did:key) by being a valid CID document — i.e. by carrying verificationMethod, authentication, assertionMethod, controller, publicKeyJwk/publicKeyMultibase triples. No separate DID document needed for the WebID-controlled case.
This converges several historically separate JSS workstreams onto one mechanism.
Current JSS state
src/webid/profile.js already declares cid: and lws: prefixes (prep work landed) but writes zero verification material. The path to conformance is additive — no breaking changes to existing profiles.
Proposed profile shape
A WebID with three keys: a Nostr secp256k1 (NIP-98), a did:key Ed25519 (LWS10), and a passkey/WebAuthn credential.
{
"@context": [
"https://www.w3.org/ns/cid/v1",
{
"foaf": "http://xmlns.com/foaf/0.1/",
"solid": "http://www.w3.org/ns/solid/terms#",
"schema": "http://schema.org/",
"pim": "http://www.w3.org/ns/pim/space#",
"ldp": "http://www.w3.org/ns/ldp#",
"owl": "http://www.w3.org/2002/07/owl#",
"lws": "https://www.w3.org/ns/lws#",
"nostr": "https://w3id.org/nostr/vocab#",
"alsoKnownAs": { "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", "@type": "@id" },
"sameAs": { "@id": "owl:sameAs", "@type": "@id" }
}
],
"id": "https://alice.solid.social/profile/card.jsonld#me",
"type": ["foaf:Person"],
"controller": "https://alice.solid.social/profile/card.jsonld#me",
"verificationMethod": [
{ "id": "...#nostr-key-1", "type": "Multikey", "controller": "...#me", "publicKeyMultibase": "fe70102…" },
{ "id": "...#did-key-1", "type": "Multikey", "controller": "...#me", "publicKeyMultibase": "z6MkpT…" },
{ "id": "...#passkey-1", "type": "JsonWebKey", "controller": "...#me", "publicKeyJwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" } }
],
"authentication": ["...#nostr-key-1", "...#did-key-1", "...#passkey-1"],
"assertionMethod": ["...#did-key-1"],
"alsoKnownAs": ["did:nostr:124c0…", "did:key:z6Mk…"],
"sameAs": "did:nostr:124c0…"
}
Notes:
https://www.w3.org/ns/cid/v1 is the first @context entry. CID-aware verifiers process this directly; existing JSS predicates (oidcIssuer, pim:storage, ldp:inbox, service, …) live in the inline prefix object so JSON-LD output is byte-stable for everything else.
- Nostr secp256k1 uses did:nostr's multikey recipe (parity byte + multicodec
0xe7,0x01 + base16-lower) so a NIP-98 verifier can decode straight to the 32-byte x-only key without dereferencing a DID document.
alsoKnownAs is the forward link to DID URIs (matches did:nostr's conventional binding); sameAs keeps the existing src/auth/did-nostr.js resolver working unchanged.
Verification flow (key K → WebID W)
- Identify subject from credential:
sub (LWS-CID JWT), pubkey hex (NIP-98), DID URI (did:key), credential ID (WebAuthn)
- Fetch the WebID profile (which is the CID document)
- Find matching
verificationMethod — by kid for JWT, by raw key match for NIP-98
- Confirm it appears in
authentication (or assertionMethod for credential issuance)
- Verify the cryptographic proof using that key
- Confirm the verification method's
controller is W (or W is its own controller)
This is profile-only for LWS-CID and WebAuthn; profile-or-DID-doc for did:nostr (existing two-way owl:sameAs ↔ alsoKnownAs from #42); DID-doc-only for did:key (the DID is the public key).
Migration path
Smallest delta — ~80–120 LOC:
| Change |
Where |
Switch @context to array form so cid/v1 is the imported context |
src/webid/profile.js:generateProfileJsonLd |
Accept keys[] parameter; emit verificationMethod + authentication (+ optional assertionMethod) when supplied |
same |
| Pass keys through from signup (nostr pubkey, did:key, passkey credential) |
src/handlers/container.js:createPodStructure, src/idp/interactions.js registration paths |
jss migrate add-keys admin command to upgrade existing pods (idempotent merge — only adds triples) |
new bin/jss.js subcommand |
Back-compat: profiles created without keys are byte-identical to today. Existing JSS resolvers (did-nostr, webid-tls) continue to work; the new design generalises the WebID-TLS pattern (cert:key → verificationMethod).
Open spec choices (FPWDs don't bind us; we get to decide)
| # |
Question |
Suggested default |
| 1 |
Profile vs DID-doc primary |
WebID is canonical for LWS-CID; DID docs supplementary via alsoKnownAs |
| 2 |
kid fragment shape |
#<purpose>-<n>, never reuse after revocation |
| 3 |
Key rotation |
Append-only verificationMethod; revoked keys removed from authentication but stay with expirationDate |
| 4 |
NIP-98 Schnorr inside CID's Multikey |
Use did:nostr's encoding verbatim; no NostrPublicKey extension |
| 5 |
Passkey/WebAuthn vocabulary |
JsonWebKey (P-256 EC); credentialId stays in private account record, not profile |
| 6 |
alsoKnownAs provenance |
Bidirectional check for did:nostr (existing); trust WebID-side for did:key (no DID doc) |
| 7 |
controller self-reference vs delegation |
Self-control only for v1; delegation/recovery later |
| 8 |
Recovery keys |
lws:recoveryMethod extension until standardised |
| 9 |
assertionMethod membership |
Only did:key for now (Schnorr/passkey are auth-only in practice) |
| 10 |
Profile MIME content negotiation |
Existing application/ld+json at profile/card.jsonld already conforms |
Phasing
This tracker captures design. Concrete shippable phases:
Phase 1 — Profile emits keys (PR-shaped):
src/webid/profile.js accepts keys[]; emits CID triples
@context array form
- New pods at signup get the keys they registered with
- No verifier-side changes yet — purely additive
- Tests for the JSON-LD structure
Phase 2 — Migration command:
jss migrate add-keys to upgrade existing pods
- Idempotent; safe to re-run
Phase 3 — LWS10-CID verifier:
Phase 4 — Cross-protocol unification:
Refs
Converges these issues
Tracker for converging the existing profile/key-linking issues (#42, #6, #4, #39, #86, #319) onto the W3C Linked Web Storage 1.0 Authentication Suite FPWDs published 2026-04-23 — and on the substrate they delegate to, W3C Controlled Identifiers v1.0.
This issue does not propose a new design from scratch. It captures the design that fell out of reading the LWS FPWDs against JSS's current state, so future PRs can reference one place.
Key insight
LWS auth FPWDs delegate identity-document semantics to W3C Controlled Identifiers 1.0. A WebID becomes LWS-conformant for self-signed auth (LWS10-CID, LWS10-did:key) by being a valid CID document — i.e. by carrying
verificationMethod,authentication,assertionMethod,controller,publicKeyJwk/publicKeyMultibasetriples. No separate DID document needed for the WebID-controlled case.This converges several historically separate JSS workstreams onto one mechanism.
Current JSS state
src/webid/profile.jsalready declarescid:andlws:prefixes (prep work landed) but writes zero verification material. The path to conformance is additive — no breaking changes to existing profiles.Proposed profile shape
A WebID with three keys: a Nostr secp256k1 (NIP-98), a did:key Ed25519 (LWS10), and a passkey/WebAuthn credential.
Notes:
https://www.w3.org/ns/cid/v1is the first@contextentry. CID-aware verifiers process this directly; existing JSS predicates (oidcIssuer,pim:storage,ldp:inbox,service, …) live in the inline prefix object so JSON-LD output is byte-stable for everything else.0xe7,0x01+ base16-lower) so a NIP-98 verifier can decode straight to the 32-byte x-only key without dereferencing a DID document.alsoKnownAsis the forward link to DID URIs (matches did:nostr's conventional binding);sameAskeeps the existingsrc/auth/did-nostr.jsresolver working unchanged.Verification flow (key K → WebID W)
sub(LWS-CID JWT), pubkey hex (NIP-98), DID URI (did:key), credential ID (WebAuthn)verificationMethod— bykidfor JWT, by raw key match for NIP-98authentication(orassertionMethodfor credential issuance)controlleris W (or W is its own controller)This is profile-only for LWS-CID and WebAuthn; profile-or-DID-doc for did:nostr (existing two-way
owl:sameAs↔alsoKnownAsfrom #42); DID-doc-only for did:key (the DID is the public key).Migration path
Smallest delta — ~80–120 LOC:
@contextto array form socid/v1is the imported contextsrc/webid/profile.js:generateProfileJsonLdkeys[]parameter; emitverificationMethod+authentication(+ optionalassertionMethod) when suppliedsrc/handlers/container.js:createPodStructure,src/idp/interactions.jsregistration pathsjss migrate add-keysadmin command to upgrade existing pods (idempotent merge — only adds triples)bin/jss.jssubcommandBack-compat: profiles created without keys are byte-identical to today. Existing JSS resolvers (did-nostr, webid-tls) continue to work; the new design generalises the WebID-TLS pattern (
cert:key→verificationMethod).Open spec choices (FPWDs don't bind us; we get to decide)
alsoKnownAskidfragment shape#<purpose>-<n>, never reuse after revocationverificationMethod; revoked keys removed fromauthenticationbut stay withexpirationDateNostrPublicKeyextensionJsonWebKey(P-256 EC);credentialIdstays in private account record, not profilealsoKnownAsprovenancecontrollerself-reference vs delegationlws:recoveryMethodextension until standardisedassertionMethodmembershipapplication/ld+jsonatprofile/card.jsonldalready conformsPhasing
This tracker captures design. Concrete shippable phases:
Phase 1 — Profile emits keys (PR-shaped):
src/webid/profile.jsacceptskeys[]; emits CID triples@contextarray formPhase 2 — Migration command:
jss migrate add-keysto upgrade existing podsPhase 3 — LWS10-CID verifier:
sub/iss/client_idtriple equality,kidlookup)Phase 4 — Cross-protocol unification:
verificationMethod/authenticationdeclarationsRefs
Converges these issues