Align DID-document generation with did:nostr 0.0.12#39
Conversation
Three conformance fixes surfaced by diffing the live nostr.social output against the 0.0.12 spec: - profile.timestamp -> profile.created_at (nostrcg/did-nostr#117), using the kind-0 created_at (now a defined context term, nostr:created_at). - Emit "modified" (dcterms:modified, ISO-8601) = max(created_at) over the signed parts composed into the document (nostrcg/did-nostr#106). Minimal docs (no signed parts) carry none. - Bound inlined follows to a subset (500) rather than serving the entire list — one live doc was inlining 4760 follows. The complete signed list is the kind-3 event on the relays (nostrcg/did-nostr#104 / #125). diddoc unit tests updated and passing (created_at, modified, bounded follows).
There was a problem hiding this comment.
Pull request overview
Updates the DID-document generator to match did:nostr 0.0.12, addressing spec drift found in live output (profile field rename, modified provenance timestamp, and bounded inlined follows).
Changes:
- Rename
profile.timestamptoprofile.created_at(kind-0created_at) for spec-aligned JSON-LD terms. - Add
modified(dcterms:modified) computed as the maxcreated_atacross composed signed parts (kinds 0/3/10002). - Bound inlined
followsto a maximum of 500 entries, keeping the full signed list in the kind-3 event.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/diddoc.js |
Implements spec-aligned profile.created_at, bounded follows, and modified computed from composed signed parts. |
test/diddoc.test.js |
Updates/extends tests to cover created_at rename, modified behavior, and follows bounding. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (const k of ['name', 'about', 'picture', 'website', 'nip05', 'lud16']) if (c[k]) p[k] = c[k]; | ||
| if (c.display_name && !p.name) p.name = c.display_name; | ||
| if (profile.created_at) p.timestamp = profile.created_at; | ||
| if (profile.created_at) p.created_at = profile.created_at; | ||
| if (Object.keys(p).length) doc.profile = p; |
There was a problem hiding this comment.
Good catch — fixed in 7a4fafe. Switched the gate to Number.isFinite(profile.created_at) so a valid created_at of 0 is preserved and the profile field stays consistent with the modified computation.
Address Copilot review on #39: the truthiness gate dropped a valid created_at of 0, while the modified computation uses Number.isFinite. Make them consistent.
| const f = followHexes | ||
| .filter((h) => HEX64.test(String(h).toLowerCase())) | ||
| .map((h) => `did:nostr:${String(h).toLowerCase()}`); | ||
| if (f.length) doc.follows = f; | ||
| if (f.length) doc.follows = f.slice(0, FOLLOWS_LIMIT); |
There was a problem hiding this comment.
Done in 58fc58c — replaced the filter/map-then-slice with a loop that collects up to FOLLOWS_LIMIT valid entries and breaks, so large kind-3 lists aren't fully processed just to be truncated.
Address Copilot review on #39: collect up to FOLLOWS_LIMIT valid entries and stop, instead of validating/mapping the whole kind-3 list before slicing.
| // dcterms:modified serialized as ISO-8601 UTC, no sub-second precision, from a | ||
| // Nostr created_at (Unix seconds). | ||
| const isoFromUnix = (sec) => new Date(sec * 1000).toISOString().replace(/\.\d{3}Z$/, 'Z'); |
There was a problem hiding this comment.
Hardened in 8199af9 — isoFromUnix now returns null for non-safe-integer or out-of-range timestamps (no RangeError in the resolver path), stamps are filtered with Number.isSafeInteger, and modified is only set when a valid ISO value is produced (never modified: null). Added a test for the out-of-range case.
| const stamps = [ | ||
| doc.profile && profile?.created_at, | ||
| doc.follows && follows?.created_at, | ||
| doc.service && relays?.created_at, | ||
| ].filter(Number.isFinite); | ||
| if (stamps.length) doc.modified = isoFromUnix(Math.max(...stamps)); |
There was a problem hiding this comment.
Hardened in 8199af9 — isoFromUnix now returns null for non-safe-integer or out-of-range timestamps (no RangeError in the resolver path), stamps are filtered with Number.isSafeInteger, and modified is only set when a valid ISO value is produced (never modified: null). Added a test for the out-of-range case.
…zation) Address Copilot review on #39: - isoFromUnix returns null for non-safe-integer or out-of-range timestamps instead of throwing RangeError in the public resolver path. - Filter stamps with Number.isSafeInteger (Nostr created_at is integer seconds) and only set modified when isoFromUnix yields a value (never modified: null). - profile.created_at gate also uses Number.isSafeInteger for consistency.
| const stamps = [ | ||
| doc.profile && profile?.created_at, | ||
| doc.follows && follows?.created_at, | ||
| doc.service && relays?.created_at, | ||
| ].filter(Number.isSafeInteger); |
There was a problem hiding this comment.
Good catch, and it surfaced a deeper bug. Fixed in 0aaa789: the kind-0 created_at gate for modified is now (doc.profile || doc.alsoKnownAs), and — the real issue — created_at is only attached to the profile when the kind-0 contributes actual profile fields (previously a bare created_at produced profile: {created_at}). An alsoKnownAs-only kind-0 now yields no profile object but still counts toward modified. Test added.
Address Copilot review on #39: a kind-0 event can contribute alsoKnownAs with no profile fields, so gating the kind-0 created_at on doc.profile alone could understate modified. Gate on (doc.profile || doc.alsoKnownAs).
The created_at was added to the profile object before the non-empty check, so a
kind-0 with only alsoKnownAs produced doc.profile = {created_at}. Attach
created_at only when the kind-0 contributes real profile fields; alsoKnownAs-only
kind-0 now yields no profile but still counts toward modified (prior commit's
gate). Fixes the test added in fcc3382.
DID-document output now conforms to did:nostr 0.0.12 (#39): - profile.timestamp -> profile.created_at (the kind-0 created_at; now a defined context term, nostr:created_at). - Emit "modified" (dcterms:modified, ISO-8601) = max(created_at) over the signed parts actually composed into the document (kind 0 / 3 / 10002). Minimal docs carry none. - Bound inlined follows to a subset (500), short-circuited; the complete signed list is the kind-3 event on the relays (no FollowsEndpoint). - Hardened the public resolver path: corrupt/out-of-range created_at no longer throws (returns no modified), Number.isSafeInteger throughout, and a kind-0 that only contributes alsoKnownAs no longer emits a bare {created_at} profile yet still counts toward modified. Refs: nostrcg/did-nostr 0.0.12 (#106 modified, #117 created_at, #104/#125 follows).
Aligns the DID-document generator with the did:nostr 0.0.12 spec. Surfaced by diffing the live
nostr.socialoutput against the new spec — three fields had drifted.Fixes
profile.timestamp→profile.created_at(Deprecateprofile.timestampin favor of document-levelmodifiednostrcg/did-nostr#117). The field holds the kind-0created_at;created_atis now the defined context term (nostr:created_at), so this also makes the profile term well-defined under JSON-LD.modified(Design: provenance model (in-graph source-derived times + content-hash ETag + blocktrails), supersedingNostr-Timestampnostrcg/did-nostr#106) —dcterms:modified, ISO-8601 UTC, computed asmax(created_at)over the signed parts actually composed into the document (kind 0 / 3 / 10002). Minimal docs (no signed parts) carry none. Representation-only changes stay out of it (those areETag/Last-Modified).followsto a subset (500) (FollowsEndpointis underspecified — mark at-risk and design the response format before publication nostrcg/did-nostr#104 / #125). One live document was inlining 4,760 follows, bloating a document that's meant to be small and cacheable. The complete, signed list is the kind-3 event on the relays.Tests
test/diddoc.test.jsupdated and passing:created_at(nottimestamp),modified= max over composed parts, and the bounded-follows cap. The rest of the suite is unaffected (the only failures locally are the Mongo-backeddb.test.js, which needs a running MongoDB).Verify against the live drift