Cooldown

mpm can refuse to install or upgrade any package version younger than a chosen release age. This is a supply-chain safeguard: malicious releases (compromised credentials, dependency confusion, account takeover) are typically detected and pulled from registries within days of publication, so a short waiting period keeps the most recent and most likely compromised versions off the system.

Recent examples include the XZ Utils backdoor and recurring vulnerabilities in the VS Code extension marketplace. A delay of even a few days would have given the community time to react.

Quick start

The cooldown applies to every install and upgrade mpm performs:

$ mpm --cooldown "7 days" upgrade --all
$ mpm --cooldown "1 week" install some-package
$ mpm --cooldown 12h --allow-unsupported-managers upgrade --all   # let unsupported managers run too

If you are unsure which value to pick, --cooldown '7 days' is a reasonable default: long enough for most compromised releases to be flagged and pulled from registries, short enough to keep security updates timely. There is no bare --cooldown shorthand: the option always takes an explicit duration.

--cooldown accepts three input shapes:

  • Friendly duration: 7 days, 1 week, 12h, 30m, 45s, a bare number of days (7), or 0 / empty to disable the gate.

  • ISO 8601 duration: P7D, PT12H, P1WT6H. Case-insensitive.

  • RFC 3339 absolute timestamp: 2024-05-01T00:00:00Z or with an offset like +02:00. Converted at parse time to now - timestamp; a timestamp in the future disables the gate.

The same value is settable as the cooldown key in any mpm configuration file (see Configuration for the full schema) or as the MPM_COOLDOWN environment variable.

Note

Durations resolve to a fixed number of seconds, assuming a day is 24 hours. The local time zone, DST transitions, and calendar boundaries are ignored. Calendar units (months, years) are rejected for the same reason: 28-31 days and 365-366 days make them unsuitable for a precise release-age cutoff. Use days or weeks instead.

How it works

When cooldown is set, mpm:

  1. Computes a UTC cutoff timestamp equal to now - cooldown.

  2. For each manager that natively enforces a release-age gate (see the support table below), injects the manager’s dedicated environment variable carrying that cutoff into every CLI call. The manager’s own resolver then excludes every version published after the cutoff, including transitive dependencies.

  3. For each manager without a native gate, skips install / upgrade with a warning (fail-closed). Pass --allow-unsupported-managers (or set require_cooldown_support = false in the config file) to run those managers anyway, without the safeguard.

  4. Leaves read-only operations (outdated, installed, search) untouched: information is never blocked, only mutations are.

The choice to delegate to each manager’s own resolver rather than reimplement the gate inside mpm is deliberate: only the resolver can apply the cutoff to the whole dependency closure (see Limitations below).

Supported managers

The table below is the source of truth for which managers mpm can gate today and the state of the upstream effort everywhere else. Statuses:

  • Enforced: mpm actively injects a cooldown environment variable on every CLI call. Listed in the cooldown_env_var framework.

  • 🔜 Shipped upstream: the manager ships a release-age gate but mpm does not (yet) plug into it.

  • 🚧 Proposed: an open pull request, RFC, or issue is on file upstream.

  • None: no public proposal found.

  • N/A: the concept does not apply (distro-curated repositories with their own staging, archived projects, meta-upgraders, …). A structural equivalent is noted when relevant.

mpm id

Status

Mechanism

Reference

antidote

🔜 Shipped upstream (since 2.2.0)

zstyle ':antidote:bundle:*' min-age <days>, whole days, no environment variable

antidote CHANGELOG

antigen

❌ None (clones a Git branch or tag, no release feed)

apk

❌ None

apm

➖ N/A (archived June 2022)

atom/apm

apt

➖ N/A (Debian’s unstabletestingstable migration is functionally similar)

Nesbitt, Package managers need to cool down

apt-cyg

➖ N/A (unmaintained since 2017; wraps Cygwin’s setup.exe repository)

transcode-open/apt-cyg

apt-mint

➖ N/A (follows apt)

asdf

❌ None (runtime version manager)

brew

🚧 Proposed (closed as not planned for users; merged for internal bottle resource resolution)

(internal) --min-release-age=1, --uploaded-prior-to

Homebrew/brew#21129

cargo

🚧 Proposed (RFC 3923 merged, nightly implementation)

-Zmin-publish-age

rust-lang/cargo#17009

