Repository navigation
0.9.6
temporal-fmt v0.9.6
Security
- Mods no longer run in the host process at all. Every mod — loose
.mjs
and.tfmodalike — now loads inside its own subprocess, started with
Node's permission model (--permissionon Node 22.13+,
--experimental-permissionon anything older), and gets zero capabilities
by default. Previouslyregister()ran via a plainimport(), which meant
a mod could read/write anywhere on disk or spawn whatever it wanted with
the exact same access as the CLI process itself — there was no boundary
between "code that formats dates" and "code that does anything Node can
do." Filesystem reads/writes, child-process spawns, and worker threads are
all refused by Node itself now unless the user explicitly grants them. - The subprocess never sees the host's environment.
process.envinside a
mod is scrubbed down toPATH/TZ/LANGplus a couple of Windows
bootstrap variables — CI tokens, database URLs, anything else living in the
parent's env doesn't cross over. There's no permission that hands this
back, either: the permission model has no flag that gates environment
variables, so the only honest option was to just not pass them in. - A mod that hangs or leaks memory can no longer take the whole CLI command
down with it.register()gets killed after 10 seconds; a runtime
override call (format()/parse()routed through a mod'soverrideFormat
etc.) gets 5; a subprocess whose RSS crosses 512MB gets killed regardless
of what it's doing. That ceiling is watched as actual resident memory from
the parent process, not V8's heap accounting —ArrayBufferallocations
count, and setting--max-old-space-sizeanywhere in the process tree
can't quietly raise it. Any of these failing only takes down that one mod;
the rest of the load pass and the command you ran keep going.
Added
.tfmodmods can declare a"permissions"array inmod.json—
fs:read,fs:write,child-process,worker, each markable required or
optional. First load prompts the user once pername@versionand caches
the answer in.temporal-fmt-permissions.json, next tomods/. A denied
required permission fails the mod before any of its code runs, with the
exact grant command in the error; a denied optional one just runs the mod
with less and shows up in the report asdowngradedinstead ofloaded.
Loose.mjsmods have no manifest to declare any of this in, so they
always run with nothing granted — that's the trade for not needing one.ctx.hasPermission(capability), so a mod can check what it actually got
and skip the parts ofregister()that need something denied, instead of
crashing partway through.scripts/managePermissions.mjs—list/grant/deny/reset
against the same cache file the load-time prompts write to, for changing an
answer without re-triggering a load.- The load report now spells out sandboxing per mod: which capabilities
were granted vs. denied, and — for any mod holding a runtime override — the
timeout and memory ceiling it's running under. MODS.md, a standalone guide with everything that used to live in the
README's "Mods" section plus all of the above; the README section is now a
pointer to it.
Changed
overrideFormat/overrideParse(and the otheroverride*points): a
mod's impl is still a closure, but that closure now lives inside the mod's
own subprocess, not the host. Everyformat()/parse()call that hits an
overridden path forwards to it over a synchronous channel and blocks for
the reply — a couple of milliseconds instead of nanoseconds, plus a bit
more on Windows, where the host has no file descriptor for its end of a
named pipe and the call rides a pair of scratch files instead. The loader
tracks, per format string, whether a mod's impl is just forwarding to the
built-in — a format string the mod never actually touches stops paying the
round trip after the first call.formatRange()batches its two endpoint calls into a single subprocess
round trip when the active override lives in a mod, instead of paying for
each endpoint separately.createFormatter()custom token handlers are the one thing that still
runs host-side, since a hotformat()path can't afford a round trip per
token lookup. The handler's source text ships out of the subprocess at
register()time and gets rebuilt there withnew Function, which only
works if the handler doesn't close over anything outside its own body — the
loader checks this by reviving the handler from source and diffing its
output against the original's, and refuses the mod if they disagree.- Network access is still unrestricted — Node's permission model has no
netcapability on any currently supported version, so there was never a
flag to gate it with. That was already true before this release; it's just
written down explicitly inMODS.mdnow instead of left for someone to
discover the hard way.