Skip to content

Commit 8199af9

Browse files
Harden modified against corrupt created_at (no throw, no null serialization)
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.
1 parent 58fc58c commit 8199af9

2 files changed

Lines changed: 24 additions & 6 deletions

File tree

src/diddoc.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ const HEX64 = /^[0-9a-f]{64}$/;
2222
// cacheable document; the complete signed list is the kind-3 event on the relays.
2323
const FOLLOWS_LIMIT = 500;
2424

25-
// dcterms:modified serialized as ISO-8601 UTC, no sub-second precision, from a
26-
// Nostr created_at (Unix seconds).
27-
const isoFromUnix = (sec) => new Date(sec * 1000).toISOString().replace(/\.\d{3}Z$/, 'Z');
25+
// dcterms:modified serialized as ISO-8601 UTC (no sub-second precision) from a
26+
// Nostr created_at (Unix seconds). Returns null for anything that is not a sane
27+
// integer timestamp or does not map to a representable date, so the public
28+
// resolver path never throws on a corrupt/hostile created_at.
29+
const isoFromUnix = (sec) => {
30+
if (!Number.isSafeInteger(sec)) return null;
31+
const d = new Date(sec * 1000);
32+
return Number.isNaN(d.getTime()) ? null : d.toISOString().replace(/\.\d{3}Z$/, 'Z');
33+
};
2834

2935
export function buildDidDocument(pubkey, { profile, follows, relays } = {}) {
3036
const hex = String(pubkey || '').toLowerCase();
@@ -52,7 +58,7 @@ export function buildDidDocument(pubkey, { profile, follows, relays } = {}) {
5258
const p = {};
5359
for (const k of ['name', 'about', 'picture', 'website', 'nip05', 'lud16']) if (c[k]) p[k] = c[k];
5460
if (c.display_name && !p.name) p.name = c.display_name;
55-
if (Number.isFinite(profile.created_at)) p.created_at = profile.created_at;
61+
if (Number.isSafeInteger(profile.created_at)) p.created_at = profile.created_at;
5662
if (Object.keys(p).length) doc.profile = p;
5763
if (Array.isArray(c.alsoKnownAs) && c.alsoKnownAs.length) doc.alsoKnownAs = c.alsoKnownAs;
5864
} catch { /* malformed kind-0 content */ }
@@ -93,8 +99,11 @@ export function buildDidDocument(pubkey, { profile, follows, relays } = {}) {
9399
doc.profile && profile?.created_at,
94100
doc.follows && follows?.created_at,
95101
doc.service && relays?.created_at,
96-
].filter(Number.isFinite);
97-
if (stamps.length) doc.modified = isoFromUnix(Math.max(...stamps));
102+
].filter(Number.isSafeInteger);
103+
if (stamps.length) {
104+
const iso = isoFromUnix(Math.max(...stamps));
105+
if (iso) doc.modified = iso;
106+
}
98107

99108
return doc;
100109
}

test/diddoc.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ test('follows is bounded; the full signed list stays in the kind-3 event', () =>
4848
assert.equal(d.follows.length, 500);
4949
});
5050

51+
test('out-of-range created_at does not throw and is omitted (no modified)', () => {
52+
const d = buildDidDocument(PK, {
53+
profile: { content: JSON.stringify({ name: 'X' }), created_at: 1e308 },
54+
});
55+
assert.equal(d.profile.name, 'X');
56+
assert.equal(d.profile.created_at, undefined); // not a safe integer -> omitted
57+
assert.equal(d.modified, undefined); // no valid stamp -> no modified
58+
});
59+
5160
test('rejects a non-hex (e.g. npub) identifier', () => {
5261
assert.equal(buildDidDocument('npub1xxx'), null);
5362
assert.equal(buildDidDocument(''), null);

0 commit comments

Comments
 (0)