feat: verify the integrity of Node.js artifacts (#9750)

This commit is contained in:
Zoltan Kochan
2025-07-12 18:04:09 +02:00
committed by GitHub
parent 88a1ce37dc
commit 1ba2e15f2c
14 changed files with 424 additions and 124 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/fetching-types": minor
---
Export type Response.

View File

@@ -0,0 +1,7 @@
---
"@pnpm/plugin-commands-env": patch
"@pnpm/node.fetcher": patch
"pnpm": patch
---
The integrities of the downloaded Node.js artifacts are verified [#9750](https://github.com/pnpm/pnpm/pull/9750).

View File

@@ -235,6 +235,7 @@
"sels",
"semistrict",
"serverjs",
"shasums",
"sheetjs",
"shlex",
"sindresorhus",
@@ -285,6 +286,7 @@
"workleap",
"wrappy",
"xmarw",
"yazl",
"zkochan",
"zoli",
"zoltan"

View File

@@ -41,6 +41,7 @@
"adm-zip": "catalog:",
"detect-libc": "catalog:",
"rename-overwrite": "catalog:",
"ssri": "catalog:",
"tempy": "catalog:"
},
"devDependencies": {
@@ -48,6 +49,7 @@
"@pnpm/node.fetcher": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@types/adm-zip": "catalog:",
"@types/ssri": "catalog:",
"node-fetch": "catalog:"
},
"engines": {

View File

@@ -0,0 +1,30 @@
import { getNormalizedArch } from './normalizeArch'
export interface NodeArtifactAddress {
basename: string
extname: string
dirname: string
}
export interface GetNodeArtifactAddressOptions {
version: string
baseUrl: string
platform: string
arch: string
}
export function getNodeArtifactAddress ({
version,
baseUrl,
platform,
arch,
}: GetNodeArtifactAddressOptions): NodeArtifactAddress {
const isWindowsPlatform = platform === 'win32'
const normalizedPlatform = isWindowsPlatform ? 'win' : platform
const normalizedArch = getNormalizedArch(platform, arch, version)
return {
dirname: `${baseUrl}v${version}`,
basename: `node-v${version}-${normalizedPlatform}-${normalizedArch}`,
extname: isWindowsPlatform ? '.zip' : '.tar.gz',
}
}

View File

@@ -1,17 +0,0 @@
import { getNormalizedArch } from './normalizeArch'
export function getNodeTarball (
nodeVersion: string,
nodeMirror: string,
processPlatform: string,
processArch: string
): { pkgName: string, tarball: string } {
const platform = processPlatform === 'win32' ? 'win' : processPlatform
const arch = getNormalizedArch(processPlatform, processArch, nodeVersion)
const extension = platform === 'win' ? 'zip' : 'tar.gz'
const pkgName = `node-v${nodeVersion}-${platform}-${arch}`
return {
pkgName,
tarball: `${nodeMirror}v${nodeVersion}/${pkgName}.${extension}`,
}
}

View File

@@ -4,15 +4,22 @@ import { PnpmError } from '@pnpm/error'
import {
type FetchFromRegistry,
type RetryTimeoutOptions,
type Response,
} from '@pnpm/fetching-types'
import { pickFetcher } from '@pnpm/pick-fetcher'
import { createCafsStore } from '@pnpm/create-cafs-store'
import { createTarballFetcher } from '@pnpm/tarball-fetcher'
import { type FetchFunction } from '@pnpm/fetcher-base'
import AdmZip from 'adm-zip'
import renameOverwrite from 'rename-overwrite'
import tempy from 'tempy'
import { isNonGlibcLinux } from 'detect-libc'
import { getNodeTarball } from './getNodeTarball'
import ssri from 'ssri'
import { getNodeArtifactAddress } from './getNodeArtifactAddress'
// Constants
const DEFAULT_NODE_MIRROR_BASE_URL = 'https://nodejs.org/download/release/'
const SHA256_REGEX = /^[a-f0-9]{64}$/
export interface FetchNodeOptions {
storeDir: string
@@ -21,16 +28,149 @@ export interface FetchNodeOptions {
retry?: RetryTimeoutOptions
}
export async function fetchNode (fetch: FetchFromRegistry, version: string, targetDir: string, opts: FetchNodeOptions): Promise<void> {
if (await isNonGlibcLinux()) {
throw new PnpmError('MUSL', 'The current system uses the "MUSL" C standard library. Node.js currently has prebuilt artifacts only for the "glibc" libc, so we can install Node.js only for glibc')
}
const nodeMirrorBaseUrl = opts.nodeMirrorBaseUrl ?? 'https://nodejs.org/download/release/'
const { tarball, pkgName } = getNodeTarball(version, nodeMirrorBaseUrl, process.platform, process.arch)
if (tarball.endsWith('.zip')) {
await downloadAndUnpackZip(fetch, tarball, targetDir, pkgName)
interface NodeArtifactInfo {
url: string
integrity: string
isZip: boolean
basename: string
}
/**
* Fetches and installs a Node.js version to the specified target directory.
*
* @param fetch - Function to fetch resources from registry
* @param version - Node.js version to install
* @param targetDir - Directory where Node.js should be installed
* @param opts - Configuration options for the fetch operation
* @throws {PnpmError} When system uses MUSL libc, integrity verification fails, or download fails
*/
export async function fetchNode (
fetch: FetchFromRegistry,
version: string,
targetDir: string,
opts: FetchNodeOptions
): Promise<void> {
await validateSystemCompatibility()
const nodeMirrorBaseUrl = opts.nodeMirrorBaseUrl ?? DEFAULT_NODE_MIRROR_BASE_URL
const artifactInfo = await getNodeArtifactInfo(fetch, version, nodeMirrorBaseUrl)
if (artifactInfo.isZip) {
await downloadAndUnpackZip(fetch, artifactInfo, targetDir)
return
}
await downloadAndUnpackTarball(fetch, artifactInfo, targetDir, opts)
}
/**
* Validates that the current system is compatible with Node.js installation.
*
* @throws {PnpmError} When system uses MUSL libc
*/
async function validateSystemCompatibility (): Promise<void> {
if (await isNonGlibcLinux()) {
throw new PnpmError(
'MUSL',
'The current system uses the "MUSL" C standard library. Node.js currently has prebuilt artifacts only for the "glibc" libc, so we can install Node.js only for glibc'
)
}
}
/**
* Gets Node.js artifact information including URL, integrity, and file type.
*
* @param fetch - Function to fetch resources from registry
* @param version - Node.js version
* @param nodeMirrorBaseUrl - Base URL for Node.js mirror
* @returns Promise resolving to artifact information
* @throws {PnpmError} When integrity file cannot be fetched or parsed
*/
async function getNodeArtifactInfo (
fetch: FetchFromRegistry,
version: string,
nodeMirrorBaseUrl: string
): Promise<NodeArtifactInfo> {
const tarball = getNodeArtifactAddress({
version,
baseUrl: nodeMirrorBaseUrl,
platform: process.platform,
arch: process.arch,
})
const tarballFileName = `${tarball.basename}${tarball.extname}`
const shasumsFileUrl = `${tarball.dirname}/SHASUMS256.txt`
const url = `${tarball.dirname}/${tarballFileName}`
const integrity = await loadArtifactIntegrity(fetch, shasumsFileUrl, tarballFileName)
return {
url,
integrity,
isZip: tarball.extname === '.zip',
basename: tarball.basename,
}
}
/**
* Loads and verifies the integrity hash for a Node.js artifact.
*
* @param fetch - Function to fetch resources from registry
* @param integritiesFileUrl - URL of the SHASUMS256.txt file
* @param fileName - Name of the file to find integrity for
* @returns Promise resolving to the integrity hash in base64 format
* @throws {PnpmError} When integrity file cannot be fetched or parsed
*/
async function loadArtifactIntegrity (
fetch: FetchFromRegistry,
integritiesFileUrl: string,
fileName: string
): Promise<string> {
const res = await fetch(integritiesFileUrl)
if (!res.ok) {
throw new PnpmError(
'NODE_FETCH_INTEGRITY_FAILED',
`Failed to fetch integrity file: ${integritiesFileUrl} (status: ${res.status})`
)
}
const body = await res.text()
const line = body.split('\n').find(line => line.trim().endsWith(` ${fileName}`))
if (!line) {
throw new PnpmError(
'NODE_INTEGRITY_HASH_NOT_FOUND',
`SHA-256 hash not found in SHASUMS256.txt for: ${fileName}`
)
}
const [sha256] = line.trim().split(/\s+/)
if (!SHA256_REGEX.test(sha256)) {
throw new PnpmError(
'NODE_MALFORMED_INTEGRITY_HASH',
`Malformed SHA-256 for ${fileName}: ${sha256}`
)
}
const buffer = Buffer.from(sha256, 'hex')
const base64 = buffer.toString('base64')
return `sha256-${base64}`
}
/**
* Downloads and unpacks a tarball using the tarball fetcher.
*
* @param fetch - Function to fetch resources from registry
* @param artifactInfo - Information about the Node.js artifact
* @param targetDir - Directory where Node.js should be installed
* @param opts - Configuration options for the fetch operation
*/
async function downloadAndUnpackTarball (
fetch: FetchFromRegistry,
artifactInfo: NodeArtifactInfo,
targetDir: string,
opts: FetchNodeOptions
): Promise<void> {
const getAuthHeader = () => undefined
const fetchers = createTarballFetcher(fetch, getAuthHeader, {
retry: opts.retry,
@@ -39,13 +179,23 @@ export async function fetchNode (fetch: FetchFromRegistry, version: string, targ
rawConfig: {},
unsafePerm: false,
})
const cafs = createCafsStore(opts.storeDir)
const fetchTarball = pickFetcher(fetchers, { tarball })
const { filesIndex } = await fetchTarball(cafs, { tarball } as any, { // eslint-disable-line @typescript-eslint/no-explicit-any
filesIndexFile: path.join(opts.storeDir, encodeURIComponent(tarball)), // TODO: change the name or don't save an index file for node.js tarballs
const fetchTarball = pickFetcher(fetchers, { tarball: artifactInfo.url }) as FetchFunction
// Create a unique index file name for Node.js tarballs
const indexFileName = `node-${encodeURIComponent(artifactInfo.url)}`
const filesIndexFile = path.join(opts.storeDir, indexFileName)
const { filesIndex } = await fetchTarball(cafs, {
tarball: artifactInfo.url,
integrity: artifactInfo.integrity,
}, {
filesIndexFile,
lockfileDir: process.cwd(),
pkg: {},
})
cafs.importPackage(targetDir, {
filesResponse: {
filesIndex: filesIndex as Record<string, string>,
@@ -56,21 +206,81 @@ export async function fetchNode (fetch: FetchFromRegistry, version: string, targ
})
}
/**
* Downloads and unpacks a zip file containing Node.js.
*
* @param fetchFromRegistry - Function to fetch resources from registry
* @param artifactInfo - Information about the Node.js artifact
* @param targetDir - Directory where Node.js should be installed
* @throws {PnpmError} When integrity verification fails or extraction fails
*/
async function downloadAndUnpackZip (
fetchFromRegistry: FetchFromRegistry,
zipUrl: string,
targetDir: string,
pkgName: string
artifactInfo: NodeArtifactInfo,
targetDir: string
): Promise<void> {
const response = await fetchFromRegistry(zipUrl)
const response = await fetchFromRegistry(artifactInfo.url)
const tmp = path.join(tempy.directory(), 'pnpm.zip')
const dest = fs.createWriteStream(tmp)
await new Promise((resolve, reject) => {
response.body!.pipe(dest).on('error', reject).on('close', resolve)
})
const zip = new AdmZip(tmp)
const nodeDir = path.dirname(targetDir)
zip.extractAllTo(nodeDir, true)
await renameOverwrite(path.join(nodeDir, pkgName), targetDir)
await fs.promises.unlink(tmp)
try {
await downloadWithIntegrityCheck(response, tmp, artifactInfo.integrity, artifactInfo.url)
await extractZipToTarget(tmp, artifactInfo.basename, targetDir)
} finally {
// Clean up temporary file
try {
await fs.promises.unlink(tmp)
} catch {
// Ignore cleanup errors
}
}
}
/**
* Downloads a file with integrity verification.
*
* @param response - Fetch response containing the file data
* @param tmpPath - Temporary file path to save the download
* @param expectedIntegrity - Expected SHA-256 integrity hash
* @param url - URL being downloaded (for error messages)
* @throws {PnpmError} When integrity verification fails
*/
async function downloadWithIntegrityCheck (
response: Response,
tmpPath: string,
expectedIntegrity: string,
url: string
): Promise<void> {
// Collect all chunks from the response
const chunks: Buffer[] = []
for await (const chunk of response.body!) {
chunks.push(chunk as Buffer)
}
const data = Buffer.concat(chunks)
// Verify integrity if provided
ssri.checkData(data, expectedIntegrity, { error: true })
// Write the verified data to file
await fs.promises.writeFile(tmpPath, data)
}
/**
* Extracts a zip file to the target directory.
*
* @param zipPath - Path to the zip file
* @param basename - Base name of the file (without extension)
* @param targetDir - Directory where contents should be extracted
* @throws {PnpmError} When extraction fails
*/
async function extractZipToTarget (
zipPath: string,
basename: string,
targetDir: string
): Promise<void> {
const zip = new AdmZip(zipPath)
const nodeDir = path.dirname(targetDir)
const extractedDir = path.join(nodeDir, basename)
zip.extractAllTo(nodeDir, true)
await renameOverwrite(extractedDir, targetDir)
}

View File

@@ -0,0 +1,66 @@
import { getNodeArtifactAddress } from '../lib/getNodeArtifactAddress'
test.each([
[
'16.0.0',
'https://nodejs.org/download/release/',
'win32',
'ia32',
{
basename: 'node-v16.0.0-win-x86',
dirname: 'https://nodejs.org/download/release/v16.0.0',
extname: '.zip',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'linux',
'arm',
{
basename: 'node-v16.0.0-linux-armv7l',
dirname: 'https://nodejs.org/download/release/v16.0.0',
extname: '.tar.gz',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'linux',
'x64',
{
basename: 'node-v16.0.0-linux-x64',
dirname: 'https://nodejs.org/download/release/v16.0.0',
extname: '.tar.gz',
},
],
[
'15.14.0',
'https://nodejs.org/download/release/',
'darwin',
'arm64',
{
basename: 'node-v15.14.0-darwin-x64',
dirname: 'https://nodejs.org/download/release/v15.14.0',
extname: '.tar.gz',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'darwin',
'arm64',
{
basename: 'node-v16.0.0-darwin-arm64',
dirname: 'https://nodejs.org/download/release/v16.0.0',
extname: '.tar.gz',
},
],
])('getNodeArtifactAddress', (version, nodeMirrorBaseUrl, platform, arch, tarball) => {
expect(getNodeArtifactAddress({
version,
baseUrl: nodeMirrorBaseUrl,
platform,
arch,
})).toStrictEqual(tarball)
})

View File

@@ -1,56 +0,0 @@
import { getNodeTarball } from '../lib/getNodeTarball'
test.each([
[
'16.0.0',
'https://nodejs.org/download/release/',
'win32',
'ia32',
{
pkgName: 'node-v16.0.0-win-x86',
tarball: 'https://nodejs.org/download/release/v16.0.0/node-v16.0.0-win-x86.zip',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'linux',
'arm',
{
pkgName: 'node-v16.0.0-linux-armv7l',
tarball: 'https://nodejs.org/download/release/v16.0.0/node-v16.0.0-linux-armv7l.tar.gz',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'linux',
'x64',
{
pkgName: 'node-v16.0.0-linux-x64',
tarball: 'https://nodejs.org/download/release/v16.0.0/node-v16.0.0-linux-x64.tar.gz',
},
],
[
'15.14.0',
'https://nodejs.org/download/release/',
'darwin',
'arm64',
{
pkgName: 'node-v15.14.0-darwin-x64',
tarball: 'https://nodejs.org/download/release/v15.14.0/node-v15.14.0-darwin-x64.tar.gz',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'darwin',
'arm64',
{
pkgName: 'node-v16.0.0-darwin-arm64',
tarball: 'https://nodejs.org/download/release/v16.0.0/node-v16.0.0-darwin-arm64.tar.gz',
},
],
])('getNodeTarball', (version, nodeMirrorBaseUrl, platform, arch, tarball) => {
expect(getNodeTarball(version, nodeMirrorBaseUrl, platform, arch)).toStrictEqual(tarball)
})

View File

@@ -58,17 +58,17 @@
"@pnpm/logger": "workspace:*",
"@pnpm/plugin-commands-env": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@types/adm-zip": "catalog:",
"@types/graceful-fs": "catalog:",
"@types/is-windows": "catalog:",
"@types/semver": "catalog:",
"@types/tar-stream": "catalog:",
"adm-zip": "catalog:",
"@types/yazl": "catalog:",
"execa": "catalog:",
"nock": "catalog:",
"node-fetch": "catalog:",
"path-name": "catalog:",
"tar-stream": "catalog:"
"tar-stream": "catalog:",
"yazl": "catalog:"
},
"engines": {
"node": ">=18.12"

View File

@@ -1,10 +1,10 @@
import AdmZip from 'adm-zip'
import { Response } from 'node-fetch'
import path from 'path'
import fs from 'fs'
import { Readable } from 'stream'
import tar from 'tar-stream'
import { globalWarn } from '@pnpm/logger'
import { ZipFile } from 'yazl'
import {
getNodeDir,
getNodeBinDir,
@@ -15,6 +15,18 @@ import {
import { tempDir } from '@pnpm/prepare'
const fetchMock = jest.fn(async (url: string) => {
if (url.endsWith('SHASUMS256.txt')) {
return new Response(`
5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v16.4.0-darwin-arm64.tar.gz
5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v16.4.0-linux-arm64.tar.gz
5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v16.4.0-linux-x64.tar.gz
a08f3386090e6511772b949d41970b75a6b71d28abb551dff9854ceb1929dae1 node-v16.4.0-win-x64.zip
5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v18.0.0-rc.3-darwin-arm64.tar.gz
5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v18.0.0-rc.3-linux-arm64.tar.gz
5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v18.0.0-rc.3-linux-x64.tar.gz
07e6121cba611b57f310a489f76c413b6246e79cffe1e9538b2478ffee11c99e node-v18.0.0-rc.3-win-x64.zip
`)
}
if (url.endsWith('.tar.gz')) {
const pack = tar.pack()
pack.finalize()
@@ -23,10 +35,15 @@ const fetchMock = jest.fn(async (url: string) => {
// The Windows code path for pnpm's node bootstrapping expects a subdir
// within the .zip file.
const pkgName = path.basename(url, '.zip')
const zip = new AdmZip()
zip.addFile(`${pkgName}/dummy-file`, Buffer.from('test'))
const zipfile = new ZipFile()
return new Response(Readable.from(zip.toBuffer()))
zipfile.addBuffer(Buffer.from('test'), `${pkgName}/dummy-file`, {
mtime: new Date(0), // fixed timestamp for determinism
mode: 0o100644, // fixed file permissions
})
zipfile.end()
return new Response(Readable.from(zipfile.outputStream))
}
return new Response(Readable.from(Buffer.alloc(0)))
@@ -94,7 +111,7 @@ test('install an rc version of Node.js', async () => {
const platform = process.platform === 'win32' ? 'win' : process.platform
const arch = process.arch
const extension = process.platform === 'win32' ? 'zip' : 'tar.gz'
expect(fetchMock.mock.calls[0][0]).toBe(
expect(fetchMock.mock.calls[1][0]).toBe(
`https://nodejs.org/download/rc/v18.0.0-rc.3/node-v18.0.0-rc.3-${platform}-${arch}.${extension}`
)
})

View File

@@ -1,7 +1,7 @@
import { type RetryTimeoutOptions } from '@zkochan/retry'
import { type Response, type RequestInit as NodeRequestInit } from 'node-fetch'
export type { RetryTimeoutOptions }
export type { RetryTimeoutOptions, Response }
export interface RequestInit extends NodeRequestInit {
retry?: RetryTimeoutOptions

66
pnpm-lock.yaml generated
View File

@@ -183,6 +183,9 @@ catalogs:
'@types/yarnpkg__lockfile':
specifier: ^1.1.9
version: 1.1.9
'@types/yazl':
specifier: ^3.3.0
version: 3.3.0
'@types/zkochan__table':
specifier: npm:@types/table@6.0.0
version: 6.0.0
@@ -651,6 +654,9 @@ catalogs:
yaml-tag:
specifier: 1.1.0
version: 1.1.0
yazl:
specifier: ^3.3.1
version: 3.3.1
overrides:
'@yarnpkg/fslib@2': '3'
@@ -2069,6 +2075,9 @@ importers:
rename-overwrite:
specifier: 'catalog:'
version: 6.0.2
ssri:
specifier: 'catalog:'
version: 10.0.5
tempy:
specifier: 'catalog:'
version: 1.0.1
@@ -2085,6 +2094,9 @@ importers:
'@types/adm-zip':
specifier: 'catalog:'
version: 0.5.7
'@types/ssri':
specifier: 'catalog:'
version: 7.1.5
node-fetch:
specifier: 'catalog:'
version: '@pnpm/node-fetch@1.0.0'
@@ -2193,9 +2205,6 @@ importers:
'@pnpm/prepare':
specifier: workspace:*
version: link:../../__utils__/prepare
'@types/adm-zip':
specifier: 'catalog:'
version: 0.5.7
'@types/graceful-fs':
specifier: 'catalog:'
version: 4.1.9
@@ -2208,9 +2217,9 @@ importers:
'@types/tar-stream':
specifier: 'catalog:'
version: 2.2.3
adm-zip:
'@types/yazl':
specifier: 'catalog:'
version: 0.5.16
version: 3.3.0
execa:
specifier: 'catalog:'
version: safe-execa@0.1.2
@@ -2226,6 +2235,9 @@ importers:
tar-stream:
specifier: 'catalog:'
version: 2.2.0
yazl:
specifier: 'catalog:'
version: 3.3.1
env/system-node-version:
dependencies:
@@ -10271,6 +10283,9 @@ packages:
'@types/yarnpkg__lockfile@1.1.9':
resolution: {integrity: sha512-GD4Fk15UoP5NLCNor51YdfL9MSdldKCqOC9EssrRw3HVfar9wUZ5y8Lfnp+qVD6hIinLr8ygklDYnmlnlQo12Q==}
'@types/yazl@3.3.0':
resolution: {integrity: sha512-mFL6lGkk2N5u5nIxpNV/K5LW3qVSbxhJrMxYGOOxZndWxMgCamr/iCsq/1t9kd8pEwhuNP91LC5qZm/qS9pOEw==}
'@typescript-eslint/eslint-plugin@6.18.1':
resolution: {integrity: sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -10821,6 +10836,10 @@ packages:
bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
buffer-crc32@1.0.0:
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
engines: {node: '>=8.0.0'}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
@@ -15360,6 +15379,9 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yazl@3.3.1:
resolution: {integrity: sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==}
yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
@@ -16326,7 +16348,7 @@ snapshots:
'@jest/console@29.7.0':
dependencies:
'@jest/types': 29.6.3
'@types/node': 18.19.34
'@types/node': 22.15.29
chalk: 4.1.2
jest-message-util: 29.7.0
jest-util: 29.7.0
@@ -16372,7 +16394,7 @@ snapshots:
dependencies:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 18.19.34
'@types/node': 22.15.29
jest-mock: 29.7.0
'@jest/expect-utils@29.7.0':
@@ -16390,7 +16412,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@sinonjs/fake-timers': 10.3.0
'@types/node': 18.19.34
'@types/node': 22.15.29
jest-message-util: 29.7.0
jest-mock: 29.7.0
jest-util: 29.7.0
@@ -16412,7 +16434,7 @@ snapshots:
'@jest/transform': 29.7.0(@babel/types@7.26.10)
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.25
'@types/node': 18.19.34
'@types/node': 22.15.29
chalk: 4.1.2
collect-v8-coverage: 1.0.2
exit: 0.1.2
@@ -18030,6 +18052,10 @@ snapshots:
'@types/yarnpkg__lockfile@1.1.9': {}
'@types/yazl@3.3.0':
dependencies:
'@types/node': 22.15.29
'@typescript-eslint/eslint-plugin@6.18.1(@typescript-eslint/parser@6.18.1(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -18825,6 +18851,8 @@ snapshots:
dependencies:
node-int64: 0.4.0
buffer-crc32@1.0.0: {}
buffer-equal-constant-time@1.0.1: {}
buffer-equal@1.0.1: {}
@@ -20991,7 +21019,7 @@ snapshots:
'@jest/expect': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 18.19.34
'@types/node': 22.15.29
chalk: 4.1.2
co: 4.6.0
dedent: 1.6.0
@@ -21088,7 +21116,7 @@ snapshots:
'@jest/environment': 29.7.0
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 18.19.34
'@types/node': 22.15.29
jest-mock: 29.7.0
jest-util: 29.7.0
@@ -21098,7 +21126,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@types/graceful-fs': 4.1.9
'@types/node': 18.19.34
'@types/node': 22.15.29
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11(patch_hash=68ebc232025360cb3dcd3081f4067f4e9fc022ab6b6f71a3230e86c7a5b337d1)
@@ -21137,7 +21165,7 @@ snapshots:
jest-mock@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 18.19.34
'@types/node': 22.15.29
jest-util: 29.7.0
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -21172,7 +21200,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0(@babel/types@7.26.10)
'@jest/types': 29.6.3
'@types/node': 18.19.34
'@types/node': 22.15.29
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.11(patch_hash=68ebc232025360cb3dcd3081f4067f4e9fc022ab6b6f71a3230e86c7a5b337d1)
@@ -21201,7 +21229,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0(@babel/types@7.26.10)
'@jest/types': 29.6.3
'@types/node': 18.19.34
'@types/node': 22.15.29
chalk: 4.1.2
cjs-module-lexer: 1.4.3
collect-v8-coverage: 1.0.2
@@ -21267,7 +21295,7 @@ snapshots:
dependencies:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 18.19.34
'@types/node': 22.15.29
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@@ -21276,7 +21304,7 @@ snapshots:
jest-worker@29.7.0:
dependencies:
'@types/node': 18.19.34
'@types/node': 22.15.29
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -24019,6 +24047,10 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
yazl@3.3.1:
dependencies:
buffer-crc32: 1.0.0
yn@3.1.1: {}
yocto-queue@0.1.0: {}

View File

@@ -102,6 +102,7 @@ catalog:
'@types/which': ^2.0.2
'@types/write-file-atomic': ^4.0.3
'@types/yarnpkg__lockfile': ^1.1.9
'@types/yazl': ^3.3.0
'@types/zkochan__table': npm:@types/table@6.0.0
'@yarnpkg/core': 4.2.0
'@yarnpkg/extensions': 2.0.3
@@ -261,6 +262,7 @@ catalog:
write-pkg: 4.0.0
write-yaml-file: ^5.0.0
yaml-tag: 1.1.0
yazl: ^3.3.1
enableGlobalVirtualStore: true