|
| 1 | +// Build a spec-conformant did:nostr DID document from indexed events. |
| 2 | +// |
| 3 | +// Implements the current spec (https://nostrcg.github.io/did-nostr/): |
| 4 | +// - @context: cid/v1 + nostr/context (cf. nostrcg/did-nostr#90/#91) |
| 5 | +// - type: DIDNostr |
| 6 | +// - Multikey VM with publicKeyMultibase = f + e701 + 02 + <x-only hex> |
| 7 | +// - enhanced: profile (kind 0), follows (kind 3), service/Relay (kind 10002) |
| 8 | +// |
| 9 | +// Per RFC JavaScriptSolidServer/JavaScriptSolidServer#572 this is intended to |
| 10 | +// converge with jss's shared `buildDidDocument` once that is extracted as a |
| 11 | +// reusable module; until then beacon builds the conformant doc directly so it |
| 12 | +// is correct now (and avoids the divergent-generator bugs of nostr-labs/nostr-beacon#3). |
| 13 | + |
| 14 | +const HEX64 = /^[0-9a-f]{64}$/; |
| 15 | + |
| 16 | +export function buildDidDocument(pubkey, { profile, follows, relays } = {}) { |
| 17 | + const hex = String(pubkey || '').toLowerCase(); |
| 18 | + if (!HEX64.test(hex)) return null; |
| 19 | + const did = `did:nostr:${hex}`; |
| 20 | + |
| 21 | + const doc = { |
| 22 | + '@context': ['https://www.w3.org/ns/cid/v1', 'https://w3id.org/nostr/context'], |
| 23 | + id: did, |
| 24 | + type: 'DIDNostr', |
| 25 | + verificationMethod: [{ |
| 26 | + id: `${did}#key1`, |
| 27 | + type: 'Multikey', |
| 28 | + controller: did, |
| 29 | + publicKeyMultibase: `fe70102${hex}`, |
| 30 | + }], |
| 31 | + authentication: ['#key1'], |
| 32 | + assertionMethod: ['#key1'], |
| 33 | + }; |
| 34 | + |
| 35 | + // kind 0 -> profile (+ alsoKnownAs) |
| 36 | + if (profile?.content) { |
| 37 | + try { |
| 38 | + const c = JSON.parse(profile.content); |
| 39 | + const p = {}; |
| 40 | + for (const k of ['name', 'about', 'picture', 'website', 'nip05', 'lud16']) if (c[k]) p[k] = c[k]; |
| 41 | + if (c.display_name && !p.name) p.name = c.display_name; |
| 42 | + if (profile.created_at) p.timestamp = profile.created_at; |
| 43 | + if (Object.keys(p).length) doc.profile = p; |
| 44 | + if (Array.isArray(c.alsoKnownAs) && c.alsoKnownAs.length) doc.alsoKnownAs = c.alsoKnownAs; |
| 45 | + } catch { /* malformed kind-0 content */ } |
| 46 | + } |
| 47 | + |
| 48 | + // kind 3 -> follows (did:nostr of each p-tag) |
| 49 | + if (Array.isArray(follows?.tags)) { |
| 50 | + const f = follows.tags |
| 51 | + .filter((t) => t[0] === 'p' && HEX64.test(String(t[1]).toLowerCase())) |
| 52 | + .map((t) => `did:nostr:${String(t[1]).toLowerCase()}`); |
| 53 | + if (f.length) doc.follows = f; |
| 54 | + } |
| 55 | + |
| 56 | + // kind 10002 -> service (Relay) |
| 57 | + if (Array.isArray(relays?.tags)) { |
| 58 | + const svc = relays.tags |
| 59 | + .filter((t) => t[0] === 'r' && t[1]) |
| 60 | + .map((t, i) => ({ id: `${did}#relay${i + 1}`, type: 'Relay', serviceEndpoint: t[1] })); |
| 61 | + if (svc.length) doc.service = svc; |
| 62 | + } |
| 63 | + |
| 64 | + return doc; |
| 65 | +} |
0 commit comments