From e2afcb16e34dbfe4912b112ed3a194ab39f59ede Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 30 Sep 2025 13:52:21 +0200 Subject: [PATCH 01/71] Implement a proxy blocking tarball requests for packages containing malware. --- package-lock.json | 10 ++ packages/safe-chain/package.json | 1 + packages/safe-chain/src/main.js | 30 ++++- .../npm/dependencyScanner/dryRunScanner.js | 7 +- .../dependencyScanner/dryRunScanner.spec.js | 14 +-- .../src/packagemanager/npm/runNpmCommand.js | 36 ++++-- .../src/packagemanager/pnpm/runPnpmCommand.js | 15 ++- .../src/packagemanager/yarn/runYarnCommand.js | 46 ++++++- .../safe-chain/src/registryProxy/certUtils.js | 114 +++++++++++++++++ .../src/registryProxy/mitmRequestHandler.js | 76 +++++++++++ .../src/registryProxy/parsePackageFromUrl.js | 48 +++++++ .../registryProxy/parsePackageFromUrl.spec.js | 114 +++++++++++++++++ .../src/registryProxy/registryProxy.js | 119 ++++++++++++++++++ .../src/registryProxy/tunnelRequestHandler.js | 20 +++ packages/safe-chain/src/utils/safeSpawn.js | 14 ++- test/e2e/package.json | 2 +- 16 files changed, 633 insertions(+), 33 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/certUtils.js create mode 100644 packages/safe-chain/src/registryProxy/mitmRequestHandler.js create mode 100644 packages/safe-chain/src/registryProxy/parsePackageFromUrl.js create mode 100644 packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js create mode 100644 packages/safe-chain/src/registryProxy/registryProxy.js create mode 100644 packages/safe-chain/src/registryProxy/tunnelRequestHandler.js diff --git a/package-lock.json b/package-lock.json index 4840448..f111431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3323,6 +3323,15 @@ "node": ">= 0.6" } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-pty": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", @@ -4878,6 +4887,7 @@ "abbrev": "3.0.1", "chalk": "5.4.1", "make-fetch-happen": "14.0.3", + "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", "ora": "8.2.0", "semver": "7.7.2" diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 32228e7..c74d44e 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -31,6 +31,7 @@ "abbrev": "3.0.1", "chalk": "5.4.1", "make-fetch-happen": "14.0.3", + "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", "ora": "8.2.0", "semver": "7.7.2" diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 81b1b3a..9dd61d6 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -4,8 +4,13 @@ import { scanCommand, shouldScanCommand } from "./scanning/index.js"; import { ui } from "./environment/userInteraction.js"; import { getPackageManager } from "./packagemanager/currentPackageManager.js"; import { initializeCliArguments } from "./config/cliArguments.js"; +import { createSafeChainProxy } from "./registryProxy/registryProxy.js"; +import chalk from "chalk"; export async function main(args) { + const proxy = createSafeChainProxy(); + await proxy.startServer(); + try { // This parses all the --safe-chain arguments and removes them from the args array args = initializeCliArguments(args); @@ -18,6 +23,29 @@ export async function main(args) { process.exit(1); } - var result = getPackageManager().runCommand(args); + var result = await getPackageManager().runCommand(args); + + await proxy.stopServer(); + const blockedRequests = proxy.getBlockedRequests(); + if (blockedRequests.length > 0) { + ui.emptyLine(); + + ui.writeInformation( + `Safe-chain: ${chalk.bold( + `blocked ${blockedRequests.length} malicious package downloads` + )}:` + ); + + for (const req of blockedRequests) { + ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); + } + + ui.emptyLine(); + ui.writeError("Exiting without installing malicious packages."); + ui.emptyLine(); + + process.exit(1); + } + process.exit(result.status); } diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js index 0db23cb..59cd236 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js @@ -8,7 +8,8 @@ export function dryRunScanner(scannerOptions) { shouldScan: (args) => shouldScanDependencies(scannerOptions, args), }; } -function scanDependencies(scannerOptions, args) { + +async function scanDependencies(scannerOptions, args) { let dryRunArgs = args; if (scannerOptions?.dryRunCommand) { @@ -31,8 +32,8 @@ function shouldScanDependencies(scannerOptions, args) { return true; } -function checkChangesWithDryRun(args) { - const dryRunOutput = dryRunNpmCommandAndOutput(args); +async function checkChangesWithDryRun(args) { + const dryRunOutput = await dryRunNpmCommandAndOutput(args); // Dry-run can return a non-zero status code in some cases // e.g., when running "npm audit fix --dry-run", it returns exit code 1 diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js index 4fb6272..88d7681 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js @@ -36,7 +36,7 @@ describe("dryRunScanner", async () => { })); const scanner = dryRunScanner(); - const result = scanner.scan(["audit", "fix"]); + const result = await scanner.scan(["audit", "fix"]); // Should not throw an error for audit fix commands assert.ok(Array.isArray(result)); @@ -53,8 +53,8 @@ describe("dryRunScanner", async () => { const scanner = dryRunScanner(); - assert.throws(() => { - scanner.scan(["install", "lodash"]); + await assert.rejects(async () => { + await scanner.scan(["install", "lodash"]); }, /Dry-run command failed with exit code 1/); }); @@ -67,7 +67,7 @@ describe("dryRunScanner", async () => { })); const scanner = dryRunScanner(); - const result = scanner.scan(["install", "lodash"]); + const result = await scanner.scan(["install", "lodash"]); assert.ok(Array.isArray(result)); assert.equal(mockWriteError.mock.callCount(), 0); @@ -83,8 +83,8 @@ describe("dryRunScanner", async () => { const scanner = dryRunScanner(); - assert.throws(() => { - scanner.scan(["audit", "fix"]); + await assert.rejects(async () => { + await scanner.scan(["audit", "fix"]); }, /Dry-run command failed with exit code 1/); }); }); @@ -99,7 +99,7 @@ describe("dryRunScanner", async () => { })); const scanner = dryRunScanner({ dryRunCommand: "install" }); - scanner.scan(["install-test", "lodash"]); + await scanner.scan(["install-test", "lodash"]); // Should call with "install" instead of "install-test" assert.equal(mockDryRunNpmCommandAndOutput.mock.callCount(), 1); diff --git a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js index 70a0d17..26a4a9d 100644 --- a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js +++ b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js @@ -1,10 +1,14 @@ -import { execSync } from "child_process"; import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -export function runNpm(args) { +export async function runNpm(args) { try { - const npmCommand = `npm ${args.join(" ")}`; - execSync(npmCommand, { stdio: "inherit" }); + const result = await safeSpawn("npm", args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); + return { status: result.status }; } catch (error) { if (error.status) { return { status: error.status }; @@ -13,17 +17,29 @@ export function runNpm(args) { return { status: 1 }; } } - return { status: 0 }; } -export function dryRunNpmCommandAndOutput(args) { +export async function dryRunNpmCommandAndOutput(args) { try { - const npmCommand = `npm ${args.join(" ")} --ignore-scripts --dry-run`; - const output = execSync(npmCommand, { stdio: "pipe" }); - return { status: 0, output: output.toString() }; + const result = await safeSpawn( + "npm", + [...args, "--ignore-scripts", "--dry-run"], + { + stdio: "pipe", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + } + ); + return { + status: result.status, + output: result.status === 0 ? result.stdout : result.stderr, + }; } catch (error) { if (error.status) { - const output = error.stdout ? error.stdout.toString() : ""; + const output = + error.stdout?.toString() ?? + error.stderr?.toString() ?? + error.message ?? + ""; return { status: error.status, output }; } else { ui.writeError("Error executing command:", error.message); diff --git a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js index 0d5133f..794d6e3 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js +++ b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js @@ -1,13 +1,20 @@ import { ui } from "../../environment/userInteraction.js"; -import { safeSpawnSync } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; -export function runPnpmCommand(args, toolName = "pnpm") { +export async function runPnpmCommand(args, toolName = "pnpm") { try { let result; if (toolName === "pnpm") { - result = safeSpawnSync("pnpm", args, { stdio: "inherit" }); + result = await safeSpawn("pnpm", args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); } else if (toolName === "pnpx") { - result = safeSpawnSync("pnpx", args, { stdio: "inherit" }); + result = await safeSpawn("pnpx", args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); } else { throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`); } diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index c89c804..2c3795c 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -1,10 +1,17 @@ -import { execSync } from "child_process"; import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -export function runYarnCommand(args) { +export async function runYarnCommand(args) { try { - const npxCommand = `yarn ${args.join(" ")}`; - execSync(npxCommand, { stdio: "inherit" }); + const env = mergeSafeChainProxyEnvironmentVariables(process.env); + await fixYarnProxyEnvironmentVariables(env); + + const result = await safeSpawn("yarn", args, { + stdio: "inherit", + env, + }); + return { status: result.status }; } catch (error) { if (error.status) { return { status: error.status }; @@ -13,5 +20,34 @@ export function runYarnCommand(args) { return { status: 1 }; } } - return { status: 0 }; +} + +async function fixYarnProxyEnvironmentVariables(env) { + // Yarn ignores standard proxy environment variables HTTPS_PROXY and NODE_EXTRA_CA_CERTS + + // Yarn v2/v3 and v4+ use different environment variables for proxy and CA certs + // When setting all variables, yarn returns an error about conflicting variables + // - v2/v3: "Usage Error: Unrecognized or legacy configuration settings found: httpsCaFilePath" + // - v4+: "Usage Error: Unrecognized or legacy configuration settings found: caFilePath" + + const version = await yarnVersion(); + const majorVersion = parseInt(version.split(".")[0]); + + if (majorVersion >= 4) { + env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; + env.YARN_HTTPS_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS; + } else if (majorVersion === 2 || majorVersion === 3) { + env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; + env.YARN_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS; + } +} + +async function yarnVersion() { + const result = await safeSpawn("yarn", ["--version"], { + stdio: "pipe", + }); + if (result.status !== 0) { + throw new Error("Failed to get yarn version"); + } + return result.stdout.trim(); } diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js new file mode 100644 index 0000000..99789f6 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -0,0 +1,114 @@ +import forge from "node-forge"; +import path from "path"; +import fs from "fs"; +import os from "os"; + +const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); +const ca = loadCa(); + +const certCache = new Map(); + +export function getCaCertPath() { + return path.join(certFolder, "ca-cert.pem"); +} + +export function generateCertForHost(hostname) { + let existingCert = certCache.get(hostname); + if (existingCert) { + return existingCert; + } + + const keys = forge.pki.rsa.generateKeyPair(2048); + const cert = forge.pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = "01"; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1); + + const attrs = [{ name: "commonName", value: hostname }]; + cert.setSubject(attrs); + cert.setIssuer(ca.certificate.subject.attributes); + cert.setExtensions([ + { + name: "subjectAltName", + altNames: [ + { + type: 2, // DNS + value: hostname, + }, + ], + }, + { + name: "keyUsage", + digitalSignature: true, + keyEncipherment: true, + }, + ]); + cert.sign(ca.privateKey, forge.md.sha256.create()); + + const result = { + privateKey: forge.pki.privateKeyToPem(keys.privateKey), + certificate: forge.pki.certificateToPem(cert), + }; + + certCache.set(hostname, result); + + return result; +} + +function loadCa() { + const keyPath = path.join(certFolder, "ca-key.pem"); + const certPath = path.join(certFolder, "ca-cert.pem"); + + if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { + const privateKeyPem = fs.readFileSync(keyPath, "utf8"); + const certPem = fs.readFileSync(certPath, "utf8"); + const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); + const certificate = forge.pki.certificateFromPem(certPem); + + // Don't return a cert that is valid for less than 1 hour + const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); + if (certificate.validity.notAfter > oneHourFromNow) { + return { privateKey, certificate }; + } + } + + const { privateKey, certificate } = generateCa(); + fs.mkdirSync(certFolder, { recursive: true }); + fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey)); + fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate)); + return { privateKey, certificate }; +} + +function generateCa() { + const keys = forge.pki.rsa.generateKeyPair(2048); + const cert = forge.pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = "01"; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1); + + const attrs = [{ name: "commonName", value: "safe-chain proxy" }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.setExtensions([ + { + name: "basicConstraints", + cA: true, + }, + { + name: "keyUsage", + keyCertSign: true, + digitalSignature: true, + keyEncipherment: true, + }, + ]); + cert.sign(keys.privateKey, forge.md.sha256.create()); + + return { + privateKey: keys.privateKey, + certificate: cert, + }; +} diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js new file mode 100644 index 0000000..3958a70 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -0,0 +1,76 @@ +import https from "https"; +import { generateCertForHost } from "./certUtils.js"; +import chalk from "chalk"; + +export function mitmConnect(req, clientSocket, isAllowed) { + const { hostname } = new URL(`http://${req.url}`); + + const server = createHttpsServer(hostname, isAllowed); + + // Establish the connection + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + + // Hand off the socket to the HTTPS server + server.emit("connection", clientSocket); +} + +function createHttpsServer(hostname, isAllowed) { + const cert = generateCertForHost(hostname); + + async function handleRequest(req, res) { + const targetUrl = `https://${hostname}${req.url}`; + + if (!(await isAllowed(targetUrl))) { + res.writeHead(403, "Forbidden - blocked by safe-chain"); + res.end("Blocked by safe-chain"); + return; + } + + // Collect request body + forwardRequest(req, hostname, res); + } + + return https.createServer( + { + key: cert.privateKey, + cert: cert.certificate, + }, + handleRequest + ); +} + +function forwardRequest(req, hostname, res) { + const proxyReq = createProxyRequest(hostname, req, res); + + proxyReq.on("error", () => { + res.writeHead(502); + res.end("Bad Gateway"); + }); + + req.on("data", (chunk) => { + proxyReq.write(chunk); + }); + + req.on("end", () => { + proxyReq.end(); + }); +} + +function createProxyRequest(hostname, req, res) { + const options = { + hostname: hostname, + port: 443, + path: req.url, + method: req.method, + headers: { ...req.headers }, + }; + + delete options.headers.host; + + const proxyReq = https.request(options, (proxyRes) => { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + }); + + return proxyReq; +} diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js new file mode 100644 index 0000000..7368b35 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -0,0 +1,48 @@ +export const knownRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; + +export function parsePackageFromUrl(url) { + let packageName, version, registry; + + for (const knownRegistry of knownRegistries) { + if (url.includes(knownRegistry)) { + registry = knownRegistry; + break; + } + } + + if (!registry || !url.endsWith(".tgz")) { + return { packageName, version }; + } + + const registryIndex = url.indexOf(registry); + const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash + + const separatorIndex = afterRegistry.indexOf("/-/"); + if (separatorIndex === -1) { + return { packageName, version }; + } + + packageName = afterRegistry.substring(0, separatorIndex); + const filename = afterRegistry.substring( + separatorIndex + 3, + afterRegistry.length - 4 + ); // Remove /-/ and .tgz + + // Extract version from filename + // For scoped packages like @babel/core, the filename is core-7.21.4.tgz + // For regular packages like lodash, the filename is lodash-4.17.21.tgz + if (packageName.startsWith("@")) { + const scopedPackageName = packageName.substring( + packageName.lastIndexOf("/") + 1 + ); + if (filename.startsWith(scopedPackageName + "-")) { + version = filename.substring(scopedPackageName.length + 1); + } + } else { + if (filename.startsWith(packageName + "-")) { + version = filename.substring(packageName.length + 1); + } + } + + return { packageName, version }; +} diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js new file mode 100644 index 0000000..0b8f700 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js @@ -0,0 +1,114 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { parsePackageFromUrl } from "./parsePackageFromUrl.js"; + +describe("parsePackageFromUrl", () => { + const testCases = [ + // Regular packages + { + url: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + expected: { packageName: "lodash", version: "4.17.21" }, + }, + { + url: "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + expected: { packageName: "express", version: "4.18.2" }, + }, + // Packages with hyphens in name + { + url: "https://registry.npmjs.org/safe-chain-test/-/safe-chain-test-1.0.0.tgz", + expected: { packageName: "safe-chain-test", version: "1.0.0" }, + }, + { + url: "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.0.tgz", + expected: { packageName: "web-vitals", version: "3.5.0" }, + }, + // Preview/prerelease versions + { + url: "https://registry.npmjs.org/safe-chain-test/-/safe-chain-test-0.0.1-security.tgz", + expected: { packageName: "safe-chain-test", version: "0.0.1-security" }, + }, + { + url: "https://registry.npmjs.org/lodash/-/lodash-5.0.0-beta.1.tgz", + expected: { packageName: "lodash", version: "5.0.0-beta.1" }, + }, + { + url: "https://registry.npmjs.org/react/-/react-18.3.0-canary-abc123.tgz", + expected: { packageName: "react", version: "18.3.0-canary-abc123" }, + }, + // Scoped packages + { + url: "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", + expected: { packageName: "@babel/core", version: "7.21.4" }, + }, + { + url: "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + expected: { packageName: "@types/node", version: "20.10.5" }, + }, + { + url: "https://registry.npmjs.org/@angular/common/-/common-17.0.8.tgz", + expected: { packageName: "@angular/common", version: "17.0.8" }, + }, + // Scoped packages with hyphens + { + url: "https://registry.npmjs.org/@safe-chain/test-package/-/test-package-2.1.0.tgz", + expected: { packageName: "@safe-chain/test-package", version: "2.1.0" }, + }, + { + url: "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.465.0.tgz", + expected: { packageName: "@aws-sdk/client-s3", version: "3.465.0" }, + }, + // Scoped packages with preview versions + { + url: "https://registry.npmjs.org/@babel/core/-/core-8.0.0-alpha.1.tgz", + expected: { packageName: "@babel/core", version: "8.0.0-alpha.1" }, + }, + { + url: "https://registry.npmjs.org/@safe-chain/security-test/-/security-test-1.0.0-security.tgz", + expected: { + packageName: "@safe-chain/security-test", + version: "1.0.0-security", + }, + }, + // Yarn registry + { + url: "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz", + expected: { packageName: "lodash", version: "4.17.21" }, + }, + { + url: "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz", + expected: { packageName: "@babel/core", version: "7.21.4" }, + }, + // Invalid URLs should return undefined values + { + url: "https://example.com/package.tgz", + expected: { packageName: undefined, version: undefined }, + }, + // URL to get package info, not tarball + { + url: "https://registry.npmjs.org/lodash", + expected: { packageName: undefined, version: undefined }, + }, + // Complex version patterns + { + url: "https://registry.npmjs.org/package-with-many-hyphens/-/package-with-many-hyphens-1.0.0-rc.1+build.123.tgz", + expected: { + packageName: "package-with-many-hyphens", + version: "1.0.0-rc.1+build.123", + }, + }, + { + url: "https://registry.npmjs.org/@scope/package-name-with-hyphens/-/package-name-with-hyphens-2.0.0-beta.2.tgz", + expected: { + packageName: "@scope/package-name-with-hyphens", + version: "2.0.0-beta.2", + }, + }, + ]; + + testCases.forEach(({ url, expected }, index) => { + it(`should parse URL ${index + 1}: ${url}`, () => { + const result = parsePackageFromUrl(url); + assert.deepEqual(result, expected); + }); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js new file mode 100644 index 0000000..fb83c0e --- /dev/null +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -0,0 +1,119 @@ +import * as http from "http"; +import { tunnelRequest } from "./tunnelRequestHandler.js"; +import { mitmConnect } from "./mitmRequestHandler.js"; +import { getCaCertPath } from "./certUtils.js"; +import { auditChanges } from "../scanning/audit/index.js"; +import { knownRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; + +const state = { + port: null, + blockedRequests: [], +}; + +export function createSafeChainProxy() { + const server = createProxyServer(); + server.on("connect", handleConnect); + + return { + startServer: () => startServer(server), + stopServer: () => stopServer(server), + getBlockedRequests: () => state.blockedRequests, + }; +} + +function getSafeChainProxyEnvironmentVariables() { + return { + HTTPS_PROXY: `http://localhost:${state.port}`, + GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`, + NODE_EXTRA_CA_CERTS: getCaCertPath(), + }; +} + +export function mergeSafeChainProxyEnvironmentVariables(env) { + const proxyEnv = getSafeChainProxyEnvironmentVariables(); + + for (const key of Object.keys(env)) { + // If we were to simply copy all env variables, we might overwrite + // the proxy settings set by safe-chain when casing varies (e.g. http_proxy vs HTTP_PROXY) + // So we only copy the variable if it's not already set in a different case + const upperKey = key.toUpperCase(); + + if (!proxyEnv[upperKey]) { + proxyEnv[upperKey] = env[key]; + } + } + + return proxyEnv; +} + +function createProxyServer() { + const server = http.createServer((_, res) => { + res.writeHead(400, "Bad Request"); + res.write( + "Safe-chain proxy: Direct http not supported. Only CONNECT requests are allowed." + ); + res.end(); + }); + + return server; +} + +function startServer(server) { + return new Promise((resolve, reject) => { + server.listen(0, () => { + const address = server.address(); + if (address && typeof address === "object") { + state.port = address.port; + resolve(); + } else { + reject(new Error("Failed to start proxy server")); + } + }); + + server.on("error", (err) => { + reject(err); + }); + }); +} + +function stopServer(server) { + return new Promise((resolve) => { + server.close(() => { + resolve(); + }); + }); +} + +function handleConnect(req, clientSocket, head) { + // CONNECT method is used for HTTPS requests + // It establishes a tunnel to the server identified by the request URL + + if (knownRegistries.some((reg) => req.url.includes(reg))) { + // For npm and yarn registries, we want to intercept and inspect the traffic + // so we can block packages with malware + mitmConnect(req, clientSocket, isAllowedUrl); + } else { + // For other hosts, just tunnel the request to the destination tcp socket + tunnelRequest(req, clientSocket, head); + } +} + +async function isAllowedUrl(url) { + const { packageName, version } = parsePackageFromUrl(url); + + // This happens when the URL is not a tarball download url. + if (!packageName || !version) { + return true; + } + + const auditResult = await auditChanges([ + { name: packageName, version, type: "add" }, + ]); + + if (!auditResult.isAllowed) { + state.blockedRequests.push({ packageName, version, url }); + return false; + } + + return true; +} diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js new file mode 100644 index 0000000..8579729 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -0,0 +1,20 @@ +import * as net from "net"; +import { ui } from "../environment/userInteraction.js"; + +export function tunnelRequest(req, clientSocket, head) { + const { port, hostname } = new URL(`http://${req.url}`); + + const serverSocket = net.connect(port || 443, hostname, () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }); + + serverSocket.on("error", (err) => { + ui.writeError( + `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}` + ); + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + }); +} diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 826ab7d..5af4740 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -22,12 +22,22 @@ export async function safeSpawn(command, args, options = {}) { const fullCommand = buildCommand(command, args); return new Promise((resolve, reject) => { const child = spawn(fullCommand, { ...options, shell: true }); + let stdout = ""; + let stderr = ""; + + child.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); child.on("close", (code) => { resolve({ status: code, - stdout: Buffer.from(""), - stderr: Buffer.from(""), + stdout: stdout, + stderr: stderr, }); }); diff --git a/test/e2e/package.json b/test/e2e/package.json index 1dc6e8a..9217808 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "description": "End-to-end tests for the Aikido Safe Chain", "scripts": { - "test": "node --test **/*.spec.js" + "test": "node --test --test-concurrency=1 **/*.spec.js" }, "keywords": [], "author": "Aikido Security", From a3f91b8b5520f13ce6c39ec9458e088253c978a5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 30 Sep 2025 13:53:59 +0200 Subject: [PATCH 02/71] Fix linting issue --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 3958a70..6a72879 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -1,6 +1,5 @@ import https from "https"; import { generateCertForHost } from "./certUtils.js"; -import chalk from "chalk"; export function mitmConnect(req, clientSocket, isAllowed) { const { hostname } = new URL(`http://${req.url}`); From 6c08c6adce62b0757f742f32b1e0bb2eecb8020c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 30 Sep 2025 15:03:49 +0200 Subject: [PATCH 03/71] Add end-to-end tests for proxy blocking malware packages --- .github/workflows/test-on-pr.yml | 2 +- test/e2e/npm.e2e.spec.js | 46 ++++++++++++++++++++++++++++++++ test/e2e/pnpm.e2e.spec.js | 22 +++++++++++++++ test/e2e/yarn.e2e.spec.js | 46 ++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 740d741..59d0549 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -82,7 +82,7 @@ jobs: # EOL compatibility testing - Node 16 (EOL Sept 2023) - node_version: "16" npm_version: "8.0.0" - yarn_version: "3.6.0" + yarn_version: "1.22.0" pnpm_version: "8.0.0" steps: diff --git a/test/e2e/npm.e2e.spec.js b/test/e2e/npm.e2e.spec.js index 0e64971..fc23ebb 100644 --- a/test/e2e/npm.e2e.spec.js +++ b/test/e2e/npm.e2e.spec.js @@ -60,6 +60,52 @@ describe("E2E: npm coverage", () => { ); }); + it(`safe-chain blocks download of malicious packages already in package.json`, async () => { + const shell = await container.openShell("zsh"); + const npmVersion = (await shell.runCommand("npm --version")).output.trim(); + const majorVersion = parseInt(npmVersion.split(".")[0]); + const minorVersion = parseInt(npmVersion.split(".")[1]); + const isBelow10_4 = + majorVersion < 10 || (majorVersion === 10 && minorVersion < 4); + await shell.runCommand( + 'echo \'{"name":"test-project","version":"1.0.0","dependencies":{"safe-chain-test":"0.0.1-security"}}\' > package.json' + ); + + var result = await shell.runCommand("npm install"); + + if (isBelow10_4) { + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes( + "Exiting without installing malicious packages." + ), + `Output did not include expected text. Output was:\n${result.output}` + ); + } else { + assert.ok( + result.output.includes("Malicious changes detected:"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes( + "Exiting without installing malicious packages." + ), + `Output did not include expected text. Output was:\n${result.output}` + ); + } + }); + it("safe-chain blocks npx from executing malicious packages", async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("npx safe-chain-test"); diff --git a/test/e2e/pnpm.e2e.spec.js b/test/e2e/pnpm.e2e.spec.js index db7eb58..c9460e6 100644 --- a/test/e2e/pnpm.e2e.spec.js +++ b/test/e2e/pnpm.e2e.spec.js @@ -60,6 +60,28 @@ describe("E2E: pnpm coverage", () => { ); }); + it(`safe-chain blocks download of malicious packages already in package.json`, async () => { + const shell = await container.openShell("zsh"); + await shell.runCommand( + 'echo \'{"name":"test-project","version":"1.0.0","dependencies":{"safe-chain-test":"0.0.1-security"}}\' > package.json' + ); + + var result = await shell.runCommand("pnpm install"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + it("safe-chain blocks pnpx from executing malicious packages", async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("pnpx safe-chain-test"); diff --git a/test/e2e/yarn.e2e.spec.js b/test/e2e/yarn.e2e.spec.js index fb22b76..df8a88b 100644 --- a/test/e2e/yarn.e2e.spec.js +++ b/test/e2e/yarn.e2e.spec.js @@ -60,6 +60,52 @@ describe("E2E: yarn coverage", () => { ); }); + it(`safe-chain blocks download of malicious packages already in package.json`, async () => { + const shell = await container.openShell("zsh"); + await shell.runCommand( + 'echo \'{"name":"test-project","version":"1.0.0","dependencies":{"safe-chain-test":"0.0.1-security"}}\' > package.json' + ); + + var result = await shell.runCommand("yarn"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`safe-chain blocks installation of malicious packages`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("yarn add safe-chain-test"); + + assert.ok( + result.output.includes("Malicious changes detected:"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + const listResult = await shell.runCommand("yarn list"); + assert.ok( + !listResult.output.includes("safe-chain-test"), + `Malicious package was installed despite safe-chain protection. Output of 'yarn list' was:\n${listResult.output}` + ); + }); + it("safe-chain blocks yarn dlx from executing malicious packages", async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("yarn dlx safe-chain-test"); From 3b145a4695363ea584e225786d6f5164be524d5f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 30 Sep 2025 15:11:00 +0200 Subject: [PATCH 04/71] Create verifyNoMaliciousPackages function in proxy --- packages/safe-chain/src/main.js | 22 +-------------- .../src/registryProxy/registryProxy.js | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 9dd61d6..1d259c9 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -5,7 +5,6 @@ import { ui } from "./environment/userInteraction.js"; import { getPackageManager } from "./packagemanager/currentPackageManager.js"; import { initializeCliArguments } from "./config/cliArguments.js"; import { createSafeChainProxy } from "./registryProxy/registryProxy.js"; -import chalk from "chalk"; export async function main(args) { const proxy = createSafeChainProxy(); @@ -26,26 +25,7 @@ export async function main(args) { var result = await getPackageManager().runCommand(args); await proxy.stopServer(); - const blockedRequests = proxy.getBlockedRequests(); - if (blockedRequests.length > 0) { - ui.emptyLine(); - - ui.writeInformation( - `Safe-chain: ${chalk.bold( - `blocked ${blockedRequests.length} malicious package downloads` - )}:` - ); - - for (const req of blockedRequests) { - ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); - } - - ui.emptyLine(); - ui.writeError("Exiting without installing malicious packages."); - ui.emptyLine(); - - process.exit(1); - } + proxy.verifyNoMaliciousPackages(); process.exit(result.status); } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index fb83c0e..c2812de 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -4,6 +4,8 @@ import { mitmConnect } from "./mitmRequestHandler.js"; import { getCaCertPath } from "./certUtils.js"; import { auditChanges } from "../scanning/audit/index.js"; import { knownRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; +import { ui } from "../environment/userInteraction.js"; +import chalk from "chalk"; const state = { port: null, @@ -18,6 +20,7 @@ export function createSafeChainProxy() { startServer: () => startServer(server), stopServer: () => stopServer(server), getBlockedRequests: () => state.blockedRequests, + verifyNoMaliciousPackages, }; } @@ -117,3 +120,27 @@ async function isAllowedUrl(url) { return true; } + +function verifyNoMaliciousPackages() { + if (state.blockedRequests.length === 0) { + return; + } + + ui.emptyLine(); + + ui.writeInformation( + `Safe-chain: ${chalk.bold( + `blocked ${state.blockedRequests.length} malicious package downloads` + )}:` + ); + + for (const req of state.blockedRequests) { + ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); + } + + ui.emptyLine(); + ui.writeError("Exiting without installing malicious packages."); + ui.emptyLine(); + + process.exit(1); +} From 95663dc5f463db4693e5ca0bc334e494818575a3 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 1 Oct 2025 08:10:49 +0200 Subject: [PATCH 05/71] Fix proxy for npm 10.0.0 -> 10.4.0 --- .../src/registryProxy/mitmRequestHandler.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 6a72879..750815e 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -17,7 +17,8 @@ function createHttpsServer(hostname, isAllowed) { const cert = generateCertForHost(hostname); async function handleRequest(req, res) { - const targetUrl = `https://${hostname}${req.url}`; + const pathAndQuery = getRequestPathAndQuery(req.url); + const targetUrl = `https://${hostname}${pathAndQuery}`; if (!(await isAllowed(targetUrl))) { res.writeHead(403, "Forbidden - blocked by safe-chain"); @@ -38,6 +39,14 @@ function createHttpsServer(hostname, isAllowed) { ); } +function getRequestPathAndQuery(url) { + if (url.startsWith("http://") || url.startsWith("https://")) { + const parsedUrl = new URL(url); + return parsedUrl.pathname + parsedUrl.search + parsedUrl.hash; + } + return url; +} + function forwardRequest(req, hostname, res) { const proxyReq = createProxyRequest(hostname, req, res); From bf97f089ca06def64a778a4609ef9cc87ce84327 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 1 Oct 2025 08:36:00 +0200 Subject: [PATCH 06/71] Change npm test version to 10.2.0 --- .github/workflows/test-on-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 59d0549..7b93768 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -46,7 +46,7 @@ jobs: include: # Common production setup - node_version: "20" - npm_version: "10.0.0" + npm_version: "10.2.0" yarn_version: "4.0.0" pnpm_version: "9.0.0" # Current Active LTS with latest tools @@ -66,7 +66,7 @@ jobs: pnpm_version: "latest" # Version pinning scenario - node_version: "22" - npm_version: "10.0.0" + npm_version: "10.2.0" yarn_version: "4.0.0" pnpm_version: "9.0.0" # Backward compatibility testing From 67304751bd1128bac3e7db77e6673bdf80f1226e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 1 Oct 2025 08:53:56 +0200 Subject: [PATCH 07/71] Handle process exit better + some PR cleanup --- packages/safe-chain/bin/aikido-npm.js | 4 +++- packages/safe-chain/bin/aikido-npx.js | 4 +++- packages/safe-chain/bin/aikido-pnpm.js | 4 +++- packages/safe-chain/bin/aikido-pnpx.js | 4 +++- packages/safe-chain/bin/aikido-yarn.js | 4 +++- packages/safe-chain/src/main.js | 2 +- .../packagemanager/npm/dependencyScanner/dryRunScanner.js | 2 +- packages/safe-chain/src/registryProxy/registryProxy.js | 6 ++++-- 8 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index 4176db1..d8b8c3e 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -6,7 +6,9 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa const packageManagerName = "npm"; initializePackageManager(packageManagerName, getNpmVersion()); -await main(process.argv.slice(2)); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); function getNpmVersion() { try { diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index 067608c..7f06c7c 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa const packageManagerName = "npx"; initializePackageManager(packageManagerName, process.versions.node); -await main(process.argv.slice(2)); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index e7bac47..7177159 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa const packageManagerName = "pnpm"; initializePackageManager(packageManagerName, process.versions.node); -await main(process.argv.slice(2)); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index 25884ce..4bb6840 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa const packageManagerName = "pnpx"; initializePackageManager(packageManagerName, process.versions.node); -await main(process.argv.slice(2)); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index a0eaaf6..002a956 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -5,4 +5,6 @@ import { initializePackageManager } from "../src/packagemanager/currentPackageMa const packageManagerName = "yarn"; initializePackageManager(packageManagerName, process.versions.node); -await main(process.argv.slice(2)); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 1d259c9..916a81f 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -27,5 +27,5 @@ export async function main(args) { await proxy.stopServer(); proxy.verifyNoMaliciousPackages(); - process.exit(result.status); + return result.status; } diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js index 59cd236..6189b2f 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js @@ -9,7 +9,7 @@ export function dryRunScanner(scannerOptions) { }; } -async function scanDependencies(scannerOptions, args) { +function scanDependencies(scannerOptions, args) { let dryRunArgs = args; if (scannerOptions?.dryRunCommand) { diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index c2812de..9155c27 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -42,7 +42,7 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { const upperKey = key.toUpperCase(); if (!proxyEnv[upperKey]) { - proxyEnv[upperKey] = env[key]; + proxyEnv[key] = env[key]; } } @@ -104,7 +104,8 @@ function handleConnect(req, clientSocket, head) { async function isAllowedUrl(url) { const { packageName, version } = parsePackageFromUrl(url); - // This happens when the URL is not a tarball download url. + // packageName and version are undefined when the URL is not a package download + // In that case, we can allow the request to proceed if (!packageName || !version) { return true; } @@ -123,6 +124,7 @@ async function isAllowedUrl(url) { function verifyNoMaliciousPackages() { if (state.blockedRequests.length === 0) { + // No malicious packages were blocked, so nothing to block return; } From 49fd0f5928ebe4caa06fcf67b0513edb7e244205 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 1 Oct 2025 09:24:18 +0200 Subject: [PATCH 08/71] Better error-handling when stopping the proxy --- packages/safe-chain/src/registryProxy/registryProxy.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 9155c27..9880129 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -81,9 +81,14 @@ function startServer(server) { function stopServer(server) { return new Promise((resolve) => { - server.close(() => { + try { + server.close(() => { + resolve(); + }); + } catch { resolve(); - }); + } + setTimeout(() => resolve(), 1000); }); } From 60543308f444e5af972f6edf066f40e171034b4f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 1 Oct 2025 10:01:04 +0200 Subject: [PATCH 09/71] Change validity of generateCertForHost to 1 hour. --- packages/safe-chain/src/registryProxy/certUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 99789f6..d5d414c 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -24,7 +24,7 @@ export function generateCertForHost(hostname) { cert.serialNumber = "01"; cert.validity.notBefore = new Date(); cert.validity.notAfter = new Date(); - cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1); + cert.validity.notAfter.setHours(cert.validity.notBefore.getHours() + 1); const attrs = [{ name: "commonName", value: hostname }]; cert.setSubject(attrs); From a6980d5108e589cc712d50ece7d664f18667f663 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 2 Oct 2025 09:06:35 +0200 Subject: [PATCH 10/71] Add upstream proxy support --- package-lock.json | 1 + packages/safe-chain/package.json | 1 + .../src/registryProxy/mitmRequestHandler.js | 6 ++ .../src/registryProxy/tunnelRequestHandler.js | 68 +++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/package-lock.json b/package-lock.json index f111431..6b74d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4886,6 +4886,7 @@ "dependencies": { "abbrev": "3.0.1", "chalk": "5.4.1", + "https-proxy-agent": "7.0.6", "make-fetch-happen": "14.0.3", "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index c74d44e..d28fe73 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -30,6 +30,7 @@ "dependencies": { "abbrev": "3.0.1", "chalk": "5.4.1", + "https-proxy-agent": "7.0.6", "make-fetch-happen": "14.0.3", "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 750815e..4be9987 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -1,5 +1,6 @@ import https from "https"; import { generateCertForHost } from "./certUtils.js"; +import { HttpsProxyAgent } from "https-proxy-agent"; export function mitmConnect(req, clientSocket, isAllowed) { const { hostname } = new URL(`http://${req.url}`); @@ -75,6 +76,11 @@ function createProxyRequest(hostname, req, res) { delete options.headers.host; + const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; + if (httpsProxy) { + options.agent = new HttpsProxyAgent(httpsProxy); + } + const proxyReq = https.request(options, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 8579729..1609664 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -2,6 +2,16 @@ import * as net from "net"; import { ui } from "../environment/userInteraction.js"; export function tunnelRequest(req, clientSocket, head) { + const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; + + if (httpsProxy) { + tunnelRequestViaProxy(req, clientSocket, head, httpsProxy); + } else { + tunnelRequestToDestination(req, clientSocket, head); + } +} + +function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); const serverSocket = net.connect(port || 443, hostname, () => { @@ -18,3 +28,61 @@ export function tunnelRequest(req, clientSocket, head) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); }); } + +function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { + const { port, hostname } = new URL(`http://${req.url}`); + const proxy = new URL(proxyUrl); + + // Connect to proxy server + const proxySocket = net.connect({ + host: proxy.hostname, + port: proxy.port, + }); + + proxySocket.on("connect", () => { + // Send CONNECT request to proxy + const connectRequest = [ + `CONNECT ${hostname}:${port || 443} HTTP/1.1`, + `Host: ${hostname}:${port || 443}`, + "", + "", + ].join("\r\n"); + + proxySocket.write(connectRequest); + }); + + let isConnected = false; + proxySocket.once("data", (data) => { + const response = data.toString(); + + // Check if CONNECT succeeded (HTTP/1.1 200) + if (response.startsWith("HTTP/1.1 200")) { + isConnected = true; + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + proxySocket.write(head); + proxySocket.pipe(clientSocket); + clientSocket.pipe(proxySocket); + } else { + ui.writeError( + `Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}` + ); + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + proxySocket.end(); + } + }); + + proxySocket.on("error", (err) => { + if (!isConnected) { + ui.writeError( + `Safe-chain: error connecting to proxy ${proxy.hostname}:${ + proxy.port || 8080 + } - ${err.message}` + ); + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + } + }); + + clientSocket.on("error", () => { + proxySocket.end(); + }); +} From 53bfb14fea840adf98a37a898d6cd8f9b86ae39a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 2 Oct 2025 09:20:59 +0200 Subject: [PATCH 11/71] Only load the malware database once --- packages/safe-chain/src/scanning/malwareDatabase.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 26f1999..0181e5e 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -8,7 +8,13 @@ import { } from "../config/configFile.js"; import { ui } from "../environment/userInteraction.js"; +let cachedMalwareDatabase = null; + export async function openMalwareDatabase() { + if (cachedMalwareDatabase) { + return cachedMalwareDatabase; + } + const malwareDatabase = await getMalwareDatabase(); function getPackageStatus(name, version) { @@ -25,13 +31,14 @@ export async function openMalwareDatabase() { return packageData.reason; } - return { + cachedMalwareDatabase = { getPackageStatus, isMalware: (name, version) => { const status = getPackageStatus(name, version); return isMalwareStatus(status); }, }; + return cachedMalwareDatabase; } async function getMalwareDatabase() { From 32f5ef9b1678ad9565d49359d73a1193408f47fc Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 2 Oct 2025 10:47:58 +0200 Subject: [PATCH 12/71] Add e2e tests to verify existing proxy is being respected. --- test/e2e/Dockerfile | 3 ++ test/e2e/safe-chain-proxy.e2e.spec.js | 60 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 test/e2e/safe-chain-proxy.e2e.spec.js diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index a84db30..3c8ce73 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -29,6 +29,9 @@ ARG PNPM_VERSION=latest SHELL ["/bin/bash", "-c"] ENV BASH_ENV=~/.bashrc +# Install a proxy +RUN apt-get update && apt-get install tinyproxy -y + # Install zsh RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.1/zsh-in-docker.sh)" # Install fish diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js new file mode 100644 index 0000000..6abbb0f --- /dev/null +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -0,0 +1,60 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: Safe chain proxy", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + // Run a new Docker container for each test + container = new DockerTestContainer(); + await container.start(); + + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + // Stop and clean up the container after each test + if (container) { + await container.stop(); + container = null; + } + }); + + it(`safe-chain proxy respects upstream proxy settings`, async () => { + // Configure and start a proxy inside the container + const proxy = await container.openShell("zsh"); + await proxy.runCommand( + `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` + ); + await proxy.runCommand("tinyproxy"); + + const shell = await container.openShell("zsh"); + await shell.runCommand( + 'export HTTPS_PROXY="http://user:password@localhost:8888"' + ); + const { output } = await shell.runCommand("npm install axios"); + + // Check if the installation was successful + assert( + output.includes("added") || output.includes("up to date"), + "npm install did not complete successfully" + ); + + const proxyLog = await container.openShell("zsh"); + const { output: logOutput } = await proxyLog.runCommand( + "cat /var/log/tinyproxy/tinyproxy.log" + ); + + // Check if the proxy log contains entries for the npm install + assert( + logOutput.includes("CONNECT registry.npmjs.org:443"), + "Proxy log does not contain expected entries" + ); + }); +}); From cc4d20e38010c094515b7bdff3599bed85b9feba Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 2 Oct 2025 15:15:04 +0200 Subject: [PATCH 13/71] Fix line explosion on Windows PowerShell --- .../src/shell-integration/helpers.js | 8 +- .../src/shell-integration/helpers.spec.js | 129 ++++++++++++++---- 2 files changed, 104 insertions(+), 33 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 4137471..23132f0 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -18,15 +18,15 @@ export const knownAikidoTools = [ * Example: "npm, npx, yarn, pnpm, and pnpx commands" */ export function getPackageManagerList() { - const tools = knownAikidoTools.map(t => t.tool); + const tools = knownAikidoTools.map((t) => t.tool); if (tools.length <= 1) { - return `${tools[0] || ''} commands`; + return `${tools[0] || ""} commands`; } if (tools.length === 2) { return `${tools[0]} and ${tools[1]} commands`; } const lastTool = tools.pop(); - return `${tools.join(', ')}, and ${lastTool} commands`; + return `${tools.join(", ")}, and ${lastTool} commands`; } export function doesExecutableExistOnSystem(executableName) { @@ -47,7 +47,7 @@ export function removeLinesMatchingPattern(filePath, pattern, eol) { eol = eol || os.EOL; const fileContent = fs.readFileSync(filePath, "utf-8"); - const lines = fileContent.split(/[\r\n\u2028\u2029]/); + const lines = fileContent.split(/\r?\n|\r|\u2028|\u2029/); const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern)); fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8"); } diff --git a/packages/safe-chain/src/shell-integration/helpers.spec.js b/packages/safe-chain/src/shell-integration/helpers.spec.js index 264cc90..4f18c36 100644 --- a/packages/safe-chain/src/shell-integration/helpers.spec.js +++ b/packages/safe-chain/src/shell-integration/helpers.spec.js @@ -16,8 +16,8 @@ describe("removeLinesMatchingPatternTests", () => { namedExports: { EOL: "\r\n", // Simulate Windows line endings tmpdir: tmpdir, - platform: () => "linux" - } + platform: () => "linux", + }, }); }); @@ -31,54 +31,59 @@ describe("removeLinesMatchingPatternTests", () => { mock.reset(); }); - it("should handle mixed line endings without wiping entire file", async () => { // Import helpers after setting up the mock const { removeLinesMatchingPattern } = await import("./helpers.js"); - + // Create a file with Unix line endings but os.EOL expects Windows const fileContent = [ "# keep this line", - "alias npm='remove-this'", + "alias npm='remove-this'", "# keep this line too", "alias yarn='remove-this-too'", - "# final line to keep" + "# final line to keep", ].join("\n"); // File has Unix line endings - + fs.writeFileSync(testFile, fileContent, "utf-8"); - + // Try to remove lines containing 'alias' const pattern = /alias.*=/; removeLinesMatchingPattern(testFile, pattern); - + const result = fs.readFileSync(testFile, "utf-8"); - + // This test will fail because the function splits on '\r\n' but file uses '\n' // So it treats the entire content as one line and if any part matches, removes everything - assert.ok(result.includes("keep this line"), "Should preserve non-matching lines"); - assert.ok(result.includes("final line to keep"), "Should preserve final line"); + assert.ok( + result.includes("keep this line"), + "Should preserve non-matching lines" + ); + assert.ok( + result.includes("final line to keep"), + "Should preserve final line" + ); }); it("should handle mixed line endings with short matching content", async () => { // Import helpers after setting up the mock const { removeLinesMatchingPattern } = await import("./helpers.js"); - - // Create a file with Unix line endings, but make the entire content short + + // Create a file with Unix line endings, but make the entire content short // to bypass the maxLineLength protection const fileContent = [ "# keep1", "alias x=y", // Short alias line that should be removed - "# keep2" + "# keep2", ].join("\n"); // File has Unix line endings, total length < 100 chars - + fs.writeFileSync(testFile, fileContent, "utf-8"); - + // Try to remove lines containing 'alias' const pattern = /alias/; removeLinesMatchingPattern(testFile, pattern); - + const result = fs.readFileSync(testFile, "utf-8"); - + // This should now be protected by the newline detection assert.ok(result.includes("keep1"), "Should preserve first line"); assert.ok(result.includes("keep2"), "Should preserve third line"); @@ -87,27 +92,93 @@ describe("removeLinesMatchingPatternTests", () => { it("should handle Unicode line separators that bypass newline detection", async () => { // Import helpers after setting up the mock const { removeLinesMatchingPattern } = await import("./helpers.js"); - + // Use Unicode line separator (U+2028) and paragraph separator (U+2029) // These are considered line breaks but aren't \n or \r - const fileContent = [ - "keep this", - "alias test=value", - "keep that" - ].join("\u2028"); // Unicode line separator - + const fileContent = ["keep this", "alias test=value", "keep that"].join( + "\u2028" + ); // Unicode line separator + fs.writeFileSync(testFile, fileContent, "utf-8"); - + // Try to remove lines containing 'alias' const pattern = /alias/; removeLinesMatchingPattern(testFile, pattern); - + const result = fs.readFileSync(testFile, "utf-8"); - + // This could still wipe everything if split() treats it as one line // but the content doesn't contain \n or \r so passes the newline check assert.ok(result.includes("keep this"), "Should preserve first part"); assert.ok(result.includes("keep that"), "Should preserve last part"); }); + it("should handle Windows CRLF line endings without creating empty lines", async () => { + // Import helpers after setting up the mock + const { removeLinesMatchingPattern } = await import("./helpers.js"); + + // Create a file with Windows CRLF line endings + const fileContent = [ + "# comment 1", + "alias npm='aikido-npm'", + "# comment 2", + "export PATH=$PATH:/usr/local/bin", + "", + "# comment 3", + ].join("\r\n"); // Windows line endings + + fs.writeFileSync(testFile, fileContent, "utf-8"); + + // Try to remove lines containing 'alias' + const pattern = /alias/; + removeLinesMatchingPattern(testFile, pattern, "\r\n"); + + const result = fs.readFileSync(testFile, "utf-8"); + + // Should preserve non-matching lines without adding empty lines + assert.ok(result.includes("# comment 1"), "Should preserve first comment"); + assert.ok(result.includes("# comment 2"), "Should preserve second comment"); + assert.ok(result.includes("# comment 3"), "Should preserve third comment"); + assert.ok(result.includes("export PATH"), "Should preserve export line"); + assert.ok(!result.includes("alias npm"), "Should remove alias line"); + + // The key test: when we split on \r\n, we should get exactly 4 lines + // Bug: if split(/[\r\n]/) was used, it creates empty lines between each real line + // because \r\n becomes two separators, resulting in: ["# comment 1", "", "# comment 2", "", "export...", "", "# comment 3", ""] + const resultLines = result.split("\r\n"); + assert.strictEqual(resultLines.length, 5, "Should have exactly 5 lines"); + }); + + it("should not remove empty lines on unix line endings", async () => { + // Import helpers after setting up the mock + const { removeLinesMatchingPattern } = await import("./helpers.js"); + + // Create a file with Unix line endings and empty lines + const fileContent = [ + "# comment 1", + "alias npm='aikido-npm'", + "# comment 2", + "export PATH=$PATH:/usr/local/bin", + "", + "# comment 3", + ].join("\n"); // Unix line endings + + fs.writeFileSync(testFile, fileContent, "utf-8"); + + // Try to remove lines containing 'alias' + const pattern = /alias/; + removeLinesMatchingPattern(testFile, pattern, "\n"); + + const result = fs.readFileSync(testFile, "utf-8"); + + // Should preserve non-matching lines including empty lines + assert.ok(result.includes("# comment 1"), "Should preserve first comment"); + assert.ok(result.includes("# comment 2"), "Should preserve second comment"); + assert.ok(result.includes("# comment 3"), "Should preserve third comment"); + assert.ok(result.includes("export PATH"), "Should preserve export line"); + assert.ok(!result.includes("alias npm"), "Should remove alias line"); + + const resultLines = result.split("\n"); + assert.strictEqual(resultLines.length, 5, "Should have exactly 5 lines"); + }); }); From ccaa7934ee94f4c382fec7cb70d9195b3d962532 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 3 Oct 2025 16:21:55 +0200 Subject: [PATCH 14/71] Improve cli output. --- packages/safe-chain/src/main.js | 8 ++++ packages/safe-chain/src/scanning/index.js | 2 +- .../src/scanning/index.scanCommand.spec.js | 44 +++++++++++-------- test/e2e/npm-ci.e2e.spec.js | 2 +- test/e2e/npm.e2e.spec.js | 2 +- test/e2e/pnpm-ci.e2e.spec.js | 2 +- test/e2e/pnpm.e2e.spec.js | 2 +- test/e2e/yarn-ci.e2e.spec.js | 2 +- test/e2e/yarn.e2e.spec.js | 2 +- 9 files changed, 41 insertions(+), 25 deletions(-) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 916a81f..3d4d5b1 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -5,6 +5,7 @@ import { ui } from "./environment/userInteraction.js"; import { getPackageManager } from "./packagemanager/currentPackageManager.js"; import { initializeCliArguments } from "./config/cliArguments.js"; import { createSafeChainProxy } from "./registryProxy/registryProxy.js"; +import chalk from "chalk"; export async function main(args) { const proxy = createSafeChainProxy(); @@ -27,5 +28,12 @@ export async function main(args) { await proxy.stopServer(); proxy.verifyNoMaliciousPackages(); + ui.emptyLine(); + ui.writeInformation( + `${chalk.green( + "✔" + )} Safe-chain: Command completed, no malicious packages found.` + ); + return result.status; } diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index 48a3e3a..d4f8613 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -61,7 +61,7 @@ export async function scanCommand(args) { } if (!audit || audit.isAllowed) { - spinner.succeed("Safe-chain: No malicious packages detected."); + spinner.stop(); } else { printMaliciousChanges(audit.disallowedChanges, spinner); await onMalwareFound(); diff --git a/packages/safe-chain/src/scanning/index.scanCommand.spec.js b/packages/safe-chain/src/scanning/index.scanCommand.spec.js index 715ecfb..f81a155 100644 --- a/packages/safe-chain/src/scanning/index.scanCommand.spec.js +++ b/packages/safe-chain/src/scanning/index.scanCommand.spec.js @@ -13,6 +13,7 @@ describe("scanCommand", async () => { setText: () => {}, succeed: () => {}, fail: () => {}, + stop: () => {}, })); const mockConfirm = mock.fn(() => true); let malwareAction = MALWARE_ACTION_PROMPT; @@ -88,29 +89,31 @@ describe("scanCommand", async () => { const { scanCommand } = await import("./index.js"); it("should succeed when there are no changes", async () => { - let successMessageWasSet = false; + let progressWasStopped = false; mockStartProcess.mock.mockImplementationOnce(() => ({ setText: () => {}, - succeed: () => { - successMessageWasSet = true; - }, + succeed: () => {}, fail: () => {}, + stop: () => { + progressWasStopped = true; + }, })); mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => []); await scanCommand(["install", "lodash"]); - assert.equal(successMessageWasSet, true); + assert.equal(progressWasStopped, true); }); it("should succeed when changes are not malicious", async () => { - let successMessageWasSet = false; + let progressWasStopped = false; mockStartProcess.mock.mockImplementationOnce(() => ({ setText: () => {}, - succeed: () => { - successMessageWasSet = true; - }, + succeed: () => {}, fail: () => {}, + stop: () => { + progressWasStopped = true; + }, })); mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ { name: "lodash", version: "4.17.21" }, @@ -118,7 +121,7 @@ describe("scanCommand", async () => { await scanCommand(["install", "lodash"]); - assert.equal(successMessageWasSet, true); + assert.equal(progressWasStopped, true); }); it("should throw an error when timing out", async () => { @@ -129,6 +132,7 @@ describe("scanCommand", async () => { fail: () => { failureMessageWasSet = true; }, + stop: () => {}, })); getScanTimeoutMock.mock.mockImplementationOnce(() => 100); mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => { @@ -149,6 +153,7 @@ describe("scanCommand", async () => { fail: () => { failureMessageWasSet = true; }, + stop: () => {}, })); mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ { name: "malicious", version: "1.0.0" }, @@ -173,6 +178,7 @@ describe("scanCommand", async () => { fail: (message) => { failureMessages.push(message); }, + stop: () => {}, })); getScanTimeoutMock.mock.mockImplementationOnce(() => 100); mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => { @@ -194,21 +200,22 @@ describe("scanCommand", async () => { it("should exit immediately when malicious changes are detected in block mode", async () => { // Set malware action to block mode for this test malwareAction = MALWARE_ACTION_BLOCK; - + // Reset mock call count mockConfirm.mock.resetCalls(); - + let failureMessageWasSet = false; let exitCode = null; - + mockStartProcess.mock.mockImplementationOnce(() => ({ setText: () => {}, succeed: () => {}, fail: () => { failureMessageWasSet = true; }, + stop: () => {}, })); - + mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ { name: "malicious", version: "1.0.0" }, ]); @@ -241,19 +248,20 @@ describe("scanCommand", async () => { it("should exit immediately when malicious changes are detected in block mode without prompting", async () => { // Set malware action to block mode for this test malwareAction = MALWARE_ACTION_BLOCK; - + // Reset mock call count mockConfirm.mock.resetCalls(); - + let processExited = false; let userWasPrompted = false; - + mockStartProcess.mock.mockImplementationOnce(() => ({ setText: () => {}, succeed: () => {}, fail: () => {}, + stop: () => {}, })); - + mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ { name: "malicious", version: "1.0.0" }, ]); diff --git a/test/e2e/npm-ci.e2e.spec.js b/test/e2e/npm-ci.e2e.spec.js index 3e08c3d..dc1c23f 100644 --- a/test/e2e/npm-ci.e2e.spec.js +++ b/test/e2e/npm-ci.e2e.spec.js @@ -36,7 +36,7 @@ describe("E2E: npm coverage using PATH", () => { const result = await shell.runCommand("npm i axios"); assert.ok( - result.output.includes("No malicious packages detected."), + result.output.includes("no malicious packages found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/npm.e2e.spec.js b/test/e2e/npm.e2e.spec.js index fc23ebb..c744835 100644 --- a/test/e2e/npm.e2e.spec.js +++ b/test/e2e/npm.e2e.spec.js @@ -31,7 +31,7 @@ describe("E2E: npm coverage", () => { const result = await shell.runCommand("npm i axios"); assert.ok( - result.output.includes("No malicious packages detected."), + result.output.includes("no malicious packages found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/pnpm-ci.e2e.spec.js b/test/e2e/pnpm-ci.e2e.spec.js index 9a8c6a2..339a5e0 100644 --- a/test/e2e/pnpm-ci.e2e.spec.js +++ b/test/e2e/pnpm-ci.e2e.spec.js @@ -36,7 +36,7 @@ describe("E2E: pnpm coverage", () => { const result = await shell.runCommand("pnpm add axios"); assert.ok( - result.output.includes("No malicious packages detected."), + result.output.includes("no malicious packages found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/pnpm.e2e.spec.js b/test/e2e/pnpm.e2e.spec.js index c9460e6..c0187d7 100644 --- a/test/e2e/pnpm.e2e.spec.js +++ b/test/e2e/pnpm.e2e.spec.js @@ -31,7 +31,7 @@ describe("E2E: pnpm coverage", () => { const result = await shell.runCommand("pnpm add axios"); assert.ok( - result.output.includes("No malicious packages detected."), + result.output.includes("no malicious packages found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/yarn-ci.e2e.spec.js b/test/e2e/yarn-ci.e2e.spec.js index 5466851..33ef4f2 100644 --- a/test/e2e/yarn-ci.e2e.spec.js +++ b/test/e2e/yarn-ci.e2e.spec.js @@ -36,7 +36,7 @@ describe("E2E: yarn coverage", () => { const result = await shell.runCommand("yarn add axios"); assert.ok( - result.output.includes("No malicious packages detected."), + result.output.includes("no malicious packages found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/yarn.e2e.spec.js b/test/e2e/yarn.e2e.spec.js index df8a88b..3909318 100644 --- a/test/e2e/yarn.e2e.spec.js +++ b/test/e2e/yarn.e2e.spec.js @@ -31,7 +31,7 @@ describe("E2E: yarn coverage", () => { const result = await shell.runCommand("yarn add axios"); assert.ok( - result.output.includes("No malicious packages detected."), + result.output.includes("no malicious packages found."), `Output did not include expected text. Output was:\n${result.output}` ); }); From 3ef4ed8bad50383b0d90b0c02027b3d3d9e86b34 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 6 Oct 2025 13:47:38 +0200 Subject: [PATCH 15/71] Update main.js code flow so proxy always gets stopped + add comment on why exit status is handled in bin/aikido-(tool).js --- packages/safe-chain/src/main.js | 44 ++++++++++++++--------- packages/safe-chain/src/scanning/index.js | 2 ++ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 3d4d5b1..da36a59 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -16,24 +16,36 @@ export async function main(args) { args = initializeCliArguments(args); if (shouldScanCommand(args)) { - await scanCommand(args); + const resultCode = await scanCommand(args); + + // Returning the exit code back to the caller allows the promise + // to be awaited in the bin files and return the correct exit code + if (resultCode !== 0) { + return resultCode; + } } + + var result = await getPackageManager().runCommand(args); + + proxy.verifyNoMaliciousPackages(); + + ui.emptyLine(); + ui.writeInformation( + `${chalk.green( + "✔" + )} Safe-chain: Command completed, no malicious packages found.` + ); + + // Returning the exit code back to the caller allows the promise + // to be awaited in the bin files and return the correct exit code + return result.status; } catch (error) { ui.writeError("Failed to check for malicious packages:", error.message); - process.exit(1); + + // Returning the exit code back to the caller allows the promise + // to be awaited in the bin files and return the correct exit code + return 1; + } finally { + await proxy.stopServer(); } - - var result = await getPackageManager().runCommand(args); - - await proxy.stopServer(); - proxy.verifyNoMaliciousPackages(); - - ui.emptyLine(); - ui.writeInformation( - `${chalk.green( - "✔" - )} Safe-chain: Command completed, no malicious packages found.` - ); - - return result.status; } diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index d4f8613..91314ae 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -62,9 +62,11 @@ export async function scanCommand(args) { if (!audit || audit.isAllowed) { spinner.stop(); + return 0; } else { printMaliciousChanges(audit.disallowedChanges, spinner); await onMalwareFound(); + return 1; } } From ea383a18de63edc309fad4464465c306058af783 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 6 Oct 2025 16:23:56 +0200 Subject: [PATCH 16/71] Insert proxy settings for npx as well --- .../src/packagemanager/npx/runNpxCommand.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js index cc78abb..b8896b7 100644 --- a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js +++ b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js @@ -1,10 +1,14 @@ -import { execSync } from "child_process"; import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -export function runNpx(args) { +export async function runNpx(args) { try { - const npxCommand = `npx ${args.join(" ")}`; - execSync(npxCommand, { stdio: "inherit" }); + const result = await safeSpawn("npx", args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); + return { status: result.status }; } catch (error) { if (error.status) { return { status: error.status }; @@ -13,5 +17,4 @@ export function runNpx(args) { return { status: 1 }; } } - return { status: 0 }; } From 28ccb550335d1346f183c60cd7ab8152ac0ee598 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 6 Oct 2025 16:55:46 +0200 Subject: [PATCH 17/71] Use safe-chain ourselves in CI/CD --- .github/workflows/build-and-release.yml | 5 +++++ .github/workflows/test-on-pr.yml | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 213d1f9..987db03 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -21,6 +21,11 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + - name: Setup safe-chain + run: | + npm i -g @aikidosec/safe-chain + safe-chain setup-ci + - name: Set version number id: get_version run: | diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 740d741..00da7e0 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -17,6 +17,11 @@ jobs: with: node-version: "lts/*" + - name: Setup safe-chain + run: | + npm i -g @aikidosec/safe-chain + safe-chain setup-ci + - name: Install dependencies run: npm ci @@ -94,6 +99,11 @@ jobs: with: node-version: "lts/*" + - name: Setup safe-chain + run: | + npm i -g @aikidosec/safe-chain + safe-chain setup-ci + - name: Install dependencies (root) run: npm ci From 240123372ab9ac0b469301ea5a0eec274b87bf70 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 10:49:04 +0200 Subject: [PATCH 18/71] Handle PR Comments --- packages/safe-chain/src/main.js | 14 ++++++++------ .../safe-chain/src/registryProxy/registryProxy.js | 13 +++++++++---- packages/safe-chain/src/scanning/index.js | 7 +++---- .../safe-chain/src/scanning/malwareDatabase.js | 2 ++ packages/safe-chain/src/utils/safeSpawn.js | 2 ++ 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index da36a59..e106e83 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -16,18 +16,20 @@ export async function main(args) { args = initializeCliArguments(args); if (shouldScanCommand(args)) { - const resultCode = await scanCommand(args); + const commandScanResult = await scanCommand(args); // Returning the exit code back to the caller allows the promise // to be awaited in the bin files and return the correct exit code - if (resultCode !== 0) { - return resultCode; + if (commandScanResult !== 0) { + return commandScanResult; } } - var result = await getPackageManager().runCommand(args); + const packageManagerResult = await getPackageManager().runCommand(args); - proxy.verifyNoMaliciousPackages(); + if (!proxy.verifyNoMaliciousPackages()) { + return 1; + } ui.emptyLine(); ui.writeInformation( @@ -38,7 +40,7 @@ export async function main(args) { // Returning the exit code back to the caller allows the promise // to be awaited in the bin files and return the correct exit code - return result.status; + return packageManagerResult.status; } catch (error) { ui.writeError("Failed to check for malicious packages:", error.message); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 9880129..3558673 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -7,6 +7,7 @@ import { knownRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; +const SERVER_STOP_TIMEOUT_MS = 1000; const state = { port: null, blockedRequests: [], @@ -19,12 +20,15 @@ export function createSafeChainProxy() { return { startServer: () => startServer(server), stopServer: () => stopServer(server), - getBlockedRequests: () => state.blockedRequests, verifyNoMaliciousPackages, }; } function getSafeChainProxyEnvironmentVariables() { + if (!state.port) { + return {}; + } + return { HTTPS_PROXY: `http://localhost:${state.port}`, GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`, @@ -63,6 +67,7 @@ function createProxyServer() { function startServer(server) { return new Promise((resolve, reject) => { + // Passing port 0 makes the OS assign an available port server.listen(0, () => { const address = server.address(); if (address && typeof address === "object") { @@ -88,7 +93,7 @@ function stopServer(server) { } catch { resolve(); } - setTimeout(() => resolve(), 1000); + setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS); }); } @@ -130,7 +135,7 @@ async function isAllowedUrl(url) { function verifyNoMaliciousPackages() { if (state.blockedRequests.length === 0) { // No malicious packages were blocked, so nothing to block - return; + return true; } ui.emptyLine(); @@ -149,5 +154,5 @@ function verifyNoMaliciousPackages() { ui.writeError("Exiting without installing malicious packages."); ui.emptyLine(); - process.exit(1); + return false; } diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index 91314ae..36f62ca 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -65,8 +65,7 @@ export async function scanCommand(args) { return 0; } else { printMaliciousChanges(audit.disallowedChanges, spinner); - await onMalwareFound(); - return 1; + return await onMalwareFound(); } } @@ -90,11 +89,11 @@ async function onMalwareFound() { if (continueInstall) { ui.writeWarning("Continuing with the installation despite the risks..."); - return; + return 0; } } ui.writeError("Exiting without installing malicious packages."); ui.emptyLine(); - process.exit(1); + return 1; } diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 0181e5e..1cb781b 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -31,6 +31,8 @@ export async function openMalwareDatabase() { return packageData.reason; } + // This implicitely caches the malware database + // that's closed over by the getPackageStatus function cachedMalwareDatabase = { getPackageStatus, isMalware: (name, version) => { diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 5af4740..c5cd913 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -22,6 +22,8 @@ export async function safeSpawn(command, args, options = {}) { const fullCommand = buildCommand(command, args); return new Promise((resolve, reject) => { const child = spawn(fullCommand, { ...options, shell: true }); + + // When stdio is piped, we need to collect the output let stdout = ""; let stderr = ""; From 8950d528d57893f5486406afd24fb3cdfdb1a204 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 10:56:31 +0200 Subject: [PATCH 19/71] Fix tests to match new behavior --- .../src/scanning/index.scanCommand.spec.js | 51 ++++--------------- 1 file changed, 10 insertions(+), 41 deletions(-) diff --git a/packages/safe-chain/src/scanning/index.scanCommand.spec.js b/packages/safe-chain/src/scanning/index.scanCommand.spec.js index f81a155..1858d10 100644 --- a/packages/safe-chain/src/scanning/index.scanCommand.spec.js +++ b/packages/safe-chain/src/scanning/index.scanCommand.spec.js @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { describe, it, mock } from "node:test"; +import { beforeEach, describe, it, mock } from "node:test"; import { setTimeout } from "node:timers/promises"; import { MALWARE_ACTION_PROMPT, @@ -88,6 +88,11 @@ describe("scanCommand", async () => { const { scanCommand } = await import("./index.js"); + beforeEach(() => { + // Reset malware action back to prompt mode for other tests + malwareAction = MALWARE_ACTION_PROMPT; + }); + it("should succeed when there are no changes", async () => { let progressWasStopped = false; mockStartProcess.mock.mockImplementationOnce(() => ({ @@ -205,7 +210,6 @@ describe("scanCommand", async () => { mockConfirm.mock.resetCalls(); let failureMessageWasSet = false; - let exitCode = null; mockStartProcess.mock.mockImplementationOnce(() => ({ setText: () => {}, @@ -220,27 +224,10 @@ describe("scanCommand", async () => { { name: "malicious", version: "1.0.0" }, ]); - // Mock process.exit - const originalExit = process.exit; - process.exit = mock.fn((code) => { - exitCode = code; - throw new Error("Process exit called"); // Prevent actual exit - }); - - try { - await assert.rejects( - scanCommand(["install", "malicious"]), - /Process exit called/ - ); - } finally { - // Restore original process.exit - process.exit = originalExit; - // Reset malware action back to prompt mode for other tests - malwareAction = MALWARE_ACTION_PROMPT; - } + const result = await scanCommand(["install", "malicious"]); assert.equal(failureMessageWasSet, true); - assert.equal(exitCode, 1); + assert.equal(result, 1); // Confirm should not have been called in block mode assert.equal(mockConfirm.mock.callCount(), 0); }); @@ -252,7 +239,6 @@ describe("scanCommand", async () => { // Reset mock call count mockConfirm.mock.resetCalls(); - let processExited = false; let userWasPrompted = false; mockStartProcess.mock.mockImplementationOnce(() => ({ @@ -271,26 +257,9 @@ describe("scanCommand", async () => { return false; }); - // Mock process.exit - const originalExit = process.exit; - process.exit = mock.fn(() => { - processExited = true; - throw new Error("Process exit called"); // Prevent actual exit - }); + const result = await scanCommand(["install", "malicious"]); - try { - await assert.rejects( - scanCommand(["install", "malicious"]), - /Process exit called/ - ); - } finally { - // Restore original process.exit - process.exit = originalExit; - // Reset malware action back to prompt mode for other tests - malwareAction = MALWARE_ACTION_PROMPT; - } - - assert.equal(processExited, true); + assert.equal(result, 1); assert.equal(userWasPrompted, false); }); }); From 16c76de0f3d066a53d65a039a6fa05189afbf416 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 11:38:21 +0200 Subject: [PATCH 20/71] Add comment on how safe-chain works with the system proxy. --- .../src/registryProxy/tunnelRequestHandler.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 1609664..95e2beb 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -5,6 +5,16 @@ export function tunnelRequest(req, clientSocket, head) { const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; if (httpsProxy) { + // If an HTTPS proxy is set, tunnel the request via the proxy + // This is the system proxy, not the safe-chain proxy + // The package manager will run via the safe-chain proxy + // The safe-chain proxy will then send the request to the system proxy + // Typical flow: package manager -> safe-chain proxy -> system proxy -> destination + + // There are 2 processes involved in this: + // 1. Safe-chain process: has HTTPS_PROXY set to system proxy + // 2. Package manager process: has HTTPS_PROXY set to safe-chain proxy + tunnelRequestViaProxy(req, clientSocket, head, httpsProxy); } else { tunnelRequestToDestination(req, clientSocket, head); From 43dcba88020ffea2fcc61ad727ca35f67fd9704c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 15:12:06 +0200 Subject: [PATCH 21/71] Wrap bun with safe-chain to block downloads of packages with malware --- packages/safe-chain/bin/aikido-bun.js | 10 +++ packages/safe-chain/bin/aikido-bunx.js | 10 +++ packages/safe-chain/package.json | 2 + .../bun/createBunPackageManager.js | 42 ++++++++++ .../packagemanager/currentPackageManager.js | 8 ++ .../src/shell-integration/helpers.js | 11 +-- .../startup-scripts/init-fish.fish | 8 ++ .../startup-scripts/init-posix.sh | 8 ++ .../startup-scripts/init-pwsh.ps1 | 8 ++ test/e2e/Dockerfile | 3 + test/e2e/bun.e2e.spec.js | 79 +++++++++++++++++++ 11 files changed, 184 insertions(+), 5 deletions(-) create mode 100755 packages/safe-chain/bin/aikido-bun.js create mode 100755 packages/safe-chain/bin/aikido-bunx.js create mode 100644 packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js create mode 100644 test/e2e/bun.e2e.spec.js diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js new file mode 100755 index 0000000..01e3972 --- /dev/null +++ b/packages/safe-chain/bin/aikido-bun.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; + +const packageManagerName = "bun"; +initializePackageManager(packageManagerName); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js new file mode 100755 index 0000000..fb378e5 --- /dev/null +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; + +const packageManagerName = "bunx"; +initializePackageManager(packageManagerName); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index d28fe73..c0d2115 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -12,6 +12,8 @@ "aikido-yarn": "bin/aikido-yarn.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", + "aikido-bun": "bin/aikido-bun.js", + "aikido-bunx": "bin/aikido-bunx.js", "safe-chain": "bin/safe-chain.js" }, "type": "module", diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js new file mode 100644 index 0000000..14faa5f --- /dev/null +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -0,0 +1,42 @@ +import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; + +export function createBunPackageManager() { + return { + runCommand: (args) => runBunCommand("bun", args), + + // For bun, we use the proxy-only approach to block package downloads, + // so we don't need to analyze commands. + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], + }; +} + +export function createBunxPackageManager() { + return { + runCommand: (args) => runBunCommand("bunx", args), + + // For bunx, we use the proxy-only approach to block package downloads, + // so we don't need to analyze commands. + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], + }; +} + +async function runBunCommand(command, args) { + try { + const result = await safeSpawn(command, args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); + return { status: result.status }; + } catch (error) { + if (error.status) { + return { status: error.status }; + } else { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + } +} diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 9497a20..2a10d86 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -1,3 +1,7 @@ +import { + createBunPackageManager, + createBunxPackageManager, +} from "./bun/createBunPackageManager.js"; import { createNpmPackageManager } from "./npm/createPackageManager.js"; import { createNpxPackageManager } from "./npx/createPackageManager.js"; import { @@ -21,6 +25,10 @@ export function initializePackageManager(packageManagerName, version) { state.packageManagerName = createPnpmPackageManager(); } else if (packageManagerName === "pnpx") { state.packageManagerName = createPnpxPackageManager(); + } else if (packageManagerName === "bun") { + state.packageManagerName = createBunPackageManager(); + } else if (packageManagerName === "bunx") { + state.packageManagerName = createBunxPackageManager(); } else { throw new Error("Unsupported package manager: " + packageManagerName); } diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 4137471..b7fdc9c 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -9,8 +9,9 @@ export const knownAikidoTools = [ { tool: "yarn", aikidoCommand: "aikido-yarn" }, { tool: "pnpm", aikidoCommand: "aikido-pnpm" }, { tool: "pnpx", aikidoCommand: "aikido-pnpx" }, - // When adding a new tool here, also update the expected alias in the tests (setup.spec.js, teardown.spec.js) - // and add the documentation for the new tool in the README.md + { tool: "bun", aikidoCommand: "aikido-bun" }, + { tool: "bunx", aikidoCommand: "aikido-bunx" }, + // When adding a new tool here, also update the documentation for the new tool in the README.md ]; /** @@ -18,15 +19,15 @@ export const knownAikidoTools = [ * Example: "npm, npx, yarn, pnpm, and pnpx commands" */ export function getPackageManagerList() { - const tools = knownAikidoTools.map(t => t.tool); + const tools = knownAikidoTools.map((t) => t.tool); if (tools.length <= 1) { - return `${tools[0] || ''} commands`; + return `${tools[0] || ""} commands`; } if (tools.length === 2) { return `${tools[0]} and ${tools[1]} commands`; } const lastTool = tools.pop(); - return `${tools.join(', ')}, and ${lastTool} commands`; + return `${tools.join(", ")}, and ${lastTool} commands`; } export function doesExecutableExistOnSystem(executableName) { diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish index 87f6a79..29d6bf3 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish @@ -46,6 +46,14 @@ function pnpx wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv end +function bun + wrapSafeChainCommand "bun" "aikido-bun" $argv +end + +function bunx + wrapSafeChainCommand "bunx" "aikido-bunx" $argv +end + function npm # If args is just -v or --version and nothing else, just run the `npm -v` command # This is because nvm uses this to check the version of npm diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index 01b23c4..353c6c0 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -42,6 +42,14 @@ function pnpx() { wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@" } +function bun() { + wrapSafeChainCommand "bun" "aikido-bun" "$@" +} + +function bunx() { + wrapSafeChainCommand "bunx" "aikido-bunx" "$@" +} + function npm() { if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then # If args is just -v or --version and nothing else, just run the npm version command diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index 7fb44d6..a449405 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -68,6 +68,14 @@ function pnpx { Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args } +function bun { + Invoke-WrappedCommand "bun" "aikido-bun" $args +} + +function bunx { + Invoke-WrappedCommand "bunx" "aikido-bunx" $args +} + function npm { # If args is just -v or --version and nothing else, just run the npm version command # This is because nvm uses this to check the version of npm diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 3c8ce73..484f5fe 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -46,6 +46,9 @@ RUN volta install npm@${NPM_VERSION} RUN volta install yarn@${YARN_VERSION} RUN volta install pnpm@${PNPM_VERSION} +# Install Bun +RUN curl -fsSL https://bun.sh/install | bash + # Copy and install Safe chain COPY --from=builder /app/*.tgz /pkgs/ RUN npm install -g /pkgs/*.tgz diff --git a/test/e2e/bun.e2e.spec.js b/test/e2e/bun.e2e.spec.js new file mode 100644 index 0000000..8dea93b --- /dev/null +++ b/test/e2e/bun.e2e.spec.js @@ -0,0 +1,79 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: bun coverage", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + // Run a new Docker container for each test + container = new DockerTestContainer(); + await container.start(); + + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + // Stop and clean up the container after each test + if (container) { + await container.stop(); + container = null; + } + }); + + it(`safe-chain succesfully installs safe packages`, async () => { + const shell = await container.openShell("bash"); + const result = await shell.runCommand("bun i axios"); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`safe-chain blocks download of malicious packages already in package.json`, async () => { + const shell = await container.openShell("bash"); + await shell.runCommand( + 'echo \'{"name":"test-project","version":"1.0.0","dependencies":{"safe-chain-test":"0.0.1-security"}}\' > package.json' + ); + + var result = await shell.runCommand("bun install"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it("safe-chain blocks bunx from downloading malicious packages", async () => { + const shell = await container.openShell("bash"); + + const result = await shell.runCommand("bunx safe-chain-test"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); +}); From d737abd24adbb4922186ec3b10a7c03977fc50d8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 16:25:56 +0200 Subject: [PATCH 22/71] Update readme for version 1.1.0 --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d36f1a0..d173650 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Aikido Safe Chain +> 🚀 **Version 1.1.0: Full Package Manager Support** +> +> Starting from version 1.1.0, Aikido Safe Chain now provides complete protection for all package managers. We've changed how we block malicious packages: instead of checking which packages are being installed, we run a lightweight proxy server that intercepts and blocks downloads of packages containing malware. This means full dependency tree protection for all package managers, not just npm. + The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx. It's **free** to use and does not require any token. The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm or pnpx from downloading or running the malware. @@ -8,16 +12,12 @@ The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [n Aikido Safe Chain works on Node.js version 18 and above and supports the following package managers: -- ✅ full coverage: **npm >= 10.4.0**: -- ⚠️ limited to scanning the install command arguments (broader scanning coming soon): - - **npm < 10.4.0** - - **npx** - - **yarn** - - **pnpm** - - **pnpx** -- 🚧 **bun**: coming soon - -Note on the limited support for npm < 10.4.0, npx, yarn, pnpm and pnpx: adding **full support for these package managers is a high priority**. In the meantime, we offer limited support already, which means that the Aikido Safe Chain will scan the package names passed as arguments to the install commands. However, it will not scan the full dependency tree of these packages. +- ✅ **npm**: +- ✅ **npx** +- ✅ **yarn** +- ✅ **pnpm** +- ✅ **pnpx** +- ✅ **bun** # Usage From b08b4e2d4ec332f2ef4a11aec205613906051240 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 15:12:06 +0200 Subject: [PATCH 23/71] Wrap bun with safe-chain to block downloads of packages with malware --- packages/safe-chain/bin/aikido-bun.js | 10 +++ packages/safe-chain/bin/aikido-bunx.js | 10 +++ packages/safe-chain/package.json | 2 + .../bun/createBunPackageManager.js | 42 ++++++++++ .../packagemanager/currentPackageManager.js | 8 ++ .../src/shell-integration/helpers.js | 5 +- .../startup-scripts/init-fish.fish | 8 ++ .../startup-scripts/init-posix.sh | 8 ++ .../startup-scripts/init-pwsh.ps1 | 8 ++ test/e2e/Dockerfile | 3 + test/e2e/bun.e2e.spec.js | 79 +++++++++++++++++++ 11 files changed, 181 insertions(+), 2 deletions(-) create mode 100755 packages/safe-chain/bin/aikido-bun.js create mode 100755 packages/safe-chain/bin/aikido-bunx.js create mode 100644 packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js create mode 100644 test/e2e/bun.e2e.spec.js diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js new file mode 100755 index 0000000..01e3972 --- /dev/null +++ b/packages/safe-chain/bin/aikido-bun.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; + +const packageManagerName = "bun"; +initializePackageManager(packageManagerName); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js new file mode 100755 index 0000000..fb378e5 --- /dev/null +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; + +const packageManagerName = "bunx"; +initializePackageManager(packageManagerName); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index d28fe73..c0d2115 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -12,6 +12,8 @@ "aikido-yarn": "bin/aikido-yarn.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", + "aikido-bun": "bin/aikido-bun.js", + "aikido-bunx": "bin/aikido-bunx.js", "safe-chain": "bin/safe-chain.js" }, "type": "module", diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js new file mode 100644 index 0000000..14faa5f --- /dev/null +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -0,0 +1,42 @@ +import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; + +export function createBunPackageManager() { + return { + runCommand: (args) => runBunCommand("bun", args), + + // For bun, we use the proxy-only approach to block package downloads, + // so we don't need to analyze commands. + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], + }; +} + +export function createBunxPackageManager() { + return { + runCommand: (args) => runBunCommand("bunx", args), + + // For bunx, we use the proxy-only approach to block package downloads, + // so we don't need to analyze commands. + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], + }; +} + +async function runBunCommand(command, args) { + try { + const result = await safeSpawn(command, args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); + return { status: result.status }; + } catch (error) { + if (error.status) { + return { status: error.status }; + } else { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + } +} diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 9497a20..2a10d86 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -1,3 +1,7 @@ +import { + createBunPackageManager, + createBunxPackageManager, +} from "./bun/createBunPackageManager.js"; import { createNpmPackageManager } from "./npm/createPackageManager.js"; import { createNpxPackageManager } from "./npx/createPackageManager.js"; import { @@ -21,6 +25,10 @@ export function initializePackageManager(packageManagerName, version) { state.packageManagerName = createPnpmPackageManager(); } else if (packageManagerName === "pnpx") { state.packageManagerName = createPnpxPackageManager(); + } else if (packageManagerName === "bun") { + state.packageManagerName = createBunPackageManager(); + } else if (packageManagerName === "bunx") { + state.packageManagerName = createBunxPackageManager(); } else { throw new Error("Unsupported package manager: " + packageManagerName); } diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 23132f0..2345022 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -9,8 +9,9 @@ export const knownAikidoTools = [ { tool: "yarn", aikidoCommand: "aikido-yarn" }, { tool: "pnpm", aikidoCommand: "aikido-pnpm" }, { tool: "pnpx", aikidoCommand: "aikido-pnpx" }, - // When adding a new tool here, also update the expected alias in the tests (setup.spec.js, teardown.spec.js) - // and add the documentation for the new tool in the README.md + { tool: "bun", aikidoCommand: "aikido-bun" }, + { tool: "bunx", aikidoCommand: "aikido-bunx" }, + // When adding a new tool here, also update the documentation for the new tool in the README.md ]; /** diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish index 87f6a79..29d6bf3 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish @@ -46,6 +46,14 @@ function pnpx wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv end +function bun + wrapSafeChainCommand "bun" "aikido-bun" $argv +end + +function bunx + wrapSafeChainCommand "bunx" "aikido-bunx" $argv +end + function npm # If args is just -v or --version and nothing else, just run the `npm -v` command # This is because nvm uses this to check the version of npm diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index 01b23c4..353c6c0 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -42,6 +42,14 @@ function pnpx() { wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@" } +function bun() { + wrapSafeChainCommand "bun" "aikido-bun" "$@" +} + +function bunx() { + wrapSafeChainCommand "bunx" "aikido-bunx" "$@" +} + function npm() { if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then # If args is just -v or --version and nothing else, just run the npm version command diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index 7fb44d6..a449405 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -68,6 +68,14 @@ function pnpx { Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args } +function bun { + Invoke-WrappedCommand "bun" "aikido-bun" $args +} + +function bunx { + Invoke-WrappedCommand "bunx" "aikido-bunx" $args +} + function npm { # If args is just -v or --version and nothing else, just run the npm version command # This is because nvm uses this to check the version of npm diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 3c8ce73..484f5fe 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -46,6 +46,9 @@ RUN volta install npm@${NPM_VERSION} RUN volta install yarn@${YARN_VERSION} RUN volta install pnpm@${PNPM_VERSION} +# Install Bun +RUN curl -fsSL https://bun.sh/install | bash + # Copy and install Safe chain COPY --from=builder /app/*.tgz /pkgs/ RUN npm install -g /pkgs/*.tgz diff --git a/test/e2e/bun.e2e.spec.js b/test/e2e/bun.e2e.spec.js new file mode 100644 index 0000000..8dea93b --- /dev/null +++ b/test/e2e/bun.e2e.spec.js @@ -0,0 +1,79 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: bun coverage", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + // Run a new Docker container for each test + container = new DockerTestContainer(); + await container.start(); + + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + // Stop and clean up the container after each test + if (container) { + await container.stop(); + container = null; + } + }); + + it(`safe-chain succesfully installs safe packages`, async () => { + const shell = await container.openShell("bash"); + const result = await shell.runCommand("bun i axios"); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`safe-chain blocks download of malicious packages already in package.json`, async () => { + const shell = await container.openShell("bash"); + await shell.runCommand( + 'echo \'{"name":"test-project","version":"1.0.0","dependencies":{"safe-chain-test":"0.0.1-security"}}\' > package.json' + ); + + var result = await shell.runCommand("bun install"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it("safe-chain blocks bunx from downloading malicious packages", async () => { + const shell = await container.openShell("bash"); + + const result = await shell.runCommand("bunx safe-chain-test"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); +}); From 41e88d422ea8a4df81caca059b8b3a99dfd95d64 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 16:34:26 +0200 Subject: [PATCH 24/71] Add mention of bun everywhere --- README.md | 15 ++++++++------- docs/shell-integration.md | 8 ++++---- packages/safe-chain/package.json | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d173650..b2d1fa4 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,21 @@ > > Starting from version 1.1.0, Aikido Safe Chain now provides complete protection for all package managers. We've changed how we block malicious packages: instead of checking which packages are being installed, we run a lightweight proxy server that intercepts and blocks downloads of packages containing malware. This means full dependency tree protection for all package managers, not just npm. -The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx. It's **free** to use and does not require any token. +The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm, pnpx, bun, and bunx. It's **free** to use and does not require any token. -The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm or pnpx from downloading or running the malware. +The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), and [bunx](https://bun.sh/docs/cli/bunx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, or bunx from downloading or running the malware. ![demo](./docs/safe-package-manager-demo.png) Aikido Safe Chain works on Node.js version 18 and above and supports the following package managers: -- ✅ **npm**: +- ✅ **npm** - ✅ **npx** - ✅ **yarn** - ✅ **pnpm** - ✅ **pnpx** - ✅ **bun** +- ✅ **bunx** # Usage @@ -34,20 +35,20 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: safe-chain setup ``` 3. **❗Restart your terminal** to start using the Aikido Safe Chain. - - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm and pnpx are loaded correctly. If you do not restart your terminal, the aliases will not be available. + - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, and bunx are loaded correctly. If you do not restart your terminal, the aliases will not be available. 4. **Verify the installation** by running: ```shell npm install safe-chain-test ``` - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. -When running `npm`, `npx`, `yarn`, `pnpm` or `pnpx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. +When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, or `bunx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. ## How it works -The Aikido Safe Chain works by intercepting the npm, npx, yarn, pnpm and pnpx commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. +The Aikido Safe Chain works by intercepting the npm, npx, yarn, pnpm, pnpx, bun, and bunx commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. -The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm and pnpx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support: +The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, and bunx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support: - ✅ **Bash** - ✅ **Zsh** diff --git a/docs/shell-integration.md b/docs/shell-integration.md index 6b2c79e..4a6ac99 100644 --- a/docs/shell-integration.md +++ b/docs/shell-integration.md @@ -2,7 +2,7 @@ ## Overview -The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents. +The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents. ## Supported Shells @@ -28,7 +28,7 @@ This command: - Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`) - Detects all supported shells on your system -- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, and `pnpx` +- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, and `bunx` ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly. @@ -77,7 +77,7 @@ The system modifies the following files to source Safe Chain startup scripts: This means the shell functions are working but the Aikido commands aren't installed or available in your PATH: - Make sure Aikido Safe Chain is properly installed on your system -- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` and `aikido-pnpx` commands exist +- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, and `aikido-bunx` commands exist - Check that these commands are in your system's PATH ### Manual Verification @@ -120,4 +120,4 @@ npm() { } ``` -Repeat this pattern for `npx`, `yarn`, `pnpm`, and `pnpx` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes. +Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, and `bunx` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes. diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index c0d2115..6a927d4 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -28,7 +28,7 @@ "keywords": [], "author": "Aikido Security", "license": "AGPL-3.0-or-later", - "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, or pnpx from downloading or running the malware.", + "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), and [bunx](https://bun.sh/docs/cli/bunx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, or bunx from downloading or running the malware.", "dependencies": { "abbrev": "3.0.1", "chalk": "5.4.1", From 79a2186c1f144fdd9ade2f84b1fde2cffcd0e6f5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 16:42:56 +0200 Subject: [PATCH 25/71] Mention proxy in "how it works" --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b2d1fa4..fd2cdff 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # Aikido Safe Chain -> 🚀 **Version 1.1.0: Full Package Manager Support** -> -> Starting from version 1.1.0, Aikido Safe Chain now provides complete protection for all package managers. We've changed how we block malicious packages: instead of checking which packages are being installed, we run a lightweight proxy server that intercepts and blocks downloads of packages containing malware. This means full dependency tree protection for all package managers, not just npm. - The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm, pnpx, bun, and bunx. It's **free** to use and does not require any token. The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), and [bunx](https://bun.sh/docs/cli/bunx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, or bunx from downloading or running the malware. @@ -46,9 +42,9 @@ When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, or `bunx` commands, th ## How it works -The Aikido Safe Chain works by intercepting the npm, npx, yarn, pnpm, pnpx, bun, and bunx commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. +The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry. When you run npm, npx, yarn, pnpm, pnpx, bun, or bunx commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine. -The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, and bunx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support: +The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, and bunx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support: - ✅ **Bash** - ✅ **Zsh** From 219a189993fc30e3b4dc98b27bf1f965a404ec6e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 19:32:25 +0200 Subject: [PATCH 26/71] Check if a socket is writable before writing to it --- .../src/registryProxy/tunnelRequestHandler.js | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 95e2beb..fa12aee 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -35,7 +35,9 @@ function tunnelRequestToDestination(req, clientSocket, head) { ui.writeError( `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}` ); - clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + if (clientSocket.writable) { + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + } }); } @@ -76,8 +78,12 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { ui.writeError( `Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}` ); - clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); - proxySocket.end(); + if (clientSocket.writable) { + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + } + if (proxySocket.writable) { + proxySocket.end(); + } } }); @@ -88,11 +94,15 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { proxy.port || 8080 } - ${err.message}` ); - clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + if (clientSocket.writable) { + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + } } }); clientSocket.on("error", () => { - proxySocket.end(); + if (proxySocket.writable) { + proxySocket.end(); + } }); } From abc0add350b41783c67f6ae39e6a2474f2163e90 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 8 Oct 2025 19:43:11 +0200 Subject: [PATCH 27/71] Downgrade safe-chain in e2e tests to 1.0.24 --- .github/workflows/test-on-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index c31138c..5661f97 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -101,7 +101,7 @@ jobs: - name: Setup safe-chain run: | - npm i -g @aikidosec/safe-chain + npm i -g @aikidosec/safe-chain@1.0.24 safe-chain setup-ci - name: Install dependencies (root) From d5620b2d126fc5483ad99a8485932795ae254946 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 9 Oct 2025 14:58:06 +0200 Subject: [PATCH 28/71] Don't set YARN_HTTPS_CA_FILE_PATH, it ignores all system CAs --- .../safe-chain/src/packagemanager/yarn/runYarnCommand.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 2c3795c..65c27a0 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -23,7 +23,9 @@ export async function runYarnCommand(args) { } async function fixYarnProxyEnvironmentVariables(env) { - // Yarn ignores standard proxy environment variables HTTPS_PROXY and NODE_EXTRA_CA_CERTS + // Yarn ignores standard proxy environment variable HTTPS_PROXY + // It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though. + // Don't use YARN_HTTPS_CA_FILE_PATH though, as it causes to ignore all system CAs // Yarn v2/v3 and v4+ use different environment variables for proxy and CA certs // When setting all variables, yarn returns an error about conflicting variables @@ -35,10 +37,8 @@ async function fixYarnProxyEnvironmentVariables(env) { if (majorVersion >= 4) { env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - env.YARN_HTTPS_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS; } else if (majorVersion === 2 || majorVersion === 3) { env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - env.YARN_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS; } } From ad7e94dac4945e51a60be25b5d431efb8321d572 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 9 Oct 2025 15:35:43 +0200 Subject: [PATCH 29/71] Add unit tests for yarn environment variables --- .../yarn/runYarnCommand.spec.js | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js new file mode 100644 index 0000000..bd3d04d --- /dev/null +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js @@ -0,0 +1,152 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; + +describe("runYarnCommand", () => { + let runYarnCommand; + let capturedEnv; + let yarnVersion; + + beforeEach(async () => { + capturedEnv = null; + yarnVersion = "4.1.0"; // Default to v4 + + // Mock safeSpawn to capture env and control yarn version + mock.module("../../utils/safeSpawn.js", { + namedExports: { + safeSpawn: async (command, args, options) => { + if (args.includes("--version")) { + // Mock yarn version check + return { status: 0, stdout: yarnVersion }; + } + // Capture the env for assertions + capturedEnv = options.env; + return { status: 0 }; + }, + }, + }); + + // Mock mergeSafeChainProxyEnvironmentVariables to return test env + mock.module("../../registryProxy/registryProxy.js", { + namedExports: { + mergeSafeChainProxyEnvironmentVariables: (env) => { + return { + ...env, + HTTPS_PROXY: "http://localhost:8080", + NODE_EXTRA_CA_CERTS: "/path/to/ca-cert.pem", + }; + }, + }, + }); + + // Mock ui to prevent console output + mock.module("../../environment/userInteraction.js", { + namedExports: { + ui: { + writeError: () => {}, + }, + }, + }); + + const module = await import("./runYarnCommand.js"); + runYarnCommand = module.runYarnCommand; + }); + + afterEach(() => { + mock.reset(); + }); + + it("should set YARN_HTTPS_PROXY for Yarn v4+", async () => { + yarnVersion = "4.1.0"; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.YARN_HTTPS_PROXY, + "http://localhost:8080", + "YARN_HTTPS_PROXY should be set to the HTTPS_PROXY value" + ); + assert.strictEqual( + capturedEnv.YARN_HTTPS_CA_FILE_PATH, + undefined, + "YARN_HTTPS_CA_FILE_PATH should NOT be set to avoid overriding system CAs" + ); + }); + + it("should set YARN_HTTPS_PROXY for Yarn v3", async () => { + yarnVersion = "3.6.4"; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.YARN_HTTPS_PROXY, + "http://localhost:8080", + "YARN_HTTPS_PROXY should be set to the HTTPS_PROXY value" + ); + assert.strictEqual( + capturedEnv.YARN_CA_FILE_PATH, + undefined, + "YARN_CA_FILE_PATH should NOT be set to avoid overriding system CAs" + ); + }); + + it("should set YARN_HTTPS_PROXY for Yarn v2", async () => { + yarnVersion = "2.4.3"; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.YARN_HTTPS_PROXY, + "http://localhost:8080", + "YARN_HTTPS_PROXY should be set to the HTTPS_PROXY value" + ); + assert.strictEqual( + capturedEnv.YARN_CA_FILE_PATH, + undefined, + "YARN_CA_FILE_PATH should NOT be set to avoid overriding system CAs" + ); + }); + + it("should not set Yarn-specific proxy vars for Yarn v1", async () => { + yarnVersion = "1.22.19"; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.YARN_HTTPS_PROXY, + undefined, + "YARN_HTTPS_PROXY should not be set for Yarn v1" + ); + assert.strictEqual( + capturedEnv.YARN_HTTPS_CA_FILE_PATH, + undefined, + "YARN_HTTPS_CA_FILE_PATH should not be set for Yarn v1" + ); + assert.strictEqual( + capturedEnv.YARN_CA_FILE_PATH, + undefined, + "YARN_CA_FILE_PATH should not be set for Yarn v1" + ); + }); + + it("should preserve NODE_EXTRA_CA_CERTS for all Yarn versions", async () => { + for (const version of ["4.1.0", "3.6.4", "2.4.3", "1.22.19"]) { + yarnVersion = version; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.NODE_EXTRA_CA_CERTS, + "/path/to/ca-cert.pem", + `NODE_EXTRA_CA_CERTS should be preserved for Yarn ${version}` + ); + } + }); + + it("should preserve HTTPS_PROXY for all Yarn versions", async () => { + for (const version of ["4.1.0", "3.6.4", "2.4.3", "1.22.19"]) { + yarnVersion = version; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.HTTPS_PROXY, + "http://localhost:8080", + `HTTPS_PROXY should be preserved for Yarn ${version}` + ); + } + }); +}); From 0afea0eed6ef559b994810eb0a81dfde54e7badb Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Thu, 9 Oct 2025 16:44:55 +0200 Subject: [PATCH 30/71] Remove `safeSpawnSync` (unused) --- packages/safe-chain/src/utils/safeSpawn.js | 5 ----- packages/safe-chain/src/utils/safeSpawn.spec.js | 13 ++++--------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index c5cd913..b8c9274 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -13,11 +13,6 @@ function buildCommand(command, args) { return `${command} ${escapedArgs.join(" ")}`; } -export function safeSpawnSync(command, args, options = {}) { - const fullCommand = buildCommand(command, args); - return spawnSync(fullCommand, { ...options, shell: true }); -} - export async function safeSpawn(command, args, options = {}) { const fullCommand = buildCommand(command, args); return new Promise((resolve, reject) => { diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index d325f8a..1ffdc25 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -2,7 +2,7 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; describe("safeSpawn", () => { - let safeSpawnSync, safeSpawn; + let safeSpawn; let spawnCalls = []; beforeEach(async () => { @@ -35,7 +35,6 @@ describe("safeSpawn", () => { // Import after mocking const safeSpawnModule = await import("./safeSpawn.js"); - safeSpawnSync = safeSpawnModule.safeSpawnSync; safeSpawn = safeSpawnModule.safeSpawn; }); @@ -45,14 +44,10 @@ describe("safeSpawn", () => { // Helper to run either sync or async variant async function runSafeSpawn(variant, command, args, options) { - if (variant === "sync") { - return safeSpawnSync(command, args, options); - } else { - return await safeSpawn(command, args, options); - } + return await safeSpawn(command, args, options); } - for (let variant of ["sync", "async"]) { + for (let variant of ["async"]) { it(`should pass basic command and arguments correctly (${variant})`, async () => { await runSafeSpawn(variant, "echo", ["hello"]); @@ -106,4 +101,4 @@ describe("safeSpawn", () => { assert.strictEqual(spawnCalls[0].options.shell, true); }); } -}); \ No newline at end of file +}); From 459f3a5b146004bc06f9d4c3778253c06294c268 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Thu, 9 Oct 2025 17:35:29 +0200 Subject: [PATCH 31/71] Remove unused import --- packages/safe-chain/src/utils/safeSpawn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index b8c9274..253417c 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,4 +1,4 @@ -import { spawnSync, spawn } from "child_process"; +import { spawn } from "child_process"; function escapeArg(arg) { // If argument contains spaces or quotes, wrap in double quotes and escape double quotes From 41ab4b1edb9d63410d46f3cb616f7999ae96d538 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Thu, 9 Oct 2025 18:03:45 +0200 Subject: [PATCH 32/71] Use oxlint instead of eslint - Less dev dependencies - Much faster - More helpful output - More sane defaults - Easier config --- .github/workflows/test-on-pr.yml | 2 +- .oxlintrc.json | 28 + eslint.config.js | 26 - package-lock.json | 3413 +---------------- package.json | 9 +- packages/safe-chain-bun/src/index.js | 1 + packages/safe-chain/package.json | 2 +- .../src/environment/userInteraction.js | 1 + 8 files changed, 164 insertions(+), 3318 deletions(-) create mode 100644 .oxlintrc.json delete mode 100644 eslint.config.js diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 5661f97..85d6aba 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -28,7 +28,7 @@ jobs: - name: Run unit tests run: npm test - - name: Run ESLint + - name: Run linting run: npm run lint - name: Create package tarball diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..a9dd70a --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,28 @@ +{ + "plugins": [ + "node", + "promise", + "eslint", + "unicorn", + "oxc", + "import" + ], + "env": { + "browser": false, + "node": true + }, + "rules": { + "eslint/no-console": "error", + "eslint/no-empty": "error" + }, + "overrides": [ + { + "files": [ + "*.spec.js" + ], + "rules": { + "eslint/no-console": "off" + } + } + ] +} diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 3db1b7f..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,26 +0,0 @@ -import js from "@eslint/js"; -import { defineConfig, globalIgnores } from "@eslint/config-helpers"; -import globals from "globals"; -import importPlugin from "eslint-plugin-import"; - -export default defineConfig([ - { - files: ["**/*.{js,mjs,cjs,ts}"], - plugins: { js }, - extends: ["js/recommended"], - }, - { - files: ["**/*.{js,mjs,cjs,ts}"], - languageOptions: { globals: globals.node }, - }, - importPlugin.flatConfigs.recommended, - { - files: ["**/*.{js,mjs,cjs}"], - languageOptions: { - ecmaVersion: "latest", - sourceType: "module", - }, - rules: {}, - }, - globalIgnores(['test/e2e', 'node_modules']), -]); diff --git a/package-lock.json b/package-lock.json index 6b74d53..0a8f765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,7 @@ "test/e2e" ], "devDependencies": { - "@eslint/js": "^9.35.0", - "eslint": "^9.35.0", - "eslint-plugin-import": "^2.32.0", - "globals": "^16.1.0", - "typescript-eslint": "^8.32.0" + "oxlint": "^1.22.0" } }, "node_modules/@aikidosec/safe-chain": { @@ -31,226 +27,6 @@ "resolved": "test/e2e", "link": true }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.2", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -330,44 +106,6 @@ "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", @@ -559,6 +297,110 @@ ], "peer": true }, + "node_modules/@oxlint/darwin-arm64": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.22.0.tgz", + "integrity": "sha512-vfgwTA1CowVaU3QXFBjfGjbPsHbdjAiJnWX5FBaq8uXS8tksGgl0ue14MK6fVnXncWK9j69LRnkteGTixxDAfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint/darwin-x64": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.22.0.tgz", + "integrity": "sha512-70x7Y+e0Ddb2Cf2IZsYGnXZrnB/MZgOTi/VkyXZucbnQcpi2VoaYS4Ve662DaNkzvTxdKOGmyJVMmD/digdJLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint/linux-arm64-gnu": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.22.0.tgz", + "integrity": "sha512-Rv94lOyEV8WEuzhjJSpCW3DbL/tlOVizPxth1v5XAFuQdM5rgpOMs3TsAf/YFUn52/qenwVglyvQZL8oAUYlpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-arm64-musl": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.22.0.tgz", + "integrity": "sha512-Aau6V6Osoyb3SFmRejP3rRhs1qhep4aJTdotFf1RVMVSLJkF7Ir0p+eGZSaIJyylFZuCCxHpud3hWasphmZnzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-x64-gnu": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.22.0.tgz", + "integrity": "sha512-6eOtv+2gHrKw/hxUkV6hJdvYhzr0Dqzb4oc7sNlWxp64jU6I19tgMwSlmtn02r34YNSn+/NpZ/ECvQrycKUUFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-x64-musl": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.22.0.tgz", + "integrity": "sha512-c4O7qD7TCEfPE/FFKYvakF2sQoIP0LFZB8F5AQK4K9VYlyT1oENNRCdIiMu6irvLelOzJzkUM0XrvUCL9Kkxrw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/win32-arm64": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.22.0.tgz", + "integrity": "sha512-6DJwF5A9VoIbSWNexLYubbuteAL23l3YN00wUL7Wt4ZfEZu2f/lWtGB9yC9BfKLXzudq8MvGkrS0szmV0bc1VQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/win32-x64": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.22.0.tgz", + "integrity": "sha512-nf8EZnIUgIrHlP9k26iOFMZZPoJG16KqZBXu5CG5YTAtVcu4CWlee9Q/cOS/rgQNGjLF+WPw8sVA5P3iGlYGQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -569,230 +411,6 @@ "node": ">=14" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -802,29 +420,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -834,23 +429,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -875,161 +453,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1044,19 +467,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/bun": { "version": "1.2.21", "resolved": "https://registry.npmjs.org/bun/-/bun-1.2.21.tgz", @@ -1115,66 +525,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -1254,60 +604,6 @@ "node": ">= 8" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1325,77 +621,6 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1437,585 +662,6 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT" }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", - "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.35.0", - "@eslint/plugin-kit": "^0.3.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2044,47 +690,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -2097,63 +702,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2174,19 +722,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2202,150 +737,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/hosted-git-info": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", @@ -2390,33 +781,6 @@ "node": ">= 14" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2426,21 +790,6 @@ "node": ">=0.8.19" } }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -2454,167 +803,6 @@ "node": ">= 12" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2624,38 +812,6 @@ "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -2668,158 +824,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -2832,59 +836,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2906,59 +857,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -2968,53 +872,6 @@ ], "license": "MIT" }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -3071,40 +928,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -3117,29 +940,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -3307,13 +1107,6 @@ "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", "license": "MIT" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -3376,121 +1169,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ora": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", @@ -3564,54 +1242,38 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "node_modules/oxlint": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.22.0.tgz", + "integrity": "sha512-/HYT1Cfanveim9QUM6KlPKJe9y+WPnh3SxIB7z1InWnag9S0nzxLaWEUiW1P4UGzh/No3KvtNmBv2IOiwAl2/w==", "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" + "bin": { + "oxc_language_server": "bin/oxc_language_server", + "oxlint": "bin/oxlint" }, "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" + "url": "https://github.com/sponsors/Boshen" }, - "engines": { - "node": ">=10" + "optionalDependencies": { + "@oxlint/darwin-arm64": "1.22.0", + "@oxlint/darwin-x64": "1.22.0", + "@oxlint/linux-arm64-gnu": "1.22.0", + "@oxlint/linux-arm64-musl": "1.22.0", + "@oxlint/linux-x64-gnu": "1.22.0", + "@oxlint/linux-x64-musl": "1.22.0", + "@oxlint/win32-arm64": "1.22.0", + "@oxlint/win32-x64": "1.22.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" + "peerDependencies": { + "oxlint-tsgolint": ">=0.2.0" }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } } }, "node_modules/p-map": { @@ -3632,29 +1294,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3664,13 +1303,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -3687,39 +1319,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -3742,112 +1341,6 @@ "node": ">=10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -3888,96 +1381,6 @@ "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3997,55 +1400,6 @@ "node": ">=10" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4067,82 +1421,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4223,20 +1501,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4266,65 +1530,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4350,55 +1555,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/tar": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", @@ -4416,193 +1572,6 @@ "node": ">=18" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", - "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@typescript-eslint/utils": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", @@ -4627,16 +1596,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/validate-npm-package-name": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", @@ -4661,105 +1620,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -4866,19 +1726,6 @@ "node": ">=18" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", "version": "1.0.0", @@ -4894,6 +1741,8 @@ "semver": "7.7.2" }, "bin": { + "aikido-bun": "bin/aikido-bun.js", + "aikido-bunx": "bin/aikido-bunx.js", "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", "aikido-pnpm": "bin/aikido-pnpm.js", diff --git a/package.json b/package.json index ad71644..0193a82 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,6 @@ "author": "Aikido Security", "license": "AGPL-3.0-or-later", "devDependencies": { - "@eslint/js": "^9.35.0", - "eslint": "^9.35.0", - "eslint-plugin-import": "^2.32.0", - "globals": "^16.1.0", - "typescript-eslint": "^8.32.0" - }, - "overrides": { - "brace-expansion@<=2.0.2": "2.0.2" + "oxlint": "^1.22.0" } } diff --git a/packages/safe-chain-bun/src/index.js b/packages/safe-chain-bun/src/index.js index fbd0f65..660e0bd 100644 --- a/packages/safe-chain-bun/src/index.js +++ b/packages/safe-chain-bun/src/index.js @@ -1,3 +1,4 @@ +// oxlint-disable no-console import { auditChanges } from "@aikidosec/safe-chain/scanning"; // Bun Security Scanner for Safe-Chain diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 6a927d4..95098b7 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'", "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'", - "lint": "eslint ." + "lint": "oxlint" }, "bin": { "aikido-npm": "bin/aikido-npm.js", diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 5b1cb88..829afa1 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -1,3 +1,4 @@ +// oxlint-disable no-console import chalk from "chalk"; import ora from "ora"; import { createInterface } from "readline"; From 5e08461859cca1f9c9a71862963949e284b9e2b7 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 10 Oct 2025 11:41:42 +0200 Subject: [PATCH 33/71] Add $schema reference for autocompletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Timo Kössler --- .oxlintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.oxlintrc.json b/.oxlintrc.json index a9dd70a..b76f2ad 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,4 +1,5 @@ { + "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": [ "node", "promise", From 5518846e9612f52b1096dbf68001953211694578 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 10 Oct 2025 11:45:34 +0200 Subject: [PATCH 34/71] Update packages/safe-chain/package.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Timo Kössler --- packages/safe-chain/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 95098b7..42bfb55 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'", "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'", - "lint": "oxlint" + "lint": "oxlint --deny-warnings" }, "bin": { "aikido-npm": "bin/aikido-npm.js", From a377fd6caae67482cfba7f4db0c576e2e707723d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 13:55:39 +0200 Subject: [PATCH 35/71] Listen to error events on sockets --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 6 ++++++ .../safe-chain/src/registryProxy/tunnelRequestHandler.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 4be9987..63a8168 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -5,6 +5,12 @@ import { HttpsProxyAgent } from "https-proxy-agent"; export function mitmConnect(req, clientSocket, isAllowed) { const { hostname } = new URL(`http://${req.url}`); + clientSocket.on("error", () => { + // NO-OP + // This can happen if the client TCP socket sends RST instead of FIN. + // Not subscribing to 'close' event will cause node to throw and crash. + }); + const server = createHttpsServer(hostname, isAllowed); // Establish the connection diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index fa12aee..c28a022 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -24,6 +24,12 @@ export function tunnelRequest(req, clientSocket, head) { function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); + clientSocket.on("error", () => { + // NO-OP + // This can happen if the client TCP socket sends RST instead of FIN. + // Not subscribing to 'close' event will cause node to throw and crash. + }); + const serverSocket = net.connect(port || 443, hostname, () => { clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); serverSocket.write(head); From 2fa14b82f3b05872540010de64fe0cac4547f04c Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 10 Oct 2025 14:57:28 +0200 Subject: [PATCH 36/71] Simplify tests --- .../safe-chain/src/utils/safeSpawn.spec.js | 97 ++++++++----------- 1 file changed, 41 insertions(+), 56 deletions(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index 1ffdc25..6417084 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -11,14 +11,6 @@ describe("safeSpawn", () => { // Mock child_process module to capture what command string gets built mock.module("child_process", { namedExports: { - spawnSync: (command, options) => { - spawnCalls.push({ command, options }); - return { - status: 0, - stdout: Buffer.from(""), - stderr: Buffer.from(""), - }; - }, spawn: (command, options) => { spawnCalls.push({ command, options }); return { @@ -42,63 +34,56 @@ describe("safeSpawn", () => { mock.reset(); }); - // Helper to run either sync or async variant - async function runSafeSpawn(variant, command, args, options) { - return await safeSpawn(command, args, options); - } + it("should pass basic command and arguments correctly", async () => { + await safeSpawn("echo", ["hello"]); - for (let variant of ["async"]) { - it(`should pass basic command and arguments correctly (${variant})`, async () => { - await runSafeSpawn(variant, "echo", ["hello"]); + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, "echo hello"); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - assert.strictEqual(spawnCalls[0].command, "echo hello"); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should escape arguments containing spaces", async () => { + await safeSpawn("echo", ["hello world"]); - it(`should escape arguments containing spaces (${variant})`, async () => { - await runSafeSpawn(variant, "echo", ["hello world"]); + assert.strictEqual(spawnCalls.length, 1); + // Argument should be escaped to prevent shell interpretation + assert.strictEqual(spawnCalls[0].command, 'echo "hello world"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - // Argument should be escaped to prevent shell interpretation - assert.strictEqual(spawnCalls[0].command, 'echo "hello world"'); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should prevent shell injection attacks", async () => { + await safeSpawn("ls", ["; rm test123.txt"]); - it(`should prevent shell injection attacks (${variant})`, async () => { - await runSafeSpawn(variant, "ls", ["; rm test123.txt"]); + assert.strictEqual(spawnCalls.length, 1); + // Malicious command should be escaped to prevent execution + assert.strictEqual(spawnCalls[0].command, 'ls "; rm test123.txt"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - // Malicious command should be escaped to prevent execution - assert.strictEqual(spawnCalls[0].command, 'ls "; rm test123.txt"'); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should escape single quotes in arguments", async () => { + await safeSpawn("echo", ["don't break"]); - it(`should escape single quotes in arguments (${variant})`, async () => { - await runSafeSpawn(variant, "echo", ["don't break"]); + assert.strictEqual(spawnCalls.length, 1); + // Single quote should be properly escaped with double quotes + assert.strictEqual(spawnCalls[0].command, 'echo "don\'t break"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - // Single quote should be properly escaped with double quotes - assert.strictEqual(spawnCalls[0].command, 'echo "don\'t break"'); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should handle double quotes with simpler escaping", async () => { + await safeSpawn("echo", ['say "hello"']); - it(`should handle double quotes with simpler escaping (${variant})`, async () => { - await runSafeSpawn(variant, "echo", ['say "hello"']); + assert.strictEqual(spawnCalls.length, 1); + // If we switch to double quotes, this should be: "say \"hello\"" + assert.strictEqual(spawnCalls[0].command, 'echo "say \\"hello\\""'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - // If we switch to double quotes, this should be: "say \"hello\"" - assert.strictEqual(spawnCalls[0].command, 'echo "say \\"hello\\""'); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should not escape arguments with only safe characters", async () => { + await safeSpawn("npm", ["install", "axios", "--save"]); - it(`should not escape arguments with only safe characters (${variant})`, async () => { - await runSafeSpawn(variant, "npm", ["install", "axios", "--save"]); - - assert.strictEqual(spawnCalls.length, 1); - // Safe arguments (alphanumeric, dash, underscore, dot, slash) shouldn't be quoted - assert.strictEqual(spawnCalls[0].command, "npm install axios --save"); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); - } + assert.strictEqual(spawnCalls.length, 1); + // Safe arguments (alphanumeric, dash, underscore, dot, slash) shouldn't be quoted + assert.strictEqual(spawnCalls[0].command, "npm install axios --save"); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); }); From 4fc33d23874cd201b5ec7f90c4dbc81f0ebe5c20 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 15:34:33 +0200 Subject: [PATCH 37/71] Add command to get the safe-chain version --- README.md | 5 +++++ packages/safe-chain/bin/safe-chain.js | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fd2cdff..1083c0e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, or `bunx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. +You can check the installed version by running: +```shell +safe-chain --version +``` + ## How it works The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry. When you run npm, npx, yarn, pnpm, pnpx, bun, or bunx commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine. diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 5a7d94b..ad88c08 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -1,6 +1,7 @@ #!/usr/bin/env node import chalk from "chalk"; +import { createRequire } from "module"; import { ui } from "../src/environment/userInteraction.js"; import { setup } from "../src/shell-integration/setup.js"; import { teardown } from "../src/shell-integration/teardown.js"; @@ -26,6 +27,8 @@ if (command === "setup") { teardown(); } else if (command === "setup-ci") { setupCi(); +} else if (command === "--version" || command === "-v" || command === "-v") { + ui.writeInformation(`Current safe-chain version: ${getVersion()}`); } else { ui.writeError(`Unknown command: ${command}.`); ui.emptyLine(); @@ -43,13 +46,15 @@ function writeHelp() { ui.writeInformation( `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan( "teardown" - )}, ${chalk.cyan("help")}` + )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan( + "--version" + )}` ); ui.emptyLine(); ui.writeInformation( `- ${chalk.cyan( "safe-chain setup" - )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm and pnpx.` + )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun and bunx.` ); ui.writeInformation( `- ${chalk.cyan( @@ -61,5 +66,16 @@ function writeHelp() { "safe-chain setup-ci" )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain --version" + )} (or ${chalk.cyan("-v")}): Display the current version of safe-chain.` + ); ui.emptyLine(); } + +function getVersion() { + const require = createRequire(import.meta.url); + const packageJson = require("../package.json"); + return packageJson.version; +} From 8aebb1b96b0f4d3412e3314af1547857608abd32 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 16:18:43 +0200 Subject: [PATCH 38/71] Remove dry-run scanner for npm, relying on the proxy to block maliscious package downloads instead --- packages/safe-chain/bin/aikido-npm.js | 13 +- packages/safe-chain/bin/aikido-npx.js | 2 +- packages/safe-chain/bin/aikido-pnpm.js | 2 +- packages/safe-chain/bin/aikido-pnpx.js | 2 +- packages/safe-chain/bin/aikido-yarn.js | 2 +- .../packagemanager/currentPackageManager.js | 4 +- .../npm/createPackageManager.js | 55 ++----- .../npm/dependencyScanner/dryRunScanner.js | 67 --------- .../dependencyScanner/dryRunScanner.spec.js | 139 ------------------ .../parsing/parseNpmInstallDryRunOutput.js | 57 ------- .../parseNpmInstallDryRunOutput.spec.js | 134 ----------------- test/e2e/npm.e2e.spec.js | 48 ++---- 12 files changed, 29 insertions(+), 496 deletions(-) delete mode 100644 packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js delete mode 100644 packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js delete mode 100644 packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js delete mode 100644 packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index d8b8c3e..0e9f302 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -1,21 +1,10 @@ #!/usr/bin/env node -import { execSync } from "child_process"; import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "npm"; -initializePackageManager(packageManagerName, getNpmVersion()); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); - -function getNpmVersion() { - try { - return execSync("npm --version").toString().trim(); - } catch { - // Default to 0.0.0 if npm is not found - // That way we don't use any unsupported features - return "0.0.0"; - } -} diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index 7f06c7c..d3dfdd6 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -4,7 +4,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "npx"; -initializePackageManager(packageManagerName, process.versions.node); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index 7177159..0a06217 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -4,7 +4,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "pnpm"; -initializePackageManager(packageManagerName, process.versions.node); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index 4bb6840..cdb6504 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -4,7 +4,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "pnpx"; -initializePackageManager(packageManagerName, process.versions.node); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index 002a956..fd87606 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -4,7 +4,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "yarn"; -initializePackageManager(packageManagerName, process.versions.node); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 2a10d86..2f019a1 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -14,9 +14,9 @@ const state = { packageManagerName: null, }; -export function initializePackageManager(packageManagerName, version) { +export function initializePackageManager(packageManagerName) { if (packageManagerName === "npm") { - state.packageManagerName = createNpmPackageManager(version); + state.packageManagerName = createNpmPackageManager(); } else if (packageManagerName === "npx") { state.packageManagerName = createNpxPackageManager(); } else if (packageManagerName === "yarn") { diff --git a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js index bf38209..731f406 100644 --- a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js @@ -1,34 +1,27 @@ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; -import { dryRunScanner } from "./dependencyScanner/dryRunScanner.js"; import { nullScanner } from "./dependencyScanner/nullScanner.js"; import { runNpm } from "./runNpmCommand.js"; import { getNpmCommandForArgs, npmInstallCommand, - npmCiCommand, - npmInstallTestCommand, - npmInstallCiTestCommand, npmUpdateCommand, - npmAuditCommand, npmExecCommand, } from "./utils/npmCommands.js"; -export function createNpmPackageManager(version) { - // From npm v10.4.0 onwards, the npm commands output detailed information - // when using the --dry-run flag. - // We use that information to scan for dependency changes. - // For older versions of npm we have to rely on parsing the command arguments. - const supportedScanners = isPriorToNpm10_4(version) - ? npm10_3AndBelowSupportedScanners - : npm10_4AndAboveSupportedScanners; - +export function createNpmPackageManager() { function isSupportedCommand(args) { - const scanner = findDependencyScannerForCommand(supportedScanners, args); + const scanner = findDependencyScannerForCommand( + commandScannerMapping, + args + ); return scanner.shouldScan(args); } function getDependencyUpdatesForCommand(args) { - const scanner = findDependencyScannerForCommand(supportedScanners, args); + const scanner = findDependencyScannerForCommand( + commandScannerMapping, + args + ); return scanner.scan(args); } @@ -39,40 +32,12 @@ export function createNpmPackageManager(version) { }; } -const npm10_4AndAboveSupportedScanners = { - [npmInstallCommand]: dryRunScanner(), - [npmUpdateCommand]: dryRunScanner(), - [npmCiCommand]: dryRunScanner(), - [npmAuditCommand]: dryRunScanner({ - skipScanWhen: (args) => !args.includes("fix"), - }), - [npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run - - // Running dry-run on install-test and install-ci-test will install & run tests. - // We only want to know if there are changes in the dependencies. - // So we run change the dry-run command to only check the install. - [npmInstallTestCommand]: dryRunScanner({ dryRunCommand: npmInstallCommand }), - [npmInstallCiTestCommand]: dryRunScanner({ dryRunCommand: npmCiCommand }), -}; - -const npm10_3AndBelowSupportedScanners = { +const commandScannerMapping = { [npmInstallCommand]: commandArgumentScanner(), [npmUpdateCommand]: commandArgumentScanner(), [npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run }; -function isPriorToNpm10_4(version) { - try { - const [major, minor] = version.split(".").map(Number); - if (major < 10) return true; - if (major === 10 && minor < 4) return true; - return false; - } catch { - // Default to true: if version parsing fails, assume it's an older version - return true; - } -} - function findDependencyScannerForCommand(scanners, args) { const command = getNpmCommandForArgs(args); if (!command) { diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js deleted file mode 100644 index 6189b2f..0000000 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +++ /dev/null @@ -1,67 +0,0 @@ -import { parseDryRunOutput } from "../parsing/parseNpmInstallDryRunOutput.js"; -import { dryRunNpmCommandAndOutput } from "../runNpmCommand.js"; -import { hasDryRunArg } from "../utils/npmCommands.js"; - -export function dryRunScanner(scannerOptions) { - return { - scan: (args) => scanDependencies(scannerOptions, args), - shouldScan: (args) => shouldScanDependencies(scannerOptions, args), - }; -} - -function scanDependencies(scannerOptions, args) { - let dryRunArgs = args; - - if (scannerOptions?.dryRunCommand) { - // Replace the first argument with the dryRunCommand (eg: "install" instead of "install-test") - dryRunArgs = [scannerOptions.dryRunCommand, ...args.slice(1)]; - } - - return checkChangesWithDryRun(dryRunArgs); -} - -function shouldScanDependencies(scannerOptions, args) { - if (hasDryRunArg(args)) { - return false; - } - - if (scannerOptions?.skipScanWhen && scannerOptions.skipScanWhen(args)) { - return false; - } - - return true; -} - -async function checkChangesWithDryRun(args) { - const dryRunOutput = await dryRunNpmCommandAndOutput(args); - - // Dry-run can return a non-zero status code in some cases - // e.g., when running "npm audit fix --dry-run", it returns exit code 1 - // when there are vulnerabilities that can be fixed. - if (dryRunOutput.status !== 0 && !canCommandReturnNonZeroOnSuccess(args)) { - throw new Error( - `Dry-run command failed with exit code ${dryRunOutput.status} and output:\n${dryRunOutput.output}` - ); - } - - if (dryRunOutput.status !== 0 && !dryRunOutput.output) { - throw new Error( - `Dry-run command failed with exit code ${dryRunOutput.status} and produced no output.` - ); - } - - const parsedOutput = parseDryRunOutput(dryRunOutput.output); - - // reverse the array to have the top-level packages first - return parsedOutput.reverse(); -} - -function canCommandReturnNonZeroOnSuccess(args) { - if (args.length < 2) { - return false; - } - - // `npm audit fix --dry-run` can return exit code 1 when it succesfully ran and - // there were vulnerabilities that could be fixed - return args[0] === "audit" && args[1] === "fix"; -} diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js deleted file mode 100644 index 88d7681..0000000 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js +++ /dev/null @@ -1,139 +0,0 @@ -import { describe, it, mock } from "node:test"; -import assert from "node:assert/strict"; - -describe("dryRunScanner", async () => { - const mockWriteError = mock.fn(); - const mockDryRunNpmCommandAndOutput = mock.fn(); - - // Mock ui module - mock.module("../../../environment/userInteraction.js", { - namedExports: { - ui: { - writeError: mockWriteError, - }, - }, - }); - - // Mock dryRunNpmCommandAndOutput function - mock.module("../runNpmCommand.js", { - namedExports: { - dryRunNpmCommandAndOutput: mockDryRunNpmCommandAndOutput, - }, - }); - - const { dryRunScanner } = await import("./dryRunScanner.js"); - - describe("doesCommandReturnNonZero", () => { - // We need to access the internal function for testing - // Since it's not exported, we'll test it indirectly through the main functionality - - it("should handle npm audit fix commands that return non-zero", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 1, - output: "found 5 vulnerabilities that can be fixed", - })); - - const scanner = dryRunScanner(); - const result = await scanner.scan(["audit", "fix"]); - - // Should not throw an error for audit fix commands - assert.ok(Array.isArray(result)); - assert.equal(mockWriteError.mock.callCount(), 0); - }); - - it("should throw error for unexpected non-zero exit codes", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 1, - output: "some error output", - })); - - const scanner = dryRunScanner(); - - await assert.rejects(async () => { - await scanner.scan(["install", "lodash"]); - }, /Dry-run command failed with exit code 1/); - }); - - it("should handle zero exit codes normally", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 0, - output: "added 1 package", - })); - - const scanner = dryRunScanner(); - const result = await scanner.scan(["install", "lodash"]); - - assert.ok(Array.isArray(result)); - assert.equal(mockWriteError.mock.callCount(), 0); - }); - - it("should throw error for non-zero exit with no output for audit fix", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 1, - output: "", - })); - - const scanner = dryRunScanner(); - - await assert.rejects(async () => { - await scanner.scan(["audit", "fix"]); - }, /Dry-run command failed with exit code 1/); - }); - }); - - describe("scanner functionality", () => { - it("should use dryRunCommand option when provided", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 0, - output: "no changes", - })); - - const scanner = dryRunScanner({ dryRunCommand: "install" }); - await scanner.scan(["install-test", "lodash"]); - - // Should call with "install" instead of "install-test" - assert.equal(mockDryRunNpmCommandAndOutput.mock.callCount(), 1); - const calledArgs = - mockDryRunNpmCommandAndOutput.mock.calls[0].arguments[0]; - assert.deepEqual(calledArgs, ["install", "lodash"]); - }); - - it("should skip scanning when hasDryRunArg returns true", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - - const scanner = dryRunScanner(); - const shouldScan = scanner.shouldScan(["install", "--dry-run"]); - - assert.equal(shouldScan, false); - // Should not call dryRunNpmCommandAndOutput since scanning is skipped - assert.equal(mockDryRunNpmCommandAndOutput.mock.callCount(), 0); - }); - - it("should skip scanning when skipScanWhen returns true", async () => { - const scanner = dryRunScanner({ - skipScanWhen: (args) => args.includes("--skip"), - }); - const shouldScan = scanner.shouldScan(["install", "--skip"]); - - assert.equal(shouldScan, false); - }); - - it("should scan when conditions are met", async () => { - const scanner = dryRunScanner(); - const shouldScan = scanner.shouldScan(["install", "lodash"]); - - assert.equal(shouldScan, true); - }); - }); -}); diff --git a/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js b/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js deleted file mode 100644 index 3c1e673..0000000 --- a/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js +++ /dev/null @@ -1,57 +0,0 @@ -export function parseDryRunOutput(output) { - const lines = output.split(/\r?\n/); - const packageChanges = []; - - for (const line of lines) { - if (line.startsWith("add ")) { - packageChanges.push(parseAdd(line)); - } else if (line.startsWith("remove ")) { - packageChanges.push(parseRemove(line)); - } else if (line.startsWith("change ")) { - packageChanges.push(parseChange(line)); - } - } - - return packageChanges; -} - -function parseAdd(line) { - const splitLine = getLineParts(line); - const packageName = splitLine[1]; - const packageVersion = splitLine[splitLine.length - 1]; - return addedPackage(packageName, packageVersion); -} - -function addedPackage(name, version) { - return { type: "add", name, version }; -} - -function parseRemove(line) { - const splitLine = getLineParts(line); - const packageName = splitLine[1]; - const packageVersion = splitLine[splitLine.length - 1]; - return removedPackage(packageName, packageVersion); -} - -function removedPackage(name, version) { - return { type: "remove", name, version }; -} - -function parseChange(line) { - const splitLine = getLineParts(line); - const packageName = splitLine[1]; - const packageVersion = splitLine[splitLine.length - 1]; - const oldVersion = splitLine[2]; - return changedPackage(packageName, packageVersion, oldVersion); -} - -function getLineParts(line) { - return line - .split(" ") - .map((part) => part.trim()) - .filter((part) => part !== ""); -} - -function changedPackage(name, version, oldVersion) { - return { type: "change", name, version, oldVersion }; -} diff --git a/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js b/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js deleted file mode 100644 index cd7c2b1..0000000 --- a/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { parseDryRunOutput } from "./parseNpmInstallDryRunOutput.js"; - -describe("parseNpmInstallDryRunOutput", () => { - it("should parse added packages", () => { - const output = ` -add @jest/transform 29.7.0 -add @jest/test-result 29.7.0 -add @jest/reporters 29.7.0 -add @jest/console 29.7.0 -add jest-cli 29.7.0 -add import-local 3.2.0 -add @jest/types 29.6.3 -add @jest/core 29.7.0 -add jest 29.7.0 - -added 267 packages in 831ms - -32 packages are looking for funding - run \`npm fund\` for details`; - - const expected = [ - { name: "@jest/transform", version: "29.7.0", type: "add" }, - { name: "@jest/test-result", version: "29.7.0", type: "add" }, - { name: "@jest/reporters", version: "29.7.0", type: "add" }, - { name: "@jest/console", version: "29.7.0", type: "add" }, - { name: "jest-cli", version: "29.7.0", type: "add" }, - { name: "import-local", version: "3.2.0", type: "add" }, - { name: "@jest/types", version: "29.6.3", type: "add" }, - { name: "@jest/core", version: "29.7.0", type: "add" }, - { name: "jest", version: "29.7.0", type: "add" }, - ]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); - - it("should parse removed packages", () => { - const output = ` -remove react 19.1.0 - - removed 1 package in 115ms`; - - const expected = [{ name: "react", version: "19.1.0", type: "remove" }]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); - - it("should parse changed packages", () => { - const output = ` -change react 19.0.0 => 19.1.0 - -changed 1 package in 204ms`; - - const expected = [ - { - name: "react", - version: "19.1.0", - oldVersion: "19.0.0", - type: "change", - }, - ]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); - - it("should parse mixed package changes", () => { - const output = ` -add @jest/transform 29.7.0 -add @jest/test-result 29.7.0 -add @jest/reporters 29.7.0 -add @jest/console 29.7.0 -add jest-cli 29.7.0 -add import-local 3.2.0 -add @jest/types 29.6.3 -add @jest/core 29.7.0 -add jest 29.7.0 -remove react 19.1.0 -change lodash 4.17.0 => 4.18.0 - -removed 1 package in 115ms`; - - const expected = [ - { name: "@jest/transform", version: "29.7.0", type: "add" }, - { name: "@jest/test-result", version: "29.7.0", type: "add" }, - { name: "@jest/reporters", version: "29.7.0", type: "add" }, - { name: "@jest/console", version: "29.7.0", type: "add" }, - { name: "jest-cli", version: "29.7.0", type: "add" }, - { name: "import-local", version: "3.2.0", type: "add" }, - { name: "@jest/types", version: "29.6.3", type: "add" }, - { name: "@jest/core", version: "29.7.0", type: "add" }, - { name: "jest", version: "29.7.0", type: "add" }, - { name: "react", version: "19.1.0", type: "remove" }, - { - name: "lodash", - version: "4.18.0", - oldVersion: "4.17.0", - type: "change", - }, - ]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); - - it("should work with npm v22.0.0", () => { - const output = ` -add @jest/types 29.6.3 -add @jest/core 29.7.0 -add jest 29.7.0 - -added 257 packages in 791ms - -44 packages are looking for funding - run \`npm fund\` for details`; - - const expected = [ - { name: "@jest/types", version: "29.6.3", type: "add" }, - { name: "@jest/core", version: "29.7.0", type: "add" }, - { name: "jest", version: "29.7.0", type: "add" }, - ]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); -}); diff --git a/test/e2e/npm.e2e.spec.js b/test/e2e/npm.e2e.spec.js index c744835..ba836e7 100644 --- a/test/e2e/npm.e2e.spec.js +++ b/test/e2e/npm.e2e.spec.js @@ -62,48 +62,24 @@ describe("E2E: npm coverage", () => { it(`safe-chain blocks download of malicious packages already in package.json`, async () => { const shell = await container.openShell("zsh"); - const npmVersion = (await shell.runCommand("npm --version")).output.trim(); - const majorVersion = parseInt(npmVersion.split(".")[0]); - const minorVersion = parseInt(npmVersion.split(".")[1]); - const isBelow10_4 = - majorVersion < 10 || (majorVersion === 10 && minorVersion < 4); await shell.runCommand( 'echo \'{"name":"test-project","version":"1.0.0","dependencies":{"safe-chain-test":"0.0.1-security"}}\' > package.json' ); var result = await shell.runCommand("npm install"); - if (isBelow10_4) { - assert.ok( - result.output.includes("blocked 1 malicious package downloads"), - `Output did not include expected text. Output was:\n${result.output}` - ); - assert.ok( - result.output.includes("- safe-chain-test"), - `Output did not include expected text. Output was:\n${result.output}` - ); - assert.ok( - result.output.includes( - "Exiting without installing malicious packages." - ), - `Output did not include expected text. Output was:\n${result.output}` - ); - } else { - assert.ok( - result.output.includes("Malicious changes detected:"), - `Output did not include expected text. Output was:\n${result.output}` - ); - assert.ok( - result.output.includes("- safe-chain-test"), - `Output did not include expected text. Output was:\n${result.output}` - ); - assert.ok( - result.output.includes( - "Exiting without installing malicious packages." - ), - `Output did not include expected text. Output was:\n${result.output}` - ); - } + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); }); it("safe-chain blocks npx from executing malicious packages", async () => { From ea92ea0731faa010fcc028210f865ce5a91ce893 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 16:19:38 +0200 Subject: [PATCH 39/71] Remove abbrev package --- packages/safe-chain/package.json | 1 - .../src/packagemanager/npm/utils/cmd-list.js | 363 +++++++++++++++++- 2 files changed, 359 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 42bfb55..98ccd52 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -30,7 +30,6 @@ "license": "AGPL-3.0-or-later", "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), and [bunx](https://bun.sh/docs/cli/bunx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, or bunx from downloading or running the malware.", "dependencies": { - "abbrev": "3.0.1", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "make-fetch-happen": "14.0.3", diff --git a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js index 187204d..8467147 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js @@ -1,7 +1,5 @@ // Based on https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js -import abbrev from "abbrev"; - const commands = [ "access", "adduser", @@ -72,6 +70,365 @@ const commands = [ "whoami", ]; +// This was ran with the abbrev package to generate the abbrevs object below +// console.log(abbrev(commands.concat(Object.keys(aliases)))); +const abbrevs = { + ac: "access", + acc: "access", + acce: "access", + acces: "access", + access: "access", + add: "add", + "add-": "add-user", + "add-u": "add-user", + "add-us": "add-user", + "add-use": "add-user", + "add-user": "add-user", + addu: "adduser", + addus: "adduser", + adduse: "adduser", + adduser: "adduser", + aud: "audit", + audi: "audit", + audit: "audit", + aut: "author", + auth: "author", + autho: "author", + author: "author", + b: "bugs", + bu: "bugs", + bug: "bugs", + bugs: "bugs", + c: "c", + ca: "cache", + cac: "cache", + cach: "cache", + cache: "cache", + ci: "ci", + cit: "cit", + "clean-install": "clean-install", + "clean-install-": "clean-install-test", + "clean-install-t": "clean-install-test", + "clean-install-te": "clean-install-test", + "clean-install-tes": "clean-install-test", + "clean-install-test": "clean-install-test", + com: "completion", + comp: "completion", + compl: "completion", + comple: "completion", + complet: "completion", + completi: "completion", + completio: "completion", + completion: "completion", + con: "config", + conf: "config", + confi: "config", + config: "config", + cr: "create", + cre: "create", + crea: "create", + creat: "create", + create: "create", + dd: "ddp", + ddp: "ddp", + ded: "dedupe", + dedu: "dedupe", + dedup: "dedupe", + dedupe: "dedupe", + dep: "deprecate", + depr: "deprecate", + depre: "deprecate", + deprec: "deprecate", + depreca: "deprecate", + deprecat: "deprecate", + deprecate: "deprecate", + dif: "diff", + diff: "diff", + "dist-tag": "dist-tag", + "dist-tags": "dist-tags", + docs: "docs", + doct: "doctor", + docto: "doctor", + doctor: "doctor", + ed: "edit", + edi: "edit", + edit: "edit", + exe: "exec", + exec: "exec", + expla: "explain", + explai: "explain", + explain: "explain", + explo: "explore", + explor: "explore", + explore: "explore", + find: "find", + "find-": "find-dupes", + "find-d": "find-dupes", + "find-du": "find-dupes", + "find-dup": "find-dupes", + "find-dupe": "find-dupes", + "find-dupes": "find-dupes", + fu: "fund", + fun: "fund", + fund: "fund", + g: "get", + ge: "get", + get: "get", + help: "help", + "help-": "help-search", + "help-s": "help-search", + "help-se": "help-search", + "help-sea": "help-search", + "help-sear": "help-search", + "help-searc": "help-search", + "help-search": "help-search", + hl: "hlep", + hle: "hlep", + hlep: "hlep", + ho: "home", + hom: "home", + home: "home", + i: "i", + ic: "ic", + in: "in", + inf: "info", + info: "info", + ini: "init", + init: "init", + inn: "innit", + inni: "innit", + innit: "innit", + ins: "ins", + inst: "inst", + insta: "insta", + instal: "instal", + install: "install", + "install-ci": "install-ci-test", + "install-ci-": "install-ci-test", + "install-ci-t": "install-ci-test", + "install-ci-te": "install-ci-test", + "install-ci-tes": "install-ci-test", + "install-ci-test": "install-ci-test", + "install-cl": "install-clean", + "install-cle": "install-clean", + "install-clea": "install-clean", + "install-clean": "install-clean", + "install-t": "install-test", + "install-te": "install-test", + "install-tes": "install-test", + "install-test": "install-test", + isnt: "isnt", + isnta: "isnta", + isntal: "isntal", + isntall: "isntall", + "isntall-": "isntall-clean", + "isntall-c": "isntall-clean", + "isntall-cl": "isntall-clean", + "isntall-cle": "isntall-clean", + "isntall-clea": "isntall-clean", + "isntall-clean": "isntall-clean", + iss: "issues", + issu: "issues", + issue: "issues", + issues: "issues", + it: "it", + la: "la", + lin: "link", + link: "link", + lis: "list", + list: "list", + ll: "ll", + ln: "ln", + logi: "login", + login: "login", + logo: "logout", + logou: "logout", + logout: "logout", + ls: "ls", + og: "ogr", + ogr: "ogr", + or: "org", + org: "org", + ou: "outdated", + out: "outdated", + outd: "outdated", + outda: "outdated", + outdat: "outdated", + outdate: "outdated", + outdated: "outdated", + ow: "owner", + own: "owner", + owne: "owner", + owner: "owner", + pa: "pack", + pac: "pack", + pack: "pack", + pi: "ping", + pin: "ping", + ping: "ping", + pk: "pkg", + pkg: "pkg", + pre: "prefix", + pref: "prefix", + prefi: "prefix", + prefix: "prefix", + pro: "profile", + prof: "profile", + profi: "profile", + profil: "profile", + profile: "profile", + pru: "prune", + prun: "prune", + prune: "prune", + pu: "publish", + pub: "publish", + publ: "publish", + publi: "publish", + publis: "publish", + publish: "publish", + q: "query", + qu: "query", + que: "query", + quer: "query", + query: "query", + r: "r", + rb: "rb", + reb: "rebuild", + rebu: "rebuild", + rebui: "rebuild", + rebuil: "rebuild", + rebuild: "rebuild", + rem: "remove", + remo: "remove", + remov: "remove", + remove: "remove", + rep: "repo", + repo: "repo", + res: "restart", + rest: "restart", + resta: "restart", + restar: "restart", + restart: "restart", + rm: "rm", + ro: "root", + roo: "root", + root: "root", + rum: "rum", + run: "run", + "run-": "run-script", + "run-s": "run-script", + "run-sc": "run-script", + "run-scr": "run-script", + "run-scri": "run-script", + "run-scrip": "run-script", + "run-script": "run-script", + s: "s", + sb: "sbom", + sbo: "sbom", + sbom: "sbom", + se: "se", + sea: "search", + sear: "search", + searc: "search", + search: "search", + set: "set", + sho: "show", + show: "show", + shr: "shrinkwrap", + shri: "shrinkwrap", + shrin: "shrinkwrap", + shrink: "shrinkwrap", + shrinkw: "shrinkwrap", + shrinkwr: "shrinkwrap", + shrinkwra: "shrinkwrap", + shrinkwrap: "shrinkwrap", + si: "sit", + sit: "sit", + star: "star", + stars: "stars", + start: "start", + sto: "stop", + stop: "stop", + t: "t", + tea: "team", + team: "team", + tes: "test", + test: "test", + to: "token", + tok: "token", + toke: "token", + token: "token", + ts: "tst", + tst: "tst", + ud: "udpate", + udp: "udpate", + udpa: "udpate", + udpat: "udpate", + udpate: "udpate", + un: "un", + und: "undeprecate", + unde: "undeprecate", + undep: "undeprecate", + undepr: "undeprecate", + undepre: "undeprecate", + undeprec: "undeprecate", + undepreca: "undeprecate", + undeprecat: "undeprecate", + undeprecate: "undeprecate", + uni: "uninstall", + unin: "uninstall", + unins: "uninstall", + uninst: "uninstall", + uninsta: "uninstall", + uninstal: "uninstall", + uninstall: "uninstall", + unl: "unlink", + unli: "unlink", + unlin: "unlink", + unlink: "unlink", + unp: "unpublish", + unpu: "unpublish", + unpub: "unpublish", + unpubl: "unpublish", + unpubli: "unpublish", + unpublis: "unpublish", + unpublish: "unpublish", + uns: "unstar", + unst: "unstar", + unsta: "unstar", + unstar: "unstar", + up: "up", + upd: "update", + upda: "update", + updat: "update", + update: "update", + upg: "upgrade", + upgr: "upgrade", + upgra: "upgrade", + upgrad: "upgrade", + upgrade: "upgrade", + ur: "urn", + urn: "urn", + v: "v", + veri: "verison", + veris: "verison", + veriso: "verison", + verison: "verison", + vers: "version", + versi: "version", + versio: "version", + version: "version", + vi: "view", + vie: "view", + view: "view", + who: "whoami", + whoa: "whoami", + whoam: "whoami", + whoami: "whoami", + why: "why", + x: "x", +}; + // These must resolve to an entry in commands const aliases = { // aliases @@ -158,8 +515,6 @@ export function deref(c) { return aliases[c]; } - const abbrevs = abbrev(commands.concat(Object.keys(aliases))); - // first deref the abbrev, if there is one // then resolve any aliases // so `npm install-cl` will resolve to `install-clean` then to `ci` From 4be412483e2f1c69cb3864e5173334391f490f09 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 16:20:56 +0200 Subject: [PATCH 40/71] Also push new lockfile --- package-lock.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a8f765..88e9fb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -411,15 +411,6 @@ "node": ">=14" } }, - "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -1731,7 +1722,6 @@ "version": "1.0.0", "license": "AGPL-3.0-or-later", "dependencies": { - "abbrev": "3.0.1", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "make-fetch-happen": "14.0.3", From 8ed2330a3cf7747a6cf657fc5289cfc9bb52183e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 13 Oct 2025 15:49:42 +0200 Subject: [PATCH 41/71] Allow the safe-chain to act as a regular http proxy too (besides the CONNECT tunneling implementation) --- .../src/registryProxy/plainHttpProxy.js | 58 +++++++++++++++++++ .../src/registryProxy/registryProxy.js | 9 +-- 2 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/plainHttpProxy.js diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js new file mode 100644 index 0000000..68a2362 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -0,0 +1,58 @@ +import * as http from "http"; +import * as https from "https"; + +export function handleHttpProxyRequest(req, res) { + const url = new URL(req.url); + + let protocol; + if (url.protocol === "http:") { + protocol = http; + } else if (url.protocol === "https:") { + protocol = https; + } else { + res.writeHead(502); + res.end(`Bad Gateway: Unsupported protocol ${url.protocol}`); + return; + } + + const proxyRequest = protocol + .request( + req.url, + { method: req.method, headers: req.headers }, + (proxyRes) => { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + + proxyRes.on("error", () => { + // Stream error while piping response + // Response headers already sent, can't send error status + }); + } + ) + .on("error", (err) => { + res.writeHead(502); + res.end(`Bad Gateway: ${err.message}`); + }); + + req.on("error", () => { + // Client request stream error + // Abort the proxy request + proxyRequest.destroy(); + }); + + res.on("error", () => { + // Client response stream error (client disconnected) + // Clean up proxy streams + proxyRequest.destroy(); + }); + + res.on("close", () => { + // Client disconnected + // Abort the proxy request to avoid unnecessary work + if (!res.writableEnded) { + proxyRequest.destroy(); + } + }); + + req.pipe(proxyRequest); +} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 3558673..2895753 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -1,6 +1,7 @@ import * as http from "http"; import { tunnelRequest } from "./tunnelRequestHandler.js"; import { mitmConnect } from "./mitmRequestHandler.js"; +import { handleHttpProxyRequest } from "./plainHttpProxy.js"; import { getCaCertPath } from "./certUtils.js"; import { auditChanges } from "../scanning/audit/index.js"; import { knownRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; @@ -54,13 +55,7 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { } function createProxyServer() { - const server = http.createServer((_, res) => { - res.writeHead(400, "Bad Request"); - res.write( - "Safe-chain proxy: Direct http not supported. Only CONNECT requests are allowed." - ); - res.end(); - }); + const server = http.createServer(handleHttpProxyRequest); return server; } From d2c155afeea4738370eacc8839a90108d16d4607 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 12:55:56 +0200 Subject: [PATCH 42/71] Add e2e test for registry over http --- test/e2e/DockerTestContainer.js | 20 +++++++++++++++++ test/e2e/safe-chain-proxy.e2e.spec.js | 31 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 483f03a..1a817eb 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -60,6 +60,26 @@ export class DockerTestContainer { } } + dockerExec(command, daemon = false) { + if (!this.isRunning) { + throw new Error("Container is not running"); + } + + try { + const dockerExecCommand = `docker exec ${daemon ? "-d " : " "}${ + this.containerName + } bash -c "${command}"`; + const output = execSync(dockerExecCommand, { + encoding: "utf-8", + stdio: "pipe", + timeout: 10000, + }); + return output; + } catch (error) { + throw new Error(`Failed to execute command: ${error.message}`); + } + } + async openShell(shell) { let ptyProcess = pty.spawn( "docker", diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 6abbb0f..3efd2aa 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -57,4 +57,35 @@ describe("E2E: Safe chain proxy", () => { "Proxy log does not contain expected entries" ); }); + + it(`safe-chain proxy allows to request through a local http registry`, async () => { + // Start a local npm registry (verdaccio) inside the container + container.dockerExec("npx -y verdaccio", true); + + // Wait for verdaccio to be ready (max 30 seconds) + for (let i = 0; i < 60; i++) { + await new Promise((resolve) => setTimeout(resolve, 500)); + try { + const curlOutput = container.dockerExec( + "curl -I http://localhost:4873/" + ); + if (curlOutput.includes("200 OK")) { + break; + } + } catch { + // ignore, this means docker exec returned -1 and verdaccio is not yet ready + } + } + + const shell = await container.openShell("bash"); + const result = await shell.runCommand( + "npm --registry http://localhost:4873 install react" + ); + + // Check if the installation was successful + assert( + result.output.includes("added"), + "npm install did not complete successfully, output: " + result.output + ); + }); }); From f4933b08d00f82dc6089ce26d3c0c64aca937a8d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:15:14 +0200 Subject: [PATCH 43/71] Add log to diagnose e2e tests --- test/e2e/DockerTestContainer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 1a817eb..196afdb 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -116,6 +116,8 @@ export class DockerTestContainer { const timeout = setTimeout(() => { // Fallback in case the command doesn't finish in a reasonable time + // oxlint-disable-next-line no-console - having this log in CI helps diagnose issues + console.log("Command timeout reached"); resolve({ allData, output: parseShellOutput(allData), command }); ptyProcess.removeListener("data", handleInput); }, 10000); From 2968960b41f691141e3b915443e794cb1086e902 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:22:58 +0200 Subject: [PATCH 44/71] Cleanup registryProxy, increase timeout on DockerTestContainer --- packages/safe-chain/src/registryProxy/registryProxy.js | 8 ++++++-- test/e2e/DockerTestContainer.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 2895753..d548999 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -16,7 +16,6 @@ const state = { export function createSafeChainProxy() { const server = createProxyServer(); - server.on("connect", handleConnect); return { startServer: () => startServer(server), @@ -55,7 +54,12 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { } function createProxyServer() { - const server = http.createServer(handleHttpProxyRequest); + const server = http.createServer( + handleHttpProxyRequest // This handles plain HTTP requests + ); + + // This handles HTTPS requests via the CONNECT method + server.on("connect", handleConnect); return server; } diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 196afdb..45b66d0 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -120,7 +120,7 @@ export class DockerTestContainer { console.log("Command timeout reached"); resolve({ allData, output: parseShellOutput(allData), command }); ptyProcess.removeListener("data", handleInput); - }, 10000); + }, 20000); function handleInput(data) { allData.push(data); From b6c31e1a5a1168eff0f3a40ac479c61d7aaf0ca4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:30:06 +0200 Subject: [PATCH 45/71] Increase time to start verdaccio --- test/e2e/safe-chain-proxy.e2e.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 3efd2aa..8a62052 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -62,8 +62,8 @@ describe("E2E: Safe chain proxy", () => { // Start a local npm registry (verdaccio) inside the container container.dockerExec("npx -y verdaccio", true); - // Wait for verdaccio to be ready (max 30 seconds) - for (let i = 0; i < 60; i++) { + // Wait for verdaccio to be ready (max 60 seconds) + for (let i = 0; i < 120; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( From c50eac977bbdfa371852e21f20674d1b50f52ce7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:34:47 +0200 Subject: [PATCH 46/71] Throw when verdaccio did not start --- test/e2e/safe-chain-proxy.e2e.spec.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 8a62052..11d01f7 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -62,6 +62,7 @@ describe("E2E: Safe chain proxy", () => { // Start a local npm registry (verdaccio) inside the container container.dockerExec("npx -y verdaccio", true); + let verdaccioStarted = false; // Wait for verdaccio to be ready (max 60 seconds) for (let i = 0; i < 120; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); @@ -70,12 +71,16 @@ describe("E2E: Safe chain proxy", () => { "curl -I http://localhost:4873/" ); if (curlOutput.includes("200 OK")) { + verdaccioStarted = true; break; } } catch { // ignore, this means docker exec returned -1 and verdaccio is not yet ready } } + if (!verdaccioStarted) { + throw new Error("Verdaccio did not start in time"); + } const shell = await container.openShell("bash"); const result = await shell.runCommand( From 37585e80735f5c06192bb462cb9052936d087769 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:44:49 +0200 Subject: [PATCH 47/71] Add more logs, handle verdaccio not starting better --- test/e2e/safe-chain-proxy.e2e.spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 11d01f7..12929df 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -72,6 +72,7 @@ describe("E2E: Safe chain proxy", () => { ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; + console.log("Verdaccio started, after " + i * 500 + "ms"); break; } } catch { @@ -79,10 +80,10 @@ describe("E2E: Safe chain proxy", () => { } } if (!verdaccioStarted) { - throw new Error("Verdaccio did not start in time"); + assert.fail("Verdaccio did not start in time"); } - const shell = await container.openShell("bash"); + const shell = await container.openShell("zsh"); const result = await shell.runCommand( "npm --registry http://localhost:4873 install react" ); From f655e8cfcb786754b81e4d1b5f4b022894e1b128 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:52:28 +0200 Subject: [PATCH 48/71] Change command to install through registry. --- test/e2e/safe-chain-proxy.e2e.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 12929df..363787f 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -83,9 +83,9 @@ describe("E2E: Safe chain proxy", () => { assert.fail("Verdaccio did not start in time"); } - const shell = await container.openShell("zsh"); + const shell = await container.openShell("bash"); const result = await shell.runCommand( - "npm --registry http://localhost:4873 install react" + "npm install lodash --registry=http://localhost:4873" ); // Check if the installation was successful From 35beeb55b09ff9822a6bfccdd50ab73e2bd894c6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 14:10:23 +0200 Subject: [PATCH 49/71] Curl url with npm package --- test/e2e/safe-chain-proxy.e2e.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 363787f..be0d6ea 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -68,7 +68,7 @@ describe("E2E: Safe chain proxy", () => { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( - "curl -I http://localhost:4873/" + "curl -I http://localhost:4873/lodash" ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; @@ -88,6 +88,8 @@ describe("E2E: Safe chain proxy", () => { "npm install lodash --registry=http://localhost:4873" ); + console.log("NPM install output:", result.output); + // Check if the installation was successful assert( result.output.includes("added"), From a2d05b0cf057cedd93bde112a3f2ee082900274f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 14:18:33 +0200 Subject: [PATCH 50/71] More logs --- packages/safe-chain/src/registryProxy/plainHttpProxy.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 68a2362..507f518 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,8 +1,10 @@ import * as http from "http"; import * as https from "https"; +// oxlint-disable no-console - just for testing, remove afterwards export function handleHttpProxyRequest(req, res) { const url = new URL(req.url); + console.log(`Proxying request to: ${req.url}`); let protocol; if (url.protocol === "http:") { @@ -23,7 +25,8 @@ export function handleHttpProxyRequest(req, res) { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); - proxyRes.on("error", () => { + proxyRes.on("error", (err) => { + console.log("Error in proxy response stream:", err); // Stream error while piping response // Response headers already sent, can't send error status }); @@ -35,18 +38,21 @@ export function handleHttpProxyRequest(req, res) { }); req.on("error", () => { + console.log("Error in client request stream"); // Client request stream error // Abort the proxy request proxyRequest.destroy(); }); res.on("error", () => { + console.log("Error in client response stream"); // Client response stream error (client disconnected) // Clean up proxy streams proxyRequest.destroy(); }); res.on("close", () => { + console.log("Client response stream closed"); // Client disconnected // Abort the proxy request to avoid unnecessary work if (!res.writableEnded) { From ee82134c19bad03b75c411929d38ebe052677c05 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 14:54:58 +0200 Subject: [PATCH 51/71] Proxyres on close and end --- .../src/registryProxy/plainHttpProxy.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 507f518..214ad0f 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -30,6 +30,22 @@ export function handleHttpProxyRequest(req, res) { // Stream error while piping response // Response headers already sent, can't send error status }); + + proxyRes.on("close", () => { + console.log("Proxy response stream closed"); + // Clean up if the proxy response stream closes + if (!res.writableEnded) { + res.end(); + } + }); + + proxyRes.on("end", () => { + console.log("Proxy response stream ended"); + // End of proxy response + if (!res.writableEnded) { + res.end(); + } + }); } ) .on("error", (err) => { From daf69964f2891ea878e6c255ae69e3ede6d3fd51 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:00:00 +0200 Subject: [PATCH 52/71] Test without safe-chain --- test/e2e/safe-chain-proxy.e2e.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index be0d6ea..0e34db0 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -62,6 +62,9 @@ describe("E2E: Safe chain proxy", () => { // Start a local npm registry (verdaccio) inside the container container.dockerExec("npx -y verdaccio", true); + const shell1 = await container.openShell("bash"); + await shell1.runCommand("safe-chain teardown"); + let verdaccioStarted = false; // Wait for verdaccio to be ready (max 60 seconds) for (let i = 0; i < 120; i++) { From bfe5820d0fbef2492ddac593acaa1352e748ea12 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:16:57 +0200 Subject: [PATCH 53/71] Log even more --- test/e2e/safe-chain-proxy.e2e.spec.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 0e34db0..8e602ca 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -59,14 +59,21 @@ describe("E2E: Safe chain proxy", () => { }); it(`safe-chain proxy allows to request through a local http registry`, async () => { + const configShell = await container.openShell("bash"); + await configShell.runCommand("touch ~/.verdaccio-config.yaml"); + await configShell.runCommand("echo 'log:' >> ~/.verdaccio-config.yaml"); + await configShell.runCommand( + "echo ' type: file' >> ~/.verdaccio-config.yaml" + ); + await configShell.runCommand( + "echo ' path: /verdaccio.log' >> ~/.verdaccio-config.yaml" + ); + // Start a local npm registry (verdaccio) inside the container - container.dockerExec("npx -y verdaccio", true); - - const shell1 = await container.openShell("bash"); - await shell1.runCommand("safe-chain teardown"); + container.dockerExec("npx -y verdaccio -c ~/.verdaccio-config.yaml", true); + // Polling until verdaccio is ready (max 60 seconds) let verdaccioStarted = false; - // Wait for verdaccio to be ready (max 60 seconds) for (let i = 0; i < 120; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); try { @@ -75,7 +82,7 @@ describe("E2E: Safe chain proxy", () => { ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; - console.log("Verdaccio started, after " + i * 500 + "ms"); + console.log("Verdaccio started, after " + i * 500 + "ms", curlOutput); break; } } catch { @@ -93,6 +100,13 @@ describe("E2E: Safe chain proxy", () => { console.log("NPM install output:", result.output); + const verdaccioLog = await container.openShell("bash"); + const { output: logOutput } = await verdaccioLog.runCommand( + "cat /verdaccio.log" + ); + + console.log("Verdaccio log output:", logOutput); + // Check if the installation was successful assert( result.output.includes("added"), From dfdce18c8ddcc2b04abf477f1bbd013aa00d0591 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:23:40 +0200 Subject: [PATCH 54/71] Fix config --- test/e2e/safe-chain-proxy.e2e.spec.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 8e602ca..a6aadd8 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -61,12 +61,14 @@ describe("E2E: Safe chain proxy", () => { it(`safe-chain proxy allows to request through a local http registry`, async () => { const configShell = await container.openShell("bash"); await configShell.runCommand("touch ~/.verdaccio-config.yaml"); - await configShell.runCommand("echo 'log:' >> ~/.verdaccio-config.yaml"); + // verdaccio.yaml + // storage: ./storage + // log: { type: file, path: ./verdaccio.log, level: info } await configShell.runCommand( - "echo ' type: file' >> ~/.verdaccio-config.yaml" + "echo 'log: { type: file, path: /verdaccio.log, level: info }' >> ~/.verdaccio-config.yaml" ); await configShell.runCommand( - "echo ' path: /verdaccio.log' >> ~/.verdaccio-config.yaml" + "echo 'storage: ./storage' >> ~/.verdaccio-config.yaml" ); // Start a local npm registry (verdaccio) inside the container From 4c76242d443d3324c94b371deebefa423885c5e9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:25:10 +0200 Subject: [PATCH 55/71] More config --- test/e2e/safe-chain-proxy.e2e.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index a6aadd8..8b2d56b 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -65,7 +65,7 @@ describe("E2E: Safe chain proxy", () => { // storage: ./storage // log: { type: file, path: ./verdaccio.log, level: info } await configShell.runCommand( - "echo 'log: { type: file, path: /verdaccio.log, level: info }' >> ~/.verdaccio-config.yaml" + "echo 'log: { type: file, path: /verdaccio.log, level: trace, colors: false }' >> ~/.verdaccio-config.yaml" ); await configShell.runCommand( "echo 'storage: ./storage' >> ~/.verdaccio-config.yaml" From b794b293d136d02f4ec908936edabfd705fc5c41 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:32:13 +0200 Subject: [PATCH 56/71] Fix config --- test/e2e/safe-chain-proxy.e2e.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 8b2d56b..51d5dd4 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -65,14 +65,14 @@ describe("E2E: Safe chain proxy", () => { // storage: ./storage // log: { type: file, path: ./verdaccio.log, level: info } await configShell.runCommand( - "echo 'log: { type: file, path: /verdaccio.log, level: trace, colors: false }' >> ~/.verdaccio-config.yaml" + "echo 'storage: ./storage' >> ~/verdaccio-config.yaml" ); await configShell.runCommand( - "echo 'storage: ./storage' >> ~/.verdaccio-config.yaml" + "echo 'log: { type: file, path: /verdaccio.log, level: trace }' >> ~/verdaccio-config.yaml" ); // Start a local npm registry (verdaccio) inside the container - container.dockerExec("npx -y verdaccio -c ~/.verdaccio-config.yaml", true); + container.dockerExec("npx -y verdaccio -c ~/verdaccio-config.yaml", true); // Polling until verdaccio is ready (max 60 seconds) let verdaccioStarted = false; From 23bce71356fd68aa003e5032c76ba3626f6b9fbc Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:40:08 +0200 Subject: [PATCH 57/71] Fix config 2 --- test/e2e/safe-chain-proxy.e2e.spec.js | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 51d5dd4..42eadfb 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -62,13 +62,37 @@ describe("E2E: Safe chain proxy", () => { const configShell = await container.openShell("bash"); await configShell.runCommand("touch ~/.verdaccio-config.yaml"); // verdaccio.yaml - // storage: ./storage - // log: { type: file, path: ./verdaccio.log, level: info } + /* +storage: ./storage +uplinks: + npmjs: + url: https://registry.npmjs.org/ +packages: + "**": + access: $all + proxy: npmjs +log: { type: file, path: ./verdaccio.log, level: trace, colors: false } + */ await configShell.runCommand( - "echo 'storage: ./storage' >> ~/verdaccio-config.yaml" + `echo 'storage: ./storage' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand(`echo 'uplinks:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand(`echo ' npmjs:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand( + `echo ' url: https://registry.npmjs.org/' >> ~/.verdaccio-config.yaml` ); await configShell.runCommand( - "echo 'log: { type: file, path: /verdaccio.log, level: trace }' >> ~/verdaccio-config.yaml" + `echo 'packages:' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand(`echo ' "**":' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand( + `echo ' access: $all' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand( + `echo ' proxy: npmjs' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand( + `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> ~/.verdaccio-config.yaml` ); // Start a local npm registry (verdaccio) inside the container From 7ae4d3bc8d817253ea9b30c712c3c035d76b7ba6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:59:43 +0200 Subject: [PATCH 58/71] Try some more config --- test/e2e/safe-chain-proxy.e2e.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 42eadfb..a81224e 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -76,6 +76,16 @@ log: { type: file, path: ./verdaccio.log, level: trace, colors: false } await configShell.runCommand( `echo 'storage: ./storage' >> ~/.verdaccio-config.yaml` ); + await configShell.runCommand(`echo 'auth:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand( + `echo ' htpasswd:' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand( + `echo ' file: ./htpasswd' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand( + `echo ' max_users: 100' >> ~/.verdaccio-config.yaml` + ); await configShell.runCommand(`echo 'uplinks:' >> ~/.verdaccio-config.yaml`); await configShell.runCommand(`echo ' npmjs:' >> ~/.verdaccio-config.yaml`); await configShell.runCommand( From 93223fe64012c1870574e60747d64284ec9a8e4d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 16:00:31 +0200 Subject: [PATCH 59/71] Try more config --- test/e2e/safe-chain-proxy.e2e.spec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index a81224e..dbc6522 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -106,7 +106,10 @@ log: { type: file, path: ./verdaccio.log, level: trace, colors: false } ); // Start a local npm registry (verdaccio) inside the container - container.dockerExec("npx -y verdaccio -c ~/verdaccio-config.yaml", true); + container.dockerExec( + "npx -y verdaccio --listen 4873 -c ~/verdaccio-config.yaml", + true + ); // Polling until verdaccio is ready (max 60 seconds) let verdaccioStarted = false; From d35a4ca3572cb20e7cf14103143fa7ca50f7ffc0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 16:05:39 +0200 Subject: [PATCH 60/71] Change config location --- test/e2e/safe-chain-proxy.e2e.spec.js | 36 +++++++++++++-------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index dbc6522..a1ffe53 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -60,7 +60,7 @@ describe("E2E: Safe chain proxy", () => { it(`safe-chain proxy allows to request through a local http registry`, async () => { const configShell = await container.openShell("bash"); - await configShell.runCommand("touch ~/.verdaccio-config.yaml"); + await configShell.runCommand("touch /.verdaccio-config.yaml"); // verdaccio.yaml /* storage: ./storage @@ -74,40 +74,38 @@ packages: log: { type: file, path: ./verdaccio.log, level: trace, colors: false } */ await configShell.runCommand( - `echo 'storage: ./storage' >> ~/.verdaccio-config.yaml` + `echo 'storage: ./storage' >> /.verdaccio-config.yaml` ); - await configShell.runCommand(`echo 'auth:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand(`echo 'auth:' >> /.verdaccio-config.yaml`); await configShell.runCommand( - `echo ' htpasswd:' >> ~/.verdaccio-config.yaml` + `echo ' htpasswd:' >> /.verdaccio-config.yaml` ); await configShell.runCommand( - `echo ' file: ./htpasswd' >> ~/.verdaccio-config.yaml` + `echo ' file: ./htpasswd' >> /.verdaccio-config.yaml` ); await configShell.runCommand( - `echo ' max_users: 100' >> ~/.verdaccio-config.yaml` + `echo ' max_users: 100' >> /.verdaccio-config.yaml` ); - await configShell.runCommand(`echo 'uplinks:' >> ~/.verdaccio-config.yaml`); - await configShell.runCommand(`echo ' npmjs:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand(`echo 'uplinks:' >> /.verdaccio-config.yaml`); + await configShell.runCommand(`echo ' npmjs:' >> /.verdaccio-config.yaml`); await configShell.runCommand( - `echo ' url: https://registry.npmjs.org/' >> ~/.verdaccio-config.yaml` + `echo ' url: https://registry.npmjs.org/' >> /.verdaccio-config.yaml` + ); + await configShell.runCommand(`echo 'packages:' >> /.verdaccio-config.yaml`); + await configShell.runCommand(`echo ' "**":' >> /.verdaccio-config.yaml`); + await configShell.runCommand( + `echo ' access: $all' >> /.verdaccio-config.yaml` ); await configShell.runCommand( - `echo 'packages:' >> ~/.verdaccio-config.yaml` - ); - await configShell.runCommand(`echo ' "**":' >> ~/.verdaccio-config.yaml`); - await configShell.runCommand( - `echo ' access: $all' >> ~/.verdaccio-config.yaml` + `echo ' proxy: npmjs' >> /.verdaccio-config.yaml` ); await configShell.runCommand( - `echo ' proxy: npmjs' >> ~/.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> ~/.verdaccio-config.yaml` + `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> /.verdaccio-config.yaml` ); // Start a local npm registry (verdaccio) inside the container container.dockerExec( - "npx -y verdaccio --listen 4873 -c ~/verdaccio-config.yaml", + "npx -y verdaccio --listen 4873 -c /verdaccio-config.yaml", true ); From b567016ddd0023260736a7edad5b2c4f5efdd11c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 16:11:34 +0200 Subject: [PATCH 61/71] Simplify test --- test/e2e/safe-chain-proxy.e2e.spec.js | 103 +++++++++++++------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index a1ffe53..0c21f88 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -60,58 +60,55 @@ describe("E2E: Safe chain proxy", () => { it(`safe-chain proxy allows to request through a local http registry`, async () => { const configShell = await container.openShell("bash"); - await configShell.runCommand("touch /.verdaccio-config.yaml"); - // verdaccio.yaml - /* -storage: ./storage -uplinks: - npmjs: - url: https://registry.npmjs.org/ -packages: - "**": - access: $all - proxy: npmjs -log: { type: file, path: ./verdaccio.log, level: trace, colors: false } - */ - await configShell.runCommand( - `echo 'storage: ./storage' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand(`echo 'auth:' >> /.verdaccio-config.yaml`); - await configShell.runCommand( - `echo ' htpasswd:' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo ' file: ./htpasswd' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo ' max_users: 100' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand(`echo 'uplinks:' >> /.verdaccio-config.yaml`); - await configShell.runCommand(`echo ' npmjs:' >> /.verdaccio-config.yaml`); - await configShell.runCommand( - `echo ' url: https://registry.npmjs.org/' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand(`echo 'packages:' >> /.verdaccio-config.yaml`); - await configShell.runCommand(`echo ' "**":' >> /.verdaccio-config.yaml`); - await configShell.runCommand( - `echo ' access: $all' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo ' proxy: npmjs' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> /.verdaccio-config.yaml` - ); + // await configShell.runCommand("touch /.verdaccio-config.yaml"); + // // verdaccio.yaml + // /* + // storage: ./storage + // uplinks: + // npmjs: + // url: https://registry.npmjs.org/ + // packages: + // "**": + // access: $all + // proxy: npmjs + // log: { type: file, path: ./verdaccio.log, level: trace, colors: false } + // */ + // await configShell.runCommand( + // `echo 'storage: ./storage' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand(`echo 'auth:' >> /.verdaccio-config.yaml`); + // await configShell.runCommand( + // `echo ' htpasswd:' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand( + // `echo ' file: ./htpasswd' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand( + // `echo ' max_users: 100' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand(`echo 'uplinks:' >> /.verdaccio-config.yaml`); + // await configShell.runCommand(`echo ' npmjs:' >> /.verdaccio-config.yaml`); + // await configShell.runCommand( + // `echo ' url: https://registry.npmjs.org/' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand(`echo 'packages:' >> /.verdaccio-config.yaml`); + // await configShell.runCommand(`echo ' "**":' >> /.verdaccio-config.yaml`); + // await configShell.runCommand( + // `echo ' access: $all' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand( + // `echo ' proxy: npmjs' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand( + // `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> /.verdaccio-config.yaml` + // ); // Start a local npm registry (verdaccio) inside the container - container.dockerExec( - "npx -y verdaccio --listen 4873 -c /verdaccio-config.yaml", - true - ); + container.dockerExec("npx -y verdaccio --listen 4873", true); - // Polling until verdaccio is ready (max 60 seconds) + // Polling until verdaccio is ready (max 30 seconds) let verdaccioStarted = false; - for (let i = 0; i < 120; i++) { + for (let i = 0; i < 30; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( @@ -137,12 +134,12 @@ log: { type: file, path: ./verdaccio.log, level: trace, colors: false } console.log("NPM install output:", result.output); - const verdaccioLog = await container.openShell("bash"); - const { output: logOutput } = await verdaccioLog.runCommand( - "cat /verdaccio.log" - ); + // const verdaccioLog = await container.openShell("bash"); + // const { output: logOutput } = await verdaccioLog.runCommand( + // "cat /verdaccio.log" + // ); - console.log("Verdaccio log output:", logOutput); + // console.log("Verdaccio log output:", logOutput); // Check if the installation was successful assert( From 24bda852d0e168a5b24fe27b09a7cfbc4451bb8f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 07:42:16 +0200 Subject: [PATCH 62/71] Redo test - start simple --- test/e2e/safe-chain-proxy.e2e.spec.js | 79 ++++++--------------------- 1 file changed, 17 insertions(+), 62 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 0c21f88..518390c 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -59,51 +59,6 @@ describe("E2E: Safe chain proxy", () => { }); it(`safe-chain proxy allows to request through a local http registry`, async () => { - const configShell = await container.openShell("bash"); - // await configShell.runCommand("touch /.verdaccio-config.yaml"); - // // verdaccio.yaml - // /* - // storage: ./storage - // uplinks: - // npmjs: - // url: https://registry.npmjs.org/ - // packages: - // "**": - // access: $all - // proxy: npmjs - // log: { type: file, path: ./verdaccio.log, level: trace, colors: false } - // */ - // await configShell.runCommand( - // `echo 'storage: ./storage' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand(`echo 'auth:' >> /.verdaccio-config.yaml`); - // await configShell.runCommand( - // `echo ' htpasswd:' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand( - // `echo ' file: ./htpasswd' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand( - // `echo ' max_users: 100' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand(`echo 'uplinks:' >> /.verdaccio-config.yaml`); - // await configShell.runCommand(`echo ' npmjs:' >> /.verdaccio-config.yaml`); - // await configShell.runCommand( - // `echo ' url: https://registry.npmjs.org/' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand(`echo 'packages:' >> /.verdaccio-config.yaml`); - // await configShell.runCommand(`echo ' "**":' >> /.verdaccio-config.yaml`); - // await configShell.runCommand( - // `echo ' access: $all' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand( - // `echo ' proxy: npmjs' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand( - // `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> /.verdaccio-config.yaml` - // ); - - // Start a local npm registry (verdaccio) inside the container container.dockerExec("npx -y verdaccio --listen 4873", true); // Polling until verdaccio is ready (max 30 seconds) @@ -112,7 +67,7 @@ describe("E2E: Safe chain proxy", () => { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( - "curl -I http://localhost:4873/lodash" + "curl -I http://localhost:4873/lodash/-/lodash-4.17.21.tgz" ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; @@ -127,24 +82,24 @@ describe("E2E: Safe chain proxy", () => { assert.fail("Verdaccio did not start in time"); } - const shell = await container.openShell("bash"); - const result = await shell.runCommand( - "npm install lodash --registry=http://localhost:4873" - ); - - console.log("NPM install output:", result.output); - - // const verdaccioLog = await container.openShell("bash"); - // const { output: logOutput } = await verdaccioLog.runCommand( - // "cat /verdaccio.log" + // const shell = await container.openShell("bash"); + // const result = await shell.runCommand( + // "npm install lodash --registry=http://localhost:4873" // ); - // console.log("Verdaccio log output:", logOutput); + // console.log("NPM install output:", result.output); - // Check if the installation was successful - assert( - result.output.includes("added"), - "npm install did not complete successfully, output: " + result.output - ); + // // const verdaccioLog = await container.openShell("bash"); + // // const { output: logOutput } = await verdaccioLog.runCommand( + // // "cat /verdaccio.log" + // // ); + + // // console.log("Verdaccio log output:", logOutput); + + // // Check if the installation was successful + // assert( + // result.output.includes("added"), + // "npm install did not complete successfully, output: " + result.output + // ); }); }); From b4f7d845631e9b52c53b888827c0d70aaed07fdd Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 07:50:13 +0200 Subject: [PATCH 63/71] Run npm install command --- test/e2e/package.json | 2 +- test/e2e/safe-chain-proxy.e2e.spec.js | 22 ++++++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/test/e2e/package.json b/test/e2e/package.json index 9217808..b34fd0b 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "description": "End-to-end tests for the Aikido Safe Chain", "scripts": { - "test": "node --test --test-concurrency=1 **/*.spec.js" + "test": "node --test --test-concurrency=1 **/safe-chain-proxy.e2e.spec.js" }, "keywords": [], "author": "Aikido Security", diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 518390c..fb4b61d 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -63,7 +63,7 @@ describe("E2E: Safe chain proxy", () => { // Polling until verdaccio is ready (max 30 seconds) let verdaccioStarted = false; - for (let i = 0; i < 30; i++) { + for (let i = 0; i < 60; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( @@ -71,7 +71,10 @@ describe("E2E: Safe chain proxy", () => { ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; - console.log("Verdaccio started, after " + i * 500 + "ms", curlOutput); + console.log( + "Verdaccio started, after " + i * 500 + "ms\n", + curlOutput + ); break; } } catch { @@ -82,19 +85,10 @@ describe("E2E: Safe chain proxy", () => { assert.fail("Verdaccio did not start in time"); } - // const shell = await container.openShell("bash"); - // const result = await shell.runCommand( - // "npm install lodash --registry=http://localhost:4873" - // ); + const shell = await container.openShell("bash"); + const result = await shell.runCommand("npm install lodash"); - // console.log("NPM install output:", result.output); - - // // const verdaccioLog = await container.openShell("bash"); - // // const { output: logOutput } = await verdaccioLog.runCommand( - // // "cat /verdaccio.log" - // // ); - - // // console.log("Verdaccio log output:", logOutput); + console.log("NPM install output:\n", result.output); // // Check if the installation was successful // assert( From 1a8d58889c410c82b4f9e51a1f28f2b221b10a9a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 07:50:56 +0200 Subject: [PATCH 64/71] Try again --- test/e2e/safe-chain-proxy.e2e.spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index fb4b61d..c475fdf 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -90,6 +90,12 @@ describe("E2E: Safe chain proxy", () => { console.log("NPM install output:\n", result.output); + const curlOutput = container.dockerExec( + "curl -I http://localhost:4873/lodash/-/lodash-4.17.21.tgz" + ); + + console.log("Curl output:\n", curlOutput); + // // Check if the installation was successful // assert( // result.output.includes("added"), From 1f2d4e86c7de83c5ca1b00e63b0467fa3c2dce4d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 07:54:35 +0200 Subject: [PATCH 65/71] Add registry to localhost again --- test/e2e/safe-chain-proxy.e2e.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index c475fdf..276d5e8 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -86,7 +86,9 @@ describe("E2E: Safe chain proxy", () => { } const shell = await container.openShell("bash"); - const result = await shell.runCommand("npm install lodash"); + const result = await shell.runCommand( + "npm install lodash --registry http://localhost:4873" + ); console.log("NPM install output:\n", result.output); From 3aec4737550a4c3e3cb6fce989173836c578f65b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 08:50:13 +0200 Subject: [PATCH 66/71] Without safe-chain --- test/e2e/safe-chain-proxy.e2e.spec.js | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 276d5e8..ffa1e79 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -14,8 +14,8 @@ describe("E2E: Safe chain proxy", () => { container = new DockerTestContainer(); await container.start(); - const installationShell = await container.openShell("zsh"); - await installationShell.runCommand("safe-chain setup"); + // const installationShell = await container.openShell("zsh"); + // await installationShell.runCommand("safe-chain setup"); }); afterEach(async () => { @@ -26,37 +26,37 @@ describe("E2E: Safe chain proxy", () => { } }); - it(`safe-chain proxy respects upstream proxy settings`, async () => { - // Configure and start a proxy inside the container - const proxy = await container.openShell("zsh"); - await proxy.runCommand( - `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` - ); - await proxy.runCommand("tinyproxy"); + // it(`safe-chain proxy respects upstream proxy settings`, async () => { + // // Configure and start a proxy inside the container + // const proxy = await container.openShell("zsh"); + // await proxy.runCommand( + // `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` + // ); + // await proxy.runCommand("tinyproxy"); - const shell = await container.openShell("zsh"); - await shell.runCommand( - 'export HTTPS_PROXY="http://user:password@localhost:8888"' - ); - const { output } = await shell.runCommand("npm install axios"); + // const shell = await container.openShell("zsh"); + // await shell.runCommand( + // 'export HTTPS_PROXY="http://user:password@localhost:8888"' + // ); + // const { output } = await shell.runCommand("npm install axios"); - // Check if the installation was successful - assert( - output.includes("added") || output.includes("up to date"), - "npm install did not complete successfully" - ); + // // Check if the installation was successful + // assert( + // output.includes("added") || output.includes("up to date"), + // "npm install did not complete successfully" + // ); - const proxyLog = await container.openShell("zsh"); - const { output: logOutput } = await proxyLog.runCommand( - "cat /var/log/tinyproxy/tinyproxy.log" - ); + // const proxyLog = await container.openShell("zsh"); + // const { output: logOutput } = await proxyLog.runCommand( + // "cat /var/log/tinyproxy/tinyproxy.log" + // ); - // Check if the proxy log contains entries for the npm install - assert( - logOutput.includes("CONNECT registry.npmjs.org:443"), - "Proxy log does not contain expected entries" - ); - }); + // // Check if the proxy log contains entries for the npm install + // assert( + // logOutput.includes("CONNECT registry.npmjs.org:443"), + // "Proxy log does not contain expected entries" + // ); + // }); it(`safe-chain proxy allows to request through a local http registry`, async () => { container.dockerExec("npx -y verdaccio --listen 4873", true); From 056a1963e3be19837e0b99d0e7f8b60670f323c5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 09:18:11 +0200 Subject: [PATCH 67/71] Remove test again --- test/e2e/safe-chain-proxy.e2e.spec.js | 95 +++++++-------------------- 1 file changed, 24 insertions(+), 71 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index ffa1e79..22a7038 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -26,82 +26,35 @@ describe("E2E: Safe chain proxy", () => { } }); - // it(`safe-chain proxy respects upstream proxy settings`, async () => { - // // Configure and start a proxy inside the container - // const proxy = await container.openShell("zsh"); - // await proxy.runCommand( - // `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` - // ); - // await proxy.runCommand("tinyproxy"); + it(`safe-chain proxy respects upstream proxy settings`, async () => { + // Configure and start a proxy inside the container + const proxy = await container.openShell("zsh"); + await proxy.runCommand( + `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` + ); + await proxy.runCommand("tinyproxy"); - // const shell = await container.openShell("zsh"); - // await shell.runCommand( - // 'export HTTPS_PROXY="http://user:password@localhost:8888"' - // ); - // const { output } = await shell.runCommand("npm install axios"); + const shell = await container.openShell("zsh"); + await shell.runCommand( + 'export HTTPS_PROXY="http://user:password@localhost:8888"' + ); + const { output } = await shell.runCommand("npm install axios"); - // // Check if the installation was successful - // assert( - // output.includes("added") || output.includes("up to date"), - // "npm install did not complete successfully" - // ); - - // const proxyLog = await container.openShell("zsh"); - // const { output: logOutput } = await proxyLog.runCommand( - // "cat /var/log/tinyproxy/tinyproxy.log" - // ); - - // // Check if the proxy log contains entries for the npm install - // assert( - // logOutput.includes("CONNECT registry.npmjs.org:443"), - // "Proxy log does not contain expected entries" - // ); - // }); - - it(`safe-chain proxy allows to request through a local http registry`, async () => { - container.dockerExec("npx -y verdaccio --listen 4873", true); - - // Polling until verdaccio is ready (max 30 seconds) - let verdaccioStarted = false; - for (let i = 0; i < 60; i++) { - await new Promise((resolve) => setTimeout(resolve, 500)); - try { - const curlOutput = container.dockerExec( - "curl -I http://localhost:4873/lodash/-/lodash-4.17.21.tgz" - ); - if (curlOutput.includes("200 OK")) { - verdaccioStarted = true; - console.log( - "Verdaccio started, after " + i * 500 + "ms\n", - curlOutput - ); - break; - } - } catch { - // ignore, this means docker exec returned -1 and verdaccio is not yet ready - } - } - if (!verdaccioStarted) { - assert.fail("Verdaccio did not start in time"); - } - - const shell = await container.openShell("bash"); - const result = await shell.runCommand( - "npm install lodash --registry http://localhost:4873" + // Check if the installation was successful + assert( + output.includes("added") || output.includes("up to date"), + "npm install did not complete successfully" ); - console.log("NPM install output:\n", result.output); - - const curlOutput = container.dockerExec( - "curl -I http://localhost:4873/lodash/-/lodash-4.17.21.tgz" + const proxyLog = await container.openShell("zsh"); + const { output: logOutput } = await proxyLog.runCommand( + "cat /var/log/tinyproxy/tinyproxy.log" ); - console.log("Curl output:\n", curlOutput); - - // // Check if the installation was successful - // assert( - // result.output.includes("added"), - // "npm install did not complete successfully, output: " + result.output - // ); + // Check if the proxy log contains entries for the npm install + assert( + logOutput.includes("CONNECT registry.npmjs.org:443"), + "Proxy log does not contain expected entries" + ); }); }); From fce7550609f217ad7c814e2a224854db93efea41 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 09:21:23 +0200 Subject: [PATCH 68/71] Cleanup debugging code from test again --- .../src/registryProxy/plainHttpProxy.js | 20 ------------------- test/e2e/DockerTestContainer.js | 2 +- test/e2e/package.json | 2 +- test/e2e/safe-chain-proxy.e2e.spec.js | 4 ++-- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 214ad0f..2cd5f24 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,10 +1,8 @@ import * as http from "http"; import * as https from "https"; -// oxlint-disable no-console - just for testing, remove afterwards export function handleHttpProxyRequest(req, res) { const url = new URL(req.url); - console.log(`Proxying request to: ${req.url}`); let protocol; if (url.protocol === "http:") { @@ -25,27 +23,12 @@ export function handleHttpProxyRequest(req, res) { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); - proxyRes.on("error", (err) => { - console.log("Error in proxy response stream:", err); - // Stream error while piping response - // Response headers already sent, can't send error status - }); - proxyRes.on("close", () => { - console.log("Proxy response stream closed"); // Clean up if the proxy response stream closes if (!res.writableEnded) { res.end(); } }); - - proxyRes.on("end", () => { - console.log("Proxy response stream ended"); - // End of proxy response - if (!res.writableEnded) { - res.end(); - } - }); } ) .on("error", (err) => { @@ -54,21 +37,18 @@ export function handleHttpProxyRequest(req, res) { }); req.on("error", () => { - console.log("Error in client request stream"); // Client request stream error // Abort the proxy request proxyRequest.destroy(); }); res.on("error", () => { - console.log("Error in client response stream"); // Client response stream error (client disconnected) // Clean up proxy streams proxyRequest.destroy(); }); res.on("close", () => { - console.log("Client response stream closed"); // Client disconnected // Abort the proxy request to avoid unnecessary work if (!res.writableEnded) { diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 45b66d0..ec1af3c 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -120,7 +120,7 @@ export class DockerTestContainer { console.log("Command timeout reached"); resolve({ allData, output: parseShellOutput(allData), command }); ptyProcess.removeListener("data", handleInput); - }, 20000); + }, 15000); function handleInput(data) { allData.push(data); diff --git a/test/e2e/package.json b/test/e2e/package.json index b34fd0b..9217808 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "description": "End-to-end tests for the Aikido Safe Chain", "scripts": { - "test": "node --test --test-concurrency=1 **/safe-chain-proxy.e2e.spec.js" + "test": "node --test --test-concurrency=1 **/*.spec.js" }, "keywords": [], "author": "Aikido Security", diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 22a7038..6abbb0f 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -14,8 +14,8 @@ describe("E2E: Safe chain proxy", () => { container = new DockerTestContainer(); await container.start(); - // const installationShell = await container.openShell("zsh"); - // await installationShell.runCommand("safe-chain setup"); + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup"); }); afterEach(async () => { From 37ef3e187b83a0b39b05160a73a15eaba582aba8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 09:25:24 +0200 Subject: [PATCH 69/71] Further cleanup --- .../safe-chain/src/registryProxy/plainHttpProxy.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 2cd5f24..29b7fe1 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -23,9 +23,17 @@ export function handleHttpProxyRequest(req, res) { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); + proxyRes.on("error", () => { + // Proxy response stream error + // Clean up client response stream + if (res.writable) { + res.end(); + } + }); + proxyRes.on("close", () => { // Clean up if the proxy response stream closes - if (!res.writableEnded) { + if (res.writable) { res.end(); } }); @@ -51,9 +59,7 @@ export function handleHttpProxyRequest(req, res) { res.on("close", () => { // Client disconnected // Abort the proxy request to avoid unnecessary work - if (!res.writableEnded) { - proxyRequest.destroy(); - } + proxyRequest.destroy(); }); req.pipe(proxyRequest); From 3e8ce13db5e1d9d72e4c38a354f959c39e352456 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 11:51:56 +0200 Subject: [PATCH 70/71] Move generated abbrevs to a separate file --- .../npm/utils/abbrevs-generated.js | 358 +++++++++++++++++ .../src/packagemanager/npm/utils/cmd-list.js | 361 +----------------- 2 files changed, 360 insertions(+), 359 deletions(-) create mode 100644 packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js diff --git a/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js b/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js new file mode 100644 index 0000000..204ffa7 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js @@ -0,0 +1,358 @@ +// This was ran with the abbrev package to generate the abbrevs object below +// console.log(abbrev(commands.concat(Object.keys(aliases)))); +export const abbrevs = { + ac: "access", + acc: "access", + acce: "access", + acces: "access", + access: "access", + add: "add", + "add-": "add-user", + "add-u": "add-user", + "add-us": "add-user", + "add-use": "add-user", + "add-user": "add-user", + addu: "adduser", + addus: "adduser", + adduse: "adduser", + adduser: "adduser", + aud: "audit", + audi: "audit", + audit: "audit", + aut: "author", + auth: "author", + autho: "author", + author: "author", + b: "bugs", + bu: "bugs", + bug: "bugs", + bugs: "bugs", + c: "c", + ca: "cache", + cac: "cache", + cach: "cache", + cache: "cache", + ci: "ci", + cit: "cit", + "clean-install": "clean-install", + "clean-install-": "clean-install-test", + "clean-install-t": "clean-install-test", + "clean-install-te": "clean-install-test", + "clean-install-tes": "clean-install-test", + "clean-install-test": "clean-install-test", + com: "completion", + comp: "completion", + compl: "completion", + comple: "completion", + complet: "completion", + completi: "completion", + completio: "completion", + completion: "completion", + con: "config", + conf: "config", + confi: "config", + config: "config", + cr: "create", + cre: "create", + crea: "create", + creat: "create", + create: "create", + dd: "ddp", + ddp: "ddp", + ded: "dedupe", + dedu: "dedupe", + dedup: "dedupe", + dedupe: "dedupe", + dep: "deprecate", + depr: "deprecate", + depre: "deprecate", + deprec: "deprecate", + depreca: "deprecate", + deprecat: "deprecate", + deprecate: "deprecate", + dif: "diff", + diff: "diff", + "dist-tag": "dist-tag", + "dist-tags": "dist-tags", + docs: "docs", + doct: "doctor", + docto: "doctor", + doctor: "doctor", + ed: "edit", + edi: "edit", + edit: "edit", + exe: "exec", + exec: "exec", + expla: "explain", + explai: "explain", + explain: "explain", + explo: "explore", + explor: "explore", + explore: "explore", + find: "find", + "find-": "find-dupes", + "find-d": "find-dupes", + "find-du": "find-dupes", + "find-dup": "find-dupes", + "find-dupe": "find-dupes", + "find-dupes": "find-dupes", + fu: "fund", + fun: "fund", + fund: "fund", + g: "get", + ge: "get", + get: "get", + help: "help", + "help-": "help-search", + "help-s": "help-search", + "help-se": "help-search", + "help-sea": "help-search", + "help-sear": "help-search", + "help-searc": "help-search", + "help-search": "help-search", + hl: "hlep", + hle: "hlep", + hlep: "hlep", + ho: "home", + hom: "home", + home: "home", + i: "i", + ic: "ic", + in: "in", + inf: "info", + info: "info", + ini: "init", + init: "init", + inn: "innit", + inni: "innit", + innit: "innit", + ins: "ins", + inst: "inst", + insta: "insta", + instal: "instal", + install: "install", + "install-ci": "install-ci-test", + "install-ci-": "install-ci-test", + "install-ci-t": "install-ci-test", + "install-ci-te": "install-ci-test", + "install-ci-tes": "install-ci-test", + "install-ci-test": "install-ci-test", + "install-cl": "install-clean", + "install-cle": "install-clean", + "install-clea": "install-clean", + "install-clean": "install-clean", + "install-t": "install-test", + "install-te": "install-test", + "install-tes": "install-test", + "install-test": "install-test", + isnt: "isnt", + isnta: "isnta", + isntal: "isntal", + isntall: "isntall", + "isntall-": "isntall-clean", + "isntall-c": "isntall-clean", + "isntall-cl": "isntall-clean", + "isntall-cle": "isntall-clean", + "isntall-clea": "isntall-clean", + "isntall-clean": "isntall-clean", + iss: "issues", + issu: "issues", + issue: "issues", + issues: "issues", + it: "it", + la: "la", + lin: "link", + link: "link", + lis: "list", + list: "list", + ll: "ll", + ln: "ln", + logi: "login", + login: "login", + logo: "logout", + logou: "logout", + logout: "logout", + ls: "ls", + og: "ogr", + ogr: "ogr", + or: "org", + org: "org", + ou: "outdated", + out: "outdated", + outd: "outdated", + outda: "outdated", + outdat: "outdated", + outdate: "outdated", + outdated: "outdated", + ow: "owner", + own: "owner", + owne: "owner", + owner: "owner", + pa: "pack", + pac: "pack", + pack: "pack", + pi: "ping", + pin: "ping", + ping: "ping", + pk: "pkg", + pkg: "pkg", + pre: "prefix", + pref: "prefix", + prefi: "prefix", + prefix: "prefix", + pro: "profile", + prof: "profile", + profi: "profile", + profil: "profile", + profile: "profile", + pru: "prune", + prun: "prune", + prune: "prune", + pu: "publish", + pub: "publish", + publ: "publish", + publi: "publish", + publis: "publish", + publish: "publish", + q: "query", + qu: "query", + que: "query", + quer: "query", + query: "query", + r: "r", + rb: "rb", + reb: "rebuild", + rebu: "rebuild", + rebui: "rebuild", + rebuil: "rebuild", + rebuild: "rebuild", + rem: "remove", + remo: "remove", + remov: "remove", + remove: "remove", + rep: "repo", + repo: "repo", + res: "restart", + rest: "restart", + resta: "restart", + restar: "restart", + restart: "restart", + rm: "rm", + ro: "root", + roo: "root", + root: "root", + rum: "rum", + run: "run", + "run-": "run-script", + "run-s": "run-script", + "run-sc": "run-script", + "run-scr": "run-script", + "run-scri": "run-script", + "run-scrip": "run-script", + "run-script": "run-script", + s: "s", + sb: "sbom", + sbo: "sbom", + sbom: "sbom", + se: "se", + sea: "search", + sear: "search", + searc: "search", + search: "search", + set: "set", + sho: "show", + show: "show", + shr: "shrinkwrap", + shri: "shrinkwrap", + shrin: "shrinkwrap", + shrink: "shrinkwrap", + shrinkw: "shrinkwrap", + shrinkwr: "shrinkwrap", + shrinkwra: "shrinkwrap", + shrinkwrap: "shrinkwrap", + si: "sit", + sit: "sit", + star: "star", + stars: "stars", + start: "start", + sto: "stop", + stop: "stop", + t: "t", + tea: "team", + team: "team", + tes: "test", + test: "test", + to: "token", + tok: "token", + toke: "token", + token: "token", + ts: "tst", + tst: "tst", + ud: "udpate", + udp: "udpate", + udpa: "udpate", + udpat: "udpate", + udpate: "udpate", + un: "un", + und: "undeprecate", + unde: "undeprecate", + undep: "undeprecate", + undepr: "undeprecate", + undepre: "undeprecate", + undeprec: "undeprecate", + undepreca: "undeprecate", + undeprecat: "undeprecate", + undeprecate: "undeprecate", + uni: "uninstall", + unin: "uninstall", + unins: "uninstall", + uninst: "uninstall", + uninsta: "uninstall", + uninstal: "uninstall", + uninstall: "uninstall", + unl: "unlink", + unli: "unlink", + unlin: "unlink", + unlink: "unlink", + unp: "unpublish", + unpu: "unpublish", + unpub: "unpublish", + unpubl: "unpublish", + unpubli: "unpublish", + unpublis: "unpublish", + unpublish: "unpublish", + uns: "unstar", + unst: "unstar", + unsta: "unstar", + unstar: "unstar", + up: "up", + upd: "update", + upda: "update", + updat: "update", + update: "update", + upg: "upgrade", + upgr: "upgrade", + upgra: "upgrade", + upgrad: "upgrade", + upgrade: "upgrade", + ur: "urn", + urn: "urn", + v: "v", + veri: "verison", + veris: "verison", + veriso: "verison", + verison: "verison", + vers: "version", + versi: "version", + versio: "version", + version: "version", + vi: "view", + vie: "view", + view: "view", + who: "whoami", + whoa: "whoami", + whoam: "whoami", + whoami: "whoami", + why: "why", + x: "x", +}; diff --git a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js index 8467147..6e67520 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js @@ -1,5 +1,7 @@ // Based on https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js +import { abbrevs } from "./abbrevs-generated.js"; + const commands = [ "access", "adduser", @@ -70,365 +72,6 @@ const commands = [ "whoami", ]; -// This was ran with the abbrev package to generate the abbrevs object below -// console.log(abbrev(commands.concat(Object.keys(aliases)))); -const abbrevs = { - ac: "access", - acc: "access", - acce: "access", - acces: "access", - access: "access", - add: "add", - "add-": "add-user", - "add-u": "add-user", - "add-us": "add-user", - "add-use": "add-user", - "add-user": "add-user", - addu: "adduser", - addus: "adduser", - adduse: "adduser", - adduser: "adduser", - aud: "audit", - audi: "audit", - audit: "audit", - aut: "author", - auth: "author", - autho: "author", - author: "author", - b: "bugs", - bu: "bugs", - bug: "bugs", - bugs: "bugs", - c: "c", - ca: "cache", - cac: "cache", - cach: "cache", - cache: "cache", - ci: "ci", - cit: "cit", - "clean-install": "clean-install", - "clean-install-": "clean-install-test", - "clean-install-t": "clean-install-test", - "clean-install-te": "clean-install-test", - "clean-install-tes": "clean-install-test", - "clean-install-test": "clean-install-test", - com: "completion", - comp: "completion", - compl: "completion", - comple: "completion", - complet: "completion", - completi: "completion", - completio: "completion", - completion: "completion", - con: "config", - conf: "config", - confi: "config", - config: "config", - cr: "create", - cre: "create", - crea: "create", - creat: "create", - create: "create", - dd: "ddp", - ddp: "ddp", - ded: "dedupe", - dedu: "dedupe", - dedup: "dedupe", - dedupe: "dedupe", - dep: "deprecate", - depr: "deprecate", - depre: "deprecate", - deprec: "deprecate", - depreca: "deprecate", - deprecat: "deprecate", - deprecate: "deprecate", - dif: "diff", - diff: "diff", - "dist-tag": "dist-tag", - "dist-tags": "dist-tags", - docs: "docs", - doct: "doctor", - docto: "doctor", - doctor: "doctor", - ed: "edit", - edi: "edit", - edit: "edit", - exe: "exec", - exec: "exec", - expla: "explain", - explai: "explain", - explain: "explain", - explo: "explore", - explor: "explore", - explore: "explore", - find: "find", - "find-": "find-dupes", - "find-d": "find-dupes", - "find-du": "find-dupes", - "find-dup": "find-dupes", - "find-dupe": "find-dupes", - "find-dupes": "find-dupes", - fu: "fund", - fun: "fund", - fund: "fund", - g: "get", - ge: "get", - get: "get", - help: "help", - "help-": "help-search", - "help-s": "help-search", - "help-se": "help-search", - "help-sea": "help-search", - "help-sear": "help-search", - "help-searc": "help-search", - "help-search": "help-search", - hl: "hlep", - hle: "hlep", - hlep: "hlep", - ho: "home", - hom: "home", - home: "home", - i: "i", - ic: "ic", - in: "in", - inf: "info", - info: "info", - ini: "init", - init: "init", - inn: "innit", - inni: "innit", - innit: "innit", - ins: "ins", - inst: "inst", - insta: "insta", - instal: "instal", - install: "install", - "install-ci": "install-ci-test", - "install-ci-": "install-ci-test", - "install-ci-t": "install-ci-test", - "install-ci-te": "install-ci-test", - "install-ci-tes": "install-ci-test", - "install-ci-test": "install-ci-test", - "install-cl": "install-clean", - "install-cle": "install-clean", - "install-clea": "install-clean", - "install-clean": "install-clean", - "install-t": "install-test", - "install-te": "install-test", - "install-tes": "install-test", - "install-test": "install-test", - isnt: "isnt", - isnta: "isnta", - isntal: "isntal", - isntall: "isntall", - "isntall-": "isntall-clean", - "isntall-c": "isntall-clean", - "isntall-cl": "isntall-clean", - "isntall-cle": "isntall-clean", - "isntall-clea": "isntall-clean", - "isntall-clean": "isntall-clean", - iss: "issues", - issu: "issues", - issue: "issues", - issues: "issues", - it: "it", - la: "la", - lin: "link", - link: "link", - lis: "list", - list: "list", - ll: "ll", - ln: "ln", - logi: "login", - login: "login", - logo: "logout", - logou: "logout", - logout: "logout", - ls: "ls", - og: "ogr", - ogr: "ogr", - or: "org", - org: "org", - ou: "outdated", - out: "outdated", - outd: "outdated", - outda: "outdated", - outdat: "outdated", - outdate: "outdated", - outdated: "outdated", - ow: "owner", - own: "owner", - owne: "owner", - owner: "owner", - pa: "pack", - pac: "pack", - pack: "pack", - pi: "ping", - pin: "ping", - ping: "ping", - pk: "pkg", - pkg: "pkg", - pre: "prefix", - pref: "prefix", - prefi: "prefix", - prefix: "prefix", - pro: "profile", - prof: "profile", - profi: "profile", - profil: "profile", - profile: "profile", - pru: "prune", - prun: "prune", - prune: "prune", - pu: "publish", - pub: "publish", - publ: "publish", - publi: "publish", - publis: "publish", - publish: "publish", - q: "query", - qu: "query", - que: "query", - quer: "query", - query: "query", - r: "r", - rb: "rb", - reb: "rebuild", - rebu: "rebuild", - rebui: "rebuild", - rebuil: "rebuild", - rebuild: "rebuild", - rem: "remove", - remo: "remove", - remov: "remove", - remove: "remove", - rep: "repo", - repo: "repo", - res: "restart", - rest: "restart", - resta: "restart", - restar: "restart", - restart: "restart", - rm: "rm", - ro: "root", - roo: "root", - root: "root", - rum: "rum", - run: "run", - "run-": "run-script", - "run-s": "run-script", - "run-sc": "run-script", - "run-scr": "run-script", - "run-scri": "run-script", - "run-scrip": "run-script", - "run-script": "run-script", - s: "s", - sb: "sbom", - sbo: "sbom", - sbom: "sbom", - se: "se", - sea: "search", - sear: "search", - searc: "search", - search: "search", - set: "set", - sho: "show", - show: "show", - shr: "shrinkwrap", - shri: "shrinkwrap", - shrin: "shrinkwrap", - shrink: "shrinkwrap", - shrinkw: "shrinkwrap", - shrinkwr: "shrinkwrap", - shrinkwra: "shrinkwrap", - shrinkwrap: "shrinkwrap", - si: "sit", - sit: "sit", - star: "star", - stars: "stars", - start: "start", - sto: "stop", - stop: "stop", - t: "t", - tea: "team", - team: "team", - tes: "test", - test: "test", - to: "token", - tok: "token", - toke: "token", - token: "token", - ts: "tst", - tst: "tst", - ud: "udpate", - udp: "udpate", - udpa: "udpate", - udpat: "udpate", - udpate: "udpate", - un: "un", - und: "undeprecate", - unde: "undeprecate", - undep: "undeprecate", - undepr: "undeprecate", - undepre: "undeprecate", - undeprec: "undeprecate", - undepreca: "undeprecate", - undeprecat: "undeprecate", - undeprecate: "undeprecate", - uni: "uninstall", - unin: "uninstall", - unins: "uninstall", - uninst: "uninstall", - uninsta: "uninstall", - uninstal: "uninstall", - uninstall: "uninstall", - unl: "unlink", - unli: "unlink", - unlin: "unlink", - unlink: "unlink", - unp: "unpublish", - unpu: "unpublish", - unpub: "unpublish", - unpubl: "unpublish", - unpubli: "unpublish", - unpublis: "unpublish", - unpublish: "unpublish", - uns: "unstar", - unst: "unstar", - unsta: "unstar", - unstar: "unstar", - up: "up", - upd: "update", - upda: "update", - updat: "update", - update: "update", - upg: "upgrade", - upgr: "upgrade", - upgra: "upgrade", - upgrad: "upgrade", - upgrade: "upgrade", - ur: "urn", - urn: "urn", - v: "v", - veri: "verison", - veris: "verison", - veriso: "verison", - verison: "verison", - vers: "version", - versi: "version", - versio: "version", - version: "version", - vi: "view", - vie: "view", - view: "view", - who: "whoami", - whoa: "whoami", - whoam: "whoami", - whoami: "whoami", - why: "why", - x: "x", -}; - // These must resolve to an entry in commands const aliases = { // aliases From 05354ba2f0be2f1eb1e4ac5ae37f418a2d67262a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 11:56:03 +0200 Subject: [PATCH 71/71] Add some more comments on why http / https is handled in different code paths --- packages/safe-chain/src/registryProxy/plainHttpProxy.js | 3 +++ packages/safe-chain/src/registryProxy/registryProxy.js | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 29b7fe1..e337b44 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -4,6 +4,9 @@ import * as https from "https"; export function handleHttpProxyRequest(req, res) { const url = new URL(req.url); + // The protocol for the plainHttpProxy should usually only be http: + // but when the client for some reason sends an https: request directly + // instead of using the CONNECT method, we should handle it gracefully. let protocol; if (url.protocol === "http:") { protocol = http; diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index d548999..b0e8dd1 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -55,7 +55,10 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { function createProxyServer() { const server = http.createServer( - handleHttpProxyRequest // This handles plain HTTP requests + // This handles direct HTTP requests (non-CONNECT requests) + // This is normally http-only traffic, but we also handle + // https for clients that don't properly use CONNECT + handleHttpProxyRequest ); // This handles HTTPS requests via the CONNECT method