From 1307bae40003c847af1c4129db0908d5ec0e47c9 Mon Sep 17 00:00:00 2001 From: gt12889 <130225802+gt12889@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:55:08 -0500 Subject: [PATCH] fix: reject timestamps above sortable range --- CHANGELOG.md | 1 + src/generators/sortable.ts | 9 ++++++++- test/sortable.test.ts | 11 ++++++++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 748a897..5dd3072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `getPrefix(value, separator?)` and `parseId(value, separator?)` now return `undefined` for non-string input instead of throwing, matching `isId` and `getTimestamp`. - `parseId()` now returns `undefined` when the ID body following the separator is empty (e.g., `"user_"`). +- `getTimestamp()` now rejects decoded values above `SORTABLE_TIME_MAX`, so non-sortable random IDs are not reported as plausible timestamps. ## [1.1.0] - 2026-08-16 diff --git a/src/generators/sortable.ts b/src/generators/sortable.ts index 5006ffb..eb119de 100644 --- a/src/generators/sortable.ts +++ b/src/generators/sortable.ts @@ -184,7 +184,14 @@ export function getTimestamp( const body = value.slice(index + separator.length); if (body.length < timestampSize) return undefined; - return decodeTime(body.slice(0, timestampSize), alphabet, timestampSize); + const timestamp = decodeTime( + body.slice(0, timestampSize), + alphabet, + timestampSize, + ); + return timestamp !== undefined && timestamp > SORTABLE_TIME_MAX + ? undefined + : timestamp; } export function getTimestampOrThrow( diff --git a/test/sortable.test.ts b/test/sortable.test.ts index e136f2d..f29c47d 100644 --- a/test/sortable.test.ts +++ b/test/sortable.test.ts @@ -170,6 +170,12 @@ describe("getTimestamp()", () => { // '-' is not in base62, so decoding must fail cleanly. expect(getTimestamp("u_----------aaaaaaaaaaaaaaaa")).toBeUndefined(); }); + + it("returns undefined for a decoded timestamp above the sortable ceiling", () => { + const value = "evt_zzzzzzzzz000000000000"; + expect(getTimestamp(value)).toBeUndefined(); + expect(() => getTimestampOrThrow(value)).toThrow(TypeError); + }); }); describe("createSortableId() — configuration", () => { @@ -362,11 +368,10 @@ describe("getDate()", () => { expect(SORTABLE_TIME_MAX).toBeLessThan(MAX_DATE_MS); }); - it("returns a Date exactly at the Date-range ceiling and undefined just past it", () => { + it("returns undefined for a timestamp outside the sortable time range", () => { const at = (t: number) => createSortableId({ now: () => t, monotonic: false })("evt"); - expect(getDate(at(MAX_DATE_MS))).toBeInstanceOf(Date); - expect(getDate(at(MAX_DATE_MS + 1))).toBeUndefined(); + expect(getDate(at(MAX_DATE_MS))).toBeUndefined(); }); });