Files
pnpm/cspell.json
Zoltan Kochan 963861cac1 perf(npm-resolver): layer abbreviated meta + attestation before full metadata in the minimumReleaseAge gate (#11704)
Follow-up to #11691 — item 2 from #11687, plus a related shortcut.

## What

When the `minimumReleaseAge` lockfile verification gate needs to know when a version was published, it used to fetch a multi-MB full metadata document per package just to read one timestamp. This PR replaces that single-step path with a four-layer lookup that pays the cheapest viable source first:

1. **Abbreviated metadata's `modified` field** — the resolver already fetches this for resolution. If the package as a whole hasn't been modified within the policy cutoff, every version it contains is at least that old; return `modified` as a conservative upper-bound and skip the rest of the chain.
2. **Local `FULL_META_DIR` mirror** — exact per-version times if a previous verification populated it.
3. **npm attestation endpoint** (`/-/npm/v1/attestations/<name>@<version>`) — a tens-of-KB Sigstore bundle whose Rekor inclusion time stands in for publish time. Wins on cold cache when the package was published with provenance.
4. **Full metadata fetch** — last resort.

## Why

The verification cache from #11691 made repeat installs against an unchanged lockfile effectively free. The remaining cost is the *first* verification on a fresh CI runner with no restored cache — particularly `pnpm install --frozen-lockfile`, where every locked package's publish timestamp has to be confirmed. Fetching the full metadata document for each package is wasteful when:

- The resolver has usually already cached abbreviated metadata, whose `modified` field alone is enough to clear stable packages (the common case).
- For recently-modified packages, the per-version attestation endpoint is orders of magnitude smaller than full metadata.

## How

### Abbreviated `modified` shortcut

`fetchFullMetadataCached` is refactored to share an internal helper with a new `fetchAbbreviatedMetadataCached`. Both do conditional GETs against their respective on-disk mirrors. On a non-frozen install the abbreviated mirror is already populated by the resolver, so the shortcut hits the local cache at headers-only cost. On `--frozen-lockfile` the fetch is still cheaper than full metadata.

If `Date.parse(modified) < cutoff`, return `modified` — it's an upper bound on every version's publish time in this package, and the verifier's `published < cutoff` check passes trivially.

### Attestation endpoint

`fetchAttestationPublishedAt` (new module) hits `/-/npm/v1/attestations/<name>@<version>`, parses the response, and reads the earliest `bundle.verificationMaterial.tlogEntries[].integratedTime` across the attestation bundles. That's the Rekor inclusion time — a couple of seconds after publish, well within tolerance for a policy that operates in minutes/hours/days. Returns `undefined` on 404 / network error / malformed body so the caller falls back.

### Per-install dedup

The lookup carries a `PublishedAtLookupContext` with four memo maps: abbreviated meta per (registry, name), local full meta per (registry, name), full meta network fetch per (registry, name), final published-at per (registry, name, version). Verifying many versions of the same package only pays the disk/network costs once.

## Trust model

**No Sigstore signature verification on the attestation path.** The trust model is identical to reading the registry's `time` field on the full metadata document — we already trust the registry to serve correct publish timestamps for the gate's purpose. The win is purely bandwidth.

Full Sigstore verification (Fulcio cert chain + Rekor inclusion proof) would harden the timestamp against a compromised registry. It pulls in the `sigstore` npm package and the TUF root — a separate dependency-surface discussion, parked as future work.

## Tests

- **13 unit tests** in `resolving/npm-resolver/test/fetchAttestationPublishedAt.test.ts`: ISO timestamp extraction, URL construction (scoped + unscoped), 404 / 5xx / network error / malformed JSON / missing fields → undefined, earliest-of-multiple-attestations, defensive number-as-integratedTime, auth header forwarding, trailing-slash normalization.
- Existing `minimumReleaseAge` + `verifyLockfileResolutions` integration suites (45 tests) still pass — the fallback chain preserves end-to-end behavior when the new shortcuts don't apply.
2026-05-17 15:54:01 +02:00

381 lines
5.8 KiB
JSON

{
"words": [
"adduser",
"adipiscing",
"agentkeepalive",
"agentkeepalive's",
"amet",
"andreineculau",
"appdata",
"applyq",
"archy",
"argumentless",
"armv",
"autocheckpoint",
"autocompleting",
"autofix",
"autofixed",
"autoinstalled",
"autozoom",
"babek",
"badheaders",
"behaviour",
"blabla",
"Bluesky",
"brasileiro",
"bryntum",
"buildx",
"cafile",
"cafs",
"camelcase",
"canonicalizer",
"cantopen",
"canva",
"cerbos",
"certfile",
"clonedeep",
"cmds",
"codeload",
"codenames",
"codesign",
"colorterm",
"comver",
"copyfiles",
"corejs",
"corepack",
"corge",
"cowsay",
"Creds",
"cryptiles",
"cves",
"cwsay",
"cyclonedx",
"deburr",
"dedup",
"denoland",
"denolib",
"deptype",
"desugared",
"desugars",
"devextreme",
"devowl",
"dgimuvys",
"didyoumean",
"dirtyforms",
"diskusage",
"dislink",
"dpkg",
"drivelist",
"duplexify",
"eagain",
"ebadplatform",
"ebusy",
"eexist",
"ehrkoext",
"eintegrity",
"eisdir",
"elifecycle",
"elit",
"embedder",
"emfile",
"enametoolong",
"endregion",
"eneedauth",
"enoent",
"enotempty",
"enten",
"eotp",
"eperm",
"epipe",
"errcode",
"esac",
"etamponi",
"exdev",
"execa",
"exploitability",
"fakehash",
"fellback",
"fetchings",
"filenamify",
"filesystem",
"filesystems",
"fnumber",
"foobarqar",
"foofoo",
"footgun",
"forgejo",
"fsevents",
"gabor",
"garply",
"gcttmf",
"getattr",
"ghes",
"ghsa",
"ghsas",
"gitea",
"globalconfig",
"globstar",
"gpgsign",
"grault",
"gruntfile",
"gwhitney",
"haptics",
"hardlink",
"hardlinked",
"hardlinking",
"hardlinks",
"hashbang",
"highmaps",
"hikljmi",
"hoistable",
"homepath",
"hosters",
"hyperdrive",
"idempotency",
"imagetools",
"imurmurhash",
"ionicons",
"isexe",
"istvan",
"italiano",
"jega",
"jhcg",
"jnbpamcxayl",
"kebabcase",
"kevva",
"keyfile",
"keyid",
"keytype",
"killcb",
"kochan",
"koorchik",
"ldid",
"ldni",
"leniolabs",
"libc",
"libnpmpublish",
"libnpx",
"libzip",
"licence",
"licences",
"lifecycles",
"linuxstatic",
"localappdata",
"lockfiles",
"loglevel",
"logstream",
"longlink",
"longpaths",
"luca",
"martensson",
"maxtimeout",
"mdast",
"metafile",
"millis",
"mintimeout",
"mmap",
"monorepolint",
"moonrepo",
"mountpoint",
"msgpack",
"msgpackr",
"msvc",
"msys",
"mycomp",
"mycompany",
"myorg",
"mypackage",
"mytoken",
"ndjson",
"nerfed",
"newversion",
"nistp",
"NOASSERTION",
"nodetouch",
"noent",
"nonexec",
"noninjected",
"nonvulnerable",
"nopadding",
"noproxy",
"nosystem",
"nothrow",
"npmcli",
"npmignore",
"npmjs",
"npmx",
"ntfs",
"nushell",
"ofjergrg",
"onclickoutside",
"oomol",
"ossl",
"outfile",
"overrider",
"packlist",
"packr",
"packument",
"pacquet",
"paralleljs",
"parallelly",
"parseable",
"partialmatch",
"pathext",
"pegjs",
"pidtree",
"pify",
"pkgname",
"pkgs",
"plotly",
"plugh",
"pnpmfile",
"pnpmfiles",
"pnpmjs",
"pnpmrc",
"pnpmtest",
"polyfilling",
"português",
"posix",
"postbuild",
"postfoo",
"postpack",
"postprepare",
"postpublish",
"postrestart",
"postshrinkwrap",
"poststart",
"poststop",
"posttest",
"postuninstall",
"postversion",
"preact",
"prefoo",
"prefs",
"preinstall",
"premajor",
"preminor",
"prepatch",
"prepublish",
"prereleases",
"prerestart",
"preshrinkwrap",
"prestart",
"prestop",
"preuninstall",
"preversion",
"prioritizer",
"promisified",
"proxied",
"pwsh",
"qrcode",
"quux",
"rcompare",
"redownload",
"refclone",
"reflattened",
"reflink",
"reflinked",
"reflinks",
"rehoist",
"reimagining",
"reka",
"Rekor",
"relinks",
"renderable",
"replit",
"reqheaders",
"rimrafed",
"rmgr",
"rpmdevtools",
"rpmlint",
"rstacruz",
"rushstack",
"safecrlf",
"scopeless",
"sdiff",
"searchexclude",
"searchlimit",
"searchopts",
"searchstaleness",
"sels",
"semistrict",
"serp",
"serverjs",
"shasums",
"sheetjs",
"shlex",
"sigstore",
"sindresorhus",
"sirv",
"SLSA",
"soporan",
"sopts",
"spdxdocs",
"SPDXID",
"srcset",
"ssri",
"stackblitz",
"stacktracey",
"stdtype",
"streamsearch",
"stringifying",
"subdep",
"subdependencies",
"subdependency",
"subdeps",
"subdir",
"subdirs",
"subpkg",
"subresource",
"supercede",
"syml",
"syncer",
"syscall",
"syscalls",
"szia",
"tabtab",
"taffydb",
"tarballtemplate",
"teambit",
"tempy",
"testcase",
"tlog",
"TLSV",
"toctou",
"todomvc",
"toplevel",
"tsgo",
"tsparticles",
"typecheck",
"unallowed",
"undeprecate",
"underperformance",
"undollar",
"unextractable",
"uninstallation",
"unnest",
"unreviewed",
"unskip",
"unstar",
"usecase",
"userconfig",
"userprofile",
"ustar",
"uuidv",
"valign",
"vuln",
"webauth",
"webcontainer",
"winst",
"workleap",
"worktree",
"worktrees",
"wrappy",
"xmarw",
"yazl",
"zkochan",
"zoli",
"zoltan"
]
}