Skip to content

Align DID-document generation with did:nostr 0.0.12#39

Merged
melvincarvalho merged 6 commits into
gh-pagesfrom
align-diddoc-0.0.12
Jun 30, 2026
Merged

Align DID-document generation with did:nostr 0.0.12#39
melvincarvalho merged 6 commits into
gh-pagesfrom
align-diddoc-0.0.12

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Aligns the DID-document generator with the did:nostr 0.0.12 spec. Surfaced by diffing the live nostr.social output against the new spec — three fields had drifted.

Fixes

  1. profile.timestampprofile.created_at (Deprecate profile.timestamp in favor of document-level modified nostrcg/did-nostr#117). The field holds the kind-0 created_at; created_at is now the defined context term (nostr:created_at), so this also makes the profile term well-defined under JSON-LD.
  2. Emit modified (Design: provenance model (in-graph source-derived times + content-hash ETag + blocktrails), superseding Nostr-Timestamp nostrcg/did-nostr#106) — dcterms:modified, ISO-8601 UTC, computed as max(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 are ETag / Last-Modified).
  3. Bound inlined follows to a subset (500) (FollowsEndpoint is 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.js updated and passing: created_at (not timestamp), modified = max over composed parts, and the bounded-follows cap. The rest of the suite is unaffected (the only failures locally are the Mongo-backed db.test.js, which needs a running MongoDB).

Verify against the live drift

curl -s https://nostr.social/.well-known/did/nostr/<pubkey>.json
# before: profile.timestamp, no modified, all follows inlined
# after:  profile.created_at, modified present, follows bounded

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).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.timestamp to profile.created_at (kind-0 created_at) for spec-aligned JSON-LD terms.
  • Add modified (dcterms:modified) computed as the max created_at across composed signed parts (kinds 0/3/10002).
  • Bound inlined follows to 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.

Comment thread src/diddoc.js Outdated
Comment on lines 53 to 56
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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread src/diddoc.js Outdated
Comment on lines +70 to +73
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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/diddoc.js Outdated
Comment on lines +25 to +27
// 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');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardened in 8199af9isoFromUnix 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.

Comment thread src/diddoc.js Outdated
Comment on lines +92 to +97
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardened in 8199af9isoFromUnix 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread src/diddoc.js
Comment on lines +98 to +102
const stamps = [
doc.profile && profile?.created_at,
doc.follows && follows?.created_at,
doc.service && relays?.created_at,
].filter(Number.isSafeInteger);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@melvincarvalho melvincarvalho merged commit f68e60f into gh-pages Jun 30, 2026
1 check passed
@melvincarvalho melvincarvalho deleted the align-diddoc-0.0.12 branch June 30, 2026 13:33
melvincarvalho added a commit that referenced this pull request Jun 30, 2026
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants