@pnpm/crypto.shasums-file 1100.1.5 → 1100.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/lib/diskCache.d.ts +59 -0
- package/lib/diskCache.js +196 -0
- package/lib/index.d.ts +28 -2
- package/lib/index.js +47 -2
- package/lib/verifyNodeShasums.d.ts +16 -0
- package/lib/verifyNodeShasums.js +24 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @pnpm/crypto.shasums-file
|
|
2
2
|
|
|
3
|
+
## 1100.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Resolving a Node.js runtime version (`devEngines.runtime` / `runtime:` specifiers) is now much faster: the per-version release metadata is cached in the pnpm cache directory after its signature is verified, and an exact stable version such as `runtime:22.23.2` no longer downloads the Node.js release index. A pinned runtime whose metadata was fetched once resolves without any network access, which removes the noticeable delay on the first `node` invocation in a project pinning an already-downloaded runtime [#13899](https://github.com/pnpm/pnpm/issues/13899).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies:
|
|
12
|
+
- @pnpm/error@1100.1.2
|
|
13
|
+
|
|
3
14
|
## 1100.1.5
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk cache for per-version runtime `SHASUMS256.txt` bodies.
|
|
3
|
+
*
|
|
4
|
+
* A release's SHASUMS file lives under a version-pinned URL
|
|
5
|
+
* (`.../v22.0.0/SHASUMS256.txt`), so its content is immutable: a body fetched
|
|
6
|
+
* once — and, for signed channels, verified once — never needs to be fetched
|
|
7
|
+
* again. Entries live under
|
|
8
|
+
* `<cacheDir>/v11/runtime-shasums/<trust>/<host>/<url path>` so clearing the
|
|
9
|
+
* cache directory clears them together with the registry metadata mirror. The
|
|
10
|
+
* layout is shared with pacquet, which reads and writes the same files.
|
|
11
|
+
*
|
|
12
|
+
* Only hand immutable URLs to this cache. The cache directory is
|
|
13
|
+
* *project-configurable* (`cacheDir`), so entries under it must never carry
|
|
14
|
+
* more authority than the project that could have written them:
|
|
15
|
+
*
|
|
16
|
+
* - Signed-channel readers persist the detached signature next to the body
|
|
17
|
+
* and re-verify it against the embedded release keys on every read (see
|
|
18
|
+
* `fetchVerifiedNodeShasumsFileCached`), so a pre-seeded entry is only
|
|
19
|
+
* accepted if it is a genuine release body.
|
|
20
|
+
* - Unverified entries are trusted on read, which grants a project nothing
|
|
21
|
+
* new: the unsigned channels' mirrors are already project-configurable
|
|
22
|
+
* (their bodies were project-controllable before the cache existed), and
|
|
23
|
+
* the musl list's download URLs are derived from the hardcoded
|
|
24
|
+
* unofficial-builds base, never from the cached body.
|
|
25
|
+
*
|
|
26
|
+
* The `<trust>` path segment keeps the two classes in disjoint subtrees so
|
|
27
|
+
* their read policies cannot be confused.
|
|
28
|
+
*/
|
|
29
|
+
export declare const RUNTIME_SHASUMS_DIR = "v11/runtime-shasums";
|
|
30
|
+
/**
|
|
31
|
+
* How the body of a cache entry was authenticated before it was written.
|
|
32
|
+
* Each class caches into its own subtree.
|
|
33
|
+
*/
|
|
34
|
+
export type ShasumsTrust = 'verified' | 'unverified';
|
|
35
|
+
export interface ShasumsCacheOpts {
|
|
36
|
+
cacheDir?: string;
|
|
37
|
+
trust: ShasumsTrust;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The cached body for `url` as UTF-8 text, via {@link readCachedBytes}.
|
|
41
|
+
* Invalid UTF-8 is a miss — a lossy decode would turn a corrupted entry
|
|
42
|
+
* into malformed rows instead of a refetch.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readCachedShasums(url: string, opts: ShasumsCacheOpts): Promise<string | undefined>;
|
|
45
|
+
/**
|
|
46
|
+
* The cached bytes for `url`, or `undefined` on any miss — a URL the mapping
|
|
47
|
+
* cannot represent, a missing or non-regular file, unreadable content, an
|
|
48
|
+
* empty file (never a valid entry, so it only signals a torn write), or a
|
|
49
|
+
* file over {@link MAX_CACHED_SHASUMS_LEN}.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readCachedBytes(url: string, opts: ShasumsCacheOpts): Promise<Buffer | undefined>;
|
|
52
|
+
/**
|
|
53
|
+
* Best-effort write of `body` for `url`: a cache-write failure only costs a
|
|
54
|
+
* refetch on the next resolve, so errors are deliberately dropped rather than
|
|
55
|
+
* failing the resolution that produced the body. The exclusively-created temp
|
|
56
|
+
* file + rename keeps concurrent writers (two installs resolving the same
|
|
57
|
+
* version) from exposing a torn body.
|
|
58
|
+
*/
|
|
59
|
+
export declare function writeCachedShasums(url: string, body: string | Uint8Array, opts: ShasumsCacheOpts): Promise<void>;
|
package/lib/diskCache.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { threadId } from 'node:worker_threads';
|
|
4
|
+
/**
|
|
5
|
+
* On-disk cache for per-version runtime `SHASUMS256.txt` bodies.
|
|
6
|
+
*
|
|
7
|
+
* A release's SHASUMS file lives under a version-pinned URL
|
|
8
|
+
* (`.../v22.0.0/SHASUMS256.txt`), so its content is immutable: a body fetched
|
|
9
|
+
* once — and, for signed channels, verified once — never needs to be fetched
|
|
10
|
+
* again. Entries live under
|
|
11
|
+
* `<cacheDir>/v11/runtime-shasums/<trust>/<host>/<url path>` so clearing the
|
|
12
|
+
* cache directory clears them together with the registry metadata mirror. The
|
|
13
|
+
* layout is shared with pacquet, which reads and writes the same files.
|
|
14
|
+
*
|
|
15
|
+
* Only hand immutable URLs to this cache. The cache directory is
|
|
16
|
+
* *project-configurable* (`cacheDir`), so entries under it must never carry
|
|
17
|
+
* more authority than the project that could have written them:
|
|
18
|
+
*
|
|
19
|
+
* - Signed-channel readers persist the detached signature next to the body
|
|
20
|
+
* and re-verify it against the embedded release keys on every read (see
|
|
21
|
+
* `fetchVerifiedNodeShasumsFileCached`), so a pre-seeded entry is only
|
|
22
|
+
* accepted if it is a genuine release body.
|
|
23
|
+
* - Unverified entries are trusted on read, which grants a project nothing
|
|
24
|
+
* new: the unsigned channels' mirrors are already project-configurable
|
|
25
|
+
* (their bodies were project-controllable before the cache existed), and
|
|
26
|
+
* the musl list's download URLs are derived from the hardcoded
|
|
27
|
+
* unofficial-builds base, never from the cached body.
|
|
28
|
+
*
|
|
29
|
+
* The `<trust>` path segment keeps the two classes in disjoint subtrees so
|
|
30
|
+
* their read policies cannot be confused.
|
|
31
|
+
*/
|
|
32
|
+
export const RUNTIME_SHASUMS_DIR = 'v11/runtime-shasums';
|
|
33
|
+
/**
|
|
34
|
+
* Upper bound on a cache entry's size. Real SHASUMS bodies are a few
|
|
35
|
+
* kilobytes; anything past this bound is not a release asset list and is
|
|
36
|
+
* never read into memory or written.
|
|
37
|
+
*/
|
|
38
|
+
const MAX_CACHED_SHASUMS_LEN = 1024 * 1024;
|
|
39
|
+
/**
|
|
40
|
+
* The cached body for `url` as UTF-8 text, via {@link readCachedBytes}.
|
|
41
|
+
* Invalid UTF-8 is a miss — a lossy decode would turn a corrupted entry
|
|
42
|
+
* into malformed rows instead of a refetch.
|
|
43
|
+
*/
|
|
44
|
+
export async function readCachedShasums(url, opts) {
|
|
45
|
+
const bytes = await readCachedBytes(url, opts);
|
|
46
|
+
if (bytes == null)
|
|
47
|
+
return undefined;
|
|
48
|
+
try {
|
|
49
|
+
return strictUtf8Decoder.decode(bytes);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const strictUtf8Decoder = new TextDecoder('utf8', { fatal: true });
|
|
56
|
+
/**
|
|
57
|
+
* The cached bytes for `url`, or `undefined` on any miss — a URL the mapping
|
|
58
|
+
* cannot represent, a missing or non-regular file, unreadable content, an
|
|
59
|
+
* empty file (never a valid entry, so it only signals a torn write), or a
|
|
60
|
+
* file over {@link MAX_CACHED_SHASUMS_LEN}.
|
|
61
|
+
*/
|
|
62
|
+
export async function readCachedBytes(url, opts) {
|
|
63
|
+
if (opts.cacheDir == null)
|
|
64
|
+
return undefined;
|
|
65
|
+
const filePath = shasumsCachePath(opts.cacheDir, opts.trust, url);
|
|
66
|
+
if (filePath == null)
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
// A FIFO planted at the entry path would block a plain open until a
|
|
70
|
+
// writer appears; O_NONBLOCK (a no-op for regular files, absent on
|
|
71
|
+
// Windows, whose directory entries cannot be named pipes) makes the open
|
|
72
|
+
// return immediately.
|
|
73
|
+
const file = await fs.promises.open(filePath, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
|
|
74
|
+
try {
|
|
75
|
+
// Checked on the opened handle, so nothing can swap the regular file
|
|
76
|
+
// for a special one between check and read.
|
|
77
|
+
if (!(await file.stat()).isFile())
|
|
78
|
+
return undefined;
|
|
79
|
+
// A bounded read rather than a stat check keeps the cap race-free: at
|
|
80
|
+
// most one byte past the bound is ever read, whatever the file's size
|
|
81
|
+
// becomes between open and read.
|
|
82
|
+
const buffer = Buffer.allocUnsafe(MAX_CACHED_SHASUMS_LEN + 1);
|
|
83
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
84
|
+
if (bytesRead === 0 || bytesRead > MAX_CACHED_SHASUMS_LEN)
|
|
85
|
+
return undefined;
|
|
86
|
+
return buffer.subarray(0, bytesRead);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await file.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Best-effort write of `body` for `url`: a cache-write failure only costs a
|
|
98
|
+
* refetch on the next resolve, so errors are deliberately dropped rather than
|
|
99
|
+
* failing the resolution that produced the body. The exclusively-created temp
|
|
100
|
+
* file + rename keeps concurrent writers (two installs resolving the same
|
|
101
|
+
* version) from exposing a torn body.
|
|
102
|
+
*/
|
|
103
|
+
export async function writeCachedShasums(url, body, opts) {
|
|
104
|
+
if (opts.cacheDir == null)
|
|
105
|
+
return;
|
|
106
|
+
if (Buffer.byteLength(body) > MAX_CACHED_SHASUMS_LEN)
|
|
107
|
+
return;
|
|
108
|
+
const filePath = shasumsCachePath(opts.cacheDir, opts.trust, url);
|
|
109
|
+
if (filePath == null)
|
|
110
|
+
return;
|
|
111
|
+
// The process id alone does not make the temp name unique (worker threads
|
|
112
|
+
// share it), so the thread id and a counter join it. The `wx` flag refuses
|
|
113
|
+
// to open a path that already exists — a colliding writer or a pre-seeded
|
|
114
|
+
// symlink fails the open instead of being followed — and any failure just
|
|
115
|
+
// skips the write.
|
|
116
|
+
const tempPath = `${filePath}.tmp-${process.pid.toString()}-${threadId.toString()}-${(tempCounter++).toString()}`;
|
|
117
|
+
try {
|
|
118
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
119
|
+
// Sync before the rename so a crash cannot persist the renamed name
|
|
120
|
+
// pointing at partially-written content — a torn SHASUMS prefix still
|
|
121
|
+
// parses and would otherwise be served (missing platform rows) until the
|
|
122
|
+
// cache is cleared.
|
|
123
|
+
const tempFile = await fs.promises.open(tempPath, 'wx');
|
|
124
|
+
try {
|
|
125
|
+
await tempFile.writeFile(body);
|
|
126
|
+
await tempFile.sync();
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await tempFile.close();
|
|
130
|
+
}
|
|
131
|
+
await fs.promises.rename(tempPath, filePath);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
try {
|
|
135
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
let tempCounter = 0;
|
|
141
|
+
/**
|
|
142
|
+
* The cache file backing `url`, or `undefined` when the URL has a shape the
|
|
143
|
+
* path mapping does not cover (non-HTTP scheme, embedded credentials, query
|
|
144
|
+
* string, empty or dot-only path segments). Returning `undefined` just
|
|
145
|
+
* disables caching for that URL.
|
|
146
|
+
*/
|
|
147
|
+
function shasumsCachePath(cacheDir, trust, url) {
|
|
148
|
+
let rest;
|
|
149
|
+
if (url.startsWith('https://')) {
|
|
150
|
+
rest = url.substring('https://'.length);
|
|
151
|
+
}
|
|
152
|
+
else if (url.startsWith('http://')) {
|
|
153
|
+
rest = url.substring('http://'.length);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
if (/[?#@]/.test(rest))
|
|
159
|
+
return undefined;
|
|
160
|
+
const firstSlash = rest.indexOf('/');
|
|
161
|
+
if (firstSlash <= 0)
|
|
162
|
+
return undefined;
|
|
163
|
+
// `:` (a port separator) is not portable in file names; `+` is the same
|
|
164
|
+
// encoding the registry metadata mirror uses for it.
|
|
165
|
+
const host = rest.substring(0, firstSlash).toLowerCase().replaceAll(':', '+');
|
|
166
|
+
const parts = [encodePathSegment(host)];
|
|
167
|
+
for (const segment of rest.substring(firstSlash + 1).split('/')) {
|
|
168
|
+
if (!segment || segment === '.' || segment === '..')
|
|
169
|
+
return undefined;
|
|
170
|
+
parts.push(encodePathSegment(segment));
|
|
171
|
+
}
|
|
172
|
+
if (parts.some((part) => part == null))
|
|
173
|
+
return undefined;
|
|
174
|
+
return path.join(cacheDir, RUNTIME_SHASUMS_DIR, trust, ...parts);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Percent-encode the bytes of `segment` that are not portable across
|
|
178
|
+
* filesystems, keeping `[A-Za-z0-9._+-]` as-is. pacquet applies the same
|
|
179
|
+
* encoding, so both tools address one file.
|
|
180
|
+
*/
|
|
181
|
+
function encodePathSegment(segment) {
|
|
182
|
+
if (segment.length > 200)
|
|
183
|
+
return undefined;
|
|
184
|
+
let encoded = '';
|
|
185
|
+
for (const byte of Buffer.from(segment)) {
|
|
186
|
+
const char = String.fromCharCode(byte);
|
|
187
|
+
if (/[\w.+-]/.test(char)) {
|
|
188
|
+
encoded += char;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
encoded += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return encoded;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=diskCache.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FetchFromRegistry } from '@pnpm/fetching.types';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { RUNTIME_SHASUMS_DIR } from './diskCache.js';
|
|
3
|
+
import { type ArmoredKey, fetchVerifiedNodeShasums } from './verifyNodeShasums.js';
|
|
4
|
+
export { fetchVerifiedNodeShasums, RUNTIME_SHASUMS_DIR };
|
|
4
5
|
export interface ShasumsFileItem {
|
|
5
6
|
integrity: string;
|
|
6
7
|
fileName: string;
|
|
@@ -13,6 +14,31 @@ export declare function fetchShasumsFile(fetch: FetchFromRegistry, shasumsUrl: s
|
|
|
13
14
|
* fetched from a repository-configurable Node.js mirror.
|
|
14
15
|
*/
|
|
15
16
|
export declare function fetchVerifiedNodeShasumsFile(fetch: FetchFromRegistry, shasumsUrl: string): Promise<ShasumsFileItem[]>;
|
|
17
|
+
export interface FetchShasumsFileCachedOpts {
|
|
18
|
+
cacheDir?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface FetchVerifiedNodeShasumsFileCachedOpts extends FetchShasumsFileCachedOpts {
|
|
21
|
+
trustedKeys?: readonly ArmoredKey[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Like {@link fetchVerifiedNodeShasumsFile}, backed by the disk cache when
|
|
25
|
+
* `opts.cacheDir` is given. The cache stores the body together with its
|
|
26
|
+
* detached signature, and a cache hit re-verifies that signature against the
|
|
27
|
+
* embedded release keys: the cache directory is project-configurable, so a
|
|
28
|
+
* pre-seeded entry must prove it is a genuine release body before it is
|
|
29
|
+
* served. Any verification failure is a miss and the pair is refetched.
|
|
30
|
+
* `shasumsUrl` must be version-pinned — a mutable URL must never be handed to
|
|
31
|
+
* the cache.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fetchVerifiedNodeShasumsFileCached(fetch: FetchFromRegistry, shasumsUrl: string, opts?: FetchVerifiedNodeShasumsFileCachedOpts): Promise<ShasumsFileItem[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Like {@link fetchShasumsFile}, backed by the disk cache when `opts.cacheDir`
|
|
36
|
+
* is given. For mirrors whose SHASUMS files carry no verifiable signature the
|
|
37
|
+
* cached body is trusted exactly as far as the TLS fetch that produced it.
|
|
38
|
+
* `shasumsUrl` must be version-pinned — a mutable URL must never be handed to
|
|
39
|
+
* the cache.
|
|
40
|
+
*/
|
|
41
|
+
export declare function fetchShasumsFileCached(fetch: FetchFromRegistry, shasumsUrl: string, opts?: FetchShasumsFileCachedOpts): Promise<ShasumsFileItem[]>;
|
|
16
42
|
export declare function parseShasumsFile(shasumsFileContent: string): ShasumsFileItem[];
|
|
17
43
|
export declare function fetchShasumsFileRaw(fetch: FetchFromRegistry, shasumsUrl: string): Promise<string>;
|
|
18
44
|
export declare function pickFileChecksumFromShasumsFile(body: string, fileName: string): string;
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PnpmError } from '@pnpm/error';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { readCachedBytes, readCachedShasums, RUNTIME_SHASUMS_DIR, writeCachedShasums } from './diskCache.js';
|
|
3
|
+
import { fetchVerifiedNodeShasums, fetchVerifiedNodeShasumsWithSignature, nodeShasumsSignatureVerifies, } from './verifyNodeShasums.js';
|
|
4
|
+
export { fetchVerifiedNodeShasums, RUNTIME_SHASUMS_DIR };
|
|
4
5
|
export async function fetchShasumsFile(fetch, shasumsUrl) {
|
|
5
6
|
return parseShasumsFile(await fetchShasumsFileRaw(fetch, shasumsUrl));
|
|
6
7
|
}
|
|
@@ -13,6 +14,50 @@ export async function fetchShasumsFile(fetch, shasumsUrl) {
|
|
|
13
14
|
export async function fetchVerifiedNodeShasumsFile(fetch, shasumsUrl) {
|
|
14
15
|
return parseShasumsFile(await fetchVerifiedNodeShasums(fetch, shasumsUrl));
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Like {@link fetchVerifiedNodeShasumsFile}, backed by the disk cache when
|
|
19
|
+
* `opts.cacheDir` is given. The cache stores the body together with its
|
|
20
|
+
* detached signature, and a cache hit re-verifies that signature against the
|
|
21
|
+
* embedded release keys: the cache directory is project-configurable, so a
|
|
22
|
+
* pre-seeded entry must prove it is a genuine release body before it is
|
|
23
|
+
* served. Any verification failure is a miss and the pair is refetched.
|
|
24
|
+
* `shasumsUrl` must be version-pinned — a mutable URL must never be handed to
|
|
25
|
+
* the cache.
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchVerifiedNodeShasumsFileCached(fetch, shasumsUrl, opts) {
|
|
28
|
+
const cacheOpts = { cacheDir: opts?.cacheDir, trust: 'verified' };
|
|
29
|
+
const signatureUrl = `${shasumsUrl}.sig`;
|
|
30
|
+
const [cachedBody, cachedSignature] = await Promise.all([
|
|
31
|
+
readCachedShasums(shasumsUrl, cacheOpts),
|
|
32
|
+
readCachedBytes(signatureUrl, cacheOpts),
|
|
33
|
+
]);
|
|
34
|
+
if (cachedBody != null && cachedSignature != null &&
|
|
35
|
+
await nodeShasumsSignatureVerifies(Buffer.from(cachedBody, 'utf8'), cachedSignature, opts?.trustedKeys)) {
|
|
36
|
+
return parseShasumsFile(cachedBody);
|
|
37
|
+
}
|
|
38
|
+
const { body, signature } = await fetchVerifiedNodeShasumsWithSignature(fetch, shasumsUrl, opts?.trustedKeys);
|
|
39
|
+
await Promise.all([
|
|
40
|
+
writeCachedShasums(shasumsUrl, body, cacheOpts),
|
|
41
|
+
writeCachedShasums(signatureUrl, signature, cacheOpts),
|
|
42
|
+
]);
|
|
43
|
+
return parseShasumsFile(body);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Like {@link fetchShasumsFile}, backed by the disk cache when `opts.cacheDir`
|
|
47
|
+
* is given. For mirrors whose SHASUMS files carry no verifiable signature the
|
|
48
|
+
* cached body is trusted exactly as far as the TLS fetch that produced it.
|
|
49
|
+
* `shasumsUrl` must be version-pinned — a mutable URL must never be handed to
|
|
50
|
+
* the cache.
|
|
51
|
+
*/
|
|
52
|
+
export async function fetchShasumsFileCached(fetch, shasumsUrl, opts) {
|
|
53
|
+
const cacheOpts = { cacheDir: opts?.cacheDir, trust: 'unverified' };
|
|
54
|
+
const cached = await readCachedShasums(shasumsUrl, cacheOpts);
|
|
55
|
+
if (cached != null)
|
|
56
|
+
return parseShasumsFile(cached);
|
|
57
|
+
const body = await fetchShasumsFileRaw(fetch, shasumsUrl);
|
|
58
|
+
await writeCachedShasums(shasumsUrl, body, cacheOpts);
|
|
59
|
+
return parseShasumsFile(body);
|
|
60
|
+
}
|
|
16
61
|
export function parseShasumsFile(shasumsFileContent) {
|
|
17
62
|
const lines = shasumsFileContent.split('\n');
|
|
18
63
|
const items = [];
|
|
@@ -22,3 +22,19 @@ export interface ArmoredKey {
|
|
|
22
22
|
* Throws when the signature is missing or does not verify against a trusted key.
|
|
23
23
|
*/
|
|
24
24
|
export declare function fetchVerifiedNodeShasums(fetch: FetchFromRegistry, shasumsUrl: string, trustedKeys?: readonly ArmoredKey[]): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* {@link fetchVerifiedNodeShasums}, additionally returning the verified
|
|
27
|
+
* detached signature so the disk cache can persist it as the entry's
|
|
28
|
+
* verification evidence.
|
|
29
|
+
*/
|
|
30
|
+
export declare function fetchVerifiedNodeShasumsWithSignature(fetch: FetchFromRegistry, shasumsUrl: string, trustedKeys?: readonly ArmoredKey[]): Promise<{
|
|
31
|
+
body: string;
|
|
32
|
+
signature: Uint8Array;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Whether `signatureBytes` is a valid detached signature of `content` under
|
|
36
|
+
* the trusted keys, reporting any unreadable input as `false` rather than
|
|
37
|
+
* throwing. This is the read-side check for persisted cache evidence, where
|
|
38
|
+
* any failure just means a cache miss.
|
|
39
|
+
*/
|
|
40
|
+
export declare function nodeShasumsSignatureVerifies(content: Uint8Array, signatureBytes: Uint8Array, trustedKeys?: readonly ArmoredKey[]): Promise<boolean>;
|
package/lib/verifyNodeShasums.js
CHANGED
|
@@ -34,6 +34,15 @@ async function readSigningKeyPackets(trustedKeys) {
|
|
|
34
34
|
* Throws when the signature is missing or does not verify against a trusted key.
|
|
35
35
|
*/
|
|
36
36
|
export async function fetchVerifiedNodeShasums(fetch, shasumsUrl, trustedKeys = NODE_RELEASE_KEYS) {
|
|
37
|
+
const { body } = await fetchVerifiedNodeShasumsWithSignature(fetch, shasumsUrl, trustedKeys);
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* {@link fetchVerifiedNodeShasums}, additionally returning the verified
|
|
42
|
+
* detached signature so the disk cache can persist it as the entry's
|
|
43
|
+
* verification evidence.
|
|
44
|
+
*/
|
|
45
|
+
export async function fetchVerifiedNodeShasumsWithSignature(fetch, shasumsUrl, trustedKeys = NODE_RELEASE_KEYS) {
|
|
37
46
|
const [shasumsBytes, signatureBytes] = await Promise.all([
|
|
38
47
|
fetchBytes(fetch, shasumsUrl, 'SHASUMS256.txt'),
|
|
39
48
|
fetchBytes(fetch, `${shasumsUrl}.sig`, 'SHASUMS256.txt.sig'),
|
|
@@ -42,7 +51,21 @@ export async function fetchVerifiedNodeShasums(fetch, shasumsUrl, trustedKeys =
|
|
|
42
51
|
throw new PnpmError('NODE_SHASUMS_SIGNATURE_INVALID', `The OpenPGP signature of ${shasumsUrl} does not match any trusted Node.js release key. ` +
|
|
43
52
|
'The downloaded Node.js runtime cannot be verified as a genuine release.');
|
|
44
53
|
}
|
|
45
|
-
return Buffer.from(shasumsBytes).toString('utf8');
|
|
54
|
+
return { body: Buffer.from(shasumsBytes).toString('utf8'), signature: signatureBytes };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether `signatureBytes` is a valid detached signature of `content` under
|
|
58
|
+
* the trusted keys, reporting any unreadable input as `false` rather than
|
|
59
|
+
* throwing. This is the read-side check for persisted cache evidence, where
|
|
60
|
+
* any failure just means a cache miss.
|
|
61
|
+
*/
|
|
62
|
+
export async function nodeShasumsSignatureVerifies(content, signatureBytes, trustedKeys = NODE_RELEASE_KEYS) {
|
|
63
|
+
try {
|
|
64
|
+
return await isSignedByTrustedKey(content, signatureBytes, trustedKeys);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
46
69
|
}
|
|
47
70
|
async function isSignedByTrustedKey(content, signatureBytes, trustedKeys) {
|
|
48
71
|
let signature;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/crypto.shasums-file",
|
|
3
|
-
"version": "1100.
|
|
3
|
+
"version": "1100.2.0",
|
|
4
4
|
"description": "Utils for working with shasums files",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@pnpm/crypto.hash": "1100.0.2",
|
|
33
|
-
"@pnpm/error": "1100.1.
|
|
33
|
+
"@pnpm/error": "1100.1.2",
|
|
34
34
|
"@pnpm/fetching.types": "1100.0.3",
|
|
35
35
|
"openpgp": "^6.3.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@jest/globals": "30.4.1",
|
|
39
39
|
"@openpgp/web-stream-tools": "0.3.1",
|
|
40
|
-
"@pnpm/crypto.shasums-file": "1100.
|
|
40
|
+
"@pnpm/crypto.shasums-file": "1100.2.0"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=22.13"
|