cask

🚧 Same as brew (inherits)

Homebrew/brew#21129

cave

❌ None (Exherbo, source-based)

choco

❌ None

chromebrew

❌ None (Chrome OS)

composer

🚧 Proposed

open PR adds cooldown

composer/composer#12692

conda

🚧 Proposed

--exclude-newer / exclude_newer (open issue + PR)

conda/conda#15759

cpan

❌ None

deb-get

❌ None

dnf

❌ None (effort focused on dnf5)

dnf5

🚧 Proposed

minimum_package_age (open issue)

rpm-software-management/dnf5#2743

emerge

❌ None

eopkg

❌ None

fink

❌ None (macOS, Debian/dpkg-based)

fisher

❌ None (clones a Git branch or tag, no release feed)

flatpak

❌ None

fwupd

➖ N/A (LVFS staged deployment)

LVFS news

gem

🚧 Proposed (Bundler PR open)

--cooldown / BUNDLE_COOLDOWN / per-source cooldown:

ruby/rubygems#9576

gh-ext

❌ None (installs extensions from git repos)

guix

❌ None

krew

❌ None

lazy

❌ None

macports

❌ None

mas

❌ None

mise

🔜 Shipped upstream

minimum_release_age env MISE_MINIMUM_RELEASE_AGE / --minimum-release-age (default 24h)

mise settings

nix

❌ None

npm

Enforced (npm ≥ 11.10)

min-release-age env npm_config_min-release-age (integer days)

npm docs

oh-my-fish

❌ None (clones a Git branch or tag, no release feed)

opam

❌ None

opkg

❌ None

pacaur

❌ None (Arch AUR helper)

pacman

❌ None

pacstall

❌ None

paru

❌ None (Arch AUR helper)

pip

Enforced (pip ≥ 26.1)

--uploaded-prior-to env PIP_UPLOADED_PRIOR_TO

pypa/pip#13674

pipx

Enforced (via pip’s env var; needs the underlying pip ≥ 26.1)

inherits PIP_UPLOADED_PRIOR_TO

pypa/pipx#1811

pkcon

➖ N/A (delegates to the distro backend)

pkg

❌ None

pkg-tools

❌ None (OpenBSD, per-release curated)

pkgin

❌ None (pkgsrc binary packages)

pnpm

Enforced (pnpm ≥ 11.0)

minimumReleaseAge env pnpm_config_minimum_release_age (minutes)

pnpm docs

ports

❌ None (FreeBSD ports)

pwsh-gallery

❌ None

scoop

🚧 Proposed

open feature request

ScoopInstaller/Scoop#6513

sdkman

❌ None

sfsu

🚧 Inherits from scoop

sheldon

❌ None (clones a Git branch or tag, no release feed)

slapt-get

❌ None (Slackware)

snap

➖ N/A (risk channels stable/candidate/beta/edge, plus snap refresh --hold up to 90 days)

snap refresh --hold

Snap docs

soar

❌ None (static-binary installer)

sorcery

❌ None (Source Mage, source-based)

steamcmd

❌ None

stew

❌ None

sun-tools

➖ N/A (legacy SVR4 tooling superseded by IPS)

Oracle: SVR4/IPS comparison

swupd

➖ N/A (Clear Linux discontinued July 2025)

Clear Linux OS

tazpkg

❌ None (SliTaz)

tlmgr

❌ None (TeX Live, curated)

topgrade

➖ N/A (meta-upgrader; delegates to each underlying manager)

urpmi

❌ None (Mageia)

uv, uvx

Enforced

exclude-newer env UV_EXCLUDE_NEWER

uv docs

vim-pack

❌ None

volta

➖ N/A (unmaintained since November 2025)

volta-cli/volta

vscode, vscodium

🚧 Proposed

proposed enterprise policy

microsoft/vscode#316867

winget

🚧 Proposed

open feature request

microsoft/winget-cli#6178

xbps

❌ None

yarn (Classic v1)

❌ None (project in maintenance mode)

yarnpkg/yarn

yarn-berry

➖ N/A (Yarn Berry removed global installs, so mpm implements no install / upgrade / remove to gate; the upstream gate, Berry ≥ 4.10, covers project installs)

npmMinimalAgeGate

Yarn settings

yay

Enforced (yay ≥ 13.0)

