Skip to content

0.9.6

Choose a tag to compare

@DirazCoder DirazCoder released this 12 Sep 15:17
· 17 commits to main since this release

temporal-fmt v0.9.6

Security

  • Mods no longer run in the host process at all. Every mod — loose .mjs
    and .tfmod alike — now loads inside its own subprocess, started with
    Node's permission model (--permission on Node 22.13+,
    --experimental-permission on anything older), and gets zero capabilities
    by default. Previously register() ran via a plain import(), 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.env inside a
    mod is scrubbed down to PATH/TZ/LANG plus 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's overrideFormat
    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 — ArrayBuffer allocations
    count, and setting --max-old-space-size anywhere 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

  • .tfmod mods can declare a "permissions" array in mod.json —
    fs:read, fs:write, child-process, worker, each markable required or
    optional. First load prompts the user once per name@version and caches
    the answer in .temporal-fmt-permissions.json, next to mods/. 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 as downgraded instead of loaded.
    Loose .mjs mods 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 of register() 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 other override* points): a
    mod's impl is still a closure, but that closure now lives inside the mod's
    own subprocess, not the host. Every format()/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 hot format() 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 with new 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
    net capability 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 in MODS.md now instead of left for someone to
    discover the hard way.