@master4n/temporal-transformer
v2.0.5
Published
TypeScript library to convert epoch timestamps (seconds, milliseconds, microseconds, nanoseconds) to human-readable dates, parse date strings to epoch, compute calendar-accurate durations, and handle IANA timezones. Powered by Luxon — small bundle, immuta
Downloads
1,395
Maintainers
Keywords
Readme
@master4n/temporal-transformer
Convert any epoch timestamp to a human-readable date in TypeScript — auto-detects seconds, milliseconds, microseconds, and nanoseconds with zero configuration. Also converts date strings to epoch, computes calendar-accurate durations, and handles any IANA timezone.
v2.x is the current line. Engine swapped from moment.js to Luxon — same API, ~60% smaller bundle (~71 KB vs ~180 KB), no maintenance-mode dependency. Upgrading from v1.x is a one-command migration thanks to the companion codemod:
npx @master4n/temporal-transformer-codemod --update-deps ./The codemod rewrites every moment-style format string in your source (
YYYY-MM-DD→yyyy-MM-dd,dddd→cccc,[literal]→'literal', etc.), handles untranslatable tokens with stderr warnings, AND bumps@master4n/temporal-transformerin everypackage.jsonit finds to^2.0.2. See MIGRATION.md for the full upgrade story; codemod repo for the tool itself.
Why temporal-transformer?
Working with epoch timestamps in TypeScript is surprisingly painful:
- You don't know if a value is in seconds, milliseconds, microseconds, or nanoseconds until it breaks in production.
new Date(epoch)silently produces wrong dates if the unit is wrong.- Parsing date strings without a format string is unreliable and locale-dependent.
- Getting the UTC offset for a given timezone requires knowing a timezone library's specific API.
temporal-transformer solves all of these in one small, fully-typed package:
// No idea what unit this is? No problem.
// (All four resolve to the same instant — 2021-06-01T11:43:20Z. Display below in IST for illustration.)
convertEpoch(1622547800).dateTime; // seconds → "2021-06-01 17:13:20.000"
convertEpoch(1622547800000).dateTime; // millis → "2021-06-01 17:13:20.000"
convertEpoch(1622547800000000).dateTime; // micros → "2021-06-01 17:13:20.000"
convertEpoch(1622547800000000000).dateTime; // nanos → "2021-06-01 17:13:20.000"Features
- Auto-detects epoch unit — seconds, milliseconds, microseconds, nanoseconds
- Epoch → human-readable date in local timezone and GMT simultaneously
- Date string → epoch with explicit format and timezone (strict parsing, no surprises)
- Calendar-accurate duration between two timestamps (years, months, days, hours, minutes, seconds)
- Timezone conversion — format any epoch in any IANA timezone
- Timezone utilities — list all available IANA timezones, get UTC offset for any zone
- Result-style safe API — every function has a
safeXxxvariant returning{ ok, value | error }instead of throwing - Safe predicates —
isValidEpoch/isValidTimezonethat never throw - Frozen results — returned objects are
Object.freeze()'d to prevent downstream tampering - Token allowlist — format strings are validated against a Luxon-token allowlist; typos throw
FormatInvalidinstead of silently producing garbage - Relative time — "3 hours 15 minutes ago" / "2 days from now"
- Full TypeScript support — strict types, enums, and interfaces exported
- Dual ESM + CJS build — works in Next.js, Node.js, Vite, webpack
- Hardened input validation — DoS-capped string and format lengths, rejects HTML/XSS payloads,
Infinity,NaN, prototype-pollution attempts, and clock-corruption edge cases - Powered by Luxon — immutable, IANA-aware via the
IntlAPI, no moment.js, Node 18+
Installation
npm install @master4n/temporal-transformer
# or
yarn add @master4n/temporal-transformer
# or
pnpm add @master4n/temporal-transformerQuick Start
import {
convertEpoch,
convertDateToEpoch,
convertEpochToTimezone,
parseToEpoch,
getDurationBetween,
isValidEpoch,
} from '@master4n/temporal-transformer';
// Convert any epoch to a readable date — unit is auto-detected
const result = convertEpoch(1622547800000);
console.log(result.dateTime); // e.g. "2021-06-01 17:13:20.000" (your local TZ — IST shown)
console.log(result.dateTimeInGMT); // "2021-06-01 11:43:20.000"
console.log(result.epochUnit); // "milliseconds"
console.log(result.relative); // e.g. "4 years 11 months ago"
// Convert a Date object to epoch values
const epoch = convertDateToEpoch(new Date(), 'America/New_York');
console.log(epoch.epochInSeconds); // e.g. 1748000000
console.log(epoch.epochInMilliseconds); // e.g. 1748000000000
// Parse a date string to epoch (strict, timezone-aware)
const parsed = parseToEpoch('2024-12-25T00:00:00', undefined, 'Asia/Kolkata');
console.log(parsed.epochInMilliseconds);
// Format an epoch in a specific timezone
const formatted = convertEpochToTimezone(1622547800000, 'Asia/Tokyo', 'yyyy-MM-dd HH:mm');
console.log(formatted); // "2021-06-02 02:43"
// Calendar-accurate duration between two timestamps
const duration = getDurationBetween(1609459200000, 1622547800000);
console.log(duration.humanReadable); // "4 months, 15 days, 17 hours, 43 minutes, 20 seconds"
// Safe validation — never throws
console.log(isValidEpoch(1622547800000)); // true
console.log(isValidEpoch('bad value')); // false
console.log(isValidEpoch(Infinity)); // falseAPI Reference
convertEpoch(epoch, format?)
Converts any epoch value to a human-readable date. Automatically detects the epoch unit (seconds / milliseconds / microseconds / nanoseconds).
Pass a whole number. Auto-detection keys off the integer magnitude. Fractional epochs (e.g.
1622547800.5) are rejected withEpochError.NotAnIntegerrather than silently misclassified — pass whole seconds / ms / µs / ns. Detection is unambiguous for the ranges callers hit in practice (seconds up to ~year 2286; ms from ~Apr 1970 on); convert any value outside those bands to milliseconds yourself before calling.
function convertEpoch(epoch: number, format?: string): EpochToDate| Parameter | Type | Default | Description |
|-----------|----------|--------------------------------|---------------------------------|
| epoch | number | — | Integer epoch value in any unit |
| format | string | 'yyyy-MM-dd HH:mm:ss.SSS' | Luxon format token (validated against SUPPORTED_FORMAT_TOKENS) |
Returns: EpochToDate
const result = convertEpoch(1622547800000, 'yyyy-MM-dd');
// { epoch: 1622547800000, epochUnit: 'milliseconds', timezone: 'Asia/Kolkata',
// dateTime: '2021-06-01', dateTimeInGMT: '2021-06-01', relative: '...' }convertDateToEpoch(date, timezone?)
Converts a Date object to epoch values (seconds and milliseconds) in the specified timezone.
function convertDateToEpoch(date: Date, timezone?: string): DateToEpoch| Parameter | Type | Default | Description |
|------------|----------|--------------------------|----------------------------------|
| date | Date | — | Any valid JavaScript Date |
| timezone | string | System local timezone | IANA timezone name |
Returns: DateToEpoch
const result = convertDateToEpoch(new Date('2024-01-15'), 'America/New_York');
// { epochInSeconds: 1705276800, epochInMilliseconds: 1705276800000,
// dateTime: '2024-01-14 19:00:00', dateTimeInGMT: '2024-01-15 00:00:00',
// timezone: 'America/New_York' }convertEpochToTimezone(epoch, timezone, format?)
Formats an epoch value as a date string in a specific IANA timezone.
function convertEpochToTimezone(epoch: number, timezone: string, format?: string): stringconvertEpochToTimezone(1622547800000, 'America/Los_Angeles', 'yyyy-MM-dd HH:mm:ss');
// "2021-06-01 04:43:20" (PDT, UTC−7 in June)
convertEpochToTimezone(1622547800000, 'Asia/Tokyo');
// "2021-06-01 20:43:20.000" (JST, UTC+9)getEpochUnit(epoch)
Returns just the detected unit of an epoch value — useful for routing logic without doing the full conversion.
function getEpochUnit(epoch: number | string): EpochUnitgetEpochUnit(1622547800); // EpochUnit.SECONDS === 'seconds'
getEpochUnit(1622547800000); // EpochUnit.MILLI_SECONDS === 'milliseconds'
getEpochUnit(1622547800000000); // EpochUnit.MICRO_SECONDS === 'microseconds'
getEpochUnit(1622547800000000000); // EpochUnit.NANO_SECONDS === 'nanoseconds'parseToEpoch(input, format?, timezone?)
Parses a date string into epoch values. Uses strict parsing when a format is provided — never silently produces a wrong date.
function parseToEpoch(input: string, format?: string, timezone?: string): DateToEpoch| Parameter | Type | Default | Description |
|------------|-------------------------|-----------------------|------------------------------------------------------|
| input | string | — | Date string to parse |
| format | string or undefined | Auto (ISO 8601) | Luxon format token (validated against SUPPORTED_FORMAT_TOKENS) |
| timezone | string | System local timezone | IANA timezone to interpret the date in |
Returns: DateToEpoch
Throws: EpochValidationError with EpochError.ParseError if the string cannot be parsed, or EpochError.TimezoneError for an unknown timezone.
// ISO 8601 auto-parsing
parseToEpoch('2024-12-25T00:00:00Z');
// Explicit format (strict mode — wrong format throws immediately)
parseToEpoch('25/12/2024', 'dd/MM/yyyy', 'Europe/London');
// With timezone context
parseToEpoch('2024-12-25 09:00:00', 'yyyy-MM-dd HH:mm:ss', 'Asia/Kolkata');getDurationBetween(fromEpoch, toEpoch)
Computes a calendar-accurate duration between two epoch timestamps.
function getDurationBetween(fromEpoch: number, toEpoch: number): ParsedDurationReturns: ParsedDuration
Throws: EpochValidationError with EpochError.RangeError if fromEpoch > toEpoch.
const d = getDurationBetween(1609459200000, 1748000000000);
console.log(d.years); // 4
console.log(d.months); // 5
console.log(d.days); // 18
console.log(d.humanReadable); // "4 years, 5 months, 18 days, ..."
console.log(d.totalMilliseconds); // exact diff in msCalendar-accurate: months and years account for actual calendar boundaries (Feb 28/29, 30/31-day months), not fixed 30.44-day approximations.
getTimezoneOffset(timezone, epoch?)
Returns the UTC offset for an IANA timezone, optionally at a specific point in time (useful for DST-aware offsets).
function getTimezoneOffset(timezone: string, epoch?: number): TimezoneOffsetgetTimezoneOffset('Asia/Kolkata');
// { offset: '+05:30', offsetMinutes: 330 }
getTimezoneOffset('America/New_York');
// { offset: '-05:00', offsetMinutes: -300 } (or -04:00 during DST)
// DST-aware: pass the epoch you care about
getTimezoneOffset('America/New_York', 1622547800000);
// { offset: '-04:00', offsetMinutes: -240 }getTimezoneList()
Returns the full list of supported IANA timezone names (600+).
function getTimezoneList(): string[]const zones = getTimezoneList();
zones.includes('Asia/Kolkata'); // true
zones.includes('America/Chicago'); // truegetEpochNow()
Returns the current moment as epoch values (seconds, milliseconds, ISO string, and detected timezone).
function getEpochNow(): EpochNowconst now = getEpochNow();
// { seconds: 1748000000, milliseconds: 1748000000000,
// iso: '2025-05-23T10:13:20.000Z', timezone: 'Asia/Kolkata' }formatDuration(milliseconds)
Formats a duration in milliseconds as a human-readable string.
function formatDuration(milliseconds: number): stringformatDuration(3661000); // "1 hour, 1 minute, 1 second"
formatDuration(86400000); // "1 day"
formatDuration(90061000); // "1 day, 1 hour, 1 minute, 1 second"
formatDuration(500); // "0 seconds"isValidEpoch(epoch) · isValidTimezone(timezone)
Safe predicates that return boolean without throwing. Useful for conditional logic and input validation at system boundaries.
function isValidEpoch(epoch: unknown): boolean
function isValidTimezone(timezone: string): booleanisValidEpoch(1622547800000); // true
isValidEpoch('1622547800000'); // true (numeric string)
isValidEpoch(null); // false
isValidEpoch(Infinity); // false
isValidEpoch(NaN); // false
isValidTimezone('UTC'); // true
isValidTimezone('Asia/Kolkata'); // true
isValidTimezone('Not/ATimezone'); // falseTypes Reference
EpochToDate
interface EpochToDate {
epoch: number; // original input
epochUnit: string; // 'seconds' | 'milliseconds' | 'microseconds' | 'nanoseconds'
timezone: string; // IANA timezone used for dateTime
dateTime: string; // formatted date in local timezone
dateTimeInGMT: string; // formatted date in GMT
relative: string; // e.g. "3 hours 15 minutes ago"
}DateToEpoch
interface DateToEpoch {
epochInSeconds: number;
epochInMilliseconds: number;
timezone: string;
dateTime: string; // formatted in the given timezone
dateTimeInGMT: string;
}ParsedDuration
interface ParsedDuration {
years: number;
months: number;
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
totalMilliseconds: number;
humanReadable: string; // e.g. "1 year, 2 months, 3 days"
}EpochNow
interface EpochNow {
seconds: number;
milliseconds: number;
iso: string; // ISO 8601 UTC string
timezone: string; // detected local timezone
}TimezoneOffset
interface TimezoneOffset {
offset: string; // e.g. "+05:30"
offsetMinutes: number; // e.g. 330
}EpochUnit enum
enum EpochUnit {
SECONDS = 'seconds',
MILLI_SECONDS = 'milliseconds',
MICRO_SECONDS = 'microseconds',
NANO_SECONDS = 'nanoseconds',
}DurationUnit type
type DurationUnit =
| 'years' | 'months' | 'weeks' | 'days'
| 'hours' | 'minutes' | 'seconds' | 'milliseconds';Epoch Unit Auto-Detection
| Absolute value range | Detected unit | Example value |
|------------------------------|---------------|-------------------------|
| < 10,000,000,000 | seconds | 1622547800 |
| < 10,000,000,000,000 | milliseconds | 1622547800000 |
| < 10,000,000,000,000,000 | microseconds | 1622547800000000 |
| < 1e19 | nanoseconds | 1622547800000000000 |
Negative epochs (dates before Unix epoch, i.e. before 1970-01-01) are fully supported.
Error Handling
All functions throw EpochValidationError (extends Error) with a message from the EpochError enum.
import { EpochValidationError, EpochError } from '@master4n/temporal-transformer';
try {
convertEpoch(null as any);
} catch (err) {
if (err instanceof EpochValidationError) {
console.log(err.message); // 'Epoch Is Undefined Or Null'
console.log(err.name); // 'EpochValidationError'
}
}| EpochError value | Thrown when |
|--------------------|----------------------------------------------------------|
| UndefinedOrNull | Input is null or undefined |
| NotANumber | Input is not numeric, is Infinity, or is NaN |
| Empty | Input is an empty or whitespace-only string |
| EpochUnit | Epoch value is too large to classify into any unit |
| DateError | Date object passed to convertDateToEpoch is invalid |
| TimezoneError | Timezone string is not a recognized IANA timezone |
| ParseError | Date string cannot be parsed (in parseToEpoch) |
| RangeError | fromEpoch > toEpoch in getDurationBetween |
Result-Style Safe API (unique feature)
Every function has a safeXxx counterpart that returns a discriminated Result<T> object instead of throwing. This is ideal for code paths handling untrusted input — API endpoints, form validation, batch processors — where try/catch becomes noise.
import { safeConvertEpoch, safeParseToEpoch, safeGetEpochNow } from '@master4n/temporal-transformer';
const result = safeConvertEpoch(req.body.timestamp);
if (result.ok) {
console.log(result.value.dateTime); // typed as EpochToDate
} else {
console.log(result.error.message); // typed as EpochValidationError
}| Safe variant | Wraps | Returns |
|-------------------------------|-----------------------------|--------------------------------------------------|
| safeConvertEpoch | convertEpoch | Result<EpochToDate, EpochValidationError> |
| safeConvertDateToEpoch | convertDateToEpoch | Result<DateToEpoch, EpochValidationError> |
| safeConvertEpochToTimezone | convertEpochToTimezone | Result<string, EpochValidationError> |
| safeParseToEpoch | parseToEpoch | Result<DateToEpoch, EpochValidationError> |
| safeGetDurationBetween | getDurationBetween | Result<ParsedDuration, EpochValidationError> |
| safeGetTimezoneOffset | getTimezoneOffset | Result<TimezoneOffset, EpochValidationError> |
| safeGetEpochNow | getEpochNow | Result<EpochNow, EpochValidationError> |
| safeGetEpochUnit | getEpochUnit | Result<EpochUnit, EpochValidationError> |
The Result<T> type
type Result<T, E = Error> =
| { ok: true; value: T; error?: never }
| { ok: false; value?: never; error: E };The discriminant is ok. TypeScript narrows value and error automatically once you branch on it.
Security Model
temporal-transformer treats every input as untrusted. The library has been audited against the OWASP-style threat model below; the test suite test/epoch/security.test.ts pins each defense.
Threats addressed
| Threat | Defense |
|----------------------------------------------|--------------------------------------------------------------------------|
| Prototype pollution via __proto__ | Object inputs are rejected with NotANumber before any property access |
| Infinity, -Infinity, NaN | Explicit isFinite guard, throws NotANumber |
| String DoS (e.g. 1M-char numeric string) | MAX_INPUT_STRING_LENGTH = 256 → throws InputTooLong |
| Format-string DoS (memory amplification) | MAX_FORMAT_STRING_LENGTH = 256 → throws FormatTooLong |
| XSS / HTML in parseToEpoch | Character allowlist regex rejects payloads before reaching moment |
| Stderr leakage of payloads | Strict ISO 8601 mode bypasses moment's js Date() fallback path |
| Timezone injection | validTimezone checks input against the IANA name list |
| Result object tampering | All return values are Object.freeze()'d |
| Internal array mutation | getTimezoneList returns a defensive copy |
| System clock corruption (Date.now()) | getEpochNow validates against ±MAX_EPOCH_MS, throws ClockOutOfRange|
| Reversed-range duration | getDurationBetween throws RangeError if from > to |
| ReDoS | All regexes are linear-time (no nested quantifiers, no backreferences) |
Known caller responsibilities
- Format string output is literal: moment's
[bracketed]literals in a format string are emitted verbatim. If you pass user-controlled format strings and output the result to HTML, you must HTML-escape on output. The library does not interpret format strings as user-supplied content. convertDateToEpoch(date)trustsdate.getTime(): aDatesubclass with an overriddenvalueOf()/getTime()returns whatever those methods return. Validateinstanceof Dateand untainted construction at your trust boundary if this matters.
Exported security constants
import {
MAX_EPOCH_MS, // 8.64e15 — JS Date upper bound
MIN_EPOCH_MS, // -8.64e15 — JS Date lower bound
MAX_INPUT_STRING_LENGTH, // 256
MAX_FORMAT_STRING_LENGTH, // 256
} from '@master4n/temporal-transformer';Reporting
If you discover a security issue, please open a private advisory on github.com/Master4Novice/temporal-transformer/security rather than a public issue. See SECURITY.md for the full policy.
Benchmarks
Real numbers, run yourself with npm run bench. See bench/RESULTS.md for the full report. Headline on Node 24 / Apple Silicon:
| Operation | Native JS | Raw Luxon | This library | Library overhead |
|---|---:|---:|---:|---:|
| getEpochUnit (auto-detect) | — | — | 170M ops/s | the unique feature is essentially free |
| isValidEpoch (safe predicate) | 227M ops/s | — | 167M ops/s | 1.4× — JIT-friendly |
| convertEpochToTimezone | — | 183K ops/s | 168K ops/s | 1.1× — thin wrapper |
| getDurationBetween (calendar) | — | 87K ops/s | 92K ops/s | actually faster than raw Luxon diff |
| parseToEpoch (ISO + allowlist) | 7.76M ops/s | 603K ops/s | 219K ops/s | 35× over native (security cost) |
| convertEpoch (dual-TZ + relative) | 2.08M ops/s | 527K ops/s | 25K ops/s | 82× — does dual-TZ format + relative time |
What these numbers mean for your code
| Use case | Verdict |
|---|---|
| Backend ingest / API endpoints (10K req/s) | Imperceptible. Use freely. |
| Per-request formatting (handful of calls) | Network/DB latency dominates by 1000×. |
| Batch processing 100K+ rows/second | convertEpoch is hot — use getEpochUnit once to determine units, then loop with raw Date. |
| Frontend display | Imperceptible. |
Bottom line: This library is a validation + ergonomics layer. Its job is to be correct (auto-detect, security guards, frozen results, Result API) — not to be the fastest format-string emitter. The auto-detect itself is essentially free; the cost is the dual-output convenience and the safety checks.
Comparison with other date libraries
| Feature | temporal-transformer | moment.js | dayjs | date-fns | luxon | Native Date |
|---|---|---|---|---|---|---|
| Auto-detect epoch unit | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Result-style safe API ({ok, value\|error}) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Frozen results (immutable by construction) | ✅ | ❌ | ✅ | ✅ | ✅ | n/a |
| Input length DoS caps | ✅ | ❌ | ❌ | ❌ | ❌ | n/a |
| Format-token allowlist (rejects typos) | ✅ | ❌ | ❌ | ❌ | ❌ | n/a |
| HTML/XSS payload rejection in parse | ✅ | ❌ | ❌ | ❌ | ❌ | n/a |
| IANA timezone support | ✅ (Intl) | ✅ (bundled) | plugin | plugin | ✅ (Intl) | partial |
| TypeScript-first | ✅ | ❌ (@types/moment) | ✅ | ✅ | ✅ | ✅ |
| Tree-shakeable | partial | ❌ | ✅ | ✅ | partial | n/a |
| Min+gzip size | ~25 KB + Luxon | ~290 KB w/tz | ~7 KB | ~13 KB (modular) | ~71 KB | 0 |
| Maintenance status | active | maintenance-only | active | active | active | spec |
| Best for | Ingesting untrusted, mixed-unit epochs | Legacy codebases | Lightweight client bundles | Functional/modular use | Heavy timezone work | Trivial / known-format use |
When to pick what
- Pick
temporal-transformerif you process timestamps where the unit isn't known up front, or you handle untrusted input and want the safe Result API. - Pick
dayjsif bundle size is paramount and you don't need timezone-aware parsing. - Pick
date-fnsif you want a treeshakeable, functional API and you control the input. - Pick
luxondirectly if you already know your epoch unit and need raw speed for timezone formatting at scale. - Pick
Datedirectly if your input is always milliseconds, you don't care about timezones beyond UTC, and you live bytoISOString(). - Pick
momentonly if you're maintaining an existing codebase that depends on it.
Common Recipes
Convert a Unix timestamp from an API response
// API returns epoch in unknown unit — temporal-transformer handles it
const ts = apiResponse.created_at; // could be seconds or ms
const { dateTime, epochUnit } = convertEpoch(ts);Validate a user-provided timestamp before processing
const userInput = req.body.timestamp;
if (!isValidEpoch(userInput)) {
return res.status(400).json({ error: 'Invalid timestamp' });
}
const { dateTime } = convertEpoch(Number(userInput));Show how long ago something happened
const { relative } = convertEpoch(event.createdAt);
console.log(`Event created ${relative}`); // "Event created 2 days 4 hours ago"Parse a date string from a form input
// User enters "25/12/2024" in a UK-locale date picker
const { epochInMilliseconds } = parseToEpoch('25/12/2024', 'dd/MM/yyyy', 'Europe/London');Compute how long a job ran
const duration = getDurationBetween(job.startedAt, job.finishedAt);
console.log(`Job ran for ${duration.humanReadable}`);Part of the @master4n toolkit
A small ecosystem of focused, agent-friendly packages:
@master4n/temporal-transformer-codemod— codemod to migrate temporal-transformer v1→v2@master4n/http-status— machine-readable HTTP status-code registry for apps & AI agents@master4n/decorators— zero-dependency TypeScript decorators (DI, validation, resilience, redaction)@master4n/master-cli— headless, JSON-first dev CLI (mfn) for humans and AI agents
Changelog
2.0.5
- Packaging:
llms.txtis now included in the published tarball (it was built but not copied intodist, so it wasn't shipped). - Discoverability: broadened npm keywords (convert-unix-timestamp-to-date, timestamp-to-date, time-ago, humanize-duration, date-converter) and added a "Part of the @master4n toolkit" section.
2.0.4
- Fix (correctness): fractional epochs are now rejected with the new
EpochError.NotAnIntegerinstead of being silently misclassified. Previously the validator stripped the decimal point and re-ran unit auto-detection, soconvertEpoch(1622547800.5)returned a 1970 date (read as16225478005ms) instead of throwing. Integer epochs are unaffected. The Result-API variants (safeConvertEpoch, …) return{ ok: false, error }as usual. - Docs: documented the integer requirement and the auto-detection's
supported magnitude range on
convertEpoch.
2.0.3
- Docs: Top-of-README callout updated to highlight the companion codemod's new one-command migration (
npx @master4n/temporal-transformer-codemod --update-deps ./). The codemod (currently2.0.3) now also bumps@master4n/temporal-transformerto^2.0.2in everypackage.jsonit finds, in addition to rewriting format strings. - Docs: Replaced unreliable bundlephobia badge with a static gzipped-size badge (
7.5 KB) linking tobench/RESULTS.md. Added a "tests passing" badge. - No code change — runtime behavior identical to 2.0.2.
2.0.2
- CI fix: Build now succeeds on Node 18 and on all Windows runners. The
@rollup/plugin-terserdependency was transitively pullingserialize-javascriptwhich callscrypto.randomUUID()from globals at module load — this throwsReferenceError: crypto is not definedin Node 18 CJS contexts and on Windows runners. Dropped the terser plugin entirely; the bundle is still tiny (~7.5 KB gzipped). Downstream bundlers can re-minify if needed. - CI: Workflow now triggers on
master,main, andv1.x-maintenance(was onlymain). Addedworkflow_dispatchfor manual runs. - CI: Set
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=trueto silence the Node 20 deprecation warning ahead of the 2026-09-16 removal. - DX: Test scripts are now cross-platform. Added
cross-envforTZ=UTCand simplified jest invocations to rely onjest.config.mjsfor ignore patterns. - No runtime API changes — drop-in upgrade from 2.0.1.
2.0.1
- Docs: Repository, homepage, and bug-tracker URLs now point at the new public single-package repos at Master4Novice/temporal-transformer and Master4Novice/temporal-transformer-codemod (previously pointed at a private monorepo).
- Docs: Added benchmark suite (
npm run bench) with results published in bench/RESULTS.md. Headline:getEpochUnit~170M ops/s,getDurationBetweenslightly faster than raw Luxon diff,convertEpoch82× slower than rawDate.toISOString()(cost of dual-TZ + relative-time). - Docs: New comparison matrix vs moment / dayjs / date-fns / luxon / native
Datewith explicit "when to pick what" guidance. - Docs: Added
SECURITY.md,CONTRIBUTING.md, GitHub issue & PR templates. - CI: Workflow now runs on Ubuntu/macOS/Windows × Node 18/20/22/24.
- No code changes — behavior identical to v2.0.0.
2.0.0
- Engine swap: moment + moment-timezone → Luxon. ~60% smaller bundle (~71 KB vs ~180 KB), no maintenance-mode dependency.
- Breaking: Format-string grammar is now Luxon syntax. Use
yyyy-MM-ddinstead ofYYYY-MM-DD,ccccinstead ofdddd,'literal'instead of[literal], and so on. Run the migration codemod:npx @master4n/temporal-transformer-codemod ./src. - Breaking: Default format is now
'yyyy-MM-dd HH:mm:ss.SSS'(was'YYYY-MM-DD HH:mm:ss.SSSSSS'). JS Date has millisecond precision; the 6-digit fractional was always zero-padded fiction. - New error:
EpochError.FormatInvalid— thrown when a format string contains a token outside the allowlist. Typos like'YYYY'(still moment syntax) now surface immediately instead of producing garbage output. - New exports:
DEFAULT_FORMAT,SUPPORTED_FORMAT_TOKENS(the runtime-enforced allowlist). - New companion package:
@master4n/temporal-transformer-codemod— one-shot CLI that rewrites moment-style format strings in your codebase. Handles greedy token matching,[literal]→'literal'escape conversion (with proper''escaping for embedded quotes), and emits warnings for tokens with no Luxon equivalent (X,x,Q). - Requirement: Node 18+ (for
Intl.supportedValuesOf). - Maintenance: v1.x stays on npm under the
legacydist-tag; install withnpm i @master4n/temporal-transformer@legacy. Security backports continue onv1.x-maintenancefor 12 months.
See MIGRATION.md for the full upgrade guide.
1.4.0
- Test infrastructure: Added golden-test baseline (
test/golden/) pinning v1.x behavior for the v2.0 parity gate. No runtime behavior changes.
1.3.0
- New (unique): Result-style safe API — every function has a
safeXxxcounterpart returning{ ok, value | error }instead of throwing (8 functions, see Result-Style Safe API) - New:
getEpochUnit(epoch)— standalone helper that returns just the detectedEpochUnit - Security: Frozen result objects (
Object.freeze) prevent downstream tampering - Security:
MAX_INPUT_STRING_LENGTH(256) caps DoS via outsized numeric/date strings — throwsInputTooLong - Security:
MAX_FORMAT_STRING_LENGTH(256) caps memory-amplification via format strings — throwsFormatTooLong - Security:
getEpochNowvalidatesDate.now()againstMIN_EPOCH_MS/MAX_EPOCH_MSand throwsClockOutOfRangeinstead of a rawRangeErroron a corrupted system clock - Security:
getTimezoneListreturns a defensive copy, preventing mutation of the shared internal array - Security:
parseToEpochnow rejects non-string input early withParseError - Security: Fallback to
'UTC'whenmoment.tz.guess()returns an empty string - New exports:
Result<T, E>,MAX_EPOCH_MS,MIN_EPOCH_MS,MAX_INPUT_STRING_LENGTH,MAX_FORMAT_STRING_LENGTH,EpochThreshold - New errors:
EpochError.InputTooLong,EpochError.FormatTooLong,EpochError.ClockOutOfRange - Docs: New Security Model section enumerating the threat model and defenses
1.2.1
- Security fix:
parseToEpochnow validates input against an allowlist of date-safe characters before passing to moment, preventing HTML/XSS payloads from reaching moment's internal parser and appearing in stderr stack traces - Security fix:
parseToEpochwithout an explicit format now uses strict ISO 8601 parsing (moment.ISO_8601, true) instead of moment's lenient auto-detection, eliminating thejs Date()fallback that could behave inconsistently across environments
1.2.0
- New:
convertEpochToTimezone— format epoch in any IANA timezone - New:
parseToEpoch— parse date strings to epoch with strict, timezone-aware parsing - New:
getDurationBetween— calendar-accurate duration between two epochs - New:
getTimezoneOffset— get UTC offset (DST-aware) for any IANA timezone - New:
getTimezoneList— list all supported IANA timezone names - New:
getEpochNow— current time as seconds, milliseconds, ISO string, and timezone - New:
formatDuration— format a duration in ms as a human-readable string - New:
isValidEpoch/isValidTimezone— safe boolean predicates (never throw) - New types:
ParsedDuration,EpochNow,TimezoneOffset,DurationUnit,StartEndUnit - New exports:
EpochUnitenum,EpochErrorenum,EpochValidationErrorclass - Fix:
validateEpochno longer crashes onInfinity,-Infinity,NaN, or whitespace-only strings - Fix: Empty-string check now trims whitespace before checking length
- Deps: Removed redundant
@types/moment; movedtypescriptand@types/nodetodevDependencies; updatedmoment-timezoneto^0.5.46andtypescriptto^5.8.3
1.1.1 and earlier
Initial release — convertEpoch and convertDateToEpoch.
Contributing
Issues and PRs are welcome at github.com/Master4Novice/temporal-transformer. The companion codemod lives at github.com/Master4Novice/temporal-transformer-codemod.
Credits
Written by Master4Novice.
License
MIT © Master4Novice