generated init.lua overlay via XDG_CONFIG_HOME (UpgradeSelect + AURPreInstall hooks)

Jguer/yay#2883

yum

➖ N/A (deprecated alias for dnf on RHEL-family)

zerobrew

❌ None

zim

❌ None (clones a Git branch or tag, no release feed)

zinit

❌ None

zplug

❌ None (clones a Git branch or tag, no release feed)

zypper

❌ None

Notes

  • brew ships internal release-age gates inside Homebrew’s bottle resource-resolution pipeline so formulae built from upstream resources get a delay automatically. Coverage expanded with Homebrew 6.0.0 to include npm and pip (Homebrew/brew#21919), PyPI (Homebrew/brew#21920), RubyGems (Homebrew/brew#22253), and Bundler (Homebrew/brew#22555). All four gate source dependencies fetched during a formula build; none expose a user-facing knob. The issue requesting one for brew install (Homebrew/brew#21129) was closed as not planned. The scope is distinct from mpm’s --cooldown, which gates the package version mpm asks brew to install: brew’s internal gate keeps build-time dependencies fresh-but-not-too-fresh, mpm’s gate delays the package version itself. Both can be in effect simultaneously without conflict.

  • pipx delegates to whichever pip lives inside its managed virtualenvs. If that pip predates 26.1 (or pipx routes resolution through uv instead), PIP_UPLOADED_PRIOR_TO is silently ignored. mpm has no clean way to inspect pipx’s internal resolver, so treat the pipx gate as best-effort.

  • yarn-berry: the gate works in Berry 4.10, but Yarn Berry removed yarn global, so mpm’s yarn-berry handler only implements search. Onboarding npmMinimalAgeGate would not change anything reachable through mpm.

  • apt, snap, fwupd all have structural delays (Debian’s migration windows, Snap risk channels, LVFS staged deployment) rather than per-version age gates. They’re marked N/A because the underlying ecosystem solves the problem in a different shape.

  • yay added user-programmable Lua hooks in v13. An UpgradeSelect hook can filter the upgrade set by AUR LastModified age (yay ships an example at doc/examples/recently_modified.lua), and the maintainer closed the dedicated minimum-release-age requests (Jguer/yay#2824, Jguer/yay#2848) in favor of it. mpm now drives that machinery for a per-run cooldown by overlaying a generated init.lua through a private XDG_CONFIG_HOME: the overlay chains the user’s real init.lua (so nothing in their config is lost) and registers two hooks keyed off the same now - cooldown cutoff (passed as MPM_COOLDOWN_EPOCH). UpgradeSelect holds back AUR upgrades younger than the floor on yay -Syu, and AURPreInstall aborts a fresh yay -S whose AUR base is younger than the floor, closing the gap UpgradeSelect alone left open (Jguer/yay#2883). This needs yay 13; an older yay stays a usable manager but cannot honor the cooldown.

  • The Arch AUR helpers (pacaur, paru) and most distro front-ends inherit whatever delay the underlying repository / AUR provides; none of them ship a dedicated cooldown setting.

Retraction paths by registry

A cooldown assumes something happens during the wait. If a compromised release can never be withdrawn, holding it for seven days only delays installing it by seven days: the version ages into eligibility unchanged. The safeguard’s value therefore rests on the registry behind each manager having a retraction path, and on that path being reachable by the resolver rather than merely advisory.

Retraction is a property of the registry, not of the manager CLI. pip, pipx, uv and uvx all resolve against PyPI; npm, pnpm and yarn all against the npm registry. The table below is keyed by registry rather than by manager, using the purl type mpm already maps to it (the PURL_MAP of meta_package_manager.specifier).

Two independent axes are tracked:

  • Retraction decides whether waiting achieves anything at all.

  • Publish date decides whether mpm could emulate a gate where no native one exists, which is what the advisory and block modes under Possible future directions are blocked on.

They really are independent. GitHub release assets expose a trustworthy server-set publication date and have no retraction path whatsoever, so a cooldown there is easy to compute and worth very little.

Retraction shapes, strongest first:

  • Yank: the artifact stays downloadable for an exact pin, but the resolver stops selecting it. The shape a cooldown wants.

  • Unpublish: the version is deleted outright. Effective, at the cost of breaking pinned installs.

  • Relabel: the artifact is moved off the default channel or label, and stays reachable only by asking for it explicitly.

  • Index revert: nothing is retracted at the artifact level. The curated index or channel pointer moves back off the bad version.

  • Flag only: the version is marked but still resolves, so a cooldown gains nothing from it.

  • None: no registry-side path, short of the upstream author deleting their own release.

A blank cell means no citable public source was found. As in the table above, absence of evidence is not recorded as absence of the capability.

Registry (purl type)

mpm managers

Retraction

Publish date

PyPI (pkg:pypi)

pip, pipx, uv, uvx

Yank (PEP 592): the file stays downloadable, but an installer “MUST ignore yanked releases, if the selection constraints can be satisfied with a non-yanked version”

upload_time_iso_8601, plus a per-file yanked flag (JSON API)

npm registry (pkg:npm)

npm, pnpm, yarn, yarn-berry

Unpublish within 72 h of publishing, then only with no dependents, under 300 weekly downloads and a single owner (policy). Past that, flag only: npm deprecate warns on install but does not stop resolution

time, mapping each version to its publication timestamp (packument)

crates.io (pkg:cargo)

cargo

Yank (cargo yank): “does not delete any data”, and “Cargo will not use a yanked version for any new project or checkout without a pre-existing lockfile”

created_at, plus yanked and yank_message (API)

RubyGems.org (pkg:gem)

gem

Unpublish: gem yank “permanently removes a gem you pushed to a server”, dropping it from the index

created_at and built_at, plus a yanked boolean (API)

Packagist (pkg:composer)

composer

Index revert: versions are fetched “from tags you create in your VCS repository” (about), so deleting the upstream tag withdraws the version. The package-level abandoned field is flag only (API)

✅ per-version time and published-time (metadata)

anaconda.org / conda-forge (pkg:conda)

conda

Relabel: “we do not allow edits or the deletion of packages on conda-forge”; a bad artifact is labelled broken and “Users will no longer be able to install them by default” (procedure)

upload_time, with the broken label carried in labels (API)

CPAN / PAUSE (pkg:cpan)

cpan

Unpublish: an author can delete a distribution from the mirrors, though “a secondary mirror network called BackPAN retains distributions even if they are deleted from CPAN” (CPAN)

AUR

yay, paru, pacaur, pikaur, trizen

None at the version level: an AUR package is a git repository with no per-version artifact to withdraw, so remediation is a maintainer push or deletion of the whole package

✅ server-set LastModified, the push timestamp mpm’s yay overlay gates on. Git commit dates are client-set (GIT_COMMITTER_DATE), forgeable, and never consulted

Homebrew taps

brew, cask

Index revert through the tap’s git history, over a package-level lifecycle: deprecate! still installs with a warning, disable! makes the action fail, removal deletes it (lifecycle)

❌ a formula records the upstream version, not its publication date

Distro binary archives (pkg:deb, pkg:rpm, pkg:alpm, pkg:apk)

apk, apt, apt-mint, deb-get, dkp-pacman, dnf, dnf5, eopkg, opkg, pacman, pkg, pkg-tools, pkgin, slapt-get, tazpkg, urpmi, xbps, yum, zypper

Index revert: removal is an archive operation and the mirror is rebuilt without the package. Debian, for one, requires filing an RM: bug against ftp.debian.org (developers-reference)

❌ the version string is the distro maintainer’s build, carrying no upstream publication date

Git-manifest indexes

chromebrew, fink, guix, krew, macports, nix, opam, scoop, sfsu, winget, zerobrew

Index revert: reverting the manifest, Portfile or derivation commit withdraws the version

❌ only the commit date, which is client-set and trivially backdated

Chocolatey Community Repository (pkg:chocolatey)

choco

Unlist: moderators can unlist individual package versions (list/unlist), and malicious scripts are grounds for banning the maintainer and deleting their packages (moderation)

PowerShell Gallery

pwsh-gallery

Yank under another name: the Gallery “does not support permanent deletion of packages”; an unlisted version leaves search and listings but “remains downloadable by specifying its exact version” (docs)

Snap Store

snap

Index revert on the channel pointer: “Promoting and demoting revisions is also a useful progression […] for when a snap needs to revert to an older version as a response to a security concern” (release management)

Flathub

flatpak

Relabel: end-of-life metadata delists the application, and the maintainer documentation describes no way to remove or roll back an individual build (maintenance)

VS Code Marketplace

vscode, vscodium

Unpublish, uniquely with client-side remediation: “the extension is removed from the Marketplace and added to a block list. If the extension has been installed, it’s automatically uninstalled by VS Code” (runtime security), against a public ledger of removals (RemovedPackages.md)

GitHub release assets

gh-ext, soar, stew, and the asdf / mise backends

None: withdrawing a build is the upstream author deleting their own release or tag. Nothing sits between them and the user

✅ server-set published_at on each release (REST API)

Upstream Git clones

antidote, antigen, fisher, lazy, oh-my-fish, sheldon, vim-pack, zim, zinit, zplug

None: a plugin is cloned straight from its upstream repository, so withdrawing a bad revision is its author force-pushing, retagging or deleting it themselves. No index sits in between

❌ a commit or tag date is set by the author and freely rewritable

Source-based recipe trees

cave, emerge, pacstall, ports, sorcery

Index revert of the recipe tree

❌ a recipe carries no publication date

LVFS

fwupd

Index revert over staged remotes: firmware moves through embargo, testing and stable, and “You should only move stable firmware to testing and stable after completing an end-to-end test with the embargo remote” (LVFS). No withdrawal from stable is documented

SDKMAN broker

sdkman

Unpublish: the vendor release endpoint “supports POST and DELETE HTTP methods” (vendor API)

Curated app stores and archives

mas, steamcmd, tlmgr

No registry of their own

apm, apt-cyg, pkcon, sun-tools, swupd, topgrade, volta

➖ Not applicable: these managers either delegate to whichever manager they front (pkcon, topgrade) or wrap a retired project or a discontinued platform

Cross-referencing against the support table: every manager mpm gates today, and nearly every one with a shipped or proposed upstream mechanism, sits on a registry in the yank, unpublish or relabel rows. The exception is mise, which shipped minimum_release_age while resolving against GitHub release assets that have no retraction path at all. So the retraction shape is a good predictor of where a cooldown pays off, and only a rough predictor of where one gets built.

Limitations

The transitive-dependency gap

mpm’s cooldown is exactly as good as the underlying resolver’s. For managers that install with a real dependency resolver (PyPI, npm, …), the native mechanism applies the cutoff to the whole tree, including transitive dependencies. For managers without a native mechanism, mpm cannot retrofit one without reimplementing the resolver: pinning only the top-level package would leave transitive dependencies fresh, which is precisely the most common attack vector. That is why unsupported managers are fail-closed rather than fail-open.

Coverage limits

Distro and system managers (apt, dnf, pacman, brew, …) generally have no per-upstream publish date attached to a package version: their version string is the distro maintainer’s package build, not the upstream release, and the threat model differs (curated repositories with their own staging and review). The concept does not cleanly map. These managers are listed in the support table as N/A.

The retraction premise

The cooldown buys time for someone else to act. Where the registry has no way to withdraw a bad release (Retraction paths by registry), that time buys nothing: the compromised version simply ages into eligibility.

In an ecosystem whose only remediation is publishing a fixed version, the gate can go further and invert. A malicious 1.2.3 lands at T, the maintainer ships a clean 1.2.4 at T+3, and a 7-day cooldown holds 1.2.4 back until T+10 while 1.2.3 clears the window at T+7. For those three days the gate steers the resolver onto the compromised version, which an ungated run would have skipped in favor of the fix. A yank closes that window, since the resolver drops 1.2.3 the moment it is withdrawn; the exposure is specific to the index revert, flag only and none rows of the retraction table.

mpm does not detect this case. It injects the cutoff and lets the resolver decide.

The cooldown is authoritative, not a floor

mpm computes the cutoff from the requested --cooldown alone and injects it. It never reads the standing policy a user may already have configured natively, and the injected variable outranks that configuration in every supported ecosystem: UV_EXCLUDE_NEWER beats both user-level and project-level exclude-newer, and npm_config_min-release-age beats .npmrc.

So a --cooldown shorter than the user’s own setting silently relaxes it for the duration of the run:

$ cat ~/.config/uv/uv.toml
exclude-newer = "7 days"

$ uv pip install --dry-run "click-extra>=8.8.1"
hint: `click-extra` was filtered by `exclude-newer` […] v8.8.1, published 2026-08-02

$ UV_EXCLUDE_NEWER="1 day" uv pip install --dry-run "click-extra>=8.8.1"
 + click-extra==8.8.1

The word “cooldown” invites reading the option as a safety minimum, so the surprise runs in the unsafe direction. This is a second fail-open path, distinct from the fail-closed treatment of unsupported managers described above: there, mpm refuses to act; here it acts with a weaker gate than the user asked the manager for.

Treating the option as a floor instead (taking the stricter of the two, or warning when --cooldown would relax a native setting) means reading each manager’s own configuration: uv.toml at two levels for uv, .npmrc at four for npm, each with its own format and precedence rules. That is the durable, manager-specific state the stateless gate deliberately forgoes, so for now the behavior is documented rather than changed. Users who rely on a strict standing policy should pass a --cooldown at least as long, or omit it and let the native setting apply untouched.

Read-only consistency

The outdated report is not filtered by the cooldown on unsupported managers, so it may list versions that the subsequent upgrade would skip. For supported managers the same environment variable also affects outdated, so the report and the upgrade stay consistent.

The stateless-gate trade-off

mpm reads each release’s current age at run time and injects the cutoff: it keeps no ledger of past observations. That is safe (a fresh push looks fresh and is held back) but coarse. A package that publishes faster than the cooldown is held back wholesale until its newest version ages in, whereas a stateful tool can still install the most recent version that has already cleared the window. Because mpm trusts the age reported at the moment of the run, it also cannot cross-check that age against earlier observations the way aur-cooldown’s persisted version-to-commit ledger does. Both capabilities need the durable state mpm deliberately forgoes.

Possible future directions

  • Detect pipx’s internal pip (or uv) at runtime. mpm’s pip manager has a hard >=26.1.0 floor, but pipx maintains its own virtualenvs whose pip may be older or whose resolution may be routed through uv (where the right env var is UV_EXCLUDE_NEWER instead of PIP_UPLOADED_PRIOR_TO). Probing the resolver per venv would let mpm refuse to advertise enforcement when the underlying pip is stale.

  • Per-package exemptions (--cooldown-exclude). The gate is currently all-or-nothing per run, so one legitimately-fresh package (a security fix, a package the user publishes themselves) forces the cooldown off for the whole tree. Both enforcing managers already expose the escape hatch natively: uv’s --exclude-newer-package takes a PACKAGE=DATE pair, npm’s min-release-age-exclude takes names or minimatch globs. The catch is that this would not fit the uniform cooldown_env_var injection: uv publishes no environment variable for --exclude-newer-package (only the plain --exclude-newer carries a UV_EXCLUDE_NEWER binding), so uv needs the exemption appended to the command line while npm can keep taking it through npm_config_min-release-age-exclude. Supporting it therefore means a per-manager hook alongside the env var rather than a one-line addition, which is worth knowing before the option is designed.

  • Onboard mechanisms as they ship upstream. Several managers have active work that would slot into the cooldown_env_var framework as a one-line addition once released: Composer (composer/composer#12692), Bundler / RubyGems (ruby/rubygems#9576), Cargo (stabilization of -Zmin-publish-age, rust-lang/cargo#17009), dnf5 (rpm-software-management/dnf5#2743), Scoop (ScoopInstaller/Scoop#6513), winget (microsoft/winget-cli#6178), VS Code (microsoft/vscode#316867).

  • Advisory mode for outdated on unsupported managers. mpm could query each package registry directly (PyPI, RubyGems, crates.io, …) to annotate outdated with a “safe latest” column: purely informational, no install-side enforcement. This avoids the transitive-resolution trap while still being useful. It requires a new HTTP client surface and a state directory for date caching, neither of which mpm has today.

  • Consult a curated compromise-window denylist. aur-cooldown pairs its age gate with aur-malware-check’s campaigns.json, denying only the versions pushed inside a dated compromise window rather than freezing a package by name. This is orthogonal to release age: a version can be old enough to clear the cooldown yet still sit inside a known-bad window. Wiring such a feed into mpm would lean on the same HTTP client and state directory the advisory mode above already calls for.

  • Block-mode for bundled-artifact managers (snap, flatpak, vscode, mas). These install self-contained artifacts with no separate transitive resolution at install time, so a “refuse if fresher than the cutoff” check would be sound without a resolver. The bottleneck is per-store API support for per-version publish dates.

Prior art