Skip to content

Resolve did:nostr globally via Nostr relays (decentralized step in resolver chain) #414

@melvincarvalho

Description

@melvincarvalho

Refs #42 (did:nostr resolution spec), #4 (NIP-98 SSO), #386 (LWS 1.0 alignment). Builds on #408 (HTTP .well-known endpoint) and #411 (subdomain-mode local index).

Where we are today

src/auth/nostr.js's verifyNostrAuth resolves a Nostr pubkey to a WebID via a 4-step chain:

  1. CID v1 verificationMethod lookup against the resource owner's WebID profile (resource-side, auth: NIP-98 verifier upgrades did:nostr → WebID via verificationMethod lookup (#4 Phase 2A via CID) #399)
  2. Local in-process index of <DATA_ROOT>/.idp/accounts/_webid_index.json (auth: serve /.well-known/did/nostr/<pubkey>.json so the pod is its own DID resolver #407 / well-known did:nostr index doesn't find subdomain-mode pods #411 — same-pod users)
  3. External HTTP .well-known/did/nostr resolver (configured URL, e.g. nostr.social) — cross-pod, but trusts the indexer
  4. Fallback to did:nostr:<pubkey> as the agent identity

Step 3 is the only "cross-pod" path today, and it relies on a single configured HTTP indexer. That's a centralization point: every JSS deployment delegates global resolution to whoever runs that indexer.

The proposal

Insert a direct Nostr relay resolution step between today's #2 (local index) and #3 (HTTP indexer):

2.5 Relay-based resolution. Connect to a configured set of Nostr relays, query for the user's DID-metadata replaceable event (kind defined by the did:nostr spec), verify the event signature, parse content as the DID document, and extract alsoKnownAs[0] as the candidate WebID. Then run the same backlink check the other steps use (does the WebID profile declare this pubkey via CID-VM?).

This is what makes did:nostr genuinely decentralized. The pubkey-to-DID-doc binding is verifiable from the Schnorr signature on the relay event alone — no trusted indexer in the loop. The user controls publication via their own relays.

Why this matters

Without this step:

  • Every JSS deployment depends on nostr.social (or whoever is configured) staying online + honest
  • A user changing pods mid-session has to wait for the indexer to re-scrape
  • The alsoKnownAs claim could be silently rewritten by a compromised/malicious indexer

With it:

  • JSS reads the user's DID doc straight from relays the user chose
  • Indexers (nostr.social, nostr.rocks) become one path among many — useful as an HTTP cache layer, not the source of truth
  • A user who runs their own relay can be fully self-sovereign — no third party in the auth path

Implementation sketch

  • Relay client: NDK or @nostr/client already in the dependency tree from elsewhere — pick whichever is lightest. Subscribe pattern, not long-lived.
  • Default relay set: configurable via JSS_NOSTR_RELAYS env (comma-separated). Default to a small set of public relays (relay.damus.io, nos.lol).
  • Per-user relay hint: optional NIP-05 lookup on the WebID's domain to find user-preferred relays before falling back to defaults.
  • DID-metadata event kind: per the did:nostr spec (likely a replaceable kind in the 30000–39999 range — confirm against current spec draft).
  • Replaceable-event race: pick the event with the highest created_at across responding relays.
  • Caching: same TTL semantics as the current did-nostr.js cache (5m success, 1m transient failure), keyed on pubkey + relay-set-fingerprint.
  • Verification: signature check + signed-by-target-pubkey check + DID-doc shape validation + alsoKnownAs backlink check (reuses existing checkCidVmBacklink from auth: serve /.well-known/did/nostr/<pubkey>.json (#407) #408).

Insert into verifyNostrAuth's chain in src/auth/nostr.js:

1. tryResolveViaCidVerificationMethod
2. resolveDidNostrLocally          (gated on idpEnabled)
2.5 resolveDidNostrViaRelays       (NEW)
3. resolveDidNostrToWebId          (HTTP indexer fallback)
4. did:nostr fallback

Test plan

  • Unit tests for resolveDidNostrViaRelays with a mocked relay client (success, no event, signature mismatch, replaceable-event ordering)
  • Integration test against relay.damus.io (or a local relay) with a published test DID-metadata event
  • Confirm the chain order: a known-local user still resolves via step 2 without ever hitting the relays
  • Confirm the HTTP indexer step still fires for users without published DID-metadata events on the configured relay set

Related deferred work

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    nostrNostr relay, did:nostr auth, NIP-related

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions