diff --git a/.config/rollup.base.config.mjs b/.config/rollup.base.config.mjs index e551e24256..c71c04bdd6 100644 --- a/.config/rollup.base.config.mjs +++ b/.config/rollup.base.config.mjs @@ -9,11 +9,6 @@ import { nodeResolve } from '@rollup/plugin-node-resolve' import replacePlugin from '@rollup/plugin-replace' import { purgePolyfills } from 'unplugin-purge-polyfills' -import { readPackageJsonSync } from '@socketsecurity/registry/lib/packages' -import { escapeRegExp } from '@socketsecurity/registry/lib/regexps' -import { spawnSync } from '@socketsecurity/registry/lib/spawn' -import { stripAnsi } from '@socketsecurity/registry/lib/strings' - import constants from '../scripts/constants.js' import socketModifyPlugin from '../scripts/rollup/socket-modify-plugin.js' import { @@ -21,6 +16,13 @@ import { isBuiltin, normalizeId, } from '../scripts/utils/packages.js' +import { + escapeRegExp, + readPackageJsonSync, + spawnSync, + stripAnsi, +} from '../scripts/utils/registry-helpers.js' +import { includedModulesInventoryPlugin } from '../src/sea/rollup-provenance.mjs' const { INLINED_SOCKET_CLI_CYCLONEDX_CDXGEN_VERSION, @@ -38,11 +40,7 @@ const { VITEST, } = constants -export const EXTERNAL_PACKAGES = [ - '@socketsecurity/registry', - 'blessed', - 'blessed-contrib', -] +export const EXTERNAL_PACKAGES = ['blessed', 'blessed-contrib'] const builtinAliases = builtinModules.reduce((o, n) => { if (!n.startsWith('node:')) { @@ -289,6 +287,15 @@ export default function baseConfig(extendConfig = {}) { }, }), ...extendPlugins, + ...(process.env.SOCKET_SEA_BUILD === 'true' + ? [ + includedModulesInventoryPlugin({ + root: rootPath, + inventoryFile: 'THIRD-PARTY-NOTICES.json', + bundleId: 'core-rollup', + }), + ] + : []), ], } } diff --git a/.config/rollup.cdxgen-sea.config.mjs b/.config/rollup.cdxgen-sea.config.mjs new file mode 100644 index 0000000000..dafe7a62a8 --- /dev/null +++ b/.config/rollup.cdxgen-sea.config.mjs @@ -0,0 +1,530 @@ +/** Bundle the cdxgen API for offline execution from the Node SEA VFS. */ +/* eslint-disable no-await-in-loop -- VFS staging writes shared paths in dependency order. */ +import { createHash } from 'node:crypto' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import url from 'node:url' + +import commonjsPlugin from '@rollup/plugin-commonjs' +import jsonPlugin from '@rollup/plugin-json' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import { parse as parseYaml } from 'yaml' + +const root = path.resolve( + path.dirname(url.fileURLToPath(import.meta.url)), + '..', +) +const curatedLegalRoot = path.join(root, 'src/sea/licenses/cdxgen-third-party') + +const curatedLegalDocuments = { + 'boolbase@1.0.0': [ + { + filename: 'boolbase-1.0.0-ISC.txt', + kind: 'license', + sourcePath: 'LICENSE (upstream file; omitted from npm tarball)', + authority: + 'https://github.com/fb55/boolbase/blob/be0bcd8a4e917a0a5895e95b523fbbed05a64871/LICENSE', + }, + ], + 'err-code@2.0.3': [ + { + filename: 'err-code-2.0.3-MIT.txt', + kind: 'license', + sourcePath: + 'MIT license (npm package README declares MIT; package author used for copyright attribution)', + authority: + 'https://raw.githubusercontent.com/spdx/license-list-data/v3.26.0/text/MIT.txt', + attributionSource: + 'https://raw.githubusercontent.com/IndigoUnited/js-err-code/92511d41a6a926c94c9d11493404867b1e92a77/README.md', + attributionInferredFrom: + 'npm package author field (IndigoUnited); upstream v2.0.3 README declares MIT but no copyright line or license file.', + }, + { + filename: 'err-code-2.0.3-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm package; lock SRI verified)', + authority: 'https://registry.npmjs.org/err-code/2.0.3', + }, + ], + 'imurmurhash@0.1.4': [ + { + filename: 'imurmurhash-0.1.4-MIT.txt', + kind: 'license', + sourcePath: 'LICENSE (upstream file; omitted from npm tarball)', + authority: 'https://github.com/jensyt/imurmurhash-js/blob/0.1.4/LICENSE', + }, + ], + 'spdx-exceptions@2.5.0': [ + { + filename: 'spdx-exceptions-2.5.0-CC-BY-3.0.txt', + kind: 'license', + sourcePath: 'CC-BY-3.0 legal code (npm README declares CC-BY-3.0)', + authority: + 'https://raw.githubusercontent.com/spdx/license-list-data/v3.26.0/text/CC-BY-3.0.txt', + }, + { + filename: 'spdx-exceptions-2.5.0-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm package; lock SRI verified)', + authority: 'https://registry.npmjs.org/spdx-exceptions/2.5.0', + }, + ], + 'spdx-license-ids@3.0.22': [ + { + filename: 'spdx-license-ids-3.0.22-CC0-1.0.txt', + kind: 'license', + sourcePath: 'CC0-1.0 legal code (npm README declares CC0-1.0)', + authority: + 'https://raw.githubusercontent.com/spdx/license-list-data/v3.26.0/text/CC0-1.0.txt', + }, + { + filename: 'spdx-license-ids-3.0.22-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm package; lock SRI verified)', + authority: 'https://registry.npmjs.org/spdx-license-ids/3.0.22', + }, + ], +} + +const legalFilenamePattern = + /^(?:license|licence|copying|copyright|notice|patents?)(?:[._ -].*)?$/i + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex') +} + +function safePart(value) { + return String(value).replace(/[^A-Za-z0-9._-]+/g, '_') || 'unknown' +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0 +} + +async function findPackageRoot(modulePath) { + let current = path.dirname(modulePath) + while (current !== path.dirname(current)) { + const packageJsonPath = path.join(current, 'package.json') + try { + const metadata = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) + if ( + typeof metadata.name === 'string' && + typeof metadata.version === 'string' + ) { + return { directory: current, metadata } + } + } catch (error) { + if (error?.code !== 'ENOENT' && !(error instanceof SyntaxError)) { + throw error + } + } + if (path.basename(current) === 'node_modules') { + break + } + current = path.dirname(current) + } +} + +async function findLegalFiles(packageRoot) { + const files = [] + const skippedLinks = [] + const visit = async (directory, relativeDirectory = '') => { + for (const entry of ( + await fs.readdir(directory, { withFileTypes: true }) + ).sort((left, right) => compareText(left.name, right.name))) { + const relativePath = path.posix.join( + relativeDirectory.split(path.sep).join('/'), + entry.name, + ) + const fullPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + if (entry.name !== 'node_modules' && entry.name !== '.git') { + await visit(fullPath, path.join(relativeDirectory, entry.name)) + } + } else if (entry.isSymbolicLink()) { + if (legalFilenamePattern.test(entry.name)) { + skippedLinks.push(relativePath) + } + } else if (entry.isFile() && legalFilenamePattern.test(entry.name)) { + files.push({ fullPath, relativePath }) + } + } + } + await visit(packageRoot) + return { files, skippedLinks } +} + +function thirdPartyNoticePlugin() { + return { + name: 'sea-cdxgen-third-party-notices', + async generateBundle() { + const packages = new Map() + const sourceModules = new Map() + const unmappedModuleCount = { count: 0 } + const lock = parseYaml( + await fs.readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8'), + ) + if (!lock || typeof lock.packages !== 'object' || !lock.packages) { + throw new Error('pnpm-lock.yaml does not contain a packages graph') + } + const lockPackages = lock.packages + const patchedDependencies = lock.patchedDependencies ?? {} + for (const id of this.getModuleIds()) { + if (id.startsWith('\0')) { + continue + } + const rawPath = id.startsWith('file:') + ? url.fileURLToPath(id) + : id.split('?')[0] + if ( + !path.isAbsolute(rawPath) || + !rawPath.split(path.sep).includes('node_modules') + ) { + continue + } + let modulePath + try { + modulePath = await fs.realpath(rawPath) + } catch (error) { + if (error?.code === 'ENOENT') { + continue + } + throw error + } + const packageInfo = await findPackageRoot(modulePath) + if (!packageInfo) { + unmappedModuleCount.count += 1 + const moduleDigest = sha256(await fs.readFile(modulePath)) + sourceModules.set(modulePath, { + sourcePath: `unmapped/${path.basename(modulePath)}-${moduleDigest.slice(0, 12)}`, + sha256: moduleDigest, + package: null, + verification: 'unverified', + verificationReason: + 'No package metadata root was found for this Rollup module.', + }) + continue + } + const { directory, metadata } = packageInfo + const lockKeyCandidates = Object.keys(lockPackages) + .filter( + key => + key === `${metadata.name}@${metadata.version}` || + key.startsWith(`${metadata.name}@${metadata.version}(`), + ) + .sort(compareText) + const lockKey = + lockKeyCandidates.length === 1 ? lockKeyCandidates[0] : null + const lockRecord = lockKey ? lockPackages[lockKey] : null + const lockContexts = lockKeyCandidates.map(candidate => ({ + packageKey: candidate, + resolution: lockPackages[candidate]?.resolution ?? null, + })) + const patch = + patchedDependencies[`${metadata.name}@${metadata.version}`] ?? null + const instancePath = + lockKey ?? + (lockKeyCandidates.length + ? `ambiguous:${lockKeyCandidates.join('|')}` + : `${metadata.name}@${metadata.version}:missing-lock-entry`) + const key = `${metadata.name}@${metadata.version}\0${instancePath}` + let record = packages.get(key) + if (!record) { + record = { + packageName: metadata.name, + version: metadata.version, + instancePath, + lockKey, + lockContexts, + lockResolution: lockRecord?.resolution ?? null, + patch: patch + ? { path: patch.path ?? null, hash: patch.hash ?? null } + : null, + curatedLegalDocuments: + curatedLegalDocuments[`${metadata.name}@${metadata.version}`] ?? + [], + license: null, + licenseDeclarations: new Map(), + roots: new Map(), + moduleCount: 0, + obligations: [], + } + packages.set(key, record) + } + const licenseDeclaration = metadata.license ?? metadata.licenses ?? null + record.licenseDeclarations.set( + JSON.stringify(licenseDeclaration), + licenseDeclaration, + ) + const declarationValues = [...record.licenseDeclarations.entries()] + .sort(([left], [right]) => compareText(left, right)) + .map(([, value]) => value) + record.license = + declarationValues.length === 1 + ? declarationValues[0] + : declarationValues + record.moduleCount += 1 + const relativeModulePath = path + .relative(directory, modulePath) + .split(path.sep) + .join('/') + sourceModules.set(modulePath, { + sourcePath: `${instancePath}/${relativeModulePath}`, + package: `${metadata.name}@${metadata.version}`, + packageInstance: instancePath, + packagePath: relativeModulePath, + sha256: sha256(await fs.readFile(modulePath)), + lockPackageKey: lockKey, + lockPackageCandidates: lockContexts, + lockResolution: lockRecord?.resolution ?? null, + declaredLockIntegrity: lockRecord?.resolution?.integrity ?? null, + patch: patch + ? { path: patch.path ?? null, hash: patch.hash ?? null } + : null, + verification: 'unverified', + verificationReason: + 'Installed module bytes were hashed but not authenticated against locked tarball or patch output; declared lock integrity does not verify generated bundle bytes.', + }) + if (!record.roots.has(directory)) { + const { files, skippedLinks } = await findLegalFiles(directory) + record.roots.set(directory, { files, skippedLinks }) + } + } + + const inventory = { + schemaVersion: 1, + generator: 'rollup-module-closure', + packages: [], + unmappedModuleCount: unmappedModuleCount.count, + obligations: [], + } + const sourceInventory = { + schemaVersion: 1, + generator: 'rollup-module-source-hashes', + lockfile: { + path: 'pnpm-lock.yaml', + lockfileVersion: lock.lockfileVersion, + }, + verification: 'unverified', + verificationReason: + 'Hashes identify installed module source files. Declared lock SRI is metadata only and does not authenticate installed, patched, or generated bundle bytes.', + modules: [...sourceModules.values()].sort((left, right) => + compareText(left.sourcePath, right.sourcePath), + ), + } + const sourceInventoryJson = `${JSON.stringify(sourceInventory, null, 2)}\n` + const sourceInventorySha256 = sha256(Buffer.from(sourceInventoryJson)) + this.emitFile({ + type: 'asset', + fileName: 'ROLLUP-SOURCE-INVENTORY.json', + source: sourceInventoryJson, + }) + inventory.sourceInventory = { + path: 'tools/cdxgen/ROLLUP-SOURCE-INVENTORY.json', + sha256: sourceInventorySha256, + moduleCount: sourceModules.size, + verification: 'unverified', + } + if (unmappedModuleCount.count > 0) { + inventory.obligations.push({ + code: 'unmapped-rollup-modules', + count: unmappedModuleCount.count, + message: + 'Some bundled node_modules modules could not be mapped to package metadata.', + }) + } + + for (const record of [...packages.values()].sort((left, right) => + compareText( + `${left.packageName}@${left.version}\0${left.instancePath}`, + `${right.packageName}@${right.version}\0${right.instancePath}`, + ), + )) { + if (record.licenseDeclarations.size > 1) { + record.obligations.push({ + code: 'conflicting-license-declarations', + message: + 'The same package name and version resolved with different license declarations.', + }) + } + if ( + record.curatedLegalDocuments.some( + document => document.attributionInferredFrom, + ) + ) { + record.obligations.push({ + code: 'inferred-copyright-attribution', + message: record.curatedLegalDocuments.find( + document => document.attributionInferredFrom, + ).attributionInferredFrom, + }) + } + const licenseIsString = + typeof record.license === 'string' && record.license.trim() !== '' + if (!licenseIsString) { + record.obligations.push({ + code: record.license + ? 'ambiguous-license-declaration' + : 'missing-license-declaration', + message: + 'package.json does not provide one unambiguous nonempty license string.', + }) + } + if ( + Array.isArray(record.license) || + (record.license && typeof record.license === 'object') + ) { + record.obligations.push({ + code: 'ambiguous-license-declaration-format', + message: + 'The license declaration is structured or deprecated and requires review.', + }) + } + + const id = `${record.packageName}@${record.version}\0${record.instancePath}` + const packageSlug = `${safePart(record.packageName)}-${safePart(record.version)}-${sha256(Buffer.from(id)).slice(0, 12)}` + const stagedFiles = new Map() + const skippedLinks = [] + for (const { files, skippedLinks: links } of record.roots.values()) { + skippedLinks.push(...links) + for (const file of files) { + const contents = await fs.readFile(file.fullPath) + const digest = sha256(contents) + const relativePath = file.relativePath.split(path.sep).join('/') + const sourcePath = path.posix.parse(relativePath) + const contentAddressedName = `${sourcePath.name}.${digest.slice(0, 12)}${sourcePath.ext}` + const outputPath = `notices/${packageSlug}/${sourcePath.dir ? `${sourcePath.dir}/` : ''}${contentAddressedName}` + stagedFiles.set(outputPath, { + contents, + sha256: digest, + relativePath, + }) + } + } + + // Several published npm tarballs omit the license text even though + // package.json declares a license. Include version-pinned authoritative + // texts and attribution material collected during the SEA audit. + for (const document of record.curatedLegalDocuments) { + const contents = await fs.readFile( + path.join(curatedLegalRoot, document.filename), + ) + const digest = sha256(contents) + const outputPath = `notices/${packageSlug}/${document.filename}` + stagedFiles.set(outputPath, { + contents, + sha256: digest, + relativePath: document.sourcePath, + kind: document.kind, + authority: document.authority, + ...(document.attributionSource + ? { attributionSource: document.attributionSource } + : {}), + ...(document.attributionInferredFrom + ? { attributionInferredFrom: document.attributionInferredFrom } + : {}), + }) + } + + const licenseFiles = [] + const noticeFiles = [] + for (const [outputPath, file] of [...stagedFiles].sort(([a], [b]) => + compareText(a, b), + )) { + this.emitFile({ + type: 'asset', + fileName: outputPath, + source: file.contents, + }) + const reference = { + path: `tools/cdxgen/${outputPath}`, + sourcePath: file.relativePath, + sha256: file.sha256, + ...(file.authority + ? { + source: file.authority, + ...(file.attributionSource + ? { attributionSource: file.attributionSource } + : {}), + ...(file.attributionInferredFrom + ? { attributionInferredFrom: file.attributionInferredFrom } + : {}), + } + : {}), + } + const basename = path.posix.basename(file.relativePath) + if ( + file.kind === 'notice' || + /^(?:notice|copyright|patents?)/i.test(basename) + ) { + noticeFiles.push(reference) + } else { + licenseFiles.push(reference) + } + } + if (licenseFiles.length === 0) { + record.obligations.push({ + code: 'missing-license-text', + message: + 'No license text file was found in the resolved package tree.', + }) + } + if (skippedLinks.length > 0) { + record.obligations.push({ + code: 'unresolved-license-symlink', + paths: [...new Set(skippedLinks)].sort(compareText), + message: + 'A license or notice symlink was not followed when collecting package texts.', + }) + } + const packageRecord = { + name: record.packageName, + version: record.version, + instancePath: record.instancePath, + lockPackageKey: record.lockKey, + lockContexts: record.lockContexts, + patch: record.patch, + license: record.license, + moduleCount: record.moduleCount, + licenseFiles, + noticeFiles, + obligations: record.obligations, + } + inventory.packages.push(packageRecord) + for (const obligation of record.obligations) { + inventory.obligations.push({ + package: `${record.packageName}@${record.version}`, + ...obligation, + }) + } + } + + this.emitFile({ + type: 'asset', + fileName: 'THIRD-PARTY-NOTICES.json', + source: `${JSON.stringify(inventory, null, 2)}\n`, + }) + }, + } +} + +export default { + input: + process.env.SEA_CDXGEN_INPUT || + path.join(root, 'node_modules/@cyclonedx/cdxgen/lib/cli/index.js'), + output: { + dir: + process.env.SEA_CDXGEN_OUTPUT || path.join(root, 'dist/sea-cdxgen-api'), + format: 'es', + entryFileNames: 'lib/cli/index.js', + chunkFileNames: 'lib/cli/chunks/[name]-[hash].js', + assetFileNames: 'lib/cli/assets/[name]-[hash][extname]', + }, + external: id => id.startsWith('node:'), + plugins: [ + nodeResolve({ preferBuiltins: true, exportConditions: ['node'] }), + jsonPlugin(), + commonjsPlugin(), + thirdPartyNoticePlugin(), + ], +} diff --git a/.config/rollup.dist.config.mjs b/.config/rollup.dist.config.mjs index 1c41116912..e9ca764655 100644 --- a/.config/rollup.dist.config.mjs +++ b/.config/rollup.dist.config.mjs @@ -11,17 +11,6 @@ import { nodeResolve } from '@rollup/plugin-node-resolve' import fastGlob from 'fast-glob' import trash from 'trash' -import { isDirEmptySync } from '@socketsecurity/registry/lib/fs' -import { hasKeys } from '@socketsecurity/registry/lib/objects' -import { - fetchPackageManifest, - readPackageJson, -} from '@socketsecurity/registry/lib/packages' -import { normalizePath } from '@socketsecurity/registry/lib/path' -import { escapeRegExp } from '@socketsecurity/registry/lib/regexps' -import { naturalCompare } from '@socketsecurity/registry/lib/sorts' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import baseConfig, { EXTERNAL_PACKAGES } from './rollup.base.config.mjs' import constants from '../scripts/constants.js' import socketModifyPlugin from '../scripts/rollup/socket-modify-plugin.js' @@ -30,6 +19,20 @@ import { isBuiltin, normalizeId, } from '../scripts/utils/packages.js' +import { + escapeRegExp, + fetchPackageManifest, + hasKeys, + isDirEmptySync, + naturalCompare, + normalizePath, + readPackageJson, + spawnAsync, +} from '../scripts/utils/registry-helpers.js' +import { + includedModulesInventoryPlugin, + writeCopiedPackageInventory, +} from '../src/sea/rollup-provenance.mjs' const { CONSTANTS, @@ -68,10 +71,14 @@ const BLESSED_CONTRIB = 'blessed-contrib' const FLAGS = 'flags' const LICENSE_MD = `LICENSE.md` const SENTRY_NODE = '@sentry/node' +const SEA_BUILD_ENV = 'SOCKET_SEA_BUILD' +const SPAWN_SYNC = 'spawn-sync' +const SPINNER = 'spinner' const SOCKET_DESCRIPTION = 'CLI for Socket.dev' const SOCKET_DESCRIPTION_WITH_SENTRY = `${SOCKET_DESCRIPTION}, includes Sentry error handling, otherwise identical to the regular \`${SOCKET_CLI_BIN_NAME}\` package` -const SOCKET_SECURITY_REGISTRY = '@socketsecurity/registry' +const REGISTRY_CONSTANTS = 'registry-constants' const UTILS = 'utils' +const ENV_HELPERS = 'env-helpers' const VENDOR = 'vendor' // A fresh regexp per call: socketModifyPlugin advances lastIndex, so a shared @@ -155,7 +162,10 @@ async function copyManifestScripts() { jarPath, path.join(destDir, 'maven-extension', 'coana-maven-extension.jar'), ) - } else if (constants.ENV[INLINED_SOCKET_CLI_PUBLISHED_BUILD]) { + } else if ( + constants.ENV[INLINED_SOCKET_CLI_PUBLISHED_BUILD] && + process.env[SEA_BUILD_ENV] !== 'true' + ) { // Fail closed: a published build without the jar would ship a package whose // `socket manifest maven` / Maven reachability silently produces an empty // SBOM. Run `pnpm run build:maven-extension` before `build:dist`. (A local @@ -178,7 +188,7 @@ async function copyBashCompletion() { } async function copyExternalPackages() { - const { blessedContribPath, blessedPath, socketRegistryPath } = constants + const { blessedContribPath, blessedPath } = constants const nmPath = path.join(constants.rootPath, NODE_MODULES) const blessedContribNmPath = path.join(nmPath, BLESSED_CONTRIB) @@ -191,9 +201,7 @@ async function copyExternalPackages() { // Copy the other packages over to ./external/. .map(n => copyPackage(n, { - strict: - // Skip adding 'use strict' directives to Socket packages. - n !== SOCKET_SECURITY_REGISTRY, + strict: true, }), ), // Copy 'blessed-contrib' license over to @@ -218,16 +226,6 @@ async function copyExternalPackages() { // would be dropped too). [blessedPath, ['lib/**/*.js', 'usr/*', 'vendor/**/*.js']], [blessedContribPath, ['lib/**/*.js', 'index.js']], - [ - socketRegistryPath, - [ - 'external/**/*.js', - 'lib/**/*.js', - 'index.js', - 'extensions.json', - 'manifest.json', - ], - ], ].map(async ({ 0: thePath, 1: ignorePatterns }) => { await removeFiles(thePath, { exclude: [...alwaysIgnoredPatterns, ...ignorePatterns], @@ -259,6 +257,24 @@ async function copyExternalPackages() { ) } +function blessedContribInventoryPlugin(filepath) { + if (process.env[SEA_BUILD_ENV] !== 'true') { + return undefined + } + const relativeEntry = path + .relative(constants.srcPath, filepath) + .split(path.sep) + .join('/') + .replace(/\.mjs$/, '') + return includedModulesInventoryPlugin({ + root: constants.rootPath, + inventoryFile: `THIRD-PARTY-NOTICES-${relativeEntry.replaceAll('/', '-')}.json`, + bundleId: `external-${relativeEntry}`, + outputRoot: constants.distPath, + mode: 'stage', + }) +} + async function copyPackage(pkgName, options) { const { strict = true } = { __proto__: null, ...options } const nmPath = path.join(constants.rootPath, NODE_MODULES) @@ -347,7 +363,7 @@ async function updatePackageLockFile() { return } try { - await spawn( + await spawnAsync( 'pnpm', [ 'install', @@ -446,6 +462,14 @@ export default async () => { const { configPath, distPath, rootPath, srcPath } = constants const nmPath = normalizePath(path.join(rootPath, NODE_MODULES)) const constantsSrcPath = normalizePath(path.join(srcPath, 'constants.mts')) + const registryConstantsSrcPath = normalizePath( + path.join(srcPath, 'registry-constants.mts'), + ) + const envHelpersSrcPath = normalizePath(path.join(srcPath, 'utils/env.mts')) + const spawnSyncSrcPath = normalizePath( + path.join(srcPath, 'utils/spawn-sync.mts'), + ) + const spinnerSrcPath = normalizePath(path.join(srcPath, 'utils/spinner.mts')) const externalSrcPath = normalizePath(path.join(srcPath, 'external')) const blessedContribSrcPath = normalizePath( path.join(externalSrcPath, BLESSED_CONTRIB), @@ -502,6 +526,14 @@ export default async () => { switch (id) { case constantsSrcPath: return CONSTANTS + case registryConstantsSrcPath: + return REGISTRY_CONSTANTS + case envHelpersSrcPath: + return ENV_HELPERS + case spawnSyncSrcPath: + return SPAWN_SYNC + case spinnerSrcPath: + return SPINNER case flagsSrcPath: return FLAGS case shadowNpmBinSrcPath: @@ -555,19 +587,33 @@ export default async () => { }), { async writeBundle() { + const updatePackageMetadata = process.env[SEA_BUILD_ENV] !== 'true' await Promise.all([ copyCoanaCache(), copyInitGradle(), copyManifestScripts(), copyBashCompletion(), - updatePackageJson(), + ...(updatePackageMetadata ? [updatePackageJson()] : []), // Remove dist/vendor.js.map file. trash([path.join(distPath, `${VENDOR}.js.map`)]), ]) // Copy external packages AFTER other operations to avoid conflicts. await copyExternalPackages() + if (process.env[SEA_BUILD_ENV] === 'true') { + await writeCopiedPackageInventory({ + root: rootPath, + packageName: BLESSED, + packageRoot: path.join(rootPath, NODE_MODULES, BLESSED), + outputPackageRoot: path.join(constants.externalPath, BLESSED), + outputRoot: distPath, + inventoryFile: 'THIRD-PARTY-NOTICES-external-blessed.json', + bundleId: 'external-blessed', + }) + } // Update package-lock.json AFTER package.json. - await updatePackageLockFile() + if (updatePackageMetadata) { + await updatePackageLockFile() + } }, }, ], @@ -581,11 +627,19 @@ export default async () => { }) ).map(filepath => { const relPath = `${path.relative(srcPath, filepath).slice(0, -4 /*.mjs*/)}.js` + const relParts = relPath.split(path.sep) + if (relParts[0] !== 'external') { + throw new Error( + `Unexpected Blessed source path outside src/external: ${filepath}`, + ) + } + const outputFile = path.join(constants.externalPath, ...relParts.slice(1)) + const provenancePlugin = blessedContribInventoryPlugin(filepath) return { input: filepath, output: [ { - file: path.join(rootPath, relPath), + file: outputFile, exports: 'auto', externalLiveBindings: false, format: 'cjs', @@ -606,10 +660,11 @@ export default async () => { ) }, plugins: [ + ...(provenancePlugin ? [provenancePlugin] : []), // Runs after copyExternalPackages() and overwrites what it rewired. socketModifyPlugin({ find: newBareBlessedRequireRegExp(), - replace: () => relativeBlessedPath(path.join(rootPath, relPath)), + replace: () => relativeBlessedPath(outputFile), }), nodeResolve({ exportConditions: ['node'], diff --git a/.config/rollup.sea.config.mjs b/.config/rollup.sea.config.mjs index b22fe8e32c..4ff2484c62 100644 --- a/.config/rollup.sea.config.mjs +++ b/.config/rollup.sea.config.mjs @@ -1,8 +1,4 @@ -/** - * Rollup configuration for building SEA bootstrap thin wrapper. - * Compiles TypeScript bootstrap to CommonJS for Node.js SEA compatibility. - */ - +/** Rollup the embedded root Socket app launcher for the Node 27 SEA VFS. */ import path from 'node:path' import url from 'node:url' @@ -10,31 +6,24 @@ import { babel as babelPlugin } from '@rollup/plugin-babel' import commonjsPlugin from '@rollup/plugin-commonjs' import { nodeResolve } from '@rollup/plugin-node-resolve' -const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) -const rootDir = path.join(__dirname, '..') - +const root = path.resolve( + path.dirname(url.fileURLToPath(import.meta.url)), + '..', +) export default { - input: - process.env.SEA_BOOTSTRAP || path.join(rootDir, 'src/sea/bootstrap.mts'), + input: process.env.SEA_BOOTSTRAP || path.join(root, 'src/sea/bootstrap.mts'), output: { - file: - process.env.SEA_OUTPUT || path.join(rootDir, 'dist/sea/bootstrap.cjs'), + file: process.env.SEA_OUTPUT || path.join(root, 'dist/sea/bootstrap.cjs'), format: 'cjs', interop: 'auto', }, - external: [ - // Only externalize Node.js built-ins for the thin wrapper. - /^node:/, - ], + external: [/^node:/], plugins: [ - nodeResolve({ - preferBuiltins: true, - exportConditions: ['node'], - }), + nodeResolve({ preferBuiltins: true, exportConditions: ['node'] }), babelPlugin({ babelHelpers: 'runtime', babelrc: false, - configFile: path.join(__dirname, 'babel.config.js'), + configFile: path.join(root, '.config/babel.config.js'), extensions: ['.mjs', '.js', '.ts', '.mts'], }), commonjsPlugin(), diff --git a/.config/rollup.synp-sea.config.mjs b/.config/rollup.synp-sea.config.mjs new file mode 100644 index 0000000000..8d5e6da89d --- /dev/null +++ b/.config/rollup.synp-sea.config.mjs @@ -0,0 +1,222 @@ +/** Bundle the pinned Synp API and its dependency/license closure for SEA VFS. */ +/* eslint-disable no-await-in-loop -- VFS staging writes shared paths in dependency order. */ +import { createHash } from 'node:crypto' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import url from 'node:url' + +import commonjsPlugin from '@rollup/plugin-commonjs' +import { nodeResolve } from '@rollup/plugin-node-resolve' + +const root = path.resolve( + path.dirname(url.fileURLToPath(import.meta.url)), + '..', +) +const legalPattern = + /^(?:license|licence|copying|copyright|notice|patents?)(?:[._ -].*)?$/i + +function digest(bytes) { + return createHash('sha256').update(bytes).digest('hex') +} + +function slug(value) { + return String(value).replace(/[^A-Za-z0-9._-]+/g, '_') || 'unknown' +} + +async function packageRootFor(modulePath) { + let current = path.dirname(modulePath) + while (current !== path.dirname(current)) { + try { + const metadata = JSON.parse( + await fs.readFile(path.join(current, 'package.json'), 'utf8'), + ) + if ( + typeof metadata.name === 'string' && + typeof metadata.version === 'string' + ) { + return { root: current, metadata } + } + } catch (error) { + if (error?.code !== 'ENOENT' && !(error instanceof SyntaxError)) { + throw error + } + } + if (path.basename(current) === 'node_modules') { + break + } + current = path.dirname(current) + } +} + +async function legalFiles(packageRoot) { + const found = [] + const visit = async (directory, relative = '') => { + for (const entry of ( + await fs.readdir(directory, { withFileTypes: true }) + ).sort((a, b) => a.name.localeCompare(b.name))) { + const relativePath = path.posix.join( + relative.split(path.sep).join('/'), + entry.name, + ) + const fullPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + if (entry.name !== 'node_modules' && entry.name !== '.git') { + await visit(fullPath, path.join(relative, entry.name)) + } + } else if ( + (entry.isFile() || entry.isSymbolicLink()) && + legalPattern.test(entry.name) + ) { + const resolvedPath = await fs.realpath(fullPath) + if ( + resolvedPath !== packageRoot && + !resolvedPath.startsWith(`${packageRoot}${path.sep}`) + ) { + throw new Error( + `Synp notice file escapes package root: ${relativePath}`, + ) + } + found.push({ fullPath: resolvedPath, relativePath }) + } + } + } + await visit(packageRoot) + return found +} + +function noticesPlugin() { + return { + name: 'sea-synp-third-party-notices', + async generateBundle() { + const packages = new Map() + const unmapped = [] + for (const id of this.getModuleIds()) { + if (id.startsWith('\0')) { + continue + } + const raw = id.startsWith('file:') + ? url.fileURLToPath(id) + : id.split('?')[0] + if ( + !path.isAbsolute(raw) || + !raw.split(path.sep).includes('node_modules') + ) { + continue + } + let real + try { + real = await fs.realpath(raw) + } catch (error) { + if (error?.code === 'ENOENT') { + continue + } + throw error + } + const info = await packageRootFor(real) + if (!info) { + unmapped.push(real) + continue + } + const key = `${info.metadata.name}@${info.metadata.version}` + if (!packages.has(key)) { + packages.set(key, { ...info, key }) + } + } + if (unmapped.length) { + throw new Error( + `Unable to map ${unmapped.length} bundled Synp module(s) to package metadata`, + ) + } + + const inventory = { + schemaVersion: 1, + generator: 'rollup-module-closure', + packages: [], + obligations: [], + } + for (const record of [...packages.values()].sort((a, b) => + a.key.localeCompare(b.key), + )) { + const files = await legalFiles(record.root) + const obligations = [] + if (!files.length) { + obligations.push({ + code: 'missing-license-text', + message: + 'No license or notice text file is present in the installed package; its package metadata license is retained.', + }) + } + const packageSlug = `${slug(record.metadata.name)}-${slug(record.metadata.version)}` + const licenseFiles = [] + const noticeFiles = [] + for (const file of files) { + const contents = await fs.readFile(file.fullPath) + const filename = `notices/${packageSlug}/${file.relativePath}` + this.emitFile({ type: 'asset', fileName: filename, source: contents }) + const reference = { + path: `tools/synp/${filename}`, + sourcePath: file.relativePath, + sha256: digest(contents), + } + if ( + /^(?:notice|copyright|patents?)/i.test( + path.posix.basename(file.relativePath), + ) + ) { + noticeFiles.push(reference) + } else { + licenseFiles.push(reference) + } + } + inventory.packages.push({ + name: record.metadata.name, + version: record.metadata.version, + license: record.metadata.license ?? record.metadata.licenses ?? null, + licenseFiles, + noticeFiles, + obligations, + }) + for (const obligation of obligations) { + inventory.obligations.push({ package: record.key, ...obligation }) + } + } + this.emitFile({ + type: 'asset', + fileName: 'THIRD-PARTY-NOTICES.json', + source: `${JSON.stringify(inventory, null, 2)}\n`, + }) + }, + } +} + +export default { + input: + process.env.SEA_SYNP_INPUT || + path.join(root, 'src/sea/adapters/synp-worker.mjs'), + output: { + dir: process.env.SEA_SYNP_OUTPUT || path.join(root, 'dist/sea-synp'), + format: 'cjs', + entryFileNames: 'bin/synp.js', + }, + external: id => + id.startsWith('node:') || + [ + 'assert', + 'buffer', + 'crypto', + 'fs', + 'fs/promises', + 'module', + 'os', + 'path', + 'stream', + 'url', + 'util', + 'worker_threads', + ].includes(id), + plugins: [ + nodeResolve({ preferBuiltins: true, exportConditions: ['node'] }), + commonjsPlugin(), + noticesPlugin(), + ], +} diff --git a/.github/actions/repo/upload-test-artifacts/action.yml b/.github/actions/repo/upload-test-artifacts/action.yml new file mode 100644 index 0000000000..9120982987 --- /dev/null +++ b/.github/actions/repo/upload-test-artifacts/action.yml @@ -0,0 +1,17 @@ +name: Upload native test evidence +description: Upload bounded PNG, SVG, JSON, and HTML evidence from a test run. +inputs: + name: + description: Artifact name + required: true + path: + description: Directory containing test evidence + required: true +outputs: + artifact-id: + description: Artifact identifier + artifact-digest: + description: SHA-256 digest of uploaded archive +runs: + using: node24 + main: index.mjs diff --git a/.github/actions/repo/upload-test-artifacts/index.mjs b/.github/actions/repo/upload-test-artifacts/index.mjs new file mode 100644 index 0000000000..5b22262974 --- /dev/null +++ b/.github/actions/repo/upload-test-artifacts/index.mjs @@ -0,0 +1,14 @@ +import { + ArtifactUploadError, + runTestArtifactUploadAction, +} from '../../../../scripts/ci/upload-artifact.mjs' + +try { + await runTestArtifactUploadAction() +} catch (error) { + const message = error instanceof ArtifactUploadError + ? error.message + : 'Test evidence upload failed. Saw: an unexpected failure; wanted a completed upload. Fix: inspect runner access and retry the job.' + process.stderr.write(`::error::${message}\n`) + process.exitCode = 1 +} diff --git a/.github/workflows/bazel-windows.yml b/.github/workflows/bazel-windows.yml index 6d55233363..bd7620b92d 100644 --- a/.github/workflows/bazel-windows.yml +++ b/.github/workflows/bazel-windows.yml @@ -7,6 +7,10 @@ on: - .github/workflows/bazel-windows.yml - package.json - pnpm-lock.yaml + - .config/rollup.sea.config.mjs + - src/sea/** + - scripts/build-package-matrix.mts + - test/package-matrix.test.mts - src/commands/manifest/bazel/** - test/fixtures/manifest-bazel/** push: @@ -15,6 +19,10 @@ on: - .github/workflows/bazel-windows.yml - package.json - pnpm-lock.yaml + - .config/rollup.sea.config.mjs + - src/sea/** + - scripts/build-package-matrix.mts + - test/package-matrix.test.mts - src/commands/manifest/bazel/** - test/fixtures/manifest-bazel/** @@ -101,7 +109,7 @@ jobs: - name: Test executable handling and prerequisites run: | $PSNativeCommandUseErrorActionPreference = $true - pnpm test:unit src/commands/manifest/bazel/bazel-bin-detect.test.mts src/commands/manifest/bazel/bazel-process.test.mts src/commands/manifest/bazel/bazel-java-shim.test.mts src/commands/manifest/bazel/bazel-python-shim.test.mts src/commands/manifest/bazel/bazel-query-runner.test.mts src/commands/manifest/bazel/bazel-cquery.test.mts + pnpm test:unit src/commands/manifest/bazel/bazel-bin-detect.test.mts src/commands/manifest/bazel/bazel-process.test.mts src/commands/manifest/bazel/bazel-java-shim.test.mts src/commands/manifest/bazel/bazel-python-shim.test.mts src/commands/manifest/bazel/bazel-query-runner.test.mts src/commands/manifest/bazel/bazel-cquery.test.mts src/sea/build-sea.test.mts test/package-matrix.test.mts - name: Install the packaged CLI run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57f9c45b06..ffb8254de9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: - name: Install Node.js shell: bash env: - NODE_VERSION: 25.9.0 + NODE_VERSION: 26.13.0 run: node scripts/ci/setup-node.mjs --version "$NODE_VERSION" - name: Download sfw @@ -320,7 +320,7 @@ jobs: - name: Install Node.js shell: bash env: - NODE_VERSION: 25.9.0 + NODE_VERSION: 26.13.0 run: node scripts/ci/setup-node.mjs --version "$NODE_VERSION" - name: Download sfw @@ -456,7 +456,7 @@ jobs: fail-fast: true matrix: # Node 20 reached end-of-life 2026-03-24; test supported majors only. - node-version: [22, 24] + node-version: [22, 24, 26.13.0] os: [ubuntu-latest] steps: - name: Checkout source diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b78aa54644..63e814fd29 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -17,7 +17,7 @@ jobs: fail-fast: true matrix: # Node 20 reached end-of-life 2026-03-24; test supported majors only. - node-version: [22, 24] + node-version: [22, 24, 26.13.0] os: [ubuntu-latest] # os: [ubuntu-latest, windows-latest] - Windows tests disbaled (see project https://linear.app/socketdev/project/autofixes-windows-support-fc2f2a45f759) steps: diff --git a/.github/workflows/native-terminal.yml b/.github/workflows/native-terminal.yml new file mode 100644 index 0000000000..da2b80a900 --- /dev/null +++ b/.github/workflows/native-terminal.yml @@ -0,0 +1,150 @@ +name: Native terminal validation + +on: + push: + branches: + - 'test/native-terminal-validation-*' + - 'feat/v1x-node27-sea-vfs-socket' + pull_request: + branches: [v1.x] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: native-terminal-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + native-terminal: + name: Native terminal (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + target: linux-x64 + - os: macos-15 + target: darwin-arm64 + - os: windows-2025 + target: win-x64 + steps: + - name: Checkout source + shell: bash + env: + CHECKOUT_REF: ${{ github.sha }} + GITHUB_TOKEN: ${{ github.token }} + SERVER_URL: ${{ github.server_url }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + git init -q + git config --local advice.detachedHead false + git remote add origin "${SERVER_URL}/${REPOSITORY}" + AUTH_B64="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" + export GIT_CONFIG_COUNT=1 + export GIT_CONFIG_KEY_0="http.${SERVER_URL}/.extraheader" + export GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${AUTH_B64}" + git fetch --no-tags --depth=1 origin "$CHECKOUT_REF" + git checkout -q --detach FETCH_HEAD + + - name: Install Node.js 26.13.0 (checksum verified) + shell: bash + run: | + set -euo pipefail + version=26.13.0 + base="https://nodejs.org/dist/v${version}" + if [[ "$RUNNER_OS" == Windows ]]; then + archive="node-v${version}-win-x64.zip" + curl -fsSLO "$base/$archive" + curl -fsSLO "$base/SHASUMS256.txt" + grep " $archive$" SHASUMS256.txt | sha256sum -c - + mkdir -p "$RUNNER_TEMP/node" + unzip -q "$archive" -d "$RUNNER_TEMP/node" + node_path="$RUNNER_TEMP/node/node-v${version}-win-x64" + cygpath -w "$node_path" >> "$GITHUB_PATH" + else + if [[ "$RUNNER_OS" == macOS ]]; then platform=darwin-arm64; else platform=linux-x64; fi + archive="node-v${version}-${platform}.tar.xz" + curl -fsSLO "$base/$archive" + curl -fsSLO "$base/SHASUMS256.txt" + grep " $archive$" SHASUMS256.txt | shasum -a 256 -c - + mkdir -p "$RUNNER_TEMP/node" + tar -xJf "$archive" -C "$RUNNER_TEMP/node" --strip-components=1 + echo "$RUNNER_TEMP/node/bin" >> "$GITHUB_PATH" + fi + if [[ "$RUNNER_OS" == Windows ]]; then "$RUNNER_TEMP/node/node-v${version}-win-x64/node.exe" -e 'if(process.versions.node!=="26.13.0")process.exit(1)'; else "$RUNNER_TEMP/node/bin/node" -e 'if(process.versions.node!=="26.13.0")process.exit(1)'; fi + + - name: Install pinned pnpm + shell: bash + run: | + set -euo pipefail + version=10.33.0 + case "$RUNNER_OS/$RUNNER_ARCH" in + Linux/X64) asset=pnpm-linux-x64; sha=8d4e8f7d778e8ac482022e2577011706a872542f6f6f233e795a4d9f978ea8b5 ;; + macOS/ARM64) asset=pnpm-macos-arm64; sha=ed8a1f140f4de457b01ebe0be3ae28e9a7e28863315dcd53d22ff1e5a32d63ae ;; + Windows/X64) asset=pnpm-win-x64.exe; sha=afc96009dc39fe23a835d65192049e6a995f342496b175585dc2beda7d42d33f ;; + *) echo "Unsupported runner: $RUNNER_OS/$RUNNER_ARCH" >&2; exit 1 ;; + esac + dir="$RUNNER_TEMP/pnpm-bin" + mkdir -p "$dir" + curl -fsSL "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset}" -o "$dir/$asset" + actual=$( (sha256sum "$dir/$asset" 2>/dev/null || shasum -a 256 "$dir/$asset") | cut -d' ' -f1 | tr -d '\134') + [[ "$actual" == "$sha" ]] || { echo "pnpm checksum mismatch" >&2; exit 1; } + if [[ "$asset" == *.exe ]]; then cp "$dir/$asset" "$dir/pnpm.exe"; else chmod +x "$dir/$asset"; ln -sf "$asset" "$dir/pnpm"; fi + if [[ "$RUNNER_OS" == Windows ]]; then dir=$(cygpath -w "$dir"); fi + echo "$dir" >> "$GITHUB_PATH" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Test screenshot and signing helpers + shell: bash + run: | + pnpm exec vitest run test/native-signing.test.mts + pnpm exec vitest run --config vitest.terminal.config.mts + + - name: Verify .NET SDK + shell: bash + run: dotnet --list-sdks | awk -F. '$1 >= 8 { found=1 } END { exit !found }' + + - name: Build prototype native executable + env: + NODE_OPTIONS: ${{ matrix.target == 'darwin-arm64' && '--max-old-space-size=4096' || '' }} + run: pnpm build:sea --prototype + + - name: Run native screenshot tests + shell: bash + env: + SOCKET_CLI_NATIVE_TEST_ARTIFACTS: build/native-test-artifacts/${{ matrix.target }} + APPLE_DEVELOPER_ID_IDENTITY: ${{ matrix.target == 'darwin-arm64' && github.event_name == 'push' && (github.ref == 'refs/heads/feat/v1x-node27-sea-vfs-socket' || startsWith(github.ref, 'refs/heads/test/native-terminal-validation-')) && secrets.APPLE_DEVELOPER_ID_IDENTITY || '' }} + APPLE_TEAM_ID: ${{ matrix.target == 'darwin-arm64' && github.event_name == 'push' && (github.ref == 'refs/heads/feat/v1x-node27-sea-vfs-socket' || startsWith(github.ref, 'refs/heads/test/native-terminal-validation-')) && secrets.APPLE_TEAM_ID || '' }} + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: ${{ matrix.target == 'darwin-arm64' && github.event_name == 'push' && (github.ref == 'refs/heads/feat/v1x-node27-sea-vfs-socket' || startsWith(github.ref, 'refs/heads/test/native-terminal-validation-')) && secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12 || '' }} + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: ${{ matrix.target == 'darwin-arm64' && github.event_name == 'push' && (github.ref == 'refs/heads/feat/v1x-node27-sea-vfs-socket' || startsWith(github.ref, 'refs/heads/test/native-terminal-validation-')) && secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS || '' }} + SOCKET_CLI_NATIVE_TEST_REQUIRE_APPLE_SIGNATURE: ${{ matrix.target == 'darwin-arm64' && github.event_name == 'push' && (github.ref == 'refs/heads/feat/v1x-node27-sea-vfs-socket' || startsWith(github.ref, 'refs/heads/test/native-terminal-validation-')) && (secrets.APPLE_DEVELOPER_ID_IDENTITY != '' || secrets.APPLE_TEAM_ID != '' || secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12 != '' || secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS != '') && '1' || '' }} + run: | + set -euo pipefail + rm -rf "$SOCKET_CLI_NATIVE_TEST_ARTIFACTS" + mkdir -p "$SOCKET_CLI_NATIVE_TEST_ARTIFACTS" + if pnpm test:native src/commands/native-cli.native.test.mts src/commands/native-cli-behavior.native.test.mts; then result=0; else result=$?; fi + if [[ $result == 0 ]]; then status=passed; else status=failed; fi + export TEST_STATUS="$status" TEST_EXIT_CODE="$result" TEST_PLATFORM="$RUNNER_OS/$RUNNER_ARCH" + node -e 'const fs=require("node:fs"); let receipt={}; try { receipt=JSON.parse(fs.readFileSync(process.env.SOCKET_CLI_NATIVE_TEST_ARTIFACTS+"/signing-receipt.json","utf8")); } catch {} process.stdout.write(JSON.stringify({status:process.env.TEST_STATUS,exitCode:Number(process.env.TEST_EXIT_CODE),platform:process.env.TEST_PLATFORM,sourceCommit:process.env.GITHUB_SHA||"unknown",sourceSha256:receipt.sourceSha256||"unknown",binarySha256:receipt.stagedSha256||"unknown",signingMode:receipt.mode||"unknown"})+"\n")' > "$SOCKET_CLI_NATIVE_TEST_ARTIFACTS/test-result.json" + exit "$result" + + - name: Generate self-contained report + if: always() + shell: bash + env: + SOCKET_CLI_NATIVE_TEST_ARTIFACTS: build/native-test-artifacts/${{ matrix.target }} + run: node scripts/test/terminal-report.mts --artifacts "$SOCKET_CLI_NATIVE_TEST_ARTIFACTS" --output "$SOCKET_CLI_NATIVE_TEST_ARTIFACTS/index.html" + + - name: Upload native terminal evidence + if: always() + uses: ./.github/actions/repo/upload-test-artifacts + with: + name: native-terminal-${{ matrix.target }} + path: build/native-test-artifacts/${{ matrix.target }}/ diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 246ab618f3..d79fc2cda7 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -223,7 +223,7 @@ jobs: name: Install Node.js shell: bash env: - NODE_VERSION: 25.9.0 + NODE_VERSION: 26.13.0 run: node scripts/ci/setup-node.mjs --version "$NODE_VERSION" - &download-sfw diff --git a/.gitignore b/.gitignore index b75a474eeb..488cd451e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,1387 @@ -.DS_Store -._.DS_Store -.Trash/ -Thumbs.db +# Ignore everything by default; explicitly allow tracked repository files below. +* +!.gitignore +!/.claude/ +!/.claude/skills/ +!/.claude/skills/bump-coana/ +!/.config/ +!/.husky/ +!/.vscode/ +!/.github/ +!/.github/actions/ +!/.github/actions/repo/ +!/.github/actions/repo/upload-artifact/ +!/.github/scripts/ +!/.github/workflows/ +!/.github/actions/repo/upload-artifact/action.yml +!/.github/actions/repo/upload-artifact/index.mjs +!/.github/actions/repo/upload-test-artifacts/ +!/.github/actions/repo/upload-test-artifacts/action.yml +!/.github/actions/repo/upload-test-artifacts/index.mjs +!/.github/scripts/pin-readme-assets.mjs +!/.github/scripts/smoke-test-tarball.sh +!/.github/workflows/bazel-windows.yml +!/.github/workflows/ci.yml +!/.github/workflows/e2e-tests.yml +!/.github/workflows/publish-npm.yml +!/.github/workflows/native-terminal.yml +!/assets/ +!/assets/fleet/ +!/assets/native-terminal/ +!/assets/native-terminal/** +!/bin/ +!/docs/ +!/docs/plans/ +!/patches/ +!/scripts/ +!/scripts/build-package-matrix.mts +!/scripts/babel/ +!/scripts/ci/ +!/scripts/lib/ +!/scripts/release/ +!/scripts/rollup/ +!/scripts/sea/ +!/scripts/sea/release/ +!/scripts/sea/release/apple/ +!/scripts/test/ +!/scripts/test/terminal-report.mts +!/scripts/utils/ +!/shadow-bin/ +!/src/ +!/src/commands/ +!/src/commands/analytics/ +!/src/commands/audit-log/ +!/src/commands/cdxgen/ +!/src/commands/ci/ +!/src/commands/config/ +!/src/commands/fix/ +!/src/commands/install/ +!/src/commands/json/ +!/src/commands/login/ +!/src/commands/logout/ +!/src/commands/manifest/ +!/src/commands/manifest/bazel/ +!/src/commands/manifest/scripts/ +!/src/commands/manifest/scripts/dotnet-tool/ +!/src/commands/manifest/scripts/maven-extension/ +!/src/commands/manifest/scripts/maven-extension/.mvn/ +!/src/commands/manifest/scripts/maven-extension/.mvn/wrapper/ +!/src/commands/manifest/scripts/maven-extension/src/ +!/src/commands/manifest/scripts/maven-extension/src/main/ +!/src/commands/manifest/scripts/maven-extension/src/main/java/ +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/ +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/ +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/ext/ +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/ +!/src/commands/manifest/scripts/test/ +!/src/commands/manifest/scripts/test/dotnet-compat/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/shared/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/App/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/Extra/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/Lib/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/test/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/test/App.Tests/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/tools/ +!/src/commands/manifest/scripts/test/dotnet-compat/basic/tools/Nuke/ +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/ +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/src/ +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/src/Api/ +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/src/Core/ +!/src/commands/manifest/scripts/test/dotnet-compat/dev/ +!/src/commands/manifest/scripts/test/dotnet-compat/evaluation/ +!/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/ +!/src/commands/manifest/scripts/test/dotnet-compat/exclude-base/ +!/src/commands/manifest/scripts/test/dotnet-compat/exclude-base/app/ +!/src/commands/manifest/scripts/test/dotnet-compat/exclude-base/app/core/ +!/src/commands/manifest/scripts/test/dotnet-compat/exclude-base/shared/ +!/src/commands/manifest/scripts/test/dotnet-compat/failures/ +!/src/commands/manifest/scripts/test/dotnet-compat/failures/broken/ +!/src/commands/manifest/scripts/test/dotnet-compat/failures/missing/ +!/src/commands/manifest/scripts/test/dotnet-compat/langs/ +!/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/ +!/src/commands/manifest/scripts/test/dotnet-compat/langs/Vb/ +!/src/commands/manifest/scripts/test/dotnet-compat/multi/ +!/src/commands/manifest/scripts/test/dotnet-compat/multi/One/ +!/src/commands/manifest/scripts/test/dotnet-compat/multi/Two/ +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/ +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Legacy/ +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Legacy/Properties/ +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Modern/ +!/src/commands/manifest/scripts/test/dotnet-compat/pair/ +!/src/commands/manifest/scripts/test/dotnet-compat/rid/ +!/src/commands/manifest/scripts/test/dotnet-compat/sibling/ +!/src/commands/manifest/scripts/test/dotnet-compat/sibling/sln/ +!/src/commands/manifest/scripts/test/dotnet-compat/sibling/src/ +!/src/commands/manifest/scripts/test/dotnet-compat/sibling/src/A/ +!/src/commands/manifest/scripts/test/gradle-compat/ +!/src/commands/manifest/scripts/test/gradle-compat/exclude-base/ +!/src/commands/manifest/scripts/test/gradle-compat/exclude-base/app/ +!/src/commands/manifest/scripts/test/gradle-compat/exclude-base/app/core/ +!/src/commands/manifest/scripts/test/gradle-compat/exclude-base/shared/ +!/src/commands/manifest/scripts/test/gradle-compat/failures/ +!/src/commands/manifest/scripts/test/gradle-compat/failures/app/ +!/src/commands/manifest/scripts/test/gradle-compat/failures/gradle/ +!/src/commands/manifest/scripts/test/gradle-compat/failures/lib/ +!/src/commands/manifest/scripts/test/gradle-compat/project/ +!/src/commands/manifest/scripts/test/maven-compat/ +!/src/commands/manifest/scripts/test/maven-compat/descriptor-failure/ +!/src/commands/manifest/scripts/test/maven-compat/descriptor-failure/a/ +!/src/commands/manifest/scripts/test/maven-compat/descriptor-failure/b/ +!/src/commands/manifest/scripts/test/maven-compat/duplicate-failure/ +!/src/commands/manifest/scripts/test/maven-compat/duplicate-failure/m1/ +!/src/commands/manifest/scripts/test/maven-compat/duplicate-failure/m2/ +!/src/commands/manifest/scripts/test/maven-compat/exclude-base/ +!/src/commands/manifest/scripts/test/maven-compat/exclude-base/app/ +!/src/commands/manifest/scripts/test/maven-compat/exclude-base/app/core/ +!/src/commands/manifest/scripts/test/maven-compat/exclude-base/shared/ +!/src/commands/manifest/scripts/test/maven-compat/graph-failure/ +!/src/commands/manifest/scripts/test/maven-compat/project/ +!/src/commands/manifest/scripts/test/maven-compat/project/app/ +!/src/commands/manifest/scripts/test/maven-compat/project/app/src/ +!/src/commands/manifest/scripts/test/maven-compat/project/app/src/main/ +!/src/commands/manifest/scripts/test/maven-compat/project/app/src/main/java/ +!/src/commands/manifest/scripts/test/maven-compat/project/app/src/main/java/demo/ +!/src/commands/manifest/scripts/test/maven-compat/project/lib/ +!/src/commands/manifest/scripts/test/maven-compat/project/lib/src/ +!/src/commands/manifest/scripts/test/maven-compat/project/lib/src/main/ +!/src/commands/manifest/scripts/test/maven-compat/project/lib/src/main/java/ +!/src/commands/manifest/scripts/test/maven-compat/project/lib/src/main/java/demo/ +!/src/commands/manifest/scripts/test/maven-compat/project/mid/ +!/src/commands/manifest/scripts/test/maven-compat/project/mid/src/ +!/src/commands/manifest/scripts/test/maven-compat/project/mid/src/main/ +!/src/commands/manifest/scripts/test/maven-compat/project/mid/src/main/java/ +!/src/commands/manifest/scripts/test/maven-compat/project/mid/src/main/java/demo/ +!/src/commands/manifest/scripts/test/maven-compat/repo-inheritance/ +!/src/commands/manifest/scripts/test/maven-compat/repo-inheritance/liba/ +!/src/commands/manifest/scripts/test/maven-compat/repo-inheritance/libb/ +!/src/commands/manifest/scripts/test/sbt-compat/ +!/src/commands/manifest/scripts/test/sbt-compat/artifact-failure/ +!/src/commands/manifest/scripts/test/sbt-compat/exclude-base/ +!/src/commands/manifest/scripts/test/sbt-compat/exclude-base/app/ +!/src/commands/manifest/scripts/test/sbt-compat/failure/ +!/src/commands/manifest/scripts/test/sbt-compat/project/ +!/src/commands/manifest/scripts/test/sbt-compat/project/src/ +!/src/commands/manifest/scripts/test/sbt-compat/project/src/main/ +!/src/commands/manifest/scripts/test/sbt-compat/project/src/main/scala/ +!/src/commands/manifest/scripts/test/sbt-compat/relocation/ +!/src/commands/npm/ +!/src/commands/npx/ +!/src/commands/oops/ +!/src/commands/optimize/ +!/src/commands/organization/ +!/src/commands/package/ +!/src/commands/package/fixtures/ +!/src/commands/patch/ +!/src/commands/pnpm/ +!/src/commands/raw-npm/ +!/src/commands/raw-npx/ +!/src/commands/repository/ +!/src/commands/scan/ +!/src/commands/threat-feed/ +!/src/commands/uninstall/ +!/src/commands/wrapper/ +!/src/commands/yarn/ +!/src/external/ +!/src/external/blessed-contrib/ +!/src/external/blessed-contrib/lib/ +!/src/external/blessed-contrib/lib/layout/ +!/src/external/blessed-contrib/lib/widget/ +!/src/external/blessed-contrib/lib/widget/charts/ +!/src/sea/ +!/src/sea/archive-paths.mts +!/src/sea/adapters/ +!/src/sea/build-lock.mts +!/src/sea/build-sea.test.mts +!/src/sea/capabilities.mts +!/src/sea/runtime.test.mts +!/src/sea/licenses/ +!/src/sea/licenses/cdxgen-third-party/ +!/src/sea/licenses/core-third-party/ +!/src/shadow/ +!/src/shadow/npm/ +!/src/shadow/npm/arborist/ +!/src/shadow/npm/arborist/lib/ +!/src/shadow/npm/arborist/lib/arborist/ +!/src/shadow/npx/ +!/src/shadow/pnpm/ +!/src/shadow/yarn/ +!/src/test/ +!/src/test/mocks/ +!/src/utils/ +!/src/utils/packages.test.mts +!/src/utils/alert/ +!/src/utils/telemetry/ +!/src/vendor/ +!/test/ +!/test/utils/ +!/test/unit/ +!/test/unit/utils/ +!/test/native-snapshots/ +!/test/native-snapshots/** +!/test/utils/native-cli-harness.mts +!/test/utils/native-command-inventory.mts +!/test/utils/optimize-svg.mts +!/test/utils/terminal-animation.mts +!/test/utils/terminal-output.mts +!/test/unit/utils/native-cli-harness.test.mts +!/test/unit/utils/native-command-inventory.test.mts +!/test/unit/utils/optimize-svg.test.mts +!/test/unit/utils/terminal-animation.test.mts +!/test/unit/utils/terminal-output.test.mts +!/test/package-matrix.test.mts +!/test/fixtures/ +!/test/fixtures/commands/ +!/test/fixtures/commands/cdxgen/ +!/test/fixtures/commands/fix/ +!/test/fixtures/commands/fix/e2e-test-js/ +!/test/fixtures/commands/fix/e2e-test-multipm/ +!/test/fixtures/commands/fix/e2e-test-multipm/npm-app/ +!/test/fixtures/commands/fix/e2e-test-multipm/pnpm-app/ +!/test/fixtures/commands/fix/e2e-test-py/ +!/test/fixtures/commands/fix/npm/ +!/test/fixtures/commands/fix/npm/monorepo/ +!/test/fixtures/commands/fix/npm/monorepo/packages/ +!/test/fixtures/commands/fix/npm/monorepo/packages/app/ +!/test/fixtures/commands/fix/npm/monorepo/packages/lib/ +!/test/fixtures/commands/fix/npm/vulnerable-deps/ +!/test/fixtures/commands/fix/pnpm/ +!/test/fixtures/commands/fix/pnpm/monorepo/ +!/test/fixtures/commands/fix/pnpm/monorepo/packages/ +!/test/fixtures/commands/fix/pnpm/monorepo/packages/app/ +!/test/fixtures/commands/fix/pnpm/monorepo/packages/lib/ +!/test/fixtures/commands/fix/pnpm/vulnerable-deps/ +!/test/fixtures/commands/fix/yarn/ +!/test/fixtures/commands/fix/yarn/monorepo/ +!/test/fixtures/commands/fix/yarn/monorepo/packages/ +!/test/fixtures/commands/fix/yarn/monorepo/packages/app/ +!/test/fixtures/commands/fix/yarn/monorepo/packages/lib/ +!/test/fixtures/commands/fix/yarn/vulnerable-deps/ +!/test/fixtures/commands/json/ +!/test/fixtures/commands/manifest/ +!/test/fixtures/commands/manifest/conda/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/dual-marker-dir/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/moduleA/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/moduleA/nested-gradle/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/moduleB/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/moduleB/independent-submodule/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/standalone-gradle/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/bare/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/upper/ +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/vendored/ +!/test/fixtures/commands/manifest/python/ +!/test/fixtures/commands/npm/ +!/test/fixtures/commands/npm/lacking-typosquat/ +!/test/fixtures/commands/npm/npm10/ +!/test/fixtures/commands/npm/npm11/ +!/test/fixtures/commands/npm/npm9/ +!/test/fixtures/commands/optimize/ +!/test/fixtures/commands/optimize/npm/ +!/test/fixtures/commands/optimize/pnpm/ +!/test/fixtures/commands/optimize/pnpm8/ +!/test/fixtures/commands/optimize/pnpm9/ +!/test/fixtures/commands/optimize/yarn/ +!/test/fixtures/commands/patch/ +!/test/fixtures/commands/patch/npm/ +!/test/fixtures/commands/patch/npm/.socket/ +!/test/fixtures/commands/patch/npm/.socket/blobs/ +!/test/fixtures/commands/patch/pnpm/ +!/test/fixtures/commands/patch/pnpm/.socket/ +!/test/fixtures/commands/patch/pnpm/.socket/blobs/ +!/test/fixtures/commands/patch/yarn/ +!/test/fixtures/commands/patch/yarn/.socket/ +!/test/fixtures/commands/patch/yarn/.socket/blobs/ +!/test/fixtures/commands/scan/ +!/test/fixtures/commands/scan/npm-test-workspace-mono/ +!/test/fixtures/commands/scan/npm-test-workspace-mono/packages/ +!/test/fixtures/commands/scan/npm-test-workspace-mono/packages/package-a/ +!/test/fixtures/commands/scan/npm-test-workspace-mono/packages/package-b/ +!/test/fixtures/commands/scan/plain-requirements-txt/ +!/test/fixtures/commands/scan/simple-npm/ +!/test/fixtures/commands/scan/uv-workspace/ +!/test/fixtures/commands/scan/uv-workspace/packages/ +!/test/fixtures/commands/scan/uv-workspace/packages/api/ +!/test/fixtures/commands/scan/uv-workspace/packages/other/ +!/test/fixtures/commands/scan/uv-workspace/packages/shared/ +!/test/fixtures/manifest-bazel/ +!/test/fixtures/manifest-bazel/bzlmod-only/ +!/test/fixtures/manifest-bazel/custom-name-bzlmod/ +!/test/fixtures/manifest-bazel/legacy-only/ +!/test/fixtures/manifest-bazel/legacy-with-load/ +!/test/fixtures/manifest-bazel/migration/ +!/test/fixtures/manifest-bazel/multi-repo-bzlmod/ +!/test/fixtures/manifest-bazel/native/ +!/test/fixtures/manifest-bazel/native/bzlmod/ +!/test/fixtures/manifest-bazel/native/workspace/ +!/test/fixtures/manifest-bazel/python-pypi/ +!/.claude/skills/bump-coana/SKILL.md +!/.config/babel.config.js +!/.config/rollup.base.config.mjs +!/.config/rollup.cdxgen-sea.config.mjs +!/.config/rollup.dist.config.mjs +!/.config/rollup.sea.config.mjs +!/.config/rollup.synp-sea.config.mjs +!/.config/tsconfig.base.json +!/.editorconfig +!/.env.dist +!/.env.external +!/.env.test +!/.env.testu +!/.gitattributes +!/.gitignore +!/.husky/commit-msg +!/.husky/pre-commit +!/.npmrc +!/.oxlintignore +!/.oxlintrc.json +!/.pnpmrc +!/.vscode/extensions.json +!/CHANGELOG.md +!/CLAUDE.md +!/DISCLOSURE +!/LICENSE +!/README.md +!/assets/fleet/badge-follow-bluesky.svg +!/assets/fleet/badge-follow-x.svg +!/assets/fleet/socket-combomark-dark.svg +!/assets/fleet/socket-combomark-light.svg +!/bin/cli.js +!/bin/npm-cli.js +!/bin/npx-cli.js +!/bin/pnpm-cli.js +!/bin/yarn-cli.js +!/biome.json +!/docs/plans/node27-sea-vfs-socket-package.md +!/eslint.config.js +!/knip.json +!/package.json +!/patches/@rollup__plugin-commonjs@28.0.6.patch +!/patches/ansi-term@0.0.2.patch +!/patches/blessed-contrib@4.11.0.patch +!/patches/blessed@0.1.81.patch +!/patches/brace-expansion@2.0.2.patch +!/patches/bresenham@0.0.3.patch +!/patches/drawille-blessed-contrib@1.0.0.patch +!/patches/drawille-canvas-blessed-contrib@0.1.3.patch +!/patches/graceful-fs@4.2.11.patch +!/patches/lodash@4.17.21.patch +!/patches/meow@13.2.0.patch +!/patches/rollup@4.50.1.patch +!/patches/string_decoder@0.10.31.patch +!/patches/tiny-updater@3.5.3.patch +!/pnpm-lock.yaml +!/requirements.json +!/scripts/babel/transform-set-proto-plugin.js +!/scripts/babel/transform-url-parse-plugin.js +!/scripts/check-commit-pii.js +!/scripts/check.mts +!/scripts/ci/check-actions.mts +!/scripts/ci/setup-node.mjs +!/scripts/ci/setup-uv.mts +!/scripts/ci/upload-artifact.mjs +!/scripts/constants.js +!/scripts/lib/is-main-module.mts +!/scripts/lib/linters.mts +!/scripts/lib/run-main.mts +!/scripts/lib/scope.mts +!/scripts/lint.mts +!/scripts/release/add-unreleased.mts +!/scripts/release/bump.mts +!/scripts/release/changelog.mts +!/scripts/release/github-api.mts +!/scripts/release/history.mts +!/scripts/release/mint-app-token.mjs +!/scripts/release/open-release-pr.mts +!/scripts/release/preflight.mts +!/scripts/release/registry.mts +!/scripts/release/release-branch.mts +!/scripts/release/version.mts +!/scripts/rollup/socket-modify-plugin.js +!/scripts/sea/release/apple/capture.mts +!/scripts/test/native.mts +!/scripts/test/native-signing.mts +!/scripts/test/terminal-animation.mts +!/scripts/test/integration.mts +!/scripts/update.mts +!/scripts/utils/fs.js +!/scripts/utils/packages.js +!/scripts/utils/registry-helpers.js +!/sd +!/shadow-bin/npm +!/shadow-bin/npx +!/shadow-bin/pnpm +!/shadow-bin/yarn +!/socket.yml +!/src/cli.mts +!/src/commands.mts +!/src/commands/analytics/analytics-fixture.json +!/src/commands/analytics/cmd-analytics.mts +!/src/commands/analytics/cmd-analytics.test.mts +!/src/commands/analytics/fetch-org-analytics.mts +!/src/commands/analytics/fetch-repo-analytics.mts +!/src/commands/analytics/handle-analytics.mts +!/src/commands/analytics/output-analytics.mts +!/src/commands/analytics/output-analytics.test.mts +!/src/commands/audit-log/audit-fixture.json +!/src/commands/audit-log/cmd-audit-log.mts +!/src/commands/audit-log/cmd-audit-log.test.mts +!/src/commands/audit-log/fetch-audit-log.mts +!/src/commands/audit-log/handle-audit-log.mts +!/src/commands/audit-log/output-audit-log.mts +!/src/commands/audit-log/output-audit-log.test.mts +!/src/commands/cdxgen/cmd-cdxgen.test.mts +!/src/commands/ci/cmd-ci.mts +!/src/commands/ci/cmd-ci.test.mts +!/src/commands/ci/fetch-default-org-slug.mts +!/src/commands/ci/fetch-default-org-slug.test.mts +!/src/commands/ci/handle-ci.mts +!/src/commands/ci/handle-ci.test.mts +!/src/commands/cli.test.mts +!/src/commands/cli.integration.test.mts +!/src/commands/config/cmd-config-auto.mts +!/src/commands/config/cmd-config-auto.test.mts +!/src/commands/config/cmd-config-get.mts +!/src/commands/config/cmd-config-get.test.mts +!/src/commands/config/cmd-config-list.mts +!/src/commands/config/cmd-config-list.test.mts +!/src/commands/config/cmd-config-set.mts +!/src/commands/config/cmd-config-set.test.mts +!/src/commands/config/cmd-config-unset.mts +!/src/commands/config/cmd-config-unset.test.mts +!/src/commands/config/cmd-config.mts +!/src/commands/config/cmd-config.test.mts +!/src/commands/config/discover-config-value.mts +!/src/commands/config/handle-config-auto.mts +!/src/commands/config/handle-config-get.mts +!/src/commands/config/handle-config-set.mts +!/src/commands/config/handle-config-unset.mts +!/src/commands/config/output-config-auto.mts +!/src/commands/config/output-config-get.mts +!/src/commands/config/output-config-list.mts +!/src/commands/config/output-config-set.mts +!/src/commands/config/output-config-unset.mts +!/src/commands/fix/branch-cleanup.integration.test.mts +!/src/commands/fix/branch-cleanup.mts +!/src/commands/fix/branch-cleanup.test.mts +!/src/commands/fix/cmd-fix.e2e.test.mts +!/src/commands/fix/cmd-fix.integration.test.mts +!/src/commands/fix/cmd-fix.mts +!/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts +!/src/commands/fix/coana-fix-pr-files.test.mts +!/src/commands/fix/coana-fix.mts +!/src/commands/fix/env-helpers.mts +!/src/commands/fix/env-helpers.test.mts +!/src/commands/fix/generated-socket-facts.mts +!/src/commands/fix/generated-socket-facts.test.mts +!/src/commands/fix/git.mts +!/src/commands/fix/handle-fix-id.test.mts +!/src/commands/fix/handle-fix-limit.test.mts +!/src/commands/fix/handle-fix.mts +!/src/commands/fix/output-fix-result.mts +!/src/commands/fix/pull-request.mts +!/src/commands/fix/types.mts +!/src/commands/install/cmd-install-completion.mts +!/src/commands/install/cmd-install-completion.test.mts +!/src/commands/install/cmd-install.mts +!/src/commands/install/cmd-install.test.mts +!/src/commands/install/handle-install-completion.mts +!/src/commands/install/output-install-completion.mts +!/src/commands/install/setup-tab-completion.mts +!/src/commands/install/socket-completion.bash +!/src/commands/json/cmd-json.mts +!/src/commands/json/cmd-json.test.mts +!/src/commands/json/cmd-json.integration.test.mts +!/src/commands/json/handle-cmd-json.mts +!/src/commands/json/output-cmd-json.mts +!/src/commands/login/apply-login.mts +!/src/commands/login/attempt-login.mts +!/src/commands/login/cmd-login-smoke.test.mts +!/src/commands/login/cmd-login.mts +!/src/commands/login/cmd-login.test.mts +!/src/commands/logout/apply-logout.mts +!/src/commands/logout/attempt-logout.mts +!/src/commands/logout/cmd-logout.mts +!/src/commands/logout/cmd-logout.test.mts +!/src/commands/manifest/README.md +!/src/commands/manifest/bazel/bazel-bin-detect.mts +!/src/commands/manifest/bazel/bazel-bin-detect.test.mts +!/src/commands/manifest/bazel/bazel-cleanup.mts +!/src/commands/manifest/bazel/bazel-cquery.mts +!/src/commands/manifest/bazel/bazel-cquery.test.mts +!/src/commands/manifest/bazel/bazel-java-shim.mts +!/src/commands/manifest/bazel/bazel-java-shim.test.mts +!/src/commands/manifest/bazel/bazel-native.e2e.test.mts +!/src/commands/manifest/bazel/bazel-output-base-check.mts +!/src/commands/manifest/bazel/bazel-output-base-check.test.mts +!/src/commands/manifest/bazel/bazel-process.mts +!/src/commands/manifest/bazel/bazel-process.test.mts +!/src/commands/manifest/bazel/bazel-pypi-discovery.mts +!/src/commands/manifest/bazel/bazel-pypi-discovery.test.mts +!/src/commands/manifest/bazel/bazel-pypi-parser.mts +!/src/commands/manifest/bazel/bazel-pypi-parser.test.mts +!/src/commands/manifest/bazel/bazel-python-shim.mts +!/src/commands/manifest/bazel/bazel-python-shim.test.mts +!/src/commands/manifest/bazel/bazel-query-runner.mts +!/src/commands/manifest/bazel/bazel-query-runner.test.mts +!/src/commands/manifest/bazel/bazel-repo-discovery.mts +!/src/commands/manifest/bazel/bazel-repo-discovery.test.mts +!/src/commands/manifest/bazel/bazel-workspace-detect.mts +!/src/commands/manifest/bazel/bazel-workspace-detect.test.mts +!/src/commands/manifest/bazel/bazel-workspace-walk.mts +!/src/commands/manifest/bazel/bazel-workspace-walk.test.mts +!/src/commands/manifest/bazel/cmd-manifest-bazel.mts +!/src/commands/manifest/bazel/cmd-manifest-bazel.test.mts +!/src/commands/manifest/bazel/extract_bazel_to_maven.mts +!/src/commands/manifest/bazel/extract_bazel_to_maven.test.mts +!/src/commands/manifest/bazel/extract_bazel_to_pypi.constructed.test.mts +!/src/commands/manifest/bazel/extract_bazel_to_pypi.mts +!/src/commands/manifest/bazel/extract_bazel_to_pypi.test.mts +!/src/commands/manifest/cmd-manifest-auto.mts +!/src/commands/manifest/cmd-manifest-auto.test.mts +!/src/commands/manifest/cmd-manifest-auto.integration.test.mts +!/src/commands/native-cli.native.test.mts +!/src/commands/native-cli-behavior.native.test.mts +!/src/commands/manifest/cmd-manifest-cdxgen.mts +!/src/commands/manifest/cmd-manifest-cdxgen.test.mts +!/src/commands/manifest/cmd-manifest-conda.mts +!/src/commands/manifest/cmd-manifest-conda.test.mts +!/src/commands/manifest/cmd-manifest-dotnet.mts +!/src/commands/manifest/cmd-manifest-dotnet.test.mts +!/src/commands/manifest/cmd-manifest-dynamic-sbom-inference.mts +!/src/commands/manifest/cmd-manifest-dynamic-sbom-inference.test.mts +!/src/commands/manifest/cmd-manifest-gradle.mts +!/src/commands/manifest/cmd-manifest-gradle.test.mts +!/src/commands/manifest/cmd-manifest-kotlin.mts +!/src/commands/manifest/cmd-manifest-kotlin.test.mts +!/src/commands/manifest/cmd-manifest-maven.mts +!/src/commands/manifest/cmd-manifest-maven.test.mts +!/src/commands/manifest/cmd-manifest-scala.mts +!/src/commands/manifest/cmd-manifest-scala.test.mts +!/src/commands/manifest/cmd-manifest-setup.mts +!/src/commands/manifest/cmd-manifest-setup.test.mts +!/src/commands/manifest/cmd-manifest.mts +!/src/commands/manifest/cmd-manifest.test.mts +!/src/commands/manifest/convert-conda-to-requirements.mts +!/src/commands/manifest/convert-conda-to-requirements.test.mts +!/src/commands/manifest/convert-dotnet-to-facts.mts +!/src/commands/manifest/convert-gradle-to-facts.mts +!/src/commands/manifest/convert-maven-to-facts.mts +!/src/commands/manifest/convert-sbt-to-facts.mts +!/src/commands/manifest/convert_gradle_to_maven.mts +!/src/commands/manifest/convert_sbt_to_maven.mts +!/src/commands/manifest/detect-manifest-actions.mts +!/src/commands/manifest/detect-manifest-actions.test.mts +!/src/commands/manifest/discover-manifest-roots.mts +!/src/commands/manifest/discover-manifest-roots.test.mts +!/src/commands/manifest/enumerate-workspaces.mts +!/src/commands/manifest/enumerate-workspaces.test.mts +!/src/commands/manifest/expand-env-var-refs.mts +!/src/commands/manifest/expand-env-var-refs.test.mts +!/src/commands/manifest/generate-recursive-manifests.mts +!/src/commands/manifest/generate-recursive-manifests.test.mts +!/src/commands/manifest/generate_auto_manifest.mts +!/src/commands/manifest/generate_auto_manifest.test.mts +!/src/commands/manifest/handle-manifest-conda.mts +!/src/commands/manifest/handle-manifest-dynamic-sbom-inference.mts +!/src/commands/manifest/handle-manifest-dynamic-sbom-inference.test.mts +!/src/commands/manifest/handle-manifest-setup.mts +!/src/commands/manifest/init.gradle +!/src/commands/manifest/manifest-flags.mts +!/src/commands/manifest/output-manifest-dynamic-sbom-inference.mts +!/src/commands/manifest/output-manifest-setup.mts +!/src/commands/manifest/output-requirements.mts +!/src/commands/manifest/parse-build-tool-opts.mts +!/src/commands/manifest/parse-build-tool-opts.test.mts +!/src/commands/manifest/run-cdxgen.mts +!/src/commands/manifest/run-manifest-facts.mts +!/src/commands/manifest/run-manifest-facts.test.mts +!/src/commands/manifest/scripts/assemble.mts +!/src/commands/manifest/scripts/assemble.test.mts +!/src/commands/manifest/scripts/build-tool.mts +!/src/commands/manifest/scripts/build-tool.test.mts +!/src/commands/manifest/scripts/dotnet-tool/.gitignore +!/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs +!/src/commands/manifest/scripts/dotnet-tool/NuGetShim.cs +!/src/commands/manifest/scripts/dotnet-tool/Patterns.cs +!/src/commands/manifest/scripts/dotnet-tool/Program.cs +!/src/commands/manifest/scripts/dotnet-tool/Reader.cs +!/src/commands/manifest/scripts/dotnet-tool/RecordsWriter.cs +!/src/commands/manifest/scripts/dotnet-tool/Restorer.cs +!/src/commands/manifest/scripts/dotnet-tool/SolutionContext.cs +!/src/commands/manifest/scripts/dotnet-tool/ToolOptions.cs +!/src/commands/manifest/scripts/dotnet-tool/build-tool.sh +!/src/commands/manifest/scripts/dotnet-tool/packages.lock.json +!/src/commands/manifest/scripts/dotnet-tool/socket-facts-dotnet.csproj +!/src/commands/manifest/scripts/exclude-paths-glob.mts +!/src/commands/manifest/scripts/exclude-paths-glob.test.mts +!/src/commands/manifest/scripts/facts.mts +!/src/commands/manifest/scripts/maven-extension/.gitignore +!/src/commands/manifest/scripts/maven-extension/.mvn/wrapper/maven-wrapper.properties +!/src/commands/manifest/scripts/maven-extension/build-jar.sh +!/src/commands/manifest/scripts/maven-extension/mvnw +!/src/commands/manifest/scripts/maven-extension/mvnw.cmd +!/src/commands/manifest/scripts/maven-extension/pom.xml +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/ext/CoanaFactsEventSpy.java +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/ext/CoanaFactsLifecycleParticipant.java +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/ext/CoanaWorkspacesLifecycleParticipant.java +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/CollectionObserver.java +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketSupport.java +!/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketWorkspacesRecordsEngine.java +!/src/commands/manifest/scripts/records.mts +!/src/commands/manifest/scripts/resolution-report-gradle.mts +!/src/commands/manifest/scripts/resolution-report-ivy.mts +!/src/commands/manifest/scripts/resolution-report-maven.mts +!/src/commands/manifest/scripts/resolution-report-nuget.mts +!/src/commands/manifest/scripts/resolution-report-nuget.test.mts +!/src/commands/manifest/scripts/resolution-report-render.mts +!/src/commands/manifest/scripts/resolution-report-render.test.mts +!/src/commands/manifest/scripts/resolution-report.mts +!/src/commands/manifest/scripts/run.mts +!/src/commands/manifest/scripts/run.test.mts +!/src/commands/manifest/scripts/sidecar.mts +!/src/commands/manifest/scripts/sidecar.test.mts +!/src/commands/manifest/scripts/socket-facts.init.gradle +!/src/commands/manifest/scripts/socket-facts.plugin.scala +!/src/commands/manifest/scripts/socket-workspaces.init.gradle +!/src/commands/manifest/scripts/socket-workspaces.plugin.scala +!/src/commands/manifest/scripts/test/README.md +!/src/commands/manifest/scripts/test/assert-reachability.py +!/src/commands/manifest/scripts/test/compat-cache.sh +!/src/commands/manifest/scripts/test/dotnet-compat/.gitignore +!/src/commands/manifest/scripts/test/dotnet-compat/assert-dotnet-list.py +!/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py +!/src/commands/manifest/scripts/test/dotnet-compat/basic/Basic.sln +!/src/commands/manifest/scripts/test/dotnet-compat/basic/shared/Linked.cs +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/App/App.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/App/Program.cs +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/Extra/E.cs +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/Extra/Extra.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/Lib/L.cs +!/src/commands/manifest/scripts/test/dotnet-compat/basic/src/Lib/Lib.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/basic/test/App.Tests/App.Tests.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/basic/test/App.Tests/T.cs +!/src/commands/manifest/scripts/test/dotnet-compat/basic/tools/Nuke/Nuke.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/Cpm.slnx +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/Directory.Build.props +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/Directory.Packages.props +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/src/Api/A.cs +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/src/Api/Api.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/src/Core/C.cs +!/src/commands/manifest/scripts/test/dotnet-compat/cpm/src/Core/Core.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/dev/Dev.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/dev/Program.cs +!/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Build.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Class.cs +!/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Lib.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/exclude-base/app/App.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/exclude-base/app/App.sln +!/src/commands/manifest/scripts/test/dotnet-compat/exclude-base/app/core/Core.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/exclude-base/shared/Shared.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/failures/broken/Broken.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/failures/missing/Missing.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/First.fs +!/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/Fs.fsproj +!/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/Library.fs +!/src/commands/manifest/scripts/test/dotnet-compat/langs/Vb/V.vb +!/src/commands/manifest/scripts/test/dotnet-compat/langs/Vb/Vb.vbproj +!/src/commands/manifest/scripts/test/dotnet-compat/lib.sh +!/src/commands/manifest/scripts/test/dotnet-compat/make-stub-feed.py +!/src/commands/manifest/scripts/test/dotnet-compat/multi/One.sln +!/src/commands/manifest/scripts/test/dotnet-compat/multi/One.slnx +!/src/commands/manifest/scripts/test/dotnet-compat/multi/One/One.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/multi/Stray.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/multi/Two.sln +!/src/commands/manifest/scripts/test/dotnet-compat/multi/Two/Two.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/nuget.config +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Legacy/Class1.cs +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Legacy/Legacy.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Legacy/Properties/AssemblyInfo.cs +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Legacy/packages.config +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Mixed.sln +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Modern/M.cs +!/src/commands/manifest/scripts/test/dotnet-compat/packages-config/Modern/Modern.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/pair/A.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/pair/B.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/rid/R.cs +!/src/commands/manifest/scripts/test/dotnet-compat/rid/Rid.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/sibling/sln/All.sln +!/src/commands/manifest/scripts/test/dotnet-compat/sibling/src/A/A.cs +!/src/commands/manifest/scripts/test/dotnet-compat/sibling/src/A/A.csproj +!/src/commands/manifest/scripts/test/dotnet-compat/smoke-test-exclude-base.sh +!/src/commands/manifest/scripts/test/dotnet-compat/smoke-test-workspaces.sh +!/src/commands/manifest/scripts/test/dotnet-compat/smoke-test.sh +!/src/commands/manifest/scripts/test/gradle-compat/.gitignore +!/src/commands/manifest/scripts/test/gradle-compat/README.md +!/src/commands/manifest/scripts/test/gradle-compat/exclude-base/app/build.gradle +!/src/commands/manifest/scripts/test/gradle-compat/exclude-base/app/core/build.gradle +!/src/commands/manifest/scripts/test/gradle-compat/exclude-base/app/settings.gradle +!/src/commands/manifest/scripts/test/gradle-compat/exclude-base/shared/build.gradle +!/src/commands/manifest/scripts/test/gradle-compat/failures/app/build.gradle +!/src/commands/manifest/scripts/test/gradle-compat/failures/build.gradle +!/src/commands/manifest/scripts/test/gradle-compat/failures/gradle/verification-metadata.xml +!/src/commands/manifest/scripts/test/gradle-compat/failures/lib/build.gradle +!/src/commands/manifest/scripts/test/gradle-compat/failures/settings.gradle +!/src/commands/manifest/scripts/test/gradle-compat/failures/transforms.gradle +!/src/commands/manifest/scripts/test/gradle-compat/project/build.gradle +!/src/commands/manifest/scripts/test/gradle-compat/project/settings.gradle +!/src/commands/manifest/scripts/test/gradle-compat/smoke-test-exclude-base.sh +!/src/commands/manifest/scripts/test/gradle-compat/smoke-test-failures.sh +!/src/commands/manifest/scripts/test/gradle-compat/smoke-test-workspaces.sh +!/src/commands/manifest/scripts/test/gradle-compat/smoke-test.sh +!/src/commands/manifest/scripts/test/make-stub-repo.sh +!/src/commands/manifest/scripts/test/maven-compat/.gitignore +!/src/commands/manifest/scripts/test/maven-compat/assert-fail-closed.py +!/src/commands/manifest/scripts/test/maven-compat/assert-no-invented-failures.py +!/src/commands/manifest/scripts/test/maven-compat/assert-records.py +!/src/commands/manifest/scripts/test/maven-compat/descriptor-failure/a/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/descriptor-failure/b/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/descriptor-failure/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/duplicate-failure/m1/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/duplicate-failure/m2/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/duplicate-failure/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/exclude-base/app/core/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/exclude-base/app/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/exclude-base/shared/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/graph-failure/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/project/app/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/project/app/src/main/java/demo/App.java +!/src/commands/manifest/scripts/test/maven-compat/project/lib/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/project/lib/src/main/java/demo/Lib.java +!/src/commands/manifest/scripts/test/maven-compat/project/mid/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/project/mid/src/main/java/demo/Mid.java +!/src/commands/manifest/scripts/test/maven-compat/project/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/repo-inheritance/liba/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/repo-inheritance/libb/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/repo-inheritance/pom.xml +!/src/commands/manifest/scripts/test/maven-compat/smoke-test-descriptor-failure.sh +!/src/commands/manifest/scripts/test/maven-compat/smoke-test-duplicate-failure.sh +!/src/commands/manifest/scripts/test/maven-compat/smoke-test-exclude-base.sh +!/src/commands/manifest/scripts/test/maven-compat/smoke-test-graph-failure.sh +!/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh +!/src/commands/manifest/scripts/test/maven-compat/smoke-test-workspaces.sh +!/src/commands/manifest/scripts/test/maven-compat/smoke-test.sh +!/src/commands/manifest/scripts/test/run-compat.sh +!/src/commands/manifest/scripts/test/sbt-compat/.gitignore +!/src/commands/manifest/scripts/test/sbt-compat/artifact-failure/build.sbt +!/src/commands/manifest/scripts/test/sbt-compat/exclude-base/app/build.sbt +!/src/commands/manifest/scripts/test/sbt-compat/failure/build.sbt +!/src/commands/manifest/scripts/test/sbt-compat/project/Base.scala +!/src/commands/manifest/scripts/test/sbt-compat/project/build.sbt +!/src/commands/manifest/scripts/test/sbt-compat/project/src/main/scala/App.scala +!/src/commands/manifest/scripts/test/sbt-compat/relocation/build.sbt +!/src/commands/manifest/scripts/test/sbt-compat/smoke-test-exclude-base.sh +!/src/commands/manifest/scripts/test/sbt-compat/smoke-test-failure.sh +!/src/commands/manifest/scripts/test/sbt-compat/smoke-test-relocation.sh +!/src/commands/manifest/scripts/test/sbt-compat/smoke-test-workspaces.sh +!/src/commands/manifest/scripts/test/sbt-compat/smoke-test.sh +!/src/commands/manifest/setup-manifest-config.mts +!/src/commands/manifest/setup-manifest-config.test.mts +!/src/commands/manifest/setup-recursive-manifest-config.mts +!/src/commands/manifest/setup-recursive-manifest-config.test.mts +!/src/commands/npm/cmd-npm-malware.test.mts +!/src/commands/npm/cmd-npm.mts +!/src/commands/npm/cmd-npm.test.mts +!/src/commands/npm/socket-npm-integration.test.mts +!/src/commands/npx/cmd-npx-malware.test.mts +!/src/commands/npx/cmd-npx.mts +!/src/commands/npx/cmd-npx.test.mts +!/src/commands/oops/cmd-oops.mts +!/src/commands/oops/cmd-oops.test.mts +!/src/commands/optimize/add-overrides.mts +!/src/commands/optimize/apply-optimization.mts +!/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts +!/src/commands/optimize/cmd-optimize.mts +!/src/commands/optimize/cmd-optimize.test.mts +!/src/commands/optimize/deps-includes-by-agent.mts +!/src/commands/optimize/get-dependency-entries.mts +!/src/commands/optimize/get-overrides-by-agent.mts +!/src/commands/optimize/handle-optimize.mts +!/src/commands/optimize/lockfile-includes-by-agent.mts +!/src/commands/optimize/ls-by-agent.mts +!/src/commands/optimize/output-optimize-result.mts +!/src/commands/optimize/shared.mts +!/src/commands/optimize/types.mts +!/src/commands/optimize/update-lockfile.mts +!/src/commands/optimize/update-manifest-by-agent.mts +!/src/commands/optimize/update-manifest-by-agent.test.mts +!/src/commands/organization/cmd-organization-dependencies.mts +!/src/commands/organization/cmd-organization-dependencies.test.mts +!/src/commands/organization/cmd-organization-list.mts +!/src/commands/organization/cmd-organization-list.test.mts +!/src/commands/organization/cmd-organization-policy-license.mts +!/src/commands/organization/cmd-organization-policy-license.test.mts +!/src/commands/organization/cmd-organization-policy-security.mts +!/src/commands/organization/cmd-organization-policy-security.test.mts +!/src/commands/organization/cmd-organization-policy.mts +!/src/commands/organization/cmd-organization-policy.test.mts +!/src/commands/organization/cmd-organization-quota.mts +!/src/commands/organization/cmd-organization-quota.test.mts +!/src/commands/organization/cmd-organization.mts +!/src/commands/organization/cmd-organization.test.mts +!/src/commands/organization/fetch-dependencies.mts +!/src/commands/organization/fetch-license-policy.mts +!/src/commands/organization/fetch-organization-list.mts +!/src/commands/organization/fetch-quota.mts +!/src/commands/organization/fetch-security-policy.mts +!/src/commands/organization/handle-dependencies.mts +!/src/commands/organization/handle-dependencies.test.mts +!/src/commands/organization/handle-license-policy.mts +!/src/commands/organization/handle-organization-list.mts +!/src/commands/organization/handle-quota.mts +!/src/commands/organization/handle-quota.test.mts +!/src/commands/organization/handle-security-policy.mts +!/src/commands/organization/output-dependencies.mts +!/src/commands/organization/output-license-policy.mts +!/src/commands/organization/output-organization-list.mts +!/src/commands/organization/output-quota.mts +!/src/commands/organization/output-security-policy.mts +!/src/commands/package/cmd-package-score.mts +!/src/commands/package/cmd-package-score.test.mts +!/src/commands/package/cmd-package-shallow.mts +!/src/commands/package/cmd-package-shallow.test.mts +!/src/commands/package/cmd-package.mts +!/src/commands/package/cmd-package.test.mts +!/src/commands/package/fetch-purl-deep-score.mts +!/src/commands/package/fetch-purls-shallow-score.mts +!/src/commands/package/fixtures/go_deep.json +!/src/commands/package/fixtures/go_shallow.json +!/src/commands/package/fixtures/maven_deep.json +!/src/commands/package/fixtures/maven_shallow.json +!/src/commands/package/fixtures/npm_deep.json +!/src/commands/package/fixtures/npm_malware.json +!/src/commands/package/fixtures/npm_shallow.json +!/src/commands/package/fixtures/nuget_deep.json +!/src/commands/package/fixtures/nuget_shallow.json +!/src/commands/package/fixtures/python_deep.json +!/src/commands/package/fixtures/python_dupes.json +!/src/commands/package/fixtures/python_shallow.json +!/src/commands/package/fixtures/ruby_deep.json +!/src/commands/package/fixtures/ruby_shallow.json +!/src/commands/package/handle-purl-deep-score.mts +!/src/commands/package/handle-purls-shallow-score.mts +!/src/commands/package/output-purls-deep-score.mts +!/src/commands/package/output-purls-deep-score.test.mts +!/src/commands/package/output-purls-shallow-malware.test.mts +!/src/commands/package/output-purls-shallow-score.mts +!/src/commands/package/output-purls-shallow-score.test.mts +!/src/commands/package/parse-package-specifiers.mts +!/src/commands/package/parse-package-specifiers.test.mts +!/src/commands/patch/cmd-patch.mts +!/src/commands/pnpm/cmd-pnpm-malware.test.mts +!/src/commands/pnpm/cmd-pnpm.mts +!/src/commands/pnpm/cmd-pnpm.test.mts +!/src/commands/raw-npm/cmd-raw-npm.mts +!/src/commands/raw-npm/cmd-raw-npm.test.mts +!/src/commands/raw-npm/run-raw-npm.mts +!/src/commands/raw-npx/cmd-raw-npx.mts +!/src/commands/raw-npx/cmd-raw-npx.test.mts +!/src/commands/raw-npx/run-raw-npx.mts +!/src/commands/repository/cmd-repository-create.mts +!/src/commands/repository/cmd-repository-create.test.mts +!/src/commands/repository/cmd-repository-del.mts +!/src/commands/repository/cmd-repository-del.test.mts +!/src/commands/repository/cmd-repository-list.mts +!/src/commands/repository/cmd-repository-list.test.mts +!/src/commands/repository/cmd-repository-smoke.test.mts +!/src/commands/repository/cmd-repository-update.mts +!/src/commands/repository/cmd-repository-update.test.mts +!/src/commands/repository/cmd-repository-view.mts +!/src/commands/repository/cmd-repository-view.test.mts +!/src/commands/repository/cmd-repository.mts +!/src/commands/repository/cmd-repository.test.mts +!/src/commands/repository/fetch-create-repo.mts +!/src/commands/repository/fetch-delete-repo.mts +!/src/commands/repository/fetch-list-all-repos.mts +!/src/commands/repository/fetch-list-repos.mts +!/src/commands/repository/fetch-update-repo.mts +!/src/commands/repository/fetch-view-repo.mts +!/src/commands/repository/handle-create-repo.mts +!/src/commands/repository/handle-delete-repo.mts +!/src/commands/repository/handle-list-repos.mts +!/src/commands/repository/handle-update-repo.mts +!/src/commands/repository/handle-view-repo.mts +!/src/commands/repository/output-create-repo.mts +!/src/commands/repository/output-delete-repo.mts +!/src/commands/repository/output-list-repos.mts +!/src/commands/repository/output-update-repo.mts +!/src/commands/repository/output-view-repo.mts +!/src/commands/repository/types.mts +!/src/commands/scan/cmd-scan-create.mts +!/src/commands/scan/cmd-scan-create.test.mts +!/src/commands/scan/cmd-scan-del.mts +!/src/commands/scan/cmd-scan-del.test.mts +!/src/commands/scan/cmd-scan-diff.mts +!/src/commands/scan/cmd-scan-diff.test.mts +!/src/commands/scan/cmd-scan-github.mts +!/src/commands/scan/cmd-scan-github.test.mts +!/src/commands/scan/cmd-scan-list.mts +!/src/commands/scan/cmd-scan-list.test.mts +!/src/commands/scan/cmd-scan-metadata.mts +!/src/commands/scan/cmd-scan-metadata.test.mts +!/src/commands/scan/cmd-scan-reach.e2e.test.mts +!/src/commands/scan/cmd-scan-reach.mts +!/src/commands/scan/cmd-scan-reach.test.mts +!/src/commands/scan/cmd-scan-report.mts +!/src/commands/scan/cmd-scan-report.test.mts +!/src/commands/scan/cmd-scan-setup.mts +!/src/commands/scan/cmd-scan-setup.test.mts +!/src/commands/scan/cmd-scan-view.mts +!/src/commands/scan/cmd-scan-view.test.mts +!/src/commands/scan/cmd-scan.mts +!/src/commands/scan/cmd-scan.test.mts +!/src/commands/scan/create-scan-from-github.mts +!/src/commands/scan/create-scan-from-github.test.mts +!/src/commands/scan/exclude-paths.mts +!/src/commands/scan/exclude-paths.test.mts +!/src/commands/scan/fetch-create-org-full-scan.mts +!/src/commands/scan/fetch-delete-org-full-scan.mts +!/src/commands/scan/fetch-diff-scan.mts +!/src/commands/scan/fetch-list-scans.mts +!/src/commands/scan/fetch-report-data.mts +!/src/commands/scan/fetch-scan-metadata.mts +!/src/commands/scan/fetch-scan.mts +!/src/commands/scan/fetch-scan.test.mts +!/src/commands/scan/fetch-supported-scan-file-names.mts +!/src/commands/scan/finalize-tier1-scan.mts +!/src/commands/scan/generate-report.mts +!/src/commands/scan/generate-report.test.mts +!/src/commands/scan/generate-uv-member-sboms.e2e.test.mts +!/src/commands/scan/generate-uv-member-sboms.mts +!/src/commands/scan/generate-uv-member-sboms.test.mts +!/src/commands/scan/handle-create-github-scan.mts +!/src/commands/scan/handle-create-new-scan.mts +!/src/commands/scan/handle-create-new-scan.test.mts +!/src/commands/scan/handle-delete-scan.mts +!/src/commands/scan/handle-diff-scan.mts +!/src/commands/scan/handle-list-scans.mts +!/src/commands/scan/handle-scan-config.mts +!/src/commands/scan/handle-scan-metadata.mts +!/src/commands/scan/handle-scan-reach.mts +!/src/commands/scan/handle-scan-reach.test.mts +!/src/commands/scan/handle-scan-report.mts +!/src/commands/scan/handle-scan-view.mts +!/src/commands/scan/output-create-new-scan.mts +!/src/commands/scan/output-create-new-scan.test.mts +!/src/commands/scan/output-delete-scan.mts +!/src/commands/scan/output-diff-scan.mts +!/src/commands/scan/output-list-scans.mts +!/src/commands/scan/output-scan-config-result.mts +!/src/commands/scan/output-scan-github.mts +!/src/commands/scan/output-scan-metadata.mts +!/src/commands/scan/output-scan-reach.mts +!/src/commands/scan/output-scan-reach.test.mts +!/src/commands/scan/output-scan-report.mts +!/src/commands/scan/output-scan-report.test.mts +!/src/commands/scan/output-scan-view.mts +!/src/commands/scan/perform-reachability-analysis.mts +!/src/commands/scan/perform-reachability-analysis.test.mts +!/src/commands/scan/reach-advisories.mts +!/src/commands/scan/reach-advisories.test.mts +!/src/commands/scan/reachability-flags.mts +!/src/commands/scan/reachability-flags.test.mts +!/src/commands/scan/reachability-units.mts +!/src/commands/scan/reachability-units.test.mts +!/src/commands/scan/run-dynamic-sbom-inference.mts +!/src/commands/scan/run-dynamic-sbom-inference.test.mts +!/src/commands/scan/setup-scan-config.mts +!/src/commands/scan/stream-scan.mts +!/src/commands/scan/suggest-org-slug.mts +!/src/commands/scan/suggest-to-persist-orgslug.mts +!/src/commands/scan/suggest_branch_slug.mts +!/src/commands/scan/suggest_target.mts +!/src/commands/scan/types.mts +!/src/commands/scan/validate-reachability-target.mts +!/src/commands/scan/warn-socket-json-only-flags.mts +!/src/commands/threat-feed/cmd-threat-feed.mts +!/src/commands/threat-feed/cmd-threat-feed.test.mts +!/src/commands/threat-feed/fetch-threat-feed.mts +!/src/commands/threat-feed/handle-threat-feed.mts +!/src/commands/threat-feed/output-threat-feed.mts +!/src/commands/threat-feed/types.mts +!/src/commands/uninstall/cmd-uninstall-completion.mts +!/src/commands/uninstall/cmd-uninstall-completion.test.mts +!/src/commands/uninstall/cmd-uninstall.mts +!/src/commands/uninstall/cmd-uninstall.test.mts +!/src/commands/uninstall/handle-uninstall-completion.mts +!/src/commands/uninstall/output-uninstall-completion.mts +!/src/commands/uninstall/teardown-tab-completion.mts +!/src/commands/wrapper/add-socket-wrapper.mts +!/src/commands/wrapper/check-socket-wrapper-setup.mts +!/src/commands/wrapper/cmd-wrapper.mts +!/src/commands/wrapper/cmd-wrapper.test.mts +!/src/commands/wrapper/postinstall-wrapper.mts +!/src/commands/wrapper/remove-socket-wrapper.mts +!/src/commands/yarn/cmd-yarn-malware.test.mts +!/src/commands/yarn/cmd-yarn.mts +!/src/commands/yarn/cmd-yarn.test.mts +!/src/constants.mts +!/src/external/blessed-contrib/lib/layout/grid.mjs +!/src/external/blessed-contrib/lib/widget/charts/bar.mjs +!/src/external/blessed-contrib/lib/widget/charts/line.mjs +!/src/external/blessed-contrib/lib/widget/table.mjs +!/src/flags.mts +!/src/instrument-with-sentry.mts +!/src/npm-cli.mts +!/src/npx-cli.mts +!/src/pnpm-cli.mts +!/src/registry-constants.mts +!/src/sea/README.md +!/src/sea/adapters/cdxgen-worker.mjs +!/src/sea/adapters/cdxgen.mts +!/src/sea/adapters/synp-worker.mjs +!/src/sea/adapters/synp.mts +!/src/sea/bootstrap.mts +!/src/sea/build-sea.mts +!/src/sea/licenses/cdxgen-third-party/boolbase-1.0.0-ISC.txt +!/src/sea/licenses/cdxgen-third-party/err-code-2.0.3-MIT.txt +!/src/sea/licenses/cdxgen-third-party/err-code-2.0.3-README.md +!/src/sea/licenses/cdxgen-third-party/imurmurhash-0.1.4-MIT.txt +!/src/sea/licenses/cdxgen-third-party/spdx-exceptions-2.5.0-CC-BY-3.0.txt +!/src/sea/licenses/cdxgen-third-party/spdx-exceptions-2.5.0-README.md +!/src/sea/licenses/cdxgen-third-party/spdx-license-ids-3.0.22-CC0-1.0.txt +!/src/sea/licenses/cdxgen-third-party/spdx-license-ids-3.0.22-README.md +!/src/sea/licenses/core-third-party/@npmcli-agent-3.0.0-README.md +!/src/sea/licenses/core-third-party/@npmcli-agent-4.0.0-README.md +!/src/sea/licenses/core-third-party/ansi-term-0.0.2-ISC.txt +!/src/sea/licenses/core-third-party/drawille-canvas-blessed-contrib-0.1.3-MIT.txt +!/src/sea/licenses/core-third-party/drawille-canvas-blessed-contrib-0.1.3-README.md +!/src/sea/licenses/core-third-party/npmcli-agent-3.0.0-ISC.txt +!/src/sea/licenses/core-third-party/npmcli-agent-4.0.0-ISC.txt +!/src/sea/licenses/core-third-party/sigstore-verify-3.1.1-Apache-2.0.txt +!/src/sea/licenses/core-third-party/sigstore-verify-3.1.1-README.md +!/src/sea/licenses/core-third-party/x256-0.0.2-MIT.txt +!/src/sea/licenses/core-third-party/x256-0.0.2-readme.markdown +!/src/sea/licenses/socket-patch-MIT.txt +!/src/sea/rollup-provenance.mjs +!/src/sea/runtime.mts +!/src/sea/stage-synp.mts +!/src/sea/stage-uv.mts +!/src/sea/uv-pins.json +!/src/shadow/common.mts +!/src/shadow/npm-base.mts +!/src/shadow/npm-base.test.mts +!/src/shadow/npm/arborist-helpers.mts +!/src/shadow/npm/arborist/index.mts +!/src/shadow/npm/arborist/lib/arborist/index.mts +!/src/shadow/npm/arborist/types.mts +!/src/shadow/npm/bin.mts +!/src/shadow/npm/bin.test.mts +!/src/shadow/npm/inject.mts +!/src/shadow/npm/install.mts +!/src/shadow/npm/paths.mts +!/src/shadow/npx/bin.mts +!/src/shadow/npx/bin.test.mts +!/src/shadow/pnpm/bin.mts +!/src/shadow/pnpm/bin.test.mts +!/src/shadow/stdio-ipc.mts +!/src/shadow/yarn/bin.mts +!/src/shadow/yarn/bin.test.mts +!/src/test/cli.test.mts +!/src/test/json-output-validation.mts +!/src/test/mocks/socket-auth.mts +!/src/types.mts +!/src/utils/advisory-ids.mts +!/src/utils/agent.mts +!/src/utils/alert/artifact.mts +!/src/utils/alert/fix.mts +!/src/utils/alert/severity.mts +!/src/utils/alerts-map.mts +!/src/utils/alerts-map.test.mts +!/src/utils/api.mts +!/src/utils/api.test.mts +!/src/utils/arrays.mts +!/src/utils/bin.mts +!/src/utils/check-input.mts +!/src/utils/cmd.mts +!/src/utils/coana-release.mts +!/src/utils/coana-release.test.mts +!/src/utils/coana.mts +!/src/utils/coana.test.mts +!/src/utils/color-or-markdown.mts +!/src/utils/completion.mts +!/src/utils/config.mts +!/src/utils/config.test.mts +!/src/utils/cve-to-ghsa.mts +!/src/utils/debug.mts +!/src/utils/determine-org-slug.mts +!/src/utils/dlx-binary.mts +!/src/utils/dlx-binary.test.mts +!/src/utils/dlx-detection.mts +!/src/utils/dlx.mts +!/src/utils/dlx.test.mts +!/src/utils/ecosystem.mts +!/src/utils/editable-json.mts +!/src/utils/env.mts +!/src/utils/errors.mts +!/src/utils/errors.test.mts +!/src/utils/escape-regexp.mts +!/src/utils/extract-names.mts +!/src/utils/extract-names.test.mts +!/src/utils/fail-msg-with-badge.mts +!/src/utils/filter-config.mts +!/src/utils/fs.mts +!/src/utils/get-or-insert.mts +!/src/utils/get-output-kind.mts +!/src/utils/git.mts +!/src/utils/git.test.mts +!/src/utils/github-errors.mts +!/src/utils/github-errors.test.mts +!/src/utils/github.mts +!/src/utils/glob-content-dedup.test.mts +!/src/utils/glob.mts +!/src/utils/glob.test.mts +!/src/utils/globs.mts +!/src/utils/lockfile.mts +!/src/utils/logger.mts +!/src/utils/map-to-object.mts +!/src/utils/map-to-object.test.mts +!/src/utils/markdown.mts +!/src/utils/markdown.test.mts +!/src/utils/meow-with-subcommands.mts +!/src/utils/ms-at-home.mts +!/src/utils/npm-config.mts +!/src/utils/npm-package-arg.mts +!/src/utils/npm-paths.mts +!/src/utils/npm-paths.test.mts +!/src/utils/npm-spec.mts +!/src/utils/objects.mts +!/src/utils/is-object-object.mjs +!/src/utils/is-object-object.d.mts +!/src/utils/organization.mts +!/src/utils/output-formatting.mts +!/src/utils/package-environment.mts +!/src/utils/package-manager.mts +!/src/utils/packages.mts +!/src/utils/path-resolve.mts +!/src/utils/path-resolve.test.mts +!/src/utils/path.mts +!/src/utils/pnpm-paths.mts +!/src/utils/pnpm-scanning.test.mts +!/src/utils/pnpm.mts +!/src/utils/promises.mts +!/src/utils/prompts.mts +!/src/utils/purl-legacy-npm-names.json +!/src/utils/purl-to-ghsa.mts +!/src/utils/purl.mts +!/src/utils/registry-manifest.json +!/src/utils/requirements.mts +!/src/utils/sdk.mts +!/src/utils/sdk.test.mts +!/src/utils/self-update.mts +!/src/utils/self-update.test.mts +!/src/utils/semver.mts +!/src/utils/serialize-result-json.mts +!/src/utils/shadow-links.mts +!/src/utils/socket-json.mts +!/src/utils/socket-json.test.mts +!/src/utils/socket-package-alert.mts +!/src/utils/socket-url.mts +!/src/utils/sorts.mts +!/src/utils/spawn-sync.mts +!/src/utils/spawn.mts +!/src/utils/spec.mts +!/src/utils/spinner.mts +!/src/utils/streams.mts +!/src/utils/strings.mts +!/src/utils/telemetry/integration.mts +!/src/utils/telemetry/integration.test.mts +!/src/utils/telemetry/service.mts +!/src/utils/telemetry/service.test.mts +!/src/utils/telemetry/types.mts +!/src/utils/terminal-link.mts +!/src/utils/tildify.mts +!/src/utils/translations.mts +!/src/utils/url.mts +!/src/utils/walk-nested-map.mts +!/src/utils/walk-nested-map.test.mts +!/src/utils/words.mts +!/src/utils/yarn-paths.mts +!/src/utils/yarn-version.mts +!/src/vendor/coana-cache.mjs +!/src/yarn-cli.mts +!/taze.config.mts +!/test/blessed-node24-strict.test.mts +!/test/dist-runtime-requires.test.mts +!/test/external-bare-requires.test.mts +!/test/fixtures/commands/cdxgen/package.json +!/test/fixtures/commands/fix/e2e-test-js/package-lock.json +!/test/fixtures/commands/fix/e2e-test-js/package.json +!/test/fixtures/commands/fix/e2e-test-multipm/npm-app/package-lock.json +!/test/fixtures/commands/fix/e2e-test-multipm/npm-app/package.json +!/test/fixtures/commands/fix/e2e-test-multipm/pnpm-app/package.json +!/test/fixtures/commands/fix/e2e-test-multipm/pnpm-app/pnpm-lock.yaml +!/test/fixtures/commands/fix/e2e-test-py/requirements.txt +!/test/fixtures/commands/fix/npm/monorepo/package-lock.json +!/test/fixtures/commands/fix/npm/monorepo/package.json +!/test/fixtures/commands/fix/npm/monorepo/packages/app/package.json +!/test/fixtures/commands/fix/npm/monorepo/packages/lib/package.json +!/test/fixtures/commands/fix/npm/vulnerable-deps/package-lock.json +!/test/fixtures/commands/fix/npm/vulnerable-deps/package.json +!/test/fixtures/commands/fix/pnpm/monorepo/package.json +!/test/fixtures/commands/fix/pnpm/monorepo/packages/app/package.json +!/test/fixtures/commands/fix/pnpm/monorepo/packages/lib/package.json +!/test/fixtures/commands/fix/pnpm/monorepo/pnpm-lock.yaml +!/test/fixtures/commands/fix/pnpm/vulnerable-deps/package.json +!/test/fixtures/commands/fix/pnpm/vulnerable-deps/pnpm-lock.yaml +!/test/fixtures/commands/fix/yarn/monorepo/package.json +!/test/fixtures/commands/fix/yarn/monorepo/packages/app/package.json +!/test/fixtures/commands/fix/yarn/monorepo/packages/lib/package.json +!/test/fixtures/commands/fix/yarn/monorepo/yarn.lock +!/test/fixtures/commands/fix/yarn/vulnerable-deps/package.json +!/test/fixtures/commands/fix/yarn/vulnerable-deps/yarn.lock +!/test/fixtures/commands/json/socket.json +!/test/fixtures/commands/manifest/conda/environment.yml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/dual-marker-dir/build.gradle +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/dual-marker-dir/pom.xml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/moduleA/nested-gradle/build.gradle +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/moduleA/pom.xml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/moduleB/independent-submodule/pom.xml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/moduleB/pom.xml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/reactor/pom.xml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/standalone-gradle/build.gradle.kts +!/test/fixtures/commands/manifest/dynamic-sbom-inference/monorepo/standalone-gradle/settings.gradle.kts +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/app-dev.pom.xml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/bare/pom +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/other.pom.xml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/pom.xml +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/pom.xml.bak +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/upper/POM.XML +!/test/fixtures/commands/manifest/dynamic-sbom-inference/pom-names/vendored/lib-1.0.pom +!/test/fixtures/commands/manifest/python/requirements.txt +!/test/fixtures/commands/npm/lacking-typosquat/package-lock.json +!/test/fixtures/commands/npm/lacking-typosquat/package.json +!/test/fixtures/commands/npm/npm10/package-lock.json +!/test/fixtures/commands/npm/npm10/package.json +!/test/fixtures/commands/npm/npm11/package-lock.json +!/test/fixtures/commands/npm/npm11/package.json +!/test/fixtures/commands/npm/npm9/package-lock.json +!/test/fixtures/commands/npm/npm9/package.json +!/test/fixtures/commands/optimize/npm/package-lock.json +!/test/fixtures/commands/optimize/npm/package.json +!/test/fixtures/commands/optimize/pnpm/package.json +!/test/fixtures/commands/optimize/pnpm/pnpm-lock.yaml +!/test/fixtures/commands/optimize/pnpm8/package.json +!/test/fixtures/commands/optimize/pnpm8/pnpm-lock.yaml +!/test/fixtures/commands/optimize/pnpm9/package.json +!/test/fixtures/commands/optimize/pnpm9/pnpm-lock.yaml +!/test/fixtures/commands/optimize/yarn/package.json +!/test/fixtures/commands/optimize/yarn/yarn.lock +!/test/fixtures/commands/patch/npm/.socket/blobs/76682a9fc3bbe62975176e2541f39a8168877d828d5cad8b56461fc36ac2b856 +!/test/fixtures/commands/patch/npm/.socket/manifest.json +!/test/fixtures/commands/patch/npm/package-lock.json +!/test/fixtures/commands/patch/npm/package.json +!/test/fixtures/commands/patch/pnpm/.socket/blobs/76682a9fc3bbe62975176e2541f39a8168877d828d5cad8b56461fc36ac2b856 +!/test/fixtures/commands/patch/pnpm/.socket/manifest.json +!/test/fixtures/commands/patch/pnpm/package.json +!/test/fixtures/commands/patch/pnpm/pnpm-lock.yaml +!/test/fixtures/commands/patch/yarn/.socket/blobs/76682a9fc3bbe62975176e2541f39a8168877d828d5cad8b56461fc36ac2b856 +!/test/fixtures/commands/patch/yarn/.socket/manifest.json +!/test/fixtures/commands/patch/yarn/package.json +!/test/fixtures/commands/patch/yarn/yarn.lock +!/test/fixtures/commands/scan/npm-test-workspace-mono/.gitignore +!/test/fixtures/commands/scan/npm-test-workspace-mono/package-lock.json +!/test/fixtures/commands/scan/npm-test-workspace-mono/package.json +!/test/fixtures/commands/scan/npm-test-workspace-mono/packages/package-a/package.json +!/test/fixtures/commands/scan/npm-test-workspace-mono/packages/package-b/index.js +!/test/fixtures/commands/scan/npm-test-workspace-mono/packages/package-b/package.json +!/test/fixtures/commands/scan/plain-requirements-txt/requirements.txt +!/test/fixtures/commands/scan/simple-npm/index.js +!/test/fixtures/commands/scan/simple-npm/package.json +!/test/fixtures/commands/scan/uv-workspace/packages/api/pyproject.toml +!/test/fixtures/commands/scan/uv-workspace/packages/other/pyproject.toml +!/test/fixtures/commands/scan/uv-workspace/packages/shared/pyproject.toml +!/test/fixtures/commands/scan/uv-workspace/pyproject.toml +!/test/fixtures/commands/scan/uv-workspace/uv.lock +!/test/fixtures/manifest-bazel/bzlmod-only/MODULE.bazel +!/test/fixtures/manifest-bazel/custom-name-bzlmod/MODULE.bazel +!/test/fixtures/manifest-bazel/legacy-only/WORKSPACE +!/test/fixtures/manifest-bazel/legacy-with-load/WORKSPACE +!/test/fixtures/manifest-bazel/legacy-with-load/maven_deps.bzl +!/test/fixtures/manifest-bazel/migration/MODULE.bazel +!/test/fixtures/manifest-bazel/migration/WORKSPACE +!/test/fixtures/manifest-bazel/multi-repo-bzlmod/MODULE.bazel +!/test/fixtures/manifest-bazel/native/bzlmod/.bazelversion +!/test/fixtures/manifest-bazel/native/bzlmod/BUILD.bazel +!/test/fixtures/manifest-bazel/native/bzlmod/MODULE.bazel +!/test/fixtures/manifest-bazel/native/maven.expected.json +!/test/fixtures/manifest-bazel/native/workspace/.bazelversion +!/test/fixtures/manifest-bazel/native/workspace/BUILD.bazel +!/test/fixtures/manifest-bazel/native/workspace/WORKSPACE +!/test/fixtures/manifest-bazel/python-pypi/requirements.expected.txt +!/test/glob.test.mts +!/test/json-output-validation.mts +!/test/manifest-reachability-assertions.test.mts +!/test/maven-compat-smoke-assertions.test.mts +!/test/mock-auth.mts +!/test/mock-auth.test.mts +!/test/mock-malware-api.mts +!/test/mock-malware-api.test.mts +!/test/package-environment.npm-version.test.mts +!/test/pin-readme-assets.test.mts +!/test/reachability-options.mts +!/test/release-add-unreleased.test.mts +!/test/release-artifact-download.test.mts +!/test/release-bump.test.mts +!/test/release-history.test.mts +!/test/release-open-pr.test.mts +!/test/release-registry.test.mts +!/test/release-version.test.mts +!/test/release-workflow.test.mts +!/test/run-with-config.mts +!/test/script-run-main.test.mts +!/test/script-scope.test.mts +!/test/script-update.test.mts +!/test/setup-node.test.mts +!/test/setup-uv.test.mts +!/test/smoke.sh +!/test/upload-artifact.test.mts +!/test/native-signing.test.mts +!/test/utils.mts +!/test/utils.test.mts +!/test/workflow-actions.test.mts +!/translations.json +!/tsconfig.dts.json +!/tsconfig.json +!/tsconfig.scripts.json +!/vitest.config.mts +!/vitest.terminal.config.mts +!/vitest.e2e.config.mts +!/vitest.integration.config.mts +!/vitest.native.config.mts + +# Local and generated files remain excluded even when they match an allowlist path. /.env /.env.local -/.nvm -/.pnpm-store -/.rollup.cache -/.type-coverage -/.vscode -/coverage -/external -/npm-debug.log -**/.cache -**/dist -**/node_modules +/.env.precommit +/.nvm/ +/.pnpm-store/ +/.rollup.cache/ +/.type-coverage/ +/build/ +/coverage/ +/coverage-e2e/ +/coverage-unit/ +/external/ +**/.cache/ +**/dist/ +**/node_modules/ *.d.ts *.d.ts.map *.tsbuildinfo -test/fixtures/commands/fix/e2e-test-js-temp-* -test/fixtures/commands/fix/e2e-test-py-temp-* - -/.claude/* -!/.claude/agents/ -!/.claude/commands/ -!/.claude/hooks/ -!/.claude/ops/ -!/.claude/settings.json -!/.claude/skills/ - -!/.vscode/extensions.json - -# Generated by the JVM compat fixtures (src/commands/manifest/scripts/test). -# The stub repos and caches are rebuilt on every run; smoke-test-workspaces.sh -# still keeps its Gradle user home inside the fixture directory. +/.repo-map/ +/upstream/ +/docs/fleet/ +/scripts/fleet/ +/test/fleet/ +/.claude/ops/ +/.claude/worktrees/ +/.github/copilot-instructions.md /src/commands/manifest/scripts/test/gradle-compat/.gradle-home/ /src/commands/manifest/scripts/test/gradle-compat/**/.gradle/ /src/commands/manifest/scripts/test/gradle-compat/project/build/ @@ -53,10 +1401,6 @@ test/fixtures/commands/fix/e2e-test-py-temp-* /src/commands/manifest/scripts/test/sbt-compat/relocation/records.tsv /src/commands/manifest/scripts/test/sbt-compat/relocation/scala-version.sbt /src/commands/manifest/scripts/test/sbt-compat/relocation/target/ - -# Generated when the dynamic SBOM inference tests walk their fixtures -# (test/fixtures/commands/manifest/dynamic-sbom-inference). Those fixtures are -# build-tool marker files only, so any build output under them is debris. /test/fixtures/commands/manifest/dynamic-sbom-inference/**/.gradle/ /test/fixtures/commands/manifest/dynamic-sbom-inference/**/build/ /test/fixtures/commands/manifest/dynamic-sbom-inference/**/target/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..c236ef7a7c --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +# Avoid advisory-feed lookups as a side effect of npm install. +audit=false diff --git a/README.md b/README.md index 8ace54a8d1..e9c7583389 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Supports version 2 format with `projectIgnorePaths` for excluding files from rep - `SOCKET_CLI_GITHUB_TOKEN` - GitHub token with repo access (alias: `GITHUB_TOKEN`) - `SOCKET_CLI_NO_API_TOKEN` - Disable default API token - `SOCKET_CLI_NPM_PATH` - Path to npm directory +- `SOCKET_CLI_SOCKET_PATCH_PATH` - Path to an executable socket-patch binary for standalone builds - `SOCKET_CLI_ORG_SLUG` - Socket organization slug - `SOCKET_CLI_ACCEPT_RISKS` - Accept npm/npx risks - `SOCKET_CLI_VIEW_ALL_RISKS` - Show all npm/npx risks diff --git a/assets/native-terminal/scan-setup-animation.svg b/assets/native-terminal/scan-setup-animation.svg new file mode 100644 index 0000000000..5164365826 --- /dev/null +++ b/assets/native-terminal/scan-setup-animation.svg @@ -0,0 +1,176 @@ + +CLI: v2.1.0-pre-release.0 | cmd: socket scan setup | org: (not set) | token: (not set) + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + +Note: This tool will set up flag and argument defaults for certain + + CLI commands. You can still override them by explicitly + + setting the flag. It is meant to be a convenience tool. + + + +This command will generate a `socket.json` file in the target cwd. + +You can choose to add this file to your repo (handy for collab) + +or to add it to the ignored files, or neither. This file is only + +used in CLI workflows. + + + +Note: For details on a flag you can run `socket <cmd> --help` + + + +? + +Which scan command do you want to configure? + +(Use arrow keys) + + +❯ socket scan create + + + socket scan github + + (cancel) + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan setup | org: (not set) | token: (not set) + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + +Note: This tool will set up flag and argument defaults for certain + + CLI commands. You can still override them by explicitly + + setting the flag. It is meant to be a convenience tool. + + + +This command will generate a `socket.json` file in the target cwd. + +You can choose to add this file to your repo (handy for collab) + +or to add it to the ignored files, or neither. This file is only + +used in CLI workflows. + + + +Note: For details on a flag you can run `socket <cmd> --help` + + + +? + +Which scan command do you want to configure? + + + socket scan create + +❯ socket scan github + + + (cancel) + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan setup | org: (not set) | token: (not set) + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + +Note: This tool will set up flag and argument defaults for certain + + CLI commands. You can still override them by explicitly + + setting the flag. It is meant to be a convenience tool. + + + +This command will generate a `socket.json` file in the target cwd. + +You can choose to add this file to your repo (handy for collab) + +or to add it to the ignored files, or neither. This file is only + +used in CLI workflows. + + + +Note: For details on a flag you can run `socket <cmd> --help` + + + +✔ + +Which scan command do you want to configure? + +(cancel) + + + + +ℹ + User canceled + + + + + +Finished + + + + + + + + + \ No newline at end of file diff --git a/assets/native-terminal/socket-help-help.png b/assets/native-terminal/socket-help-help.png new file mode 100644 index 0000000000..7d6757b251 Binary files /dev/null and b/assets/native-terminal/socket-help-help.png differ diff --git a/assets/native-terminal/socket-scan-setup-canceled.png b/assets/native-terminal/socket-scan-setup-canceled.png new file mode 100644 index 0000000000..8c8fa4fd5b Binary files /dev/null and b/assets/native-terminal/socket-scan-setup-canceled.png differ diff --git a/assets/native-terminal/socket-scan-setup-github-selected.png b/assets/native-terminal/socket-scan-setup-github-selected.png new file mode 100644 index 0000000000..d89d25022d Binary files /dev/null and b/assets/native-terminal/socket-scan-setup-github-selected.png differ diff --git a/assets/native-terminal/socket-scan-setup-menu.png b/assets/native-terminal/socket-scan-setup-menu.png new file mode 100644 index 0000000000..9f43e60089 Binary files /dev/null and b/assets/native-terminal/socket-scan-setup-menu.png differ diff --git a/bin/cli.js b/bin/cli.js index 65a478640e..a5e36a2e4c 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -11,9 +11,7 @@ void (async () => { const { default: constants } = require( path.join(rootPath, 'dist/constants.js'), ) - const { spawn } = require( - path.join(rootPath, 'external/@socketsecurity/registry/lib/spawn.js'), - ) + const { spawn } = require(path.join(rootPath, 'dist/utils.js')) process.exitCode = 1 diff --git a/biome.json b/biome.json index 46538f5231..52de9e0657 100644 --- a/biome.json +++ b/biome.json @@ -14,6 +14,7 @@ "!**/.rollup.cache", "!**/.type-coverage", "!**/.vscode", + "!test/native-snapshots", "!.claude/worktrees", "!**/coverage", "!src/vendor/coana-cache.mjs", diff --git a/docs/plans/node27-sea-vfs-socket-package.md b/docs/plans/node27-sea-vfs-socket-package.md new file mode 100644 index 0000000000..85d1665a78 --- /dev/null +++ b/docs/plans/node27-sea-vfs-socket-package.md @@ -0,0 +1,538 @@ +# Plan: self-contained `socket` CLI using Node 27 SEA and VFS + +## Status and scope + +Implementation plan for a new artifact built from the root `socket` package on the `v1.x` line. This is a separate distribution variant: do not change or replace `@socketsecurity/cli`, `@socketsecurity/cli-with-sentry`, their package identities, public behavior, npm approval/trusted-publishing behavior, or current JavaScript launchers. The requested removal of `@socketsecurity/registry` and `@socketregistry/packageurl-js` is an intentional dependency/content change in the root `socket` package; preserve the existing npm release contract and avoid unrelated package changes. The standalone asset lifecycle is a separate SEA release/tag namespace with an independently incremented revision mapped to Socket version and source SHA, avoiding immutable npm release coupling. The deliverable is a platform-specific standalone Socket CLI application built with the latest Node.js 27 release available when implementation starts, using Node SEA and VFS support, and signed/released by adapting the established `socket-cli-native` workflow contract. + +### Implementation progress (2026-11-09) + +The implementation is on `feat/v1x-node27-sea-vfs-socket`, rebased on `origin/v1.x` at Socket `1.14.1`. The `--prototype` builder pins Node `v27.0.0-alpha.0.0.0` and verifies its archive against official `SHASUMS256.txt`. It uses Node's direct `--build-sea` output, stages inputs in private OS temporary directories outside checkout `dist`, preserves relative VFS-main loading, and packages the root `socket` distribution, external tree, runtime data, verified Node license, target-native socket-patch, the first-party .NET facts helper, and darwin-arm64 cdxgen/Synp bundles. It builds the .NET facts helper before Rollup copies the distribution; independent tool payload preparation runs concurrently and is awaited with `Promise.allSettled` before cleanup, while the main distribution build waits for the .NET helper. The helper is embedded, but its command uses the user's project-selected .NET SDK at runtime; the SDK is not part of the SEA. `.npmrc` disables npm install audit lookups. The pinned pnpm 10 install does not run vulnerability auditing automatically; `pnpm audit` is explicit and absent from this build path. The helper's `dotnet restore` disables NuGet auditing; pinned input integrity checks remain enabled. `SOCKET_SEA_BUILD=true` skips ordinary package/lockfile build-hook rewrites. The prototype is macOS arm64 evidence only; it is not production-signed or target-qualified. + +**Latest integrated build (2026-11-09; source version 1.14.1).** `pnpm build:sea -- --prototype` completed for darwin-arm64 with Node `v27.0.0-alpha.0.0.0`. The 181,738,144-byte executable SHA-256 is `54c36b1620eeaea007daa4583a4a98cd4b2dacff5efc63492d12e0b2add1c3ff`; `codesign --verify` succeeds for its ad-hoc signature, and `--version` reports `1.14.1`. Its manifest records the embedded `socket-facts-dotnet.dll` helper closure and documents the external .NET SDK requirement. NuGet restore succeeded from cached packages with auditing disabled. Checkout `dist` remains isolated from the SEA build. The VFS includes the verified Node archive `LICENSE`, target socket-patch, cdxgen 12.1.2, Synp 1.9.14, pinned uv 0.12.15, and core/external source-and-notice inventory. Its sidecar records six distribution inventories and 256 notice documents; uv is advertised for this prototype target with a recorded unresolved Rust transitive-license inventory obligation. cdxgen and Synp remain unavailable pending authenticated source bytes and unresolved legal obligations. npm lock v3 and Yarn Classic lock v1 adapter smokes succeeded. The adapter validates required lock edges, permits absent optional/peer dependencies, isolates cdxgen from project `node_modules` and host fallback files, preserves project identity and source paths, cleans owned lock/temp files before exit, and maps cancellation to conventional signal exit codes. Static review found no remaining adapter blockers. No repository test suites were run. This is still an ad-hoc signed prototype, not Developer ID/notarized or target-qualified release output. + +**Restricted cdxgen/Synp implementation and smoke (qualification-gated).** The builder packages the pinned `@cyclonedx/cdxgen@12.1.2` API and Synp 1.9.14 API in the darwin-arm64 VFS bundle, with relative Rollup chunks, package metadata, data, Workers, and notices. A restricted Worker calls `createNodejsBom()` directly; it never invokes cdxgen's CLI `prepareEnv()` or manager-selection path. The cdxgen adapter accepts one root npm `package-lock.json` v3 project or Yarn Classic lock v1 through Synp's v1 converter. It rejects Berry, workspaces, local/link dependencies, unsupported modes, malformed lock graphs and unresolved required edges; optional/peer absences are allowed. It calls the API against a sanitized temporary project with only `package.json` and the lockfile, so host `node_modules`, `bower.json`, minified JS, plugins and fallback scans cannot affect results. It preserves unnamed-project identity and rewrites supported source/bin paths back to the original project. Synp requires existing `node_modules`, creates the output lock exclusively, verifies its inode/size/digest, and removes only the exact lock it owns before command exit. Cancellation reaches both Workers and cleanup, including SIGHUP/SIGINT/SIGTERM status propagation. Direct API smokes produced CycloneDX 1.6 BOMs from npm v3 and Synp v1 fixtures. A hostile ambient `NODE_OPTIONS` preload, `NODE_PATH`, `CDXGEN_ALLOWED_COMMANDS=*`, external `CDXGEN_PLUGINS_DIR`, and `FETCH_LICENSE=true` did not widen the Worker environment; `--deep` returns a clear unsupported-option error. The cdxgen and Synp payloads remain unavailable in the manifest until source provenance and remaining notice obligations are qualified. If enabled later, the cdxgen capability is limited to one npm project, `pre-build`, no installs/Babel/recursion/evidence/deep/plugins/native analyzers, only darwin-arm64. Shrinkwrap, workspaces, Yarn Berry, other lock versions, and all other cdxgen modes remain unavailable. + +**Proof limits and promotion gates.** The Worker blocks `spawn`, `spawnSync`, `exec`, `execSync`, `execFile`, `execFileSync`, `fork`, HTTP(S) request/get, direct TCP/TLS connects, HTTP/2 connects, UDP socket creation, and global fetch before API import; Node builtin ESM exports are synchronized after patching. This is a strict guard around the exercised API path, not an OS sandbox or a claim against arbitrary `process.binding()` use. Valid/empty/malformed lock behavior, unsupported flags, hostile environment settings, output/source identity, optional/peer graph edges, cleanup and direct npm/Yarn API results have focused smoke evidence. Before promotion, repeat those checks on each declared target runner, establish complete module/network observation, authenticate package source bytes against locked tarballs and applied patches, resolve legal/copyright obligations, and verify the actual signed release bytes. cdxgen's API can otherwise log a partial BOM and scan local project files; sanitized project isolation and strict graph validation prevent those observed fallbacks in the supported path. The Worker uses `execArgv: []` plus replacement env, and the SEA uses `execArgvExtension: "none"` with `--no-global-search-paths`. + +**Current source and notice inventory.** The cdxgen Rollup closure records 140 package instances and deterministic installed-module source hashes with package/lock locator, declared SRI, and patch context. Source authentication is explicitly `unverified`; lock SRI does not authenticate installed, patched or generated bytes. Curated legal documents cleared the five former missing-text items (`boolbase@1.0.0`, `err-code@2.0.3`, `imurmurhash@0.1.4`, `spdx-exceptions@2.5.0`, `spdx-license-ids@3.0.22`), but `err-code@2.0.3` still has an inferred copyright-holder obligation. The core Rollup closure covers 1,035 module files across 229 package instances; the copied Blessed closure and four Blessed-contrib entrypoints have separate inventories. Curated texts cleared six missing-text gaps (`@npmcli/agent@3.0.0`, `@npmcli/agent@4.0.0`, `@sigstore/verify@3.1.1`, `x256@0.0.2`, `ansi-term@0.0.2`, `drawille-canvas-blessed-contrib@0.1.3`), while five inferred copyright-holder obligations remain for both `@npmcli/agent` versions, `x256`, `ansi-term`, and `drawille-canvas-blessed-contrib`. The six distribution sidecars and 256 notice documents are VFS-addressed and hashed in the external build manifest. Synp's 25-package notice inventory has two missing-license-text obligations, and its staged payload hash does not authenticate installed/transformed module sources. Keep cdxgen and Synp unavailable; their source/authentication and legal evidence are still incomplete. Core distribution notices also need final legal review before any production release. + +**Coana source spike (2026-11-09).** The active product version is `15.14.0` (`src/constants.mts`), invoked by both reachability analysis and `socket fix`. It is not DLX: `src/utils/coana-release.mts` honors `SOCKET_CLI_COANA_LOCAL_PATH`, otherwise resolves the versioned Socket component through the authenticated Socket API, caches it, and launches `cli-wrapper.mjs`. The CLI itself can fetch its `analyzers` component and bootstrap Python/uv/Go tools. The local cache inspected for evidence contains 15.14.0 CLI digest `ac9dc9c7d49f6af238b9004ccdf2635822bbbc7a409a1ee211cc51e04b7803dd` (6.7 MiB) and analyzer digest `9497418bf673d2f0b02667e8a4bdc7d0de3dda87a7f6b9c405f2c69c54d2837b` (112 MiB / 181 files); these user-cache bytes are not reproducible CI inputs and must not be treated as source-authenticated payloads. The bundle request does not carry a platform key, so the component must be independently inventoried before use. Coana supports macOS x64/arm64 and Linux glibc x64/arm64; Windows is explicitly rejected. Linux ARM64 Ruby is unavailable because its packaged Ruby analyzer is an x86-64 ELF despite its ARM64 path. Darwin tree-sitter addons are ARM64 Mach-O with ad-hoc linker signatures; all native signature states and loaders need individual inspection. Coana invokes JS analyzers through child Node processes (`process.execPath`) and can install Python/Go/tooling at runtime (`uv tool run`, venv/pip, pyenv/uv, Go); it cannot run unchanged inside a Worker or call the outer SEA executable as `node`. A bundled plain Node child runtime or narrowly refactored child dispatcher is required, in dependency order with Python/uv closure. Its CLI/analyzer component roots lack LICENSE/NOTICE files; recursive redistribution rights and notices are unresolved. Implement with target capabilities split by ecosystem, explicit override > embedded precedence, and no Coana/API/cache/toolchain fallback when the bundled target payload is absent. Windows, Linux musl, unsupported Ruby and any analyzer with unresolved native/license closure must fail clearly without acquisition. + +**uv/Python source spike (2026-11-09).** `--uv-members` is a single production uv path in `src/commands/scan/generate-uv-member-sboms.mts`; it invokes `uv export --format cyclonedx1.5 --frozen --offline --no-python-downloads --all-extras` twice. Pin uv `0.12.15` and embed independently SHA-256-verified assets for all six Node target triples (darwin x64/arm64, Linux GNU x64/arm64, Windows x64/arm64); upstream `scripts/ci/setup-uv.mts` only pins Linux, so SEA needs its own complete exact pin table and release/license source. Preserve `SOCKET_CLI_UV_PATH` override precedence, validate a selected override without fallback, and extract only the target-native embedded executable for child launch. Python is used only by Bazel Maven/PyPI extraction shims (`src/commands/manifest/bazel/bazel-python-shim.mts` and its two extraction callers), not by uv SBOM export. These SEA paths currently require explicit `SOCKET_CLI_PYTHON_PATH`; future embedded Python must follow override > target payload and be a relocatable runtime with stdlib/shared-library/license closure. Do not install host/system Python. Verify Bazel queries do not trigger Socket-owned Python/toolchain acquisition and keep project-declared remote repositories distinct. Python artifact selection/redistribution across all six targets remains unqualified; do not advertise Python support until a runtime smoke succeeds per target. + +**Build trust and startup hardening.** The Node build cache is keyed by the official archive checksum, but a matching directory name alone is not integrity evidence. Each build downloads and verifies the Node archive against official `SHASUMS256.txt`, extracts into a private owned tree, and compares the complete cached file/symlink/mode tree against that extraction before reuse. A mismatched cache is left untouched and that build uses its private verified runtime, avoiding concurrent destructive repair races; corrupt-cache cleanup remains maintenance work. Set SEA `execArgvExtension: "none"` and `execArgv: ["--no-global-search-paths"]` to exclude ambient Node startup options and global module search paths; a hostile `NODE_OPTIONS` preload and `NODE_PATH` sentinel did not affect the integrated cdxgen run. The `constants.spinner` getter loads the dedicated `dist/spinner.js` leaf's default singleton so prompt pause/resume and normal command progress share one spinner instance. + +**Archived signing workflow audit.** The archived repository at `/Users/jdalton/projects/socket-cli-native` (HEAD `96d439e1ab786d6500e085560c2dcf115289a22b`) provides reusable contracts in `.github/workflows/publish-native.yml`, `scripts/repo/build/sea/sign.mts`, `scripts/fleet/apple-sign.mts`, and `scripts/fleet/apple-notarize.mts`. Follow their signing, notarization, and qualification contracts; verify every helper is tracked in the target checkout before importing it. The SEA wrapper requires macOS Developer ID Application signing with hardened runtime, strict expected Team ID verification, notarization `Accepted` plus receipt UUID, and post-notary signature verification; bare binaries cannot be stapled, so a stapled DMG/pkg is a separate offline-first-launch decision. Preserve/sign inner payloads before embedding when evidence requires it, then outer sign → strict verify → notary submit/accepted receipt → verify again → final SHA/receipt → native-runner qualification of the transferred bytes. Required secrets include the P12/cert password, Team ID and Apple notarization credentials (Apple ID/app-specific password or ASC key credentials). The archived build job currently exposes signing secrets before its script-level requested-source-SHA comparison and has no protected environment declaration; the new SEA workflow must validate tag/version/source SHA and clean detached checkout before secrets are made available, put credentials only on a protected macOS signing job, and keep qualification/release jobs credential-free. Existing workflow artifacts are not by themselves a SEA release coordinator, and no Authenticode requirement is inferred. + +**Clean-checkout signing implementation boundary (Astra review, 2026-11-09).** The current checkout contains Apple helpers under ignored `scripts/fleet/`, but `git ls-files` confirms they are untracked and `.git/info/exclude` ignores them; `@socketsecurity/lib-stable` is not an installable dependency here. Clean CI cannot import those files or the archived local setup action. Port the narrow Apple operations into tracked `scripts/sea/release/apple/` modules using Node builtins and explicit bounded subprocesses, with source attribution to the inspected native revision. The archived uploader only handles exactly three npm tarballs; SEA needs a distinct artifact archive/receipt path. The workflow must be manually authorized from protected `v1.x`, validate full source SHA/version and a clean detached checkout before any Apple secrets are available, build the exact darwin-arm64 artifact without credentials, transfer it immutably, then sign/notarize only those transferred bytes in a protected macOS Environment. Require a private temporary keychain, one unambiguous Developer ID Application identity, hardened runtime, explicit JIT entitlements, strict Team ID/chain/entitlement verification, accepted notary status with UUID, and post-notary hash/signature verification. Submit a ZIP of the bare Mach-O to `notarytool`; do not claim that the executable itself is stapled. A separate credential-free macOS job verifies immutable artifact hashes, applies/readbacks quarantine on its test copy, performs online Gatekeeper assessment, and qualifies declared capabilities. Keep `prototype-qualification` separate from production eligibility: the current `--prototype` build, unverified package sources, unresolved notices and absent embedded manifest/release attestation cannot become a production release just because notarization succeeds. The final receipt binds repository/workflow/source SHA, Socket and standalone revision, Node/target/run, embedded-manifest and sidecar digests, pre/post-sign executable digests and size, Team ID, entitlements, accepted notary UUID/status and submission-ZIP hash. The embedded manifest must be produced before archive assembly; the post-sign receipt is separate because the final executable cannot embed its own final digest. Defer publication and immutable release revision allocation until legal/source gates and target qualification are enforceable. + +The generic `spawnDlx()` rejects SEA calls before package-manager detection or launching a shadow manager, preventing future helper call sites from silently adding a DLX install path. The builder embeds the target-compatible socket-patch binary in the VFS archive, validates package metadata and lockfile integrity at build time, records its license, and its runtime digest-check/extraction/execution/cleanup path has local smoke evidence. Coana fails closed before cache/API/download work unless `SOCKET_CLI_COANA_LOCAL_PATH` selects an explicitly supported executable; its JavaScript mode remains unavailable until its Worker/runtime and child-launch behavior are qualified. Startup npm update metadata checks are disabled inside SEA. Synp 1.9.14's API returns serialized lock data while its CLI performs validation and file writing. The bundled adapter creates `package-lock.json` exclusively, tracks its digest and file identity, and removes only that owned file before exit. Synp and cdxgen payloads are embedded but remain capability-gated pending source authentication and notice/legal qualification. + +`@socketregistry/packageurl-js` and `@socketsecurity/registry` are both removed from the root package graph. Runtime and Rollup helper imports were replaced with narrow local attributed ports; prompt handling uses exact-pinned direct `@inquirer` dependencies instead of vendored registry files. `pnpm why` confirms neither removed package remains in the installed dependency graph. The Optimize editable-package wrapper still differs in JSON comments/order behavior. No tests have been run. Prototype verification is macOS arm64 only; production signing and other-target verification remain outstanding. + +Local ports cover currently consumed methods and retain upstream notices. Node SEA does not permit `vfsArchive` with named `assets`, so socket-patch and its SHA-256 are files within the VFS archive; the runtime adapter reads them through VFS and extracts the binary only for native execution. The initial build confirms staging isolation. Node 27 VFS startup required removing a `constants -> utils -> constants` runtime load and providing an explicit bootstrap VFS root. The current three-command smoke pass confirms the issue is resolved for core CLI and socket-patch help. Blessed outputs honor the staged external directory. + +**Node release availability at research time (2026-11-08):** the official Node distribution index lists `v27.0.0-alpha.0.0.0` as the only Node 27 release and `v26.13.0` as the latest stable release. Node 27 is currently Alpha until Current is scheduled for 2027-04-22; prerelease APIs/ABI may change and are not intended for production. The user requested the latest Node 27 SEA, not stable-only. Therefore discovery and implementation should select the latest Node 27 release available at each pinned build, whether prerelease or stable, record its exact version/commit/checksum, and label any artifact built from prerelease Node as experimental/pre-release with its support limits. Do not silently substitute another major or use a floating `latest` download. Recheck the [official release index](https://nodejs.org/dist/index.json) and [Node release schedule](https://github.com/nodejs/Release/blob/main/README.md); verify every SEA/VFS behavior against the exact selected Node build before relying on it. + +The artifact must be self-contained for every executable, runtime, package, library, or support file that Socket CLI or a child tool would otherwise download or install at runtime in order to perform a supported Socket command. Nothing in this category may be fetched, installed, or updated at runtime. This includes the Socket application and production JS dependency graph, Node 27, cdxgen, Synp, Coana CLI, socket-patch, uv, the Python runtime/CLI/modules actually required by supported paths, Blessed and its patched dependency closure, and any toolchain distribution that a Socket-invoked wrapper would otherwise download (including supported Gradle/Maven/sbt distributions). Put loadable JS/data in SEA VFS; package native executables, runtimes and path-dependent files inside the signed artifact and extract them safely only when the OS requires a real path. + +Keep project data resolution distinct: scanning or building a user project can require fetching that project’s own dependencies, plugins, SDK artifacts, or remote source. Those are project inputs, not Socket’s helper installation. However, a Socket-launched tool must not silently download its own executable/runtime or a toolchain distribution. If a project selects a tool version that is not bundled, fail with a clear unsupported-version message; never fall back to downloading it. Allowed runtime network purposes are explicit: Socket API requests; GitHub/project manifest-file retrieval requested by a scan; user-project dependency/plugin/artifact resolution required by the selected analysis; and patch payload data requested by socket-patch for apply/rollback/repair. Forbidden purposes are Socket app updates that replace executable bytes, helper/runtime/toolchain installation, child-process bootstrap, and self-update of embedded tools. Do not define this rule as a ban on executable bytes or particular hosts: an allowed project dependency/plugin/patch may itself contain executable code and may share a CDN/registry host with build payloads. Treat patch blobs as requested project data (not tool acquisition), verify via the patch protocol, and do not permit socket-patch `--update` to replace its bundled executable. Classify the startup npm self-update metadata check separately; it may query metadata but cannot download/install the newer app, and SEA should disable it by default or make it an explicit non-blocking metadata-only option. + +Inventory every direct and transitive executable/runtime launch. Bundle the best compatible binary/runtime variants for each declared target. A tool or feature may be unavailable on one target when no compatible bundled variant works there; record that capability as unavailable, expose a clear command-time diagnostic, and do not attempt a download or incompatible fallback. This does not block publishing that target or other working features. Socket-owned helper acquisition remains forbidden at runtime on every target. Arbitrary project-selected versions outside the target's embedded matrix fail before launching. Project-selected dependency downloads may remain, but wrapper/tool self-installation is forbidden. Do not silently classify an application-managed download as “external.” + +### Target-specific initial capability set + +These are the capabilities to evaluate for the first standalone release, not a requirement that every platform support every feature. Each artifact must include a generated, documented capability manifest listing supported commands/features, exact bundled tool versions and variants, and unavailable features with concise reasons. Select only binary/runtime variants that match the target OS, architecture, ABI/libc, and required loader. A missing or incompatible variant disables only the affected capability on that artifact. It does not block that platform's artifact or unrelated capabilities. Runtime selection must never probe another platform's payload or download a substitute. + +Capability-manifest availability describes the embedded default for that target. Resolve explicit environment-variable/CLI overrides first: a valid user-selected external path may satisfy a capability whose embedded default is unavailable, following the existing override contract. If the override is absent and the embedded capability is unavailable, fail clearly before helper launch. If an explicit override is invalid, fail on that selection and do not fall back to the embedded tool or a download. Core Node/SEA availability is not overridden by a helper path. + +- Core `socket` CLI startup, help/version, config/auth, API-backed scan operations, and built-in command routing. +- `manifest cdxgen`, including cdxgen plus Synp conversion for supported Yarn lock formats. +- Scan/fix reachability through the pinned Coana CLI and `socket patch` through the pinned socket-patch binary. +- Python/Bazel manifest paths and explicit `scan create --uv-members` through embedded Python/uv versions. +- Gradle/Maven/sbt manifest and facts paths for an explicit supported wrapper/tool version matrix, including the Maven extension JAR and Java runtime/toolchain needed by those commands. Project dependencies/plugins remain project inputs; bundled launchers alone do not make arbitrary project builds offline. +- Blessed threat-feed TUI and the repository-patched JS dependency closure. +- Explicitly invoked `socket npm/npx/pnpm/yarn` and `raw-npx` remain user-directed package-manager operations; they are not the mechanism for acquiring hidden Socket helpers. Document their arbitrary package execution/network semantics as a distinct user request. + +Missing or invalid assets for a capability declared embedded-available must fail that target build. At invocation, validate an explicit external override before checking embedded availability; a valid selected override may run even if the manifest says the embedded default is unavailable. Without that override, an unavailable embedded capability fails clearly before helper launch or network bootstrap. An invalid explicit override fails without fallback. None of these capability states may prevent unrelated features or the artifact from building. Never advertise an embedded feature whose payload has not passed target-native validation. + +### Initial command-to-capability map + +This map sets the audit granularity. Discovery may split or combine rows where source behavior proves the same payload closure; it must not collapse distinct ecosystem or mode closures into a single broad “tool works” claim. + +| Command family | Capability branches to inventory | +| --- | --- | +| Core help/config/auth/API scan/audit/package/org/repo commands | SEA core JS/VFS, config and credential handling, Socket API; metadata/self-update lookup tracked separately from executable updates. | +| Scan create/setup and CI | Core API; optional `git.metadata` only when repository/branch is inferred; optional manifest generation, dynamic JVM inference, `uv-members`, Coana reachability and browser opening each have independent capability IDs. | +| Scan reach and fix | Coana by analyzer/ecosystem and platform; Python/uv/JDK/Go/.NET helper closures as required. Fix additionally needs Coana `compute-fixes` plus separate `git.mutation/transport` and any manager install behavior. | +| `manifest cdxgen` | Base cdxgen per ecosystem and mode; Synp Yarn conversion; separate deep, SQLite evidence/include-crypto, .NET/DOSAI, Swift/SourceKitten, Atom/Java and project-tool bootstrap branches. Unsupported modes must be rejected before cdxgen `prepareEnv()` runs. | +| Gradle/Kotlin/Maven/Scala/sbt/auto/dynamic inference | Per-tool bounded distribution/version matrix, Java version, project wrapper handling, compiler/launcher boot closure (including sbt Scala under fresh global base), and Socket emitter scripts/JARs materialized at real paths. Legacy and facts paths are separate call sites to inventory. | +| Bazel manifest | Bazel/Bazelisk exact version plus separate Maven and PyPI emitters; Java and Python shims; workspace toolchain bootstrap disabled; Windows process cancellation (`taskkill`) separately handled. | +| `socket optimize` | Manager-specific query/list, lockfile parse and install/update capabilities for npm, pnpm, Yarn Classic/Berry and Bun; Bun `bun.lockb` fallback; Vlt is detected but unsupported by Optimize. Do not route through Synp. | +| Explicit manager commands and shadows | `npm`, `npx`, `pnpm`, `yarn`, `raw-npm`, `raw-npx`; selected manager/runtime, IPC injection, completion/shadow files and Windows shim behavior. These user-directed package operations are distinct from hidden helper acquisition. | +| Threat feed | API/data request independent of interactive Blessed TUI; TUI is a separate capability with terminal/terminfo/tmux/Windows command integration. | +| `socket patch` | Target-native socket-patch binary, signature and real-path extraction; patch payload API traffic allowed; binary self-update forbidden. | +| Conda manifest conversion, completion, report opening | Conda-to-requirements is in-process JS; completion shell script needs a persistent real path; browser opening is optional OS integration with URL fallback. | + +## Repo facts + +- Root `package.json` is named `socket`; its bins include `bin/cli.js`, `bin/npm-cli.js`, `bin/npx-cli.js`, `bin/pnpm-cli.js`, and `bin/yarn-cli.js`. +- `pnpm build:sea` already exists (`src/sea/build-sea.mts`), but `src/sea/README.md` describes a thin wrapper that downloads `@socketsecurity/cli` on first run. That does not meet this request. Replace/redefine that path or rename the old experiment so `build:sea` unambiguously creates the bundled root `socket` product. +- A pre-existing `.claude/plans/socket-sea-package.md` describes an older “single published package”/node-smol target approach. Treat it as superseded for this work: the current request keeps npm variants and release behavior intact, builds the root `socket` package as a separate Node 27 SEA/VFS artifact, and allows target-specific capability omissions. Do not copy the old single-package or node-smol assumptions into implementation. +- `src/utils/dlx.mts` launches CycloneDX and Synp through package-manager DLX paths. `src/utils/coana-release.mts` downloads a Coana component into a cache. `src/commands/patch/cmd-patch.mts` launches the Rust `socket-patch` executable. `src/commands/scan/generate-uv-member-sboms.mts` invokes `uv`; other manifest paths probe or launch Python. +- `src/constants.mts` pins `COANA_CLI_VERSION`; `INLINED_SOCKET_CLI_CYCLONEDX_CDXGEN_VERSION` is build-time injected. `package.json` has `@cyclonedx/cdxgen` as a dev dependency and `@socketsecurity/socket-patch` as a production dependency. Avoid duplicate unmanaged version sources. +- The archived `/Users/jdalton/projects/socket-cli-native` repo at inspected revision `96d439e1ab786d6500e085560c2dcf115289a22b` provides the signing/release precedent: `.github/workflows/publish-native.yml` builds a source/version-bound artifact on macOS CI with CI-only Apple credentials, `scripts/repo/build/sea/sign.mts` performs Developer ID signing, strict verification and notarization, the workflow hashes/receipts and transfers immutable artifacts, and native target runners execute the transferred bytes before publication. Its eight targets are not automatically the SEA target set: exact Node 27 SEA platform support controls which Socket SEA targets can ship. Existing evidence signs/notarizes macOS artifacts; use the same fleet signing helpers and receipt contract for the SEA product, and do not claim Windows Authenticode unless a separate approved signer path is actually found and added. Preserve credential CI-only handling and clean temporary keychains. Local `codesign --sign -` remains only an ad-hoc development signature, not production signing. + +## Decisions required in a feasibility spike + +The remaining decisions are organized as a bounded gap-closure program below. These are discovery/prototype tasks; they do not require blocking product design for every target at once. Close product-level gaps (production-feasible selected Node 27 build, SEA behavior, artifact identity/integrity and a valid release path) before implementation. Resolve helper feasibility per capability and target; an unavailable helper marks only its feature unavailable on that artifact. + +1. **Node 27 version:** as of 2026-11-08 the latest Node 27 release is Alpha. Pin the latest available 27.x release at each build by exact version/commit and verified checksum/signature; do not fetch a floating `latest` artifact. If the selected build is prerelease, label the resulting artifact pre-release/experimental and validate its actual API/ABI rather than waiting for stable or silently using another major. If no Node 27 build is available, pause artifact assembly until one is published. +2. **SEA/VFS contract:** use the exact Node 27 alpha source/docs for discovery, then verify every API and behavior against the exact latest Node 27 build selected for the artifact (stable or prerelease). Node 27 provides `node --build-sea ` with `main`, `mainFormat`, `executable`, `output`, `useVfs`, `vfsArchive`, `assets`, `execArgv`, and `execArgvExtension`; the VFS module loader serves CommonJS/ESM modules from the mount. `vfsArchive` embeds a ZIP as-is, inflates files on access, and Node 27 alpha documents Worker-thread remounting from the archive; plain `assets` mode does not offer that worker remount. VFS is Stability 1 / early development, SEA is Stability 1.1 / active development, and `node:vfs` itself is experimental. `useVfs` cannot be combined with `useSnapshot` or `useCodeCache`. Node docs conflict on native addons: SEA docs say `.node` files must be materialized for `process.dlopen()`, while the Node 27 VFS loader/source/tests show automatic loading through a private real image. Do not bake either assumption in: spike native addons and their dependent shared libraries on the exact target release; native executables and tools that demand a real path still require signed payload materialization. `vfsArchive` and `assets` are mutually exclusive; archive entries are read through `fs`, not `sea.getAsset()`. Validate the full ZIP yourself because the builder's archive validation may be shallow. Node VFS is not a security sandbox, and a source-level Node 27 implementation discrepancy suggests the in-memory ZIP VFS may be writable despite read-only documentation; test that property and do not rely on it as an integrity boundary. The mount path is runtime-selected, and in VFS mode the main script's `__dirname`/`import.meta.dirname` is the VFS root. CJS resolution can still consult `NODE_PATH` and global folders; set `--no-global-search-paths` and package `execArgvExtension: "none"` unless compatibility evidence requires a documented alternative. Prove helper entrypoint and child-process behavior. Verify Node 27 compatibility despite cdxgen declaring engines through Node 25. Use `--build-sea`, not the older `--experimental-sea-config`, unless the pinned docs establish a necessary exception. Record evidence and limits in the reviewed ADR. +3. **OS/architecture matrix:** evaluate macOS arm64/x64, Linux glibc x64/arm64, Windows x64/arm64 against Node SEA and each helper's availability, launch semantics and signing support. Reviewed Node 27 docs/source describe macOS x64 SEA as unsupported/experimental and skip SEA tests there; treat darwin-x64 as provisional and publish only if the exact selected Node 27 runtime passes a release-grade SEA build, signature injection, and smoke validation. Alpine/musl is excluded by the reviewed SEA docs; do not promise a musl target unless a supported Node SEA build is demonstrated. The current upstream `BUILDING.md` states Linux glibc minimums of 2.34/kernel 5.14, macOS 13.5+, Windows 10/Server 2016+; confirm these against the exact selected Node 27 binary and expose the minimums in artifact metadata. Windows ARM64 is Tier 2 in that source and needs native runner coverage. These are core-runtime/target artifact constraints, unlike a missing helper, which disables only its own capability. Publish a per-artifact capability manifest. Discovery found a Coana defect: cached Coana 15.14.0 `tree-sitter-ruby` under `analyzers/bundle/node_modules/tree-sitter-ruby/prebuilds/linux-arm64/` is x86-64 ELF (same build ID as x64), while adjacent tree-sitter core is AArch64. Either replace it with a working AArch64 payload or mark Coana Ruby analysis unavailable on Linux ARM64. Require ELF/Mach-O/PE header and architecture checks for every native payload. +4. **Signing and release (decision fixed from archived native repo):** adapt `/Users/jdalton/projects/socket-cli-native` CI-only signing path and receipt model. Reuse `fleet/apple-sign.mts`, `fleet/apple-notarize.mts`, strict Apple signature verification and temporary-keychain helpers through tracked workflow code; credentials stay in CI. Preserve and verify upstream signatures on inner native payloads where present; only apply ad-hoc signing to a helper when the existing install/launch baseline or target runtime requires it. Then hash/embed final payload bytes, build/inject the outer SEA, Developer ID sign it, and submit a ZIP containing the bare Mach-O for notarization (Apple does not staple a ticket to a bare CLI Mach-O). Verify expected identity and notarization on downloaded/quarantined bytes, then compute final hashes and provenance. Preserve an accepted notary submission receipt and notary log where available; do not infer that an outer SEA notarization fully evaluated opaque VFS members. The existing native workflow's cross-target jobs qualify transferred artifacts on native runners and its coordinator binds artifact hashes to a source SHA/run. Its inspected workflow checks the requested tag and its build script compares the selected source SHA, but it does not declare an explicit protected GitHub Environment; the SEA workflow must add that protection and reject unauthorized source SHAs before exposing signing credentials. Create a separate immutable SEA revision/tag namespace; bind each revision to root Socket version, exact source SHA, Node/helper manifest and target. Do not make Authenticode a prerequisite because the established native release does not provide it; only claim such signing if a distinct approved Windows signer is introduced. Target omission follows Node SEA support and actual native qualification. This is no longer a provider discovery gate; remaining work is to implement the SEA-specific jobs/receipts and verify the exact produced bytes. The default is to preserve the bytes/signatures and launch behavior of tools already acquired and executed through the current npmx/DLX/package-manager paths. Ad-hoc inner signing or app-owned quarantine cleanup is conditional on target evidence, not a blanket rewrite. +5. **Python scope:** identify which exact Python CLI(s)/commands are intended. Choose a redistributable runtime per platform/architecture and include required standard library, extension modules, shared libraries, modules, and licenses. Python is not a portable lone executable; test loader paths, certificates, symlinks, and extension ABI. +6. **Coana redistribution:** verify whether every supported Coana component can be staged at build time and redistributed. Preserve size/SHA-256 validation and licensing. Runtime fetch is forbidden; `--reach-version` accepts only a version embedded in the signed artifact and otherwise fails before execution. +7. **Release distribution:** current `publish-npm.yml` creates and publishes immutable `vX.Y.Z` GitHub releases without binary assets; live releases have empty asset sets, and active tag rules prevent updating/deleting `v*`. The existing artifact transfer action only accepts exactly three npm tarballs. Prefer a separate SEA release/tag namespace and monotonically increasing standalone revision so Node/helper security rebuilds do not require an npm release/version change; map every artifact to the exact Socket version/source SHA. If sharing a GitHub release is chosen, add a coordinator that stages every declared SEA target plus npm tarballs before the single draft publication. Define target allowlist/completeness, collisions, retries, partial failure recovery, provenance source SHA, and `v1.x latest=false`. Do not attach SEA assets after publishing an immutable npm release; changed bytes require a new identity. +8. **Artifact budget:** agree on max per-target compressed/uncompressed size and cold-start extraction cost after inventory reports real numbers. + +9. **Capability taxonomy and UI:** map each user-visible command/flag to one or more capabilities and required payload IDs. Decide whether partial `manifest cdxgen` coverage is presented as ecosystem-level sub-capabilities or a single cdxgen capability, and make unsupported reasons discoverable through help/version/diagnostics. The CLI must not imply broad cdxgen, Coana, JVM, Bazel, or Python support when only a subset of the relevant native/tool closure is present. +10. **Tool download closure:** trace every child tool's own provisioning behavior, including cdxgen Atom, nvm Node, SDKMAN Java/build tools, rbenv Ruby and `gem install bundler`; Coana analyzer fetches, `uv tool run --from installer`, `uv python install`, pyenv/Python and Go toolchain provisioning; Bazelisk Bazel; Maven/Gradle/sbt wrappers; Python package managers; container engines; and package-manager shadows. cdxgen calls `prepareEnv()` from its CLI entrypoint even though Socket defaults lifecycle to `pre-build`; verify and remove every reachable helper bootstrap. Its Maven resolver invokes a project `mvnw wrapper:wrapper` before selecting `MVN_CMD`/`MAVEN_CMD`. The sbt facts path creates a fresh global base and sbt provisions Scala under its boot directory, so a warm `~/.sbt` is not proof of closure. Coana Python also runs external `which` (replace with direct executable resolution or classify that sub-capability unavailable), installs helper packages with `uv tool run --from installer`, creates a fresh Mambalade venv, and has pip/wheel fallback paths; bundle the helper wheels/installer or disable that sub-capability. Also trace rustup-mediated Cargo, Gradle toolchain resolvers and Bazel repository rules for automatic runtime acquisition. For each launch choose one: ship the selected tool/runtime distribution; use a direct API or in-process library; honor the user's explicit external override; or mark only the affected capability unavailable. For Coana Go analysis, project Go modules may resolve as project inputs, but Go toolchain acquisition is prohibited; select a bundled Go version and set `GOTOOLCHAIN=local`, or mark that analyzer capability unavailable. Disable automatic pyenv/uv Python acquisition in embedded-default mode. Project dependency/plugin downloads explicitly requested by analysis remain a separate allowed category. +11. **Override and inherited environment contract:** the current plan preserves explicit tool paths; finalize precedence and determine which ambient `PATH`, `JAVA_HOME`, Python, npm config, proxy/certificate and wrapper variables intentionally override embedded defaults. A valid explicit override must select the external tool; unset defaults use embedded bytes; invalid overrides fail without fallback. Define which child environment variables are forwarded and how to prevent accidental shadowing. Audit the copied `@socketsecurity/registry` spawn semantics: its `options.env` is merged over ambient `process.env`, so a nominal allowlist may still leak omitted variables. SEA-owned spawn paths need true replace-env semantics; inspect `coana-release.mts` and socket-patch which currently spread ambient env. Test with sentinel secrets and loader-injection variables (`NODE_OPTIONS`, `JAVA_TOOL_OPTIONS`, `PYTHONPATH`) to prove only intentionally forwarded values reach children. +12. **Package-manager operations versus helper resolution (decision fixed):** preserve today's `npm`, `npx`/`npmx`, `pnpm`, `yarn`, raw manager and `socket optimize` semantics to the extent those flows work with the target's bundled, platform-compatible Node/npmx runtime and selected package manager. Bundle the best compatible target variants of executable/runtime/package-manager components that these Socket command paths currently require; do not introduce a new restriction merely because a component is normally obtained through the existing `npmx`/`dlx` mechanism. If a selected variant cannot be embedded or does not work on one target, mark only the affected command capability unavailable there with a clear diagnostic. Do not let a supported command bootstrap Socket-owned tools at runtime. Arbitrary user-requested package resolution remains the same user-directed network/package behavior as today. `socket optimize` detects npm, pnpm, Yarn Classic/Berry and Bun; Vlt is detected but Optimize rejects it because it lacks needed override behavior. Bun may parse `bun.lockb`; Optimize runs the chosen manager to modify/install the project lockfile. Honor explicit manager/runtime paths according to the override policy. +13. **Native dependency and ABI closure:** define checks for executable format/architecture, libc/loader, Node ABI/N-API, shared-library dependencies, macOS deployment target, Windows runtime DLLs, executable modes, symlinks and codesigning. Fail only the affected capability if validation fails. Generate the matrix from actual payload headers and target-native smoke results, not package metadata claims. +14. **Redistribution and notices:** establish license provenance for Coana CLI/analyzers and its wheels/native payloads, cdxgen's embedded JARs (including Soot/JNA), Java/Python distributions, plugin binaries and all helper closures. Missing license terms can make that payload unavailable; preserve other capabilities. Define required notices/source offers and SBOM component granularity. +15. **Runtime update lifecycle:** set ownership and cadence for updating Node 27 and every embedded helper; define CVE response SLA, support branches, rebuild reproducibility, and behavior for stale artifact detection. Embedded tools do not self-update; updates ship as a newly signed standalone artifact. +16. **Artifact layout/install/uninstall:** settle executable naming, archive/package format per OS, install paths, extracted resource cache location and permissions, multi-version coexistence, upgrade/rollback, uninstall cleanup, and package form for the archived ZIP-based notary submission (a bare Mach-O cannot be stapled). Keep user cache and project files outside app-owned cleanup. +17. **Credential/API flow:** verify Socket auth/config lookup and token refresh from SEA, redact secrets in child env/logs, support proxy/custom CA behavior, and ensure a bundled helper cannot inherit credentials it does not need. Define API-backed core CLI behavior as distinct from helper download/network policy. +18. **Supported command boundary:** inventory the full cdxgen CLI options actually passed by Socket and all reachable modes after merging CLI flags, project `.cdxgenrc`/`.cdxgen.json/.yml/.yaml`, environment and defaults. Socket's `pre-build` lifecycle does not prove `prepareEnv()` cannot bootstrap; deep mode can invoke Cargo independently of `install-deps`. Decide which modes need OS package managers, container runtimes, .NET 10, Swift/Xcode, Ruby/Composer/Go/Cargo, SDKMAN/nvm/rbenv or remote services. Any mode not represented by a working target payload must be rejected before preparation starts, not after it launches a downloader. +19. **Release artifact identity and source authorization:** prefer separate SEA version/tag identity with an independently incremented standalone revision mapped to Socket version and source SHA. Specify target allowlist/omissions, capability-manifest inclusion, checksum/provenance verifier, draft retry rules and no-replacement policy. The signing workflow must validate that the selected source SHA is authorized by protected release branch/tag policy; workflow dispatch SHA and checked-out source SHA are distinct. Keep trusted workflow code on a protected ref, reject unauthorized source commits before exposing signing credentials, and attest final downloadable subjects with workflow identity/run, source SHA, Node/helper input-manifest digest, capability manifest and SBOM. Test substitution of wrong source, target, run, manifest and final bytes. A capability change after signing requires rebuilding/re-signing/re-attesting the artifact, not editing a sidecar. Publishing a subset of targets must accurately disclose omissions. + +#### Provisional Node SEA target baseline + +These are discovery statuses from Node commit [`fe34c8e`](https://github.com/nodejs/node/tree/fe34c8eae5514797d6dc21a4bdec39c9913d9322), not release promises. Recheck against the exact selected Node 27 binaries/docs before building. Upstream [`SEA platform support`](https://github.com/nodejs/node/blob/fe34c8eae5514797d6dc21a4bdec39c9913d9322/doc/api/single-executable-applications.md#platform-support), [`SEA test exclusions`](https://github.com/nodejs/node/blob/fe34c8eae5514797d6dc21a4bdec39c9913d9322/test/sea/sea.status) and [`platform requirements`](https://github.com/nodejs/node/blob/fe34c8eae5514797d6dc21a4bdec39c9913d9322/BUILDING.md#platform-list) produce this initial core matrix: + +| Artifact target | Provisional baseline | Core SEA disposition | +| --- | --- | --- | +| `darwin-arm64` | macOS 13.5+, Node Tier 1 | Candidate; validate target-native VFS, injection, signing and first-launch behavior. | +| `darwin-x64` | macOS 13.5+, Node Experimental | Provisional/unavailable until the selected Node build demonstrably supports SEA; current source says unsupported and skips SEA tests. | +| `linux-x64-glibc` | kernel 5.14+, glibc 2.34+ | Candidate; validate Node binary/runtime and artifact on minimum baseline. | +| `linux-arm64-glibc` | kernel 5.14+, glibc 2.34+ | Candidate; validate target-native Node/SEA build and artifact. | +| `win32-x64` | Windows 10/Server 2016+, Node Tier 1 | Candidate; validate injection, Authenticode and native process behavior. | +| `win32-arm64` | Windows 10+, Node Tier 2 | Candidate only with native runner coverage and verified signing route. | +| `linux-x64-musl` / `linux-arm64-musl` | Distinct libc targets | No artifact until a supported Node SEA build and complete helper closure are demonstrated; Alpine is excluded by current docs. | + +The SEA injector recognizes target ELF/PE/Mach-O bytes rather than inferring them from the builder host. Pin builder and target Node to the exact same version and validate the upstream build configuration includes SEA/LIEF support. For custom builds verify shared Node/ICU/OpenSSL configuration and record the exact binary hashes. Node support for a core target is a product-level artifact gate; helper failures remain capability-local. + +#### Initial helper capability findings + +These are candidate dispositions from static payload/header review only; “candidate” is not a support claim until native-host integration and signing tests pass. `darwin-x64` rows are conditional on core SEA becoming supported. Windows Coana limits are independent of other Windows features. + +| Capability | `darwin-arm64` | `darwin-x64` | `linux-x64-glibc` | `linux-arm64-glibc` | `win32-x64/arm64` | +| --- | --- | --- | --- | --- | --- | +| Core/API commands | Candidate | Core SEA unavailable/provisional | Candidate | Candidate | Candidate | +| `socket patch` apply/rollback | Matching Mach-O candidate | Matching Mach-O payload, core SEA gate | Static PIE candidate | glibc-linked AArch64 candidate | Matching PE candidates | +| Coana reach JS/Go/PHP/Rust | Matching payload candidates | Matching payload, core SEA gate | Matching payload candidates | Matching payload candidates | Unavailable: Coana rejects Windows | +| Coana Ruby reach | Matching addon header candidate | Matching addon, core SEA gate | Matching addon candidate | Unavailable until mispacked x86-64 addon is replaced | Unavailable: Coana rejects Windows | +| Coana fix computation | Candidate, separate smoke needed | Candidate, core SEA gate | Candidate, separate smoke needed | Candidate; Ruby reach defect does not imply all fixes fail | Unavailable: explicit Coana Windows guard | +| cdxgen base + Synp | Candidate | Conditional on core SEA | Candidate | Candidate | Candidate | +| cdxgen .NET deep/DOSAI | Requires bundled .NET 10 shared runtime | Same, and core SEA gate | Same | Same | Same | +| cdxgen evidence/include-crypto | SQLite N-API closure pending | Same, and core SEA gate | Same | Same | Same | +| cdxgen Swift semantic/evidence | SourceKitten mode and runtime closure pending | Same, and core SEA gate | Swift runtime libraries missing | Swift runtime libraries missing | Unavailable with current plugin closure | +| `scan create --uv-members` | Bundled uv 0.12.15 staged and archive-checksum/version-verified; integrated SEA build verified | Core SEA gate | Bundled uv 0.12.15 staged and archive-checksum/version-verified; host-native SEA qualification pending | Bundled uv 0.12.15 staged and archive-checksum/version-verified; host-native SEA qualification pending | Bundled uv 0.12.15 staged and archive-checksum/version-verified; host-native SEA qualification pending | +| Python/Bazel/JVM | Separate runtime/tool/version closure pending | Core SEA gate | Separate runtime/tool/version closure pending | Separate runtime/tool/version closure pending | Separate runtime/tool/version closure pending | + +The Linux ARM64 `socket-patch` package requires glibc's `/lib/ld-linux-aarch64.so.1`; the npm package name does not imply musl support. Coana's SPAR/Goana files being statically linked or named `musl` does not make its tree-sitter/Python closure musl-compatible. The Linux-musl cdxgen packages also need staging fixes for Trivy executable mode and DOSAI resolver names; those defects do not affect glibc cdxgen rows. Promote candidate rows only after checking final signed hashes, actual slices/headers, loader and dynamic dependencies, minimum OS/libc, modes, and a representative command for the exact feature trigger. + +## Source-derived helper and external-tool inventory + +This inventory is based on production source call sites, not package-name guesses. It is a starting verified list; implementation must repeat the scan after changes and classify every process spawn that can affect a user command. + +| Tool/path | Exact source behavior | Current version source | Standalone packaging direction | Boundary / caveat | +| --- | --- | --- | --- | --- | +| `@cyclonedx/cdxgen` | `src/utils/dlx.mts` `spawnCdxgenDlx`; called by `src/commands/manifest/run-cdxgen.mts` for `manifest cdxgen`. | Root devDependency `12.1.2`, inlined from package metadata by `.config/rollup.base.config.mjs`. | Bundle exact JS package closure and target-compatible native/data assets; invoke directly through resolver. | cdxgen declares Node engines `^20, ^22, ^24 or ^25`, excluding Node 27. The CLI calls `prepareEnv()` before `createBom`; Socket's `pre-build` lifecycle is not itself proof that no tool bootstrap runs. It can use nvm Node, SDKMAN Java/build tools, rbenv Ruby and `gem install bundler`; Maven resolution probes a project `mvnw wrapper:wrapper`. Remove/disable those acquisition paths for embedded operation or make only the corresponding ecosystems unavailable. Inventory every cdxgen launch and license. Optional plugin contents vary by target: the x64 plugin package omits osquery (the ARM64 package contains a universal x64/arm64 osquery candidate, relevant only if x64 SEA becomes viable), Linux musl has missing tools and resolver naming/permission gaps, dosai requires the .NET 10 shared runtime on every packaged OS/architecture variant, and Atom brings Java 21+ with a large native/JAR closure. cdxgen's Ruby parser package contains Linux x64 native extensions, not a portable Ruby runtime. SQLite is loaded for evidence/include-crypto paths and its package currently has no shipped `.node` binary, so gate those options until the target N-API closure is built and verified. SourceKitten payloads are non-executable mode 0644 and their Linux binaries require Swift/Foundation libraries absent from the plugin package; gate Swift semantic/evidence support until the complete runtime closure works. Define support at ecosystem/feature granularity and clearly disable only the missing ecosystem features on affected targets; npm package presence alone does not prove ecosystem feature parity. | +| `synp` | `spawnSynpDlx`; only production call is `manifest run-cdxgen.mts`. For Node ecosystem manifests with a Yarn lock and no package-lock, runs `synp --source-file ./yarn.lock`; on success tells cdxgen to analyze as npm. Current cleanup uses an exit callback and an ownership boolean; standalone implementation must use an owned temporary path or safe ownership check, and cleanup on success, failure, cancellation and spawn errors without deleting a user-created lockfile. It is not used by `socket optimize`. | Root devDependency `1.9.14`, inlined from package metadata. | Bundle exact JS package closure; preserve exact args, project cwd, success/failure fallback and temporary-file cleanup. | Synp API `yarnToNpm(packageDir)` reads `yarn.lock`, `package.json`, and existing `node_modules`; it does not install/download. Initial support should be Yarn Classic only. Gate Yarn Berry (the parser is `@yarnpkg/lockfile` v1), workspace conversion (`withWorkspace` is experimental), and incomplete install trees because Synp infers actual installed packages. Cleanup must use ownership-digest/identity tokens and a `finally` around cdxgen Worker completion. | +| `@coana-tech/cli` (Coana) | `spawnCoana` from scan reachability and fix flows; custom Socket API component resolution/cache, not generic npx. | `COANA_CLI_VERSION` in `src/constants.mts` (`15.14.0` at research time); optional command override. | Stage the pinned default component plus every analyzer/helper subcomponent it would otherwise acquire into the signed target payload; remove runtime acquisition/fallback for embedded defaults. Honor explicit supported local-path or version selectors as external overrides; never fetch a missing override implicitly. | Coana 15.14.0 source calls `fetchReleaseComponents(...['analyzers'])` when `REPOS_PATH` is absent; transitive analyzer payloads are therefore in scope. It also runs external `which`, can invoke `uv tool run --from installer==0.7.0`, creates a fresh Mambalade venv and can install pip/wheel, provisions Python through uv/pyenv, and can fetch a Go toolchain even in dependency-offline mode. Resolve `COANA_UV_EXECUTABLE`, `COANA_POETRY_EXECUTABLE`, and `REPOS_PATH` explicitly; disabling project dependency downloads does not disable these helper/runtime bootstraps. Its source rejects Windows for both reachability analysis and fix computation/upgrading, so both Coana-backed capabilities are unavailable there unless another supported backend is bundled. The cached Linux ARM64 Ruby analyzer contains x86-64 ELF instead of AArch64; either repair it or disable the affected Ruby analysis capability on that target. Coana's cached release lacks LICENSE files; verify redistribution rights before claiming Coana support. | +| `@socketsecurity/socket-patch` | `src/commands/patch/cmd-patch.mts` resolves package binary and spawns it. | Root production dependency `2.0.0`. | Include correct Rust executable for each supported target; invoke through resolver without `node_modules/.bin`. | Verify package artifact contents, native ABI/target coverage, license and platform signing. | +| Git | Scan/CI infer repository, remote, branch and commit data; fix flows perform checkout/add/commit/push/reset/clean/diff; see `src/utils/git.mts`, `src/commands/scan/cmd-scan-create.mts`, `src/commands/ci/handle-ci.mts`, `src/commands/fix/coana-fix.mts`. | Currently resolved from host PATH; project/user installation. | Either bundle pinned per-target Git plus required transport/credential integration, replace bounded metadata calls in-process, or mark only Git-dependent sub-capabilities unavailable. | Separate `git.metadata` from `git.mutation/transport`; SSH, credential helpers, hooks and remote transports are separate closure items. API-backed scan still works when optional Git inference is unavailable. | +| `uv` | `src/commands/scan/generate-uv-member-sboms.mts` spawns `uv export` for explicit `--uv-members`. | Embedded build pin `0.12.15` in `src/sea/uv-pins.json`; explicit `SOCKET_CLI_UV_PATH` override remains supported. | Build staging fetches target-specific GitHub release archives, verifies pinned SHA-256, safely extracts only the executable, validates architecture/version, includes MIT/Apache texts and digest metadata in VFS, and extracts to an app-owned temporary directory at runtime when no override is set. Preserves `export --frozen --offline --no-python-downloads`; no fallback to PATH/network. | Integrated darwin-arm64 SEA build succeeded. Linux glibc and Windows pins are staged by the same helper but need native-host SEA validation. Rust transitive-crate license inventory is unresolved; uv capability is technically available in the prototype but production redistribution remains unqualified. Bundling uv does not bundle Python or all Python scanning support. | +| Bazelisk/Bazel | Bazel manifest discovery prefers Bazelisk, which honors workspace `.bazelversion`; that can bootstrap/download a Bazel binary. | User PATH/workspace pin today; repo CI currently pins Bazelisk 1.26.0 and Bazel 7.4.1. | Embed approved Bazelisk and a bounded set of Bazel versions; route Bazelisk to embedded versions only, or resolve supported version and fail before network bootstrap. | Required Bazel path cannot fall back to downloading Bazel. Workspace repository/rule downloads are project-input resolution and need separate network policy. | +| Java/JDK | Bazel JVM extraction checks `JAVA_HOME`/PATH; Maven, Gradle and sbt launchers need a Java runtime, and Maven wrapper's downloader may use javac/java. | User environment/project toolchain today. | Bundle the best compatible per-target Java runtime/toolchain; if unavailable, mark only affected JVM/Bazel capabilities unavailable. | Validate native library needs, licensing, architecture and any toolchain minimums; tests must run with no host Java. | +| cdxgen optional/native packages | Locked `@cyclonedx/cdxgen` closure includes optional `@appthreat/atom`, `atom-parsetools`, `@appthreat/sqlite3`, `@cdxgen/cdxgen-plugins-bin` and OS-specific plugin-bin packages. | cdxgen `12.1.2`, optional packages pinned by its package manifest/lockfile. | Generate per-target closure from lockfile/package metadata, including native addons, plugin executables, and required Node/Java runtimes; do not use only the developer host's installed optional packages. Preserve executable modes and verify cdxgen's platform-specific binary resolver names against shipped files. | cdxgen declares Node engines `^20, ^22, ^24 or ^25`, excluding Node 27. `@appthreat/atom` invokes Node via `NODE_CMD`/PATH and requires Java 21+; pre-generation may call `nvm install`, which must be disabled/replaced or explicitly fail when the requested project Node version is not locally supported. Native addon loading differs between SEA/VFS docs and source. Audit nested notices: the Atom closure includes LGPL-licensed Soot and JNA's dual license, and Coana's cached release has no license file. | +| Python / `python3` | Python probing/shim in Bazel manifest discovery (`bazel-python-shim.mts`); Python also appears in cdxgen ecosystem subprocesses and test scripts. | Currently resolved from the environment or helper-specific dependency; SEA must resolve Socket-owned Python work to the embedded runtime. | Bundle the pinned Python runtime and modules needed by capabilities declared available; if unavailable on a target, report affected Python-backed features as unavailable without blocking other features or downloading Python at runtime. | Tests/scripts using Python are not production runtime evidence; separate test tooling from shipped behavior. | +| Gradle, Maven, sbt and project wrappers | Manifest conversion/facts commands invoke wrappers or user-selected PATH tools; Maven wrapper may download Maven, Gradle/sbt projects may download their pinned distributions/dependencies. | Project wrapper/config or user PATH. | Bundle the supported tool distributions and prevent wrappers from downloading another distribution at runtime. Keep project dependencies/plugins as project inputs. Enumerate supported project-declared versions; reject unbundled versions before execution. | The standalone artifact supports an explicit matrix of bundled tool versions; arbitrary wrapper-selected versions are rejected before wrapper startup, which prevents the wrapper from downloading its own distribution. Sbt provisions Scala under its fresh `sbt.global.base/boot`; bundle that selected compiler/launcher closure or mark the affected sbt facts capability unavailable. Do not rely on a warm user `~/.sbt` cache. | +| Blessed TUI (`blessed`, `blessed-contrib`) | Dynamic requires in `src/commands/threat-feed/output-threat-feed.mts`. | Root pinned dependencies (`blessed` `0.1.81`, `blessed-contrib` `4.11.0`) with repo patches. | Include patched JS dependency closure in SEA/VFS; preserve dynamic require paths. | No native Blessed executable identified. Validate terminfo and pseudo-terminal behavior. | +| User package-manager operations | `socket npm/npx/pnpm/yarn`, `raw-npm`/`raw-npx`, and shadow launchers wrap the user-selected manager. `socket optimize` invokes manager-specific queries and installation in `src/commands/optimize/ls-by-agent.mts`/`src/utils/agent.mts`; it applies Socket overrides and does not call Synp or generic DLX. | User-installed manager and user-supplied package spec. Optimize supports npm, pnpm, Yarn Classic/Berry and Bun; Bun may parse `bun.lockb`; vlt is detected but rejected by Optimize. | Treat these as user-directed operations: use the explicitly selected/installed package manager, and fail clearly if the required manager/runtime is absent rather than downloading it. A separately bundled plain Node runtime may serve as the JS execution host only where invocation semantics require it; never execute manager scripts by treating the SEA executable as `node`. Do not use these operations as hidden fallbacks to acquire Socket-owned helpers. | Arbitrary user-selected packages cannot be prebundled; this exception does not permit helper/runtime/toolchain downloads. Corepack/packageManager resolution must not bootstrap a missing manager implicitly. | +| OS service integrations | Opening generated report URL (`open`, PowerShell, `xdg-open`), Blessed's optional tmux/version and terminal commands, Windows Bazel cancellation via `taskkill`, completion script sourcing, and shadow launcher shim files. | Host OS/service PATH and copied `dist` assets. | Split optional service integrations from core commands. Use browser URL fallback if opener unavailable; package completion/shadow scripts as persistent real-path assets or private SEA dispatch; invoke only documented OS services and report unavailable optional integrations cleanly. | These are not Socket helper downloads, but must be classified and tested. Threat-feed API JSON remains available independently of interactive TUI. | +| `dlxBinary` helper | `src/utils/dlx-binary.mts` implements URL binary caching, but current production-source search found no caller. | No active call-site pin. | Do not bundle it as a tool until a real production caller is found. | Re-run import/call graph scan during implementation. | + +### cdxgen source audit and first SEA capability boundary (2026-11-09) + +The installed root devDependency is `@cyclonedx/cdxgen@12.1.2`, Apache-2.0, lock integrity `sha512-kOyK3kdDhZDtRAhJ7/GpOPu6PWdc6622Jc9DPNHiua67m3hvEisUgptM7ssyOAdiycXmG9rDpy8t13E4doh/9A==`. Its CLI entry is `bin/cdxgen.js`; its ESM API is `lib/cli/index.js`. The package declares Node engines `^20 || ^22 || ^24 || ^25`, so Node 27 needs explicit qualification. Package-relative layout matters: helpers derive package root from `import.meta.url`, and eager data reads cover licenses/SPDX, ecosystem/framework inventories, aliases, OS data, crypto mappings, and schemas. Preserve the package tree and dynamic import paths in VFS. + +Do not invoke the general CLI for the first standalone capability. `bin/cdxgen.js` calls `prepareEnv()` before BOM generation even with `--no-install-deps`; that can install Node via NVM, and Rush has an unguarded install path. Broad API imports also probe the host by running Linux `ldd` and `pnpm root -g`. Generic `createBom()` performs container detection before ecosystem selection. The first candidate is direct `createNodejsBom()` with explicit npm project type, an existing `package-lock.json`, no dependency install, no Babel, no recursion, and no versioned runtime. Disable/suppress plugin import-time probes, reject unknown or widening `CDXGEN_*`/cwd config, and reject deep/evidence/crypto/formulation/server/protobuf/archive/container/Rush modes before cdxgen code can bootstrap anything. This remains unavailable until the API adapter and exact VFS closure pass a target-native smoke with helper process/network acquisition instrumented to fail. + +The npm-only boundary excludes substantial closures: Atom/Java plus JARs and Java/PHP/Ruby/Scala launchers; Python `piptree.js` plus interpreter/pip modules; SQLite native addon for evidence; SourceKitten/Swift runtime; Trivy/osquery/dosai native plugins; SDKMAN/NVM/rbenv/bundler and project tool wrappers. `uv sync` can install a managed Python. These are separate capabilities and remain unavailable until their full helper/runtime/data closure is bundled and qualified. Native plugin package signature files are not signature verification. cdxgen's Linux musl package also has missing tools/resolver and permission issues; dosai needs .NET 10; Ruby parser extensions are Linux x64-specific. Per-platform capability records must name the missing closure rather than infer support from npm package presence. + +The VFS Worker proof does not establish cdxgen compatibility: a Worker shares PID/cwd, and cdxgen or dependencies may launch native/JS children directly. Audit child execution for the npm mode, reject unexpected launches, and preserve explicit argv/env/stdin/out/error/exit semantics. Keep `runCdxgenDlx()` guarded for all unsupported modes so future callers cannot fall through to npm/pnpm/yarn DLX. + +Synp 1.9.14 is MIT with lock SRI `sha512-0e4u7KtrCrMqvuXvDN4nnHSEQbPlONtJuoolRWzut0PfuT2mEOvIFnYFHEpn5YPIOv7S5Ubher0b04jmYRQOzQ==`. Its API returns serialized package-lock content; the CLI adds validation/write/exit behavior. The local adapter currently uses exclusive creation and digest plus device/inode ownership checks. Preserve current failure fallback or surface a clear standalone diagnostic; never use broad cleanup that could remove a user lockfile. Its conversion depends on the installed `node_modules` graph and may omit optional packages absent on the machine that prepared it, so fixtures must establish accuracy and the user-project network policy must remain separate. + +### Existing override and executable-selection policy + +The embedded payload is the default, not a forced choice when the user has explicitly configured a tool path. For each existing environment-variable or CLI override, preserve its intended precedence: when unset, resolve the pinned embedded tool; when set to a valid path/version, use that explicit selection instead of the embedded tool. Do not reinterpret an explicit override as permission for Socket to download or install a missing tool. If the selected path is missing, invalid, incompatible, or cannot be executed, fail clearly without silently falling back to the embedded version or a download. Record override-specific compatibility and network behavior in the ADR and user docs. This policy applies to Socket-owned helper selection; user-project build-tool and interpreter selection remains subject to the frozen supported matrix and no-bootstrap rule. + +| Existing selector | Current source | SEA policy | +| --- | --- | --- | +| `SOCKET_CLI_COANA_LOCAL_PATH` | `src/utils/coana-release.mts` | When unset, use the embedded pinned Coana component. When set, execute the configured local path (including a deliberate wrapper path) instead of the embedded component, after validating it exists and is executable for the target. Never download as fallback. Document that the override selects externally managed code and may have its own network behavior. | +| `COANA_UV_EXECUTABLE`, `COANA_POETRY_EXECUTABLE` | Coana 15.14.0 CLI environment handling | When unset, resolve the bundled compatible executable/runtime; when explicitly set, honor the selected external path and validate it. Never use uv/Poetry to install a missing Socket helper. | +| `REPOS_PATH` | Coana analyzer repository path | When set, honor the explicit local analyzer path; when unset, use bundled analyzer payloads. Never fetch analyzer components in standalone mode, including when the supplied path is invalid. | +| `GO`, `GOTOOLCHAIN` and Go-related PATH | Coana Go analyzer | Resolve the bundled Go version when not explicitly overridden; prevent Go's auto-toolchain acquisition (`GOTOOLCHAIN=local`). Project Go modules remain project inputs. An unavailable Go version disables only Go analysis. | +| `--reach-version`, `--fix-version` | `src/commands/scan` and `src/commands/fix/cmd-fix.mts` | `SOCKET_CLI_COANA_LOCAL_PATH` has highest precedence and, when set, selects that validated external executable. Otherwise default to the embedded version. A version option may select only a version already embedded for the target or already available through a user-managed local installation supported by the existing CLI contract; it must never trigger Socket-managed fetch/install. An unavailable version fails clearly before execution. Apply the same precedence and validation to both flags; no fallback after an explicit invalid selection. | +| `--bazel` | `src/commands/manifest/bazel/bazel-bin-detect.mts` | Honor an explicit executable path instead of the embedded Bazel selection, including an intentional user wrapper, subject to documented invocation semantics. Do not mistake explicit selection for permission to let Bazelisk bootstrap an unbundled Bazel; reject or disable that bootstrap before execution. | +| Maven/Gradle/sbt `bin` and project wrappers | `src/commands/manifest/scripts/build-tool.mts` | Honor explicit project wrapper/bin selection instead of the embedded distribution where that is current CLI behavior. Inspect the selected version and prevent wrapper-driven tool distribution downloads; use a local distribution already present or fail with an actionable message. Project dependencies/plugins remain project-input resolution. Current wrapper names are POSIX-specific; decide and test Windows behavior before declaring that target supported. | +| `PATH`, `JAVA_HOME`, Python/uv environment | `src/commands/manifest/bazel/bazel-process.mts`, `bazel-java-shim.mts`, `bazel-python-shim.mts`, uv call sites | For Socket-owned helpers with no explicit selector, prefer the embedded path. Where the existing contract intentionally uses PATH/JAVA_HOME or a project-selected interpreter, honor that explicit environment selection instead of the embedded tool, validate availability/compatibility, and fail rather than download if absent. Construct child environments deliberately and document precedence so unrelated inherited PATH entries cannot accidentally shadow embedded helpers. | + +The ADR must list every environment variable and CLI selector by name, current source behavior, whether it overrides the embedded default, precedence, supported path/version formats, executable type, compatibility checks, and whether the selected tool may perform its own network requests. Distinguish intentional user-selected external execution from hidden helper acquisition in the network-purpose policy. Test each selector unset/set, precedence against the embedded default, valid and missing paths, malicious PATH/JAVA_HOME, sentinel secret variables, `NODE_OPTIONS`/`NODE_PATH`, `JAVA_TOOL_OPTIONS`, `PYTHONPATH` and native loader variables, symlinked and wrapper executables, `.cmd`/shell scripts, wrong version, and absence of download fallback. Verify the spawn adapter truly replaces inherited `process.env`; the support-library spawn helper merges ambient values beneath `options.env`, while current Coana and socket-patch paths explicitly spread ambient env. Explicit selectors should be honored when valid; tests must not require every selected executable to be embedded. + +**Source-crawl completion check:** search production source (excluding tests/fixtures) for `spawnDlx`, `spawnCdxgenDlx`, `spawnSynpDlx`, `dlxBinary`, `spawnCoana`, `spawn`, `execFile`, `exec`, `npx`, `npm exec`, `pnpm dlx`, `yarn dlx`, `bunx`, `pipx`, `uvx`, `python`, and dynamic executable resolution. For each result classify: (A) embedded runtime payload acquired and verified at build time, (B) Socket API or project-input data fetched at runtime, (C) user-requested package-manager execution, (D) test/build-only, (E) explicit external tool override, or (F) OS service/ambient project tool with a named availability contract. Any runtime executable/toolchain acquisition discovered is a plan defect and must be embedded or cause the affected feature/version to fail before launch. Add a test or documented rationale for each production runtime result. Search transitive helper behavior (especially cdxgen, Coana, uv, Python CLIs, and Maven/Gradle/sbt wrappers), not just Socket direct calls. Any binary/runtime download reachable from a supported Socket command is in scope and must become a build-time artifact input. + + +## Dependency removal and owned implementations + +This workstream is a first-class part of the standalone artifact, not a post-build cleanup. The goal is to stop depending on the Socket support library (`@socketsecurity/registry`, the root source dependency matching the described role) and `@socketregistry/packageurl-js` where the CLI uses only specific methods. Copy only the specific methods/types required into clearly owned Socket CLI modules, then remove the dependency references and prove the package still behaves identically. Do not copy whole packages, generated bundles, or unrelated utilities. + +### Confirmed package URL use + +Current direct usage is visible in: + +- `src/utils/purl.mts`: imports `PackageURL` and `PurlQualifiers`; constructs instances, parses with `PackageURL.fromString`, and exposes local create/get/normalize helpers. +- `src/utils/socket-url.mts`: uses `PackageURL` as a type through `getPurlObject`. +- `src/utils/spec.mts`: uses `PackageURL` as a type for version resolution. +- `package.json` directly lists `@socketregistry/packageurl-js` and package.json `pnpm.overrides` maps `packageurl-js` to that package. Remove both references if unused after migration; update lockfile and any workspace/importer catalog consistently. + +The root package directly depends on `@socketsecurity/registry` (currently `1.1.17`), imported through 395 production-source import declarations across its `lib/*` subpaths. The heaviest surfaces are `logger` (164 import lines), `debug` (55), `spawn` (34), `objects` (20), `spinner` (17), `arrays` (17), `fs` (15), `prompts` (14), `strings` (12), `packages` (9), and smaller path/env/bin/url/regex/sort/promise/agent/stream/glob/constants helpers. This is the concrete support-library dependency to audit for the requested method-by-method ownership migration; treat `@socketsecurity/registry` as the candidate intended library and confirm package identity during plan review before removal. Do not remove it wholesale without mapping all used symbols. Existing comments, coverage reports, or bundled output are not authoritative source inventories. + +### Method-by-method extraction process + +1. **Establish provenance and scope.** Confirm whether the intended library is `@socketsecurity/registry` and identify its authoritative repository/version; enumerate every imported symbol used by root `socket` source. For package URLs, inspect the exact locked `@socketregistry/packageurl-js` implementation/version. Record each call site, symbol, edge-case behavior, license/copyright, and relevant upstream test vectors. +2. **Create a minimal API inventory.** For `@socketsecurity/registry`, inventory direct imported functions/types, root exports such as `getManifestData`, dynamic requires, constant internals, and build-script/Rollup consumers; map each symbol to helper closure and data files. For package URLs, define the precise in-scope contract: `PackageURL` fields/constructor/parser/serializer behavior and the qualifier type/normalization actually consumed by CLI code. Use import/reference searches plus TypeScript/compiler dependency analysis; generated `dist` bundles are not source of truth. +3. **Copy narrowly into owned modules.** Add focused modules such as `src/utils/package-url.mts` (or another repo-consistent name) and focused owned modules grouped by concern (for example logging, spawn, filesystem, package helpers), without retaining registry namespace naming unless needed for clear provenance. Copy only the individual methods needed, adapt imports to repo-owned types/helpers, retain essential upstream attribution/license notices, and add comments naming upstream source/version/commit and any intentional deviations. Do not copy unrelated exports or entire package trees. Include the minimal transitive helper/type/data closure proven necessary for each copied method; enumerate every copied dependency with provenance and tests. +4. **Keep the API internal and minimal.** Preserve existing call-site semantics with compatibility wrappers where that reduces churn, but avoid re-exporting a general package URL or socket-lib library as a new public API. Normalize thrown/invalid-input behavior and `throws: false` behavior explicitly. Preserve object shape, optional fields, qualifiers, namespaces, subpaths, versions, and serialization contracts used by the CLI. +5. **Remove dependencies comprehensively.** Remove direct dependency entries, the `packageurl-js` pnpm override if unused, aliases that exist only for these packages, bundler externals, ambient typings, package-specific checks, and build-time imports after confirming no other root-source consumer needs them. Regenerate the pnpm lockfile canonically. Migrate build bootstrap/config imports before removing the package so `pnpm build` remains functional. Reproduce required registry-owned manifest/extension JSON data with a documented source, license, update process and parity tests. Do not change separately vendored projects unless they feed the product. +6. **Verify dependency graph and output.** Use package manager dependency graph tools and Rollup metadata to prove root Socket-owned source/build code no longer imports the removed libraries. Record independently required helper copies in the target manifest/SBOM; do not require unrelated helper dependency graphs to remove their own legitimate implementation. Check npm pack contents/import closure as appropriate. Account for other packages that independently depend on packageurl-js; the goal is to remove Socket CLI's own use, not blindly eliminate a legitimate transitive dependency from tools such as cdxgen unless separately replaced and validated. + +### Behavioral and legal guardrails + +- Use upstream tests/specification as a differential oracle before changing call sites. Cover PURL parsing/serialization, encoding and normalization, scoped npm names, Maven namespace separators, qualifiers/subpaths, Unicode and reserved bytes, malformed escapes, empty/absent versions, invalid types, and error classes/messages where callers depend on them. Include round-trip and adversarial cases; do not silently weaken validation. +- Preserve Socket's existing `normalizePurl`, `createPurlObject`, and `getPurlObject` public/internal semantics unless a separate behavior change is explicitly approved. Keep project-specific policy layered outside the copied standards implementation. +- Keep copyright and license notices for copied code; verify license permits copying/modification and retain required attribution. Prefer a small clean-room reimplementation from the spec when upstream license or code provenance makes copying unsuitable, but document that choice and retain parity tests. +- Add focused ownership tests that do not import the removed dependencies. During migration, differential tests may temporarily compare old and new implementations; remove the old package from final runtime/test imports when the migration is complete. +- Run source and built-output scans to prevent accidental imports reappearing. Update dependency documentation and bundle inventory to show the maintained local implementation and its upstream/spec reference. + +### Acceptance gates for dependency removal + +- Root Socket-owned source/build bootstrap has no import or runtime resolution of `@socketsecurity/registry` or `@socketregistry/packageurl-js` after migration; direct aliases/overrides and lockfile importer edges are removed where no longer needed. Independent helper graphs such as cdxgen may retain their own packageurl dependency; inventory those separately and do not claim global absence. +- Every copied method is enumerated with source provenance, license handling, owned module, call sites, and test coverage. No unrelated upstream files or exports were copied. +- CLI PURL behavior passes the differential/spec corpus and all existing tests; consumers no longer rely on `PackageURL` class identity from an external package. +- Rollup production graph and standalone payload prove the removed implementations are not included as hidden transitive code. Any independent helper dependency is explicitly documented and does not undermine the claimed `socket` package reduction. +- Standard npm package behavior and the new SEA artifact both use the same owned implementation. No SEA-only divergent PURL behavior is allowed. + +## Proposed architecture + +### Build and payload + +1. Build only the root `socket` application at the same source revision/version as the intended release. Explicitly exclude `@socketsecurity/cli` and `@socketsecurity/cli-with-sentry` from product naming, package resolution, metadata, and release claims. +2. Produce a dedicated standalone entry and Rollup bundle for production JS. Put JS modules and ordinary data into SEA VFS. Put executables, native addons, interpreter files, dynamic libraries, and assets requiring filesystem paths into a target-specific resource payload. +3. Treat the Node 27 SEA executable as the product entrypoint, not a general Node CLI runtime. Include separately pinned, signed plain Node child runtimes only where actual helper or explicitly selected package-manager behavior requires `node ` + await mkdir(path.dirname(output), { recursive: true }) + await writeFile(output, html) + console.log(`Wrote ${records.length} image(s) to ${output}`) + return 0 +} + +if (isMainModule(import.meta.url)) { + runMain(main, { + describe: + 'builds a self-contained HTML report from native terminal screenshots', + help: 'Usage: node scripts/test/terminal-report.mts [--artifacts ] [--output ]', + }) +} diff --git a/scripts/utils/packages.js b/scripts/utils/packages.js index 4139649b86..604d59152c 100644 --- a/scripts/utils/packages.js +++ b/scripts/utils/packages.js @@ -5,13 +5,12 @@ const Module = require('node:module') const path = require('node:path') const vm = require('node:vm') -const { isValidPackageName } = require('@socketsecurity/registry/lib/packages') +const { findUpSync } = require('./fs') const { isRelative, + isValidPackageName, normalizePath, -} = require('@socketsecurity/registry/lib/path') - -const { findUpSync } = require('./fs') +} = require('./registry-helpers.js') const { createRequire, isBuiltin } = Module diff --git a/scripts/utils/registry-helpers.js b/scripts/utils/registry-helpers.js new file mode 100644 index 0000000000..59d935956e --- /dev/null +++ b/scripts/utils/registry-helpers.js @@ -0,0 +1,261 @@ +'use strict' + +/* + * Narrow build-time copies of methods previously imported from + * @socketsecurity/registry 1.1.17. Copyright (c) Socket Inc., MIT License. + * Keep this file limited to the exact methods consumed by Rollup scripts. + */ + +const { spawn, spawnSync: spawnSyncRaw } = require('node:child_process') +const { readFileSync, readdirSync } = require('node:fs') +const path = require('node:path') + +const ansiRegExp = + // eslint-disable-next-line no-control-regex -- The expression removes ANSI control codes. + /(?:\u001B\][\s\S]*?(?:\u0007|\u001B\\|\u009C)|[\u001B\u009B][[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])/g + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function stripAnsi(value) { + return String(value).replace(ansiRegExp, '') +} + +function normalizePath(value) { + const source = Buffer.isBuffer(value) + ? value.toString() + : value instanceof URL + ? value.pathname + : String(value) + if (source.length < 2) { + return source === '\\' ? '/' : source + } + const unc = source.startsWith('\\\\') || source.startsWith('//') + const normalized = path.posix.normalize(source.replaceAll('\\', '/')) + return unc && !normalized.startsWith('//') ? `/${normalized}` : normalized +} + +function readPackageJsonSync(directory) { + const filename = directory.endsWith('package.json') + ? directory + : path.join(directory, 'package.json') + return JSON.parse(readFileSync(filename, 'utf8')) +} + +async function readPackageJson(directory, options = {}) { + const filename = directory.endsWith('package.json') + ? directory + : path.join(directory, 'package.json') + let source + try { + source = await require('node:fs/promises').readFile(filename, 'utf8') + } catch (error) { + if (options.throws === false) { + return null + } + throw error + } + let content + try { + content = JSON.parse(source) + } catch (error) { + if (options.throws === false) { + return null + } + throw error + } + if (!options.editable) { + return content + } + let original = source + const indent = /\n([ \t]+)"/.exec(source)?.[1] ?? ' ' + const editable = { + content, + update(data) { + for (const [key, value] of Object.entries(data)) { + if (value === undefined) { + delete content[key] + } else { + content[key] = value + } + } + return editable + }, + async save(saveOptions = {}) { + const next = saveOptions.sort + ? Object.fromEntries( + Object.entries(content).sort(([a], [b]) => a.localeCompare(b)), + ) + : content + const output = `${JSON.stringify(next, null, indent)}\n` + if (saveOptions.ignoreWhitespace && output.trim() === original.trim()) { + return false + } + if (output === original) { + return false + } + await require('node:fs/promises').writeFile(filename, output) + original = output + return true + }, + } + return editable +} + +function fetchPackageManifest(spec) { + const result = spawnSyncRaw('npm', ['view', spec, 'version', '--json'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + if (result.error) { + throw result.error + } + if (result.status !== 0) { + throw new Error( + `npm view ${spec} failed: ${stripAnsi(result.stderr || '').trim()}`, + ) + } + const response = JSON.parse(String(result.stdout || 'null')) + const version = + Array.isArray(response) && response.length === 1 ? response[0] : response + if (typeof version !== 'string' || !version) { + throw new Error(`npm view ${spec} did not return a version`) + } + return { name: spec.slice(0, spec.lastIndexOf('@')) || spec, version } +} + +function spawnSync(command, args, options = {}) { + const result = spawnSyncRaw(command, args, { + ...options, + encoding: options.encoding ?? 'utf8', + }) + if (typeof result.stdout === 'string') { + result.stdout = result.stdout.trim() + } + if (typeof result.stderr === 'string') { + result.stderr = result.stderr.trim() + } + return result +} + +function spawnAsync(command, args, options = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, options) + child.once('error', reject) + child.once('close', (code, signal) => { + if (code === 0) { + resolve({ code, signal }) + } else { + reject( + Object.assign(new Error(`${command} exited with ${code ?? signal}`), { + code, + signal, + }), + ) + } + }) + }) +} + +function isDirEmptySync(directory) { + try { + const ignored = new Set([ + '.git', + '.npmrc', + 'node_modules', + '.DS_Store', + '.gitignore', + '.hg', + '.lock-wscript', + '.npmignore', + '.svn', + '.env', + '.eslintcache', + '.nvm', + '.tap', + '.vscode', + 'Thumbs.db', + 'bower_components', + ]) + return readdirSync(directory).every( + name => + ignored.has(name) || + name.startsWith('.wafpickle-') || + name.startsWith('._') || + /^\..*\.swp$/.test(name) || + /^.*\.orig$/.test(name) || + /^.*\.tsbuildinfo$/.test(name), + ) + } catch (error) { + return error?.code === 'ENOENT' + } +} + +function hasKeys(value) { + return value != null && Object.keys(value).length > 0 +} + +let naturalCompareFn +function naturalCompare(a, b) { + naturalCompareFn ??= new Intl.Collator(undefined, { + numeric: true, + sensitivity: 'base', + }).compare + return naturalCompareFn(a, b) +} + +function isValidPackageName(name) { + if (typeof name !== 'string' || name.length === 0) { + return false + } + if (name.startsWith('.') || name.startsWith('_') || name.trim() !== name) { + return false + } + if (['node_modules', 'favicon.ico'].includes(name.toLowerCase())) { + return false + } + if (encodeURIComponent(name) === name) { + return true + } + + const scopeMatch = /^(?:@([^/]+?)\/)?([^/]+?)$/.exec(name) + if (!scopeMatch) { + return false + } + const [, scope, packageName] = scopeMatch + if (packageName.startsWith('.')) { + return false + } + return ( + encodeURIComponent(scope ?? '') === (scope ?? '') && + encodeURIComponent(packageName) === packageName + ) +} + +function isRelative(value) { + return ( + typeof value === 'string' && + (value === '.' || + value.startsWith('./') || + value.startsWith('../') || + value.startsWith('.\\') || + value.startsWith('..\\')) + ) +} + +module.exports = { + escapeRegExp, + fetchPackageManifest, + hasKeys, + isDirEmptySync, + isRelative, + isValidPackageName, + naturalCompare, + normalizePath, + readPackageJson, + readPackageJsonSync, + spawnAsync, + spawnSync, + stripAnsi, +} diff --git a/src/cli.mts b/src/cli.mts index b56c5d5894..ff268a1893 100755 --- a/src/cli.mts +++ b/src/cli.mts @@ -5,13 +5,12 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import meow from 'meow' import { messageWithCauses, stackWithCauses } from 'pony-cause' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { rootAliases, rootCommands } from './commands.mts' import constants from './constants.mts' +import { debugDir, debugFn } from './utils/debug.mts' import { AuthError, InputError, captureException } from './utils/errors.mts' import { failMsgWithBadge } from './utils/fail-msg-with-badge.mts' +import { logger } from './utils/logger.mts' import { meowWithSubcommands } from './utils/meow-with-subcommands.mts' import { runSelfUpdateCheck } from './utils/self-update.mts' import { serializeResultJson } from './utils/serialize-result-json.mts' diff --git a/src/commands/analytics/cmd-analytics.mts b/src/commands/analytics/cmd-analytics.mts index 1845d1e2ff..afe0f6afd4 100644 --- a/src/commands/analytics/cmd-analytics.mts +++ b/src/commands/analytics/cmd-analytics.mts @@ -1,5 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleAnalytics } from './handle-analytics.mts' import constants, { FLAG_JSON, @@ -9,6 +7,7 @@ import constants, { import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/analytics/output-analytics.mts b/src/commands/analytics/output-analytics.mts index d27b7d7901..3832f395e6 100644 --- a/src/commands/analytics/output-analytics.mts +++ b/src/commands/analytics/output-analytics.mts @@ -1,11 +1,10 @@ import fs from 'node:fs/promises' import { createRequire } from 'node:module' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants from '../../constants.mts' import { debugFileOp } from '../../utils/debug.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { mdTableStringNumber } from '../../utils/markdown.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import { fileLink } from '../../utils/terminal-link.mts' diff --git a/src/commands/audit-log/cmd-audit-log.mts b/src/commands/audit-log/cmd-audit-log.mts index e06e5ac638..cc92cc6479 100644 --- a/src/commands/audit-log/cmd-audit-log.mts +++ b/src/commands/audit-log/cmd-audit-log.mts @@ -1,5 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleAuditLog } from './handle-audit-log.mts' import constants, { FLAG_JSON, @@ -10,6 +8,7 @@ import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/audit-log/output-audit-log.mts b/src/commands/audit-log/output-audit-log.mts index 7a07d2a044..2e68afa049 100644 --- a/src/commands/audit-log/output-audit-log.mts +++ b/src/commands/audit-log/output-audit-log.mts @@ -1,14 +1,13 @@ import { createRequire } from 'node:module' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants, { FLAG_JSON, OUTPUT_JSON, OUTPUT_MARKDOWN, } from '../../constants.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { mdTable } from '../../utils/markdown.mts' import { msAtHome } from '../../utils/ms-at-home.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' diff --git a/src/commands/ci/cmd-ci.mts b/src/commands/ci/cmd-ci.mts index 056de0dc0a..1b5432879f 100644 --- a/src/commands/ci/cmd-ci.mts +++ b/src/commands/ci/cmd-ci.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleCi } from './handle-ci.mts' import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/ci/fetch-default-org-slug.mts b/src/commands/ci/fetch-default-org-slug.mts index 038186a1dd..51d5ac9429 100644 --- a/src/commands/ci/fetch-default-org-slug.mts +++ b/src/commands/ci/fetch-default-org-slug.mts @@ -1,7 +1,6 @@ -import { debugFn } from '@socketsecurity/registry/lib/debug' - import constants from '../../constants.mts' import { getConfigValueOrUndef } from '../../utils/config.mts' +import { debugFn } from '../../utils/debug.mts' import { fetchOrganization } from '../organization/fetch-organization-list.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/ci/handle-ci.mts b/src/commands/ci/handle-ci.mts index 62a4bb7db4..2be7d0dff8 100644 --- a/src/commands/ci/handle-ci.mts +++ b/src/commands/ci/handle-ci.mts @@ -1,14 +1,13 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { envAsString } from '@socketsecurity/registry/lib/env' -import { logger } from '@socketsecurity/registry/lib/logger' - import { getDefaultOrgSlug } from './fetch-default-org-slug.mts' import constants from '../../constants.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' +import { envAsString } from '../../utils/env.mts' import { detectDefaultBranch, getRepoName, gitBranch, } from '../../utils/git.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import { handleCreateNewScan } from '../scan/handle-create-new-scan.mts' diff --git a/src/commands/cli.test.mts b/src/commands/cli.integration.test.mts similarity index 100% rename from src/commands/cli.test.mts rename to src/commands/cli.integration.test.mts diff --git a/src/commands/config/cmd-config-auto.mts b/src/commands/config/cmd-config-auto.mts index f67b73a3fe..cc1bf18fba 100644 --- a/src/commands/config/cmd-config-auto.mts +++ b/src/commands/config/cmd-config-auto.mts @@ -1,5 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleConfigAuto } from './handle-config-auto.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' @@ -9,6 +7,7 @@ import { isSupportedConfigKey, } from '../../utils/config.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/config/cmd-config-get.mts b/src/commands/config/cmd-config-get.mts index 1f71c58a95..a9e47aacd5 100644 --- a/src/commands/config/cmd-config-get.mts +++ b/src/commands/config/cmd-config-get.mts @@ -1,5 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleConfigGet } from './handle-config-get.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' @@ -9,6 +7,7 @@ import { isSupportedConfigKey, } from '../../utils/config.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/config/cmd-config-list.mts b/src/commands/config/cmd-config-list.mts index a42c8986dd..ce3c07837e 100644 --- a/src/commands/config/cmd-config-list.mts +++ b/src/commands/config/cmd-config-list.mts @@ -1,10 +1,9 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { outputConfigList } from './output-config-list.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/config/cmd-config-set.mts b/src/commands/config/cmd-config-set.mts index 99368a63bd..9d9387f7df 100644 --- a/src/commands/config/cmd-config-set.mts +++ b/src/commands/config/cmd-config-set.mts @@ -1,5 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleConfigSet } from './handle-config-set.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' @@ -9,6 +7,7 @@ import { isSupportedConfigKey, } from '../../utils/config.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/config/cmd-config-unset.mts b/src/commands/config/cmd-config-unset.mts index 2d042ec046..5a5ace0c70 100644 --- a/src/commands/config/cmd-config-unset.mts +++ b/src/commands/config/cmd-config-unset.mts @@ -1,5 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleConfigUnset } from './handle-config-unset.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' @@ -9,6 +7,7 @@ import { isSupportedConfigKey, } from '../../utils/config.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/config/handle-config-set.mts b/src/commands/config/handle-config-set.mts index 2256633d6b..1765dea0b6 100644 --- a/src/commands/config/handle-config-set.mts +++ b/src/commands/config/handle-config-set.mts @@ -1,7 +1,6 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' - import { outputConfigSet } from './output-config-set.mts' import { updateConfigValue } from '../../utils/config.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import type { CResult, OutputKind } from '../../types.mts' import type { LocalConfig } from '../../utils/config.mts' diff --git a/src/commands/config/output-config-auto.mts b/src/commands/config/output-config-auto.mts index 6d4388a938..1a04feea55 100644 --- a/src/commands/config/output-config-auto.mts +++ b/src/commands/config/output-config-auto.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' -import { select } from '@socketsecurity/registry/lib/prompts' - import { isConfigFromFlag, updateConfigValue } from '../../utils/config.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' +import { select } from '../../utils/prompts.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/config/output-config-get.mts b/src/commands/config/output-config-get.mts index 8cc27aa104..436f29dc2d 100644 --- a/src/commands/config/output-config-get.mts +++ b/src/commands/config/output-config-get.mts @@ -1,7 +1,6 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { isConfigFromFlag } from '../../utils/config.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/config/output-config-list.mts b/src/commands/config/output-config-list.mts index d565c9cdc2..c4db946daf 100644 --- a/src/commands/config/output-config-list.mts +++ b/src/commands/config/output-config-list.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { getConfigValue, getSupportedConfigKeys, isConfigFromFlag, isSensitiveConfigKey, } from '../../utils/config.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { OutputKind } from '../../types.mts' diff --git a/src/commands/config/output-config-set.mts b/src/commands/config/output-config-set.mts index 308b3fc99b..5cc4e34f7b 100644 --- a/src/commands/config/output-config-set.mts +++ b/src/commands/config/output-config-set.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/config/output-config-unset.mts b/src/commands/config/output-config-unset.mts index ab8a4069ce..66947d0e93 100644 --- a/src/commands/config/output-config-unset.mts +++ b/src/commands/config/output-config-unset.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/fix/branch-cleanup.integration.test.mts b/src/commands/fix/branch-cleanup.integration.test.mts index cf31567c60..25f2df284c 100644 --- a/src/commands/fix/branch-cleanup.integration.test.mts +++ b/src/commands/fix/branch-cleanup.integration.test.mts @@ -5,8 +5,6 @@ import path from 'node:path' import trash from 'trash' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { cleanupErrorBranches, cleanupFailedPrBranches, @@ -19,6 +17,7 @@ import { gitDeleteRemoteBranch, gitRemoteBranchExists, } from '../../utils/git.mts' +import { spawn } from '../../utils/spawn.mts' describe('branch-cleanup integration tests', () => { let tempDir: string diff --git a/src/commands/fix/branch-cleanup.mts b/src/commands/fix/branch-cleanup.mts index 7493fdb03e..542a7f1822 100644 --- a/src/commands/fix/branch-cleanup.mts +++ b/src/commands/fix/branch-cleanup.mts @@ -5,10 +5,9 @@ * Critical distinction: Remote branches are sacred when a PR exists, disposable when they don't. */ -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - +import { debugFn } from '../../utils/debug.mts' import { gitDeleteBranch, gitDeleteRemoteBranch } from '../../utils/git.mts' +import { logger } from '../../utils/logger.mts' /** * Clean up a stale branch (both remote and local). diff --git a/src/commands/fix/branch-cleanup.test.mts b/src/commands/fix/branch-cleanup.test.mts index 61e4f6c3f7..3301fa97f0 100644 --- a/src/commands/fix/branch-cleanup.test.mts +++ b/src/commands/fix/branch-cleanup.test.mts @@ -17,11 +17,11 @@ const mockDebugFn = vi.hoisted(() => vi.fn()) const mockGitDeleteBranch = vi.hoisted(() => vi.fn()) const mockGitDeleteRemoteBranch = vi.hoisted(() => vi.fn()) -vi.mock('@socketsecurity/registry/lib/logger', () => ({ +vi.mock('../../utils/logger.mts', () => ({ logger: mockLogger, })) -vi.mock('@socketsecurity/registry/lib/debug', () => ({ +vi.mock('../../utils/debug.mts', () => ({ debugFn: mockDebugFn, })) diff --git a/src/commands/fix/cmd-fix.e2e.test.mts b/src/commands/fix/cmd-fix.e2e.test.mts index d0acab8b6f..b94a821e3f 100644 --- a/src/commands/fix/cmd-fix.e2e.test.mts +++ b/src/commands/fix/cmd-fix.e2e.test.mts @@ -5,10 +5,9 @@ import path from 'node:path' import { describe, expect } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { cmdit, spawnSocketCli, testPath } from '../../../test/utils.mts' import constants, { FLAG_ID } from '../../constants.mts' +import { logger } from '../../utils/logger.mts' const fixtureBaseDir = path.join(testPath, 'fixtures/commands/fix') const systemTmpDir = tmpdir() diff --git a/src/commands/fix/cmd-fix.mts b/src/commands/fix/cmd-fix.mts index a6b8c384ee..c0aac19eed 100644 --- a/src/commands/fix/cmd-fix.mts +++ b/src/commands/fix/cmd-fix.mts @@ -3,13 +3,6 @@ import path from 'node:path' import terminalLink from 'terminal-link' -import { - arrayUnique, - joinAnd, - joinOr, -} from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleFix } from './handle-fix.mts' import constants, { COANA_CLI_VERSION, @@ -17,6 +10,7 @@ import constants, { FLAG_ID, } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' +import { arrayUnique, joinAnd, joinOr } from '../../utils/arrays.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' import { @@ -24,6 +18,7 @@ import { parseEcosystemChoices, } from '../../utils/ecosystem.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts index b62add74b1..8d40524d5b 100644 --- a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts +++ b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts @@ -2,9 +2,8 @@ import { promises as fs } from 'node:fs' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { coanaFix } from './coana-fix.mts' +import { logger } from '../../utils/logger.mts' import type { FixConfig } from './types.mts' diff --git a/src/commands/fix/coana-fix.mts b/src/commands/fix/coana-fix.mts index 570b63302a..04efa3773f 100644 --- a/src/commands/fix/coana-fix.mts +++ b/src/commands/fix/coana-fix.mts @@ -2,12 +2,6 @@ import { promises as fs } from 'node:fs' import os from 'node:os' import path from 'node:path' -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { readJsonSync } from '@socketsecurity/registry/lib/fs' -import { logger } from '@socketsecurity/registry/lib/logger' -import { pluralize } from '@socketsecurity/registry/lib/words' - import { cleanupErrorBranches, cleanupFailedPrBranches, @@ -28,14 +22,16 @@ import { GQL_PR_STATE_OPEN, } from '../../constants.mts' import { handleApiCall } from '../../utils/api.mts' +import { joinAnd } from '../../utils/arrays.mts' import { spawnCoana } from '../../utils/coana-release.mts' import { isReachabilityReportPath, isSocketFactsFile, } from '../../utils/coana.mts' import { findSocketYmlSync } from '../../utils/config.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import { getErrorCause } from '../../utils/errors.mts' -import { withTmpDir } from '../../utils/fs.mts' +import { readJsonSync, withTmpDir } from '../../utils/fs.mts' import { gitCheckoutBranch, gitCommit, @@ -52,8 +48,10 @@ import { fetchGhsaDetails, setGitRemoteGithubRepoUrl, } from '../../utils/github.mts' +import { logger } from '../../utils/logger.mts' import { getPackageFilesForScan } from '../../utils/path-resolve.mts' import { setupSdk } from '../../utils/sdk.mts' +import { pluralize } from '../../utils/words.mts' import { excludePathToScanIgnores } from '../scan/exclude-paths.mts' import { fetchSupportedScanFileNames } from '../scan/fetch-supported-scan-file-names.mts' @@ -62,7 +60,7 @@ import type { FixConfig } from './types.mts' import type { CResult } from '../../types.mts' import type { FactsOnlyTypes } from '../../utils/coana.mts' import type { PURL_Type } from '../../utils/ecosystem.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { Spinner } from '../../utils/spinner.mts' type DiscoverGhsaIdsOptions = { coanaVersion?: string | undefined diff --git a/src/commands/fix/env-helpers.mts b/src/commands/fix/env-helpers.mts index 65e1c632ca..34d68fc13f 100644 --- a/src/commands/fix/env-helpers.mts +++ b/src/commands/fix/env-helpers.mts @@ -1,10 +1,9 @@ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { debugFn, isDebug } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { getSocketFixPrs } from './pull-request.mts' import constants from '../../constants.mts' +import { joinAnd } from '../../utils/arrays.mts' +import { debugFn, isDebug } from '../../utils/debug.mts' import { getBaseBranch, getRepoInfo } from '../../utils/git.mts' +import { logger } from '../../utils/logger.mts' import type { PrMatch } from './pull-request.mts' import type { RepoInfo } from '../../utils/git.mts' diff --git a/src/commands/fix/git.mts b/src/commands/fix/git.mts index 8af26bef81..70a01c6382 100644 --- a/src/commands/fix/git.mts +++ b/src/commands/fix/git.mts @@ -1,6 +1,5 @@ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' - import constants from '../../constants.mts' +import { joinAnd } from '../../utils/arrays.mts' import type { GhsaDetails } from '../../utils/github.mts' diff --git a/src/commands/fix/handle-fix-id.test.mts b/src/commands/fix/handle-fix-id.test.mts index a00d82b976..c2359f56ea 100644 --- a/src/commands/fix/handle-fix-id.test.mts +++ b/src/commands/fix/handle-fix-id.test.mts @@ -8,7 +8,7 @@ const mockLogger = vi.hoisted(() => ({ warn: vi.fn(), })) -vi.mock('@socketsecurity/registry/lib/logger', () => ({ +vi.mock('../../utils/logger.mts', () => ({ logger: mockLogger, })) diff --git a/src/commands/fix/handle-fix-limit.test.mts b/src/commands/fix/handle-fix-limit.test.mts index d994021b39..3244669c58 100644 --- a/src/commands/fix/handle-fix-limit.test.mts +++ b/src/commands/fix/handle-fix-limit.test.mts @@ -4,9 +4,8 @@ import path from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { coanaFix } from './coana-fix.mts' +import { logger } from '../../utils/logger.mts' import type { FixConfig } from './types.mts' diff --git a/src/commands/fix/handle-fix.mts b/src/commands/fix/handle-fix.mts index a21437e7e5..586a897553 100644 --- a/src/commands/fix/handle-fix.mts +++ b/src/commands/fix/handle-fix.mts @@ -1,13 +1,12 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { coanaFix } from './coana-fix.mts' import { outputFixResult } from './output-fix-result.mts' import { resolveAdvisoryIds } from '../../utils/advisory-ids.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' +import { logger } from '../../utils/logger.mts' import type { FixConfig } from './types.mts' import type { OutputKind } from '../../types.mts' -import type { Remap } from '@socketsecurity/registry/lib/objects' +import type { Remap } from '../../utils/objects.mts' export type HandleFixConfig = Remap< FixConfig & { diff --git a/src/commands/fix/output-fix-result.mts b/src/commands/fix/output-fix-result.mts index d2f6b2aacf..f1c5169e58 100644 --- a/src/commands/fix/output-fix-result.mts +++ b/src/commands/fix/output-fix-result.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/fix/pull-request.mts b/src/commands/fix/pull-request.mts index 0af5171250..71433671c7 100644 --- a/src/commands/fix/pull-request.mts +++ b/src/commands/fix/pull-request.mts @@ -1,8 +1,5 @@ import { RequestError } from '@octokit/request-error' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { isNonEmptyString } from '@socketsecurity/registry/lib/strings' - import { getSocketFixBranchPattern, getSocketFixPullRequestBody, @@ -15,6 +12,7 @@ import { GQL_PR_STATE_OPEN, UNKNOWN_VALUE, } from '../../constants.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import { formatErrorWithDetail } from '../../utils/errors.mts' import { gitDeleteRemoteBranch } from '../../utils/git.mts' import { @@ -25,9 +23,10 @@ import { getOctokitGraphql, writeCache, } from '../../utils/github.mts' +import { isNonEmptyString } from '../../utils/strings.mts' +import type { JsonContent } from '../../utils/fs.mts' import type { OctokitResponse } from '@octokit/types' -import type { JsonContent } from '@socketsecurity/registry/lib/fs' export type OpenSocketFixPrOptions = { baseBranch?: string | undefined diff --git a/src/commands/fix/types.mts b/src/commands/fix/types.mts index 42ccabe91d..be5a3af667 100644 --- a/src/commands/fix/types.mts +++ b/src/commands/fix/types.mts @@ -1,7 +1,7 @@ import type { FactsOnlyTypes } from '../../utils/coana.mts' import type { PURL_Type } from '../../utils/ecosystem.mts' import type { RangeStyle } from '../../utils/semver.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { Spinner } from '../../utils/spinner.mts' export type FixConfig = { all: boolean diff --git a/src/commands/install/cmd-install-completion.mts b/src/commands/install/cmd-install-completion.mts index 817daab9e4..03fcedbec6 100644 --- a/src/commands/install/cmd-install-completion.mts +++ b/src/commands/install/cmd-install-completion.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleInstallCompletion } from './handle-install-completion.mts' import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/install/output-install-completion.mts b/src/commands/install/output-install-completion.mts index 03e9c2bc26..0b0f78eb65 100644 --- a/src/commands/install/output-install-completion.mts +++ b/src/commands/install/output-install-completion.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/install/setup-tab-completion.mts b/src/commands/install/setup-tab-completion.mts index 58fcf1b180..0f348857c1 100644 --- a/src/commands/install/setup-tab-completion.mts +++ b/src/commands/install/setup-tab-completion.mts @@ -2,10 +2,9 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { debugFn } from '@socketsecurity/registry/lib/debug' - import constants from '../../constants.mts' import { getBashrcDetails } from '../../utils/completion.mts' +import { debugFn } from '../../utils/debug.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/json/cmd-json.test.mts b/src/commands/json/cmd-json.integration.test.mts similarity index 100% rename from src/commands/json/cmd-json.test.mts rename to src/commands/json/cmd-json.integration.test.mts diff --git a/src/commands/json/output-cmd-json.mts b/src/commands/json/output-cmd-json.mts index 72d3aff4dd..3206653b97 100644 --- a/src/commands/json/output-cmd-json.mts +++ b/src/commands/json/output-cmd-json.mts @@ -1,13 +1,9 @@ import { existsSync } from 'node:fs' import path from 'node:path' -import { - safeReadFileSync, - safeStatsSync, -} from '@socketsecurity/registry/lib/fs' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants, { REDACTED, SOCKET_JSON } from '../../constants.mts' +import { safeReadFileSync, safeStatsSync } from '../../utils/fs.mts' +import { logger } from '../../utils/logger.mts' import { tildify } from '../../utils/tildify.mts' export async function outputCmdJson(cwd: string) { diff --git a/src/commands/login/attempt-login.mts b/src/commands/login/attempt-login.mts index 79a6a015b2..9ffa4b1618 100644 --- a/src/commands/login/attempt-login.mts +++ b/src/commands/login/attempt-login.mts @@ -1,7 +1,3 @@ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' -import { confirm, password, select } from '@socketsecurity/registry/lib/prompts' - import { applyLogin } from './apply-login.mts' import constants, { CONFIG_KEY_API_BASE_URL, @@ -9,19 +5,22 @@ import constants, { CONFIG_KEY_API_TOKEN, CONFIG_KEY_DEFAULT_ORG, } from '../../constants.mts' +import { joinAnd } from '../../utils/arrays.mts' import { getConfigValueOrUndef, isConfigFromFlag, updateConfigValue, } from '../../utils/config.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { getEnterpriseOrgs, getOrgSlugs } from '../../utils/organization.mts' +import { confirm, password, select } from '../../utils/prompts.mts' import { setupSdk } from '../../utils/sdk.mts' import { socketDocsLink } from '../../utils/terminal-link.mts' import { setupTabCompletion } from '../install/setup-tab-completion.mts' import { fetchOrganization } from '../organization/fetch-organization-list.mts' -import type { Choice, Separator } from '@socketsecurity/registry/lib/prompts' +import type { Choice, Separator } from '../../utils/prompts.mts' type OrgChoice = Choice type OrgChoices = Array diff --git a/src/commands/login/cmd-login.mts b/src/commands/login/cmd-login.mts index b54ceb2277..b96267ef7b 100644 --- a/src/commands/login/cmd-login.mts +++ b/src/commands/login/cmd-login.mts @@ -1,10 +1,10 @@ import isInteractive from '@socketregistry/is-interactive/index.cjs' -import { logger } from '@socketsecurity/registry/lib/logger' import { attemptLogin } from './attempt-login.mts' import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { InputError } from '../../utils/errors.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/logout/attempt-logout.mts b/src/commands/logout/attempt-logout.mts index 1e1dd0dc93..ba5666753d 100644 --- a/src/commands/logout/attempt-logout.mts +++ b/src/commands/logout/attempt-logout.mts @@ -1,7 +1,6 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { applyLogout } from './apply-logout.mts' import { isConfigFromFlag } from '../../utils/config.mts' +import { logger } from '../../utils/logger.mts' export function attemptLogout() { try { diff --git a/src/commands/logout/cmd-logout.mts b/src/commands/logout/cmd-logout.mts index 0c6c7a5953..0b221e9b5d 100644 --- a/src/commands/logout/cmd-logout.mts +++ b/src/commands/logout/cmd-logout.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { attemptLogout } from './attempt-logout.mts' import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import type { diff --git a/src/commands/manifest/bazel/bazel-bin-detect.mts b/src/commands/manifest/bazel/bazel-bin-detect.mts index c18e1f7eb6..8ef67aa5cc 100644 --- a/src/commands/manifest/bazel/bazel-bin-detect.mts +++ b/src/commands/manifest/bazel/bazel-bin-detect.mts @@ -1,10 +1,17 @@ -import { existsSync } from 'node:fs' +import { + accessSync, + existsSync, + constants as fsConstants, + statSync, +} from 'node:fs' import path from 'node:path' import { findBazelExecutable, + isNodeScript, resolveBazelExecutable, } from './bazel-process.mts' +import { isStandaloneSea } from '../../../sea/runtime.mts' import { InputError } from '../../../utils/errors.mts' /** @@ -21,6 +28,39 @@ export async function resolveBazelBinary( env: NodeJS.ProcessEnv, cwd: string, ): Promise { + if (isStandaloneSea()) { + const override = explicit ?? env['SOCKET_CLI_BAZEL_PATH'] + if (!override) { + throw new InputError( + 'Bazel is not bundled with this standalone Socket CLI. Set SOCKET_CLI_BAZEL_PATH or pass --bazel with an installed native Bazel executable.', + ) + } + const resolved = path.resolve(cwd, override) + try { + if (!existsSync(resolved) || !statSync(resolved).isFile()) { + throw new Error('not a file') + } + if (process.platform !== 'win32') { + accessSync(resolved, fsConstants.X_OK) + } + } catch { + throw new InputError( + `The standalone Bazel path '${resolved}' is missing or not executable.`, + ) + } + const bin = resolveBazelExecutable(resolved) + if (/^bazelisk(?:\.exe)?$/i.test(path.basename(bin))) { + throw new InputError( + 'Standalone Socket requires an installed Bazel binary and will not use Bazelisk to download a Bazel version.', + ) + } + if (bin === process.execPath || isNodeScript(bin)) { + throw new InputError( + 'Standalone Socket cannot launch Bazel through a JavaScript launcher because process.execPath is the SEA executable. Set SOCKET_CLI_BAZEL_PATH to a native Bazel executable.', + ) + } + return bin + } if (explicit) { const resolved = path.resolve(cwd, explicit) if (!existsSync(resolved)) { diff --git a/src/commands/manifest/bazel/bazel-cleanup.mts b/src/commands/manifest/bazel/bazel-cleanup.mts index 74b000b779..7334166950 100644 --- a/src/commands/manifest/bazel/bazel-cleanup.mts +++ b/src/commands/manifest/bazel/bazel-cleanup.mts @@ -11,14 +11,13 @@ import os from 'node:os' import path from 'node:path' import { setTimeout as sleep } from 'node:timers/promises' -import { logger } from '@socketsecurity/registry/lib/logger' - import { buildBazelStartupFlags, getBazelEnvironmentValue, spawnBazel, } from './bazel-process.mts' import { getErrorCause, isErrnoException } from '../../../utils/errors.mts' +import { logger } from '../../../utils/logger.mts' import type { BazelQueryOptions } from './bazel-query-runner.mts' diff --git a/src/commands/manifest/bazel/bazel-cquery.test.mts b/src/commands/manifest/bazel/bazel-cquery.test.mts index cfee4198d6..603cfc505f 100644 --- a/src/commands/manifest/bazel/bazel-cquery.test.mts +++ b/src/commands/manifest/bazel/bazel-cquery.test.mts @@ -1,17 +1,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ +vi.mock('../../../utils/spawn.mts', () => ({ spawn: vi.fn(), })) -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { buildMetadataCqueryArgv, parseCqueryJsonproto, runMetadataCqueryForRepo, versionlessCoordinate, } from './bazel-cquery.mts' +import { spawn } from '../../../utils/spawn.mts' // Sample envelope shape Bazel 5+ emits: `{ "results": [ { "target": {...} } ] }`. // Two rules: one with `tags`/`maven_coordinates` (rules_jvm_external shape) diff --git a/src/commands/manifest/bazel/bazel-native.e2e.test.mts b/src/commands/manifest/bazel/bazel-native.e2e.test.mts index bb200e7886..a91b88d846 100644 --- a/src/commands/manifest/bazel/bazel-native.e2e.test.mts +++ b/src/commands/manifest/bazel/bazel-native.e2e.test.mts @@ -14,8 +14,6 @@ import path from 'node:path' import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { resolveBazelBinary } from './bazel-bin-detect.mts' import { cleanUpBazelOutputRoot, @@ -29,6 +27,7 @@ import { } from './bazel-process.mts' import { provisionPythonShim } from './bazel-python-shim.mts' import { extractBazelToMaven } from './extract_bazel_to_maven.mts' +import { spawn } from '../../../utils/spawn.mts' const FIXTURES = path.resolve( import.meta.dirname, diff --git a/src/commands/manifest/bazel/bazel-process.mts b/src/commands/manifest/bazel/bazel-process.mts index fa0edbe293..2b59921c8a 100644 --- a/src/commands/manifest/bazel/bazel-process.mts +++ b/src/commands/manifest/bazel/bazel-process.mts @@ -10,13 +10,12 @@ import path from 'node:path' import which from 'which' -import { resolveBinPathSync } from '@socketsecurity/registry/lib/bin' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants from '../../../constants.mts' +import { resolveBinPathSync } from '../../../utils/bin.mts' import { InputError, isErrnoException } from '../../../utils/errors.mts' +import { spawn } from '../../../utils/spawn.mts' -import type { SpawnOptions } from '@socketsecurity/registry/lib/spawn' +import type { SpawnOptions } from '../../../utils/spawn.mts' import type { ChildProcess } from 'node:child_process' function findEnvironmentKey( @@ -98,11 +97,11 @@ export function resolveBazelExecutable(file: string): string { } } -function isNodeScript(bin: string): boolean { +export function isNodeScript(bin: string): boolean { if (/\.[cm]?js$/i.test(bin)) { return true } - if (path.extname(bin) || process.platform !== 'win32' || !existsSync(bin)) { + if (path.extname(bin) || !existsSync(bin)) { return false } const fd = openSync(bin, 'r') diff --git a/src/commands/manifest/bazel/bazel-process.test.mts b/src/commands/manifest/bazel/bazel-process.test.mts index 9c8d6871c7..61e6858edd 100644 --- a/src/commands/manifest/bazel/bazel-process.test.mts +++ b/src/commands/manifest/bazel/bazel-process.test.mts @@ -1,17 +1,16 @@ import childProcess from 'node:child_process' import { chmodSync, - copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, - rmSync, statSync, symlinkSync, writeFileSync, } from 'node:fs' +import { rm as rmAsync } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { setTimeout as sleep } from 'node:timers/promises' @@ -70,21 +69,27 @@ describe('Bazel process invocation', () => { writeFileSync(launcher, LAUNCHER) }) - afterEach(() => { + afterEach(async () => { for (const pid of servers.splice(0)) { if (isAlive(pid)) { process.kill(pid, 'SIGKILL') + // eslint-disable-next-line no-await-in-loop -- Wait for each server to release Windows file handles. + expect(await waitForExit(pid)).toBe(true) } } - rmSync(cwd, { recursive: true, force: true, maxRetries: 5 }) + await rmAsync(cwd, { + recursive: true, + force: true, + maxRetries: 10, + retryDelay: 100, + }) }) it('passes arguments unchanged to a native executable', async () => { - const executable = path.join( - cwd, - process.platform === 'win32' ? 'native launcher.exe' : 'native launcher', - ) - copyFileSync(process.execPath, executable) + // Use the actual Node executable rather than copying it. Node 26 builds on + // macOS dynamically link libnode via @rpath, which a copied binary cannot + // resolve from this temporary directory. + const executable = process.execPath const result = await spawnBazel(executable, [launcher, ...ARGUMENTS], { cwd, }) @@ -427,8 +432,10 @@ if (args.includes('info')) { process.stdout.write(${JSON.stringify(`install_base expect(Date.now() - startedAt).toBeLessThan(15_000) } finally { const pid = readChildPid(record) - process.kill(pid, 'SIGKILL') - // Windows keeps the temporary directory locked until the process has exited. + if (isAlive(pid)) { + process.kill(pid, 'SIGKILL') + } + // Windows can report process exit before inherited handles release the cwd. expect(await waitForExit(pid)).toBe(true) } }, 30_000) diff --git a/src/commands/manifest/bazel/bazel-pypi-discovery.mts b/src/commands/manifest/bazel/bazel-pypi-discovery.mts index 3b62610e86..93589ba50c 100644 --- a/src/commands/manifest/bazel/bazel-pypi-discovery.mts +++ b/src/commands/manifest/bazel/bazel-pypi-discovery.mts @@ -1,9 +1,8 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { getErrorCause } from '../../../utils/errors.mts' +import { logger } from '../../../utils/logger.mts' import type { RepoProbe } from './bazel-repo-discovery.mts' diff --git a/src/commands/manifest/bazel/bazel-pypi-discovery.test.mts b/src/commands/manifest/bazel/bazel-pypi-discovery.test.mts index d4a9a8437e..5c85c3d4cb 100644 --- a/src/commands/manifest/bazel/bazel-pypi-discovery.test.mts +++ b/src/commands/manifest/bazel/bazel-pypi-discovery.test.mts @@ -5,14 +5,13 @@ import { fileURLToPath } from 'node:url' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { discoverPypiHubs, parseBazelModPipExtensionCandidates, parsePypiHubCandidates, validatePypiHub, } from './bazel-pypi-discovery.mts' +import { logger } from '../../../utils/logger.mts' import type { RepoProbe } from './bazel-repo-discovery.mts' diff --git a/src/commands/manifest/bazel/bazel-python-shim.mts b/src/commands/manifest/bazel/bazel-python-shim.mts index 20951b9b43..6c63cec6d6 100644 --- a/src/commands/manifest/bazel/bazel-python-shim.mts +++ b/src/commands/manifest/bazel/bazel-python-shim.mts @@ -1,9 +1,17 @@ import { execFileSync } from 'node:child_process' -import { mkdtempSync, symlinkSync } from 'node:fs' +import { + accessSync, + constants as fsConstants, + mkdtempSync, + rmSync, + statSync, + symlinkSync, +} from 'node:fs' import os from 'node:os' import path from 'node:path' import { findBazelExecutable, prependBazelPath } from './bazel-process.mts' +import { isStandaloneSea } from '../../../sea/runtime.mts' import { InputError } from '../../../utils/errors.mts' export type PythonShimResult = { @@ -37,7 +45,7 @@ function isPython3( function notPython3Error(executable: string): InputError { return new InputError( - `Python 3 is required for Bazel manifest extraction. ${executable} did not run as Python 3. Install Python 3 and ensure python${process.platform === 'win32' ? '.exe' : ''} is on PATH.`, + `Python 3 is required for Bazel manifest extraction. ${executable} did not run as Python 3. Install Python 3 and ensure python${process.platform === 'win32' ? '.exe' : ''} is on PATH or set SOCKET_CLI_PYTHON_PATH in a standalone build.`, ) } @@ -45,6 +53,61 @@ export async function provisionPythonShim( env: NodeJS.ProcessEnv, cwd: string, ): Promise { + if (isStandaloneSea()) { + const override = env['SOCKET_CLI_PYTHON_PATH'] + if (!override) { + throw new InputError( + 'Python is not bundled with this standalone Socket CLI. Set SOCKET_CLI_PYTHON_PATH to an installed Python 3 executable for Bazel manifest extraction.', + ) + } + const executable = path.resolve(cwd, override) + try { + accessSync( + executable, + process.platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK, + ) + if (!statSync(executable).isFile()) { + throw new Error('not a file') + } + } catch { + throw new InputError( + `SOCKET_CLI_PYTHON_PATH must point to an executable Python 3 binary, but '${executable}' is missing or not executable.`, + ) + } + if ( + process.platform === 'win32' && + path.extname(executable).toLowerCase() !== '.exe' + ) { + throw new InputError( + 'Bazel on Windows requires SOCKET_CLI_PYTHON_PATH to point to a native Python 3 python.exe.', + ) + } + if (!isPython3(executable, env, cwd)) { + throw notPython3Error(executable) + } + if (process.platform === 'win32') { + if (path.basename(executable).toLowerCase() !== 'python.exe') { + throw new InputError( + "SOCKET_CLI_PYTHON_PATH must name python.exe on Windows so its adjacent DLLs and standard library remain available. Point it to the Python installation's python.exe.", + ) + } + return { + env: prependBazelPath(env, path.dirname(executable)), + shimDir: undefined, + } + } + const shimDir = mkdtempSync(path.join(os.tmpdir(), 'socket-bazel-python-')) + try { + for (const name of ['python', 'python3']) { + const alias = path.join(shimDir, name) + symlinkSync(executable, alias) + } + } catch (error) { + rmSync(shimDir, { recursive: true, force: true }) + throw error + } + return { env: prependBazelPath(env, shimDir), shimDir } + } const python = await findBazelExecutable('python', env, cwd) if (python) { if ( diff --git a/src/commands/manifest/bazel/bazel-python-shim.test.mts b/src/commands/manifest/bazel/bazel-python-shim.test.mts index af1d8446d3..1dad2fa82b 100644 --- a/src/commands/manifest/bazel/bazel-python-shim.test.mts +++ b/src/commands/manifest/bazel/bazel-python-shim.test.mts @@ -9,10 +9,17 @@ import { import os from 'node:os' import path from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { findBazelExecutable, getBazelEnvironment } from './bazel-process.mts' +import { + findBazelExecutable, + getBazelEnvironment, + getBazelEnvironmentValue, +} from './bazel-process.mts' import { provisionPythonShim } from './bazel-python-shim.mts' +import { isStandaloneSea } from '../../../sea/runtime.mts' + +vi.mock('../../../sea/runtime.mts', () => ({ isStandaloneSea: vi.fn() })) const python = await findBazelExecutable( process.platform === 'win32' ? 'python' : 'python3', @@ -25,6 +32,7 @@ describe('provisionPythonShim', () => { const shims: string[] = [] beforeEach(() => { + vi.mocked(isStandaloneSea).mockReturnValue(false) cwd = mkdtempSync(path.join(os.tmpdir(), 'bazel python ')) }) @@ -122,4 +130,23 @@ describe('provisionPythonShim', () => { expect(readlinkSync(path.join(result.shimDir, 'python'))).toBe(python) }, ) + + it.skipIf(!python || process.platform !== 'win32')( + 'keeps an overridden Windows interpreter beside its runtime files', + async () => { + if (!python) { + throw new Error('Python fixture is unavailable') + } + vi.mocked(isStandaloneSea).mockReturnValue(true) + const env = getBazelEnvironment({ + SOCKET_CLI_PYTHON_PATH: path.relative(cwd, python), + Path: 'existing-path', + }) + const result = await provisionPythonShim(env, cwd) + expect(result.shimDir).toBeUndefined() + expect(getBazelEnvironmentValue(result.env, 'PATH')).toBe( + `${path.dirname(python)}${path.delimiter}existing-path`, + ) + }, + ) }) diff --git a/src/commands/manifest/bazel/bazel-query-runner.mts b/src/commands/manifest/bazel/bazel-query-runner.mts index 470f1cb285..af8e7d4065 100644 --- a/src/commands/manifest/bazel/bazel-query-runner.mts +++ b/src/commands/manifest/bazel/bazel-query-runner.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { buildBazelStartupFlags as buildStartupFlags, spawnBazel, } from './bazel-process.mts' import constants from '../../../constants.mts' import { InputError } from '../../../utils/errors.mts' +import { logger } from '../../../utils/logger.mts' import type { RepoProbe } from './bazel-repo-discovery.mts' diff --git a/src/commands/manifest/bazel/bazel-repo-discovery.mts b/src/commands/manifest/bazel/bazel-repo-discovery.mts index 931c79b25a..4b42293a7f 100644 --- a/src/commands/manifest/bazel/bazel-repo-discovery.mts +++ b/src/commands/manifest/bazel/bazel-repo-discovery.mts @@ -12,7 +12,7 @@ * No Starlark source is read by this module. All semantic interpretation * comes from Bazel itself (`mod show_extension`, `cquery`). */ -import { logger } from '@socketsecurity/registry/lib/logger' +import { logger } from '../../../utils/logger.mts' // The importer token Bazel prints for a hub generated for the root module // itself (`(imported by , …)`). Hubs imported only by rulesets diff --git a/src/commands/manifest/bazel/bazel-repo-discovery.test.mts b/src/commands/manifest/bazel/bazel-repo-discovery.test.mts index c5fd907465..7835c0aa3b 100644 --- a/src/commands/manifest/bazel/bazel-repo-discovery.test.mts +++ b/src/commands/manifest/bazel/bazel-repo-discovery.test.mts @@ -1,7 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { CONVENTIONAL_MAVEN_REPO_NAMES, classifyProbeResult, @@ -9,6 +7,7 @@ import { parseShowExtensionOutput, probeCandidate, } from './bazel-repo-discovery.mts' +import { logger } from '../../../utils/logger.mts' import type { ProbeResult, diff --git a/src/commands/manifest/bazel/bazel-workspace-walk.mts b/src/commands/manifest/bazel/bazel-workspace-walk.mts index 0de8a4cf80..2e800cc2cf 100644 --- a/src/commands/manifest/bazel/bazel-workspace-walk.mts +++ b/src/commands/manifest/bazel/bazel-workspace-walk.mts @@ -28,7 +28,7 @@ import { readdirSync } from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' +import { logger } from '../../../utils/logger.mts' // Hard ceiling on workspace roots; 16 sits well above realistic monorepo // counts while tightening the guard against pathological inputs. diff --git a/src/commands/manifest/bazel/bazel-workspace-walk.test.mts b/src/commands/manifest/bazel/bazel-workspace-walk.test.mts index 1cbf55daaf..185b68f356 100644 --- a/src/commands/manifest/bazel/bazel-workspace-walk.test.mts +++ b/src/commands/manifest/bazel/bazel-workspace-walk.test.mts @@ -10,9 +10,8 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { findWorkspaceRoots } from './bazel-workspace-walk.mts' +import { logger } from '../../../utils/logger.mts' function touch(file: string): void { mkdirSync(path.dirname(file), { recursive: true }) diff --git a/src/commands/manifest/bazel/cmd-manifest-bazel.mts b/src/commands/manifest/bazel/cmd-manifest-bazel.mts index c4bac58711..759fd1a098 100644 --- a/src/commands/manifest/bazel/cmd-manifest-bazel.mts +++ b/src/commands/manifest/bazel/cmd-manifest-bazel.mts @@ -1,15 +1,14 @@ import path from 'node:path' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { extractBazelToMaven } from './extract_bazel_to_maven.mts' import { extractBazelToPypi } from './extract_bazel_to_pypi.mts' import { DRY_RUN_BAILING_NOW, SOCKET_JSON } from '../../../constants.mts' import { commonFlags } from '../../../flags.mts' import { checkCommandInput } from '../../../utils/check-input.mts' +import { debugFn } from '../../../utils/debug.mts' import { InputError } from '../../../utils/errors.mts' import { getOutputKind } from '../../../utils/get-output-kind.mts' +import { logger } from '../../../utils/logger.mts' import { meowOrExit } from '../../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../../utils/output-formatting.mts' import { readOrDefaultSocketJson } from '../../../utils/socket-json.mts' diff --git a/src/commands/manifest/bazel/cmd-manifest-bazel.test.mts b/src/commands/manifest/bazel/cmd-manifest-bazel.test.mts index 4936f484f3..324a02c5ef 100644 --- a/src/commands/manifest/bazel/cmd-manifest-bazel.test.mts +++ b/src/commands/manifest/bazel/cmd-manifest-bazel.test.mts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' +import { logger } from '../../../utils/logger.mts' // Mock the extractor so the `run` wiring test can assert which timeout reaches // it without a real Bazel toolchain. The `cmdit`/spawnSocketCli tests below diff --git a/src/commands/manifest/bazel/extract_bazel_to_maven.mts b/src/commands/manifest/bazel/extract_bazel_to_maven.mts index e71814daeb..6b3df68da6 100644 --- a/src/commands/manifest/bazel/extract_bazel_to_maven.mts +++ b/src/commands/manifest/bazel/extract_bazel_to_maven.mts @@ -1,8 +1,6 @@ import { existsSync, promises as fs, mkdirSync, readdirSync } from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { resolveBazelBinary } from './bazel-bin-detect.mts' import { BAZEL_OUTPUT_ROOT_PREFIX, @@ -33,6 +31,7 @@ import { import { findWorkspaceRoots } from './bazel-workspace-walk.mts' import { getErrorCause } from '../../../utils/errors.mts' import { IGNORED_DIRS } from '../../../utils/glob.mts' +import { logger } from '../../../utils/logger.mts' import type { CqueryRepoResult, ExtractedArtifact } from './bazel-cquery.mts' import type { BazelQueryOptions } from './bazel-query-runner.mts' diff --git a/src/commands/manifest/bazel/extract_bazel_to_maven.test.mts b/src/commands/manifest/bazel/extract_bazel_to_maven.test.mts index b0ccca669d..928313eb23 100644 --- a/src/commands/manifest/bazel/extract_bazel_to_maven.test.mts +++ b/src/commands/manifest/bazel/extract_bazel_to_maven.test.mts @@ -11,8 +11,6 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - // Mock collaborators BEFORE importing the orchestrator. The orchestrator // composes pure-function discovery + the metadata cquery + a workspace // walker; mocking these lets us drive end-to-end behaviour without a @@ -58,7 +56,7 @@ vi.mock('./bazel-cquery.mts', () => ({ runMetadataCqueryForRepo: vi.fn(), })) // Quiet the spawn calls reapBazelServer makes during cleanup. -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ +vi.mock('../../../utils/spawn.mts', () => ({ spawn: vi.fn(async () => ({ code: 0, stdout: '', stderr: '' })), })) @@ -75,6 +73,7 @@ import { extractBazelToMaven, normalizeToMavenInstallJson, } from './extract_bazel_to_maven.mts' +import { logger } from '../../../utils/logger.mts' import type { CqueryRepoResult, ExtractedArtifact } from './bazel-cquery.mts' diff --git a/src/commands/manifest/bazel/extract_bazel_to_pypi.mts b/src/commands/manifest/bazel/extract_bazel_to_pypi.mts index 6895fb761f..db5fd34787 100644 --- a/src/commands/manifest/bazel/extract_bazel_to_pypi.mts +++ b/src/commands/manifest/bazel/extract_bazel_to_pypi.mts @@ -1,8 +1,6 @@ import { existsSync, promises as fs, mkdirSync } from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { resolveBazelBinary } from './bazel-bin-detect.mts' import { removeBazelTempdir } from './bazel-cleanup.mts' import { validateOutputBase } from './bazel-output-base-check.mts' @@ -33,6 +31,7 @@ import { getBazelInvocationFlags, } from './bazel-workspace-detect.mts' import { InputError, getErrorCause } from '../../../utils/errors.mts' +import { logger } from '../../../utils/logger.mts' import type { PypiHubCandidate } from './bazel-pypi-discovery.mts' import type { diff --git a/src/commands/manifest/cmd-manifest-auto.test.mts b/src/commands/manifest/cmd-manifest-auto.integration.test.mts similarity index 100% rename from src/commands/manifest/cmd-manifest-auto.test.mts rename to src/commands/manifest/cmd-manifest-auto.integration.test.mts diff --git a/src/commands/manifest/cmd-manifest-auto.mts b/src/commands/manifest/cmd-manifest-auto.mts index e8451ba575..921d4270cd 100644 --- a/src/commands/manifest/cmd-manifest-auto.mts +++ b/src/commands/manifest/cmd-manifest-auto.mts @@ -1,16 +1,15 @@ import path from 'node:path' -import { debugDir } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { detectManifestActions } from './detect-manifest-actions.mts' import { generateAutoManifest } from './generate_auto_manifest.mts' import { excludePathsFlag } from './manifest-flags.mts' import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' +import { debugDir } from '../../utils/debug.mts' import { withTmpDir } from '../../utils/fs.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/cmd-manifest-cdxgen.mts b/src/commands/manifest/cmd-manifest-cdxgen.mts index defe312dd4..dc01c3100b 100644 --- a/src/commands/manifest/cmd-manifest-cdxgen.mts +++ b/src/commands/manifest/cmd-manifest-cdxgen.mts @@ -1,16 +1,21 @@ +import { existsSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' + import terminalLink from 'terminal-link' import yargsParse from 'yargs-parser' - -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' -import { isPath } from '@socketsecurity/registry/lib/path' -import { pluralize } from '@socketsecurity/registry/lib/words' +import colors from 'yoctocolors-cjs' import { runCdxgen } from './run-cdxgen.mts' import constants, { FLAG_HELP } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' +import { isStandaloneSea } from '../../sea/runtime.mts' +import { joinAnd } from '../../utils/arrays.mts' import { filterFlags, isHelpFlag } from '../../utils/cmd.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' +import { isPath } from '../../utils/path.mts' +import { pluralize } from '../../utils/words.mts' import type { CliCommandConfig, @@ -292,6 +297,25 @@ async function run( return } + // The standalone build must not start cdxgen just to display its help: + // cdxgen is an optional payload and can be unavailable for this target. + // Keep this local help useful without resolving or installing that payload. + if (yargv.help && isStandaloneSea()) { + logger.log(` +Usage + socket manifest cdxgen [options] [path] + +Options + -h, --help Show this help + -o, --output Output file (default: socket-cdx.json) + -t, --type npm, nodejs, javascript, js, typescript, ts, or tsx + --lifecycle pre-build Read dependency metadata without running project installs + +Generates a CycloneDX SBOM from a root npm package-lock.json v3. +Only one npm project is supported in this standalone build. Other cdxgen modes are unavailable.`) + return + } + // Change defaults when not passing the --help flag. if (!yargv.help) { // Make 'lifecycle' default to 'pre-build', which also sets 'install-deps' to `false`, @@ -316,6 +340,30 @@ async function run( const { spawnPromise } = await runCdxgen(yargv) + if (isStandaloneSea()) { + // The embedded Worker exposes an awaited result. Avoid translating its + // synthetic exit event into process.exit(), which can bypass CLI error + // formatting and telemetry drain on rejection or cancellation. + const result = (await spawnPromise) as unknown as { + code: number | null + signal: NodeJS.Signals | null + } + if (result.signal) { + process.exitCode = 128 + (os.constants.signals[result.signal] ?? 1) + } else { + process.exitCode = typeof result.code === 'number' ? result.code : 1 + if ( + result.code === 0 && + typeof yargv.output === 'string' && + yargv.output !== '-' && + existsSync(path.resolve(process.cwd(), yargv.output)) + ) { + logger.log(colors.cyanBright(`${yargv.output} created!`)) + } + } + return + } + // See https://nodejs.org/api/child_process.html#event-exit. spawnPromise.process.on('exit', (code, signalName) => { if (signalName) { diff --git a/src/commands/manifest/cmd-manifest-cdxgen.test.mts b/src/commands/manifest/cmd-manifest-cdxgen.test.mts index eff84b1c1d..b8627ec916 100644 --- a/src/commands/manifest/cmd-manifest-cdxgen.test.mts +++ b/src/commands/manifest/cmd-manifest-cdxgen.test.mts @@ -2,9 +2,6 @@ import path from 'node:path' import { describe, expect, it } from 'vitest' -import { LOG_SYMBOLS } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { cleanOutput, cmdit, @@ -18,6 +15,8 @@ import constants, { FLAG_VERSION, REDACTED, } from '../../constants.mts' +import { LOG_SYMBOLS } from '../../utils/logger.mts' +import { spawn } from '../../utils/spawn.mts' import type { MatcherContext } from '@vitest/expect' diff --git a/src/commands/manifest/cmd-manifest-conda.mts b/src/commands/manifest/cmd-manifest-conda.mts index 2617c208bc..1e5750faad 100644 --- a/src/commands/manifest/cmd-manifest-conda.mts +++ b/src/commands/manifest/cmd-manifest-conda.mts @@ -1,7 +1,5 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { findCondaFile } from './detect-manifest-actions.mts' import { handleManifestConda } from './handle-manifest-conda.mts' import constants, { @@ -15,6 +13,7 @@ import constants, { import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/cmd-manifest-dotnet.mts b/src/commands/manifest/cmd-manifest-dotnet.mts index 8ce0292366..d17a6de1fd 100644 --- a/src/commands/manifest/cmd-manifest-dotnet.mts +++ b/src/commands/manifest/cmd-manifest-dotnet.mts @@ -1,9 +1,5 @@ import path from 'node:path' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { safeStatsSync } from '@socketsecurity/registry/lib/fs' -import { logger } from '@socketsecurity/registry/lib/logger' - import { convertDotnetToFacts } from './convert-dotnet-to-facts.mts' import { resolveEcosystemConfig } from './generate-recursive-manifests.mts' import { excludePathsFlag } from './manifest-flags.mts' @@ -12,7 +8,10 @@ import constants, { SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' +import { debugFn } from '../../utils/debug.mts' +import { safeStatsSync } from '../../utils/fs.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/cmd-manifest-dynamic-sbom-inference.mts b/src/commands/manifest/cmd-manifest-dynamic-sbom-inference.mts index 054d8b56f6..e88262563b 100644 --- a/src/commands/manifest/cmd-manifest-dynamic-sbom-inference.mts +++ b/src/commands/manifest/cmd-manifest-dynamic-sbom-inference.mts @@ -1,7 +1,5 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleManifestDynamicSbomInference } from './handle-manifest-dynamic-sbom-inference.mts' import { excludePathsFlag } from './manifest-flags.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' @@ -9,6 +7,7 @@ import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { assertValidExcludePaths } from '../scan/exclude-paths.mts' diff --git a/src/commands/manifest/cmd-manifest-gradle.mts b/src/commands/manifest/cmd-manifest-gradle.mts index c30bd27243..0a04b6c4ef 100644 --- a/src/commands/manifest/cmd-manifest-gradle.mts +++ b/src/commands/manifest/cmd-manifest-gradle.mts @@ -1,8 +1,5 @@ import path from 'node:path' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { convertGradleToFacts } from './convert-gradle-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' import { excludePathsFlag } from './manifest-flags.mts' @@ -12,7 +9,9 @@ import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' +import { debugFn } from '../../utils/debug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/cmd-manifest-kotlin.mts b/src/commands/manifest/cmd-manifest-kotlin.mts index 030ca6a893..fd2479621a 100644 --- a/src/commands/manifest/cmd-manifest-kotlin.mts +++ b/src/commands/manifest/cmd-manifest-kotlin.mts @@ -1,8 +1,5 @@ import path from 'node:path' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { convertGradleToFacts } from './convert-gradle-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' import { excludePathsFlag } from './manifest-flags.mts' @@ -12,7 +9,9 @@ import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' +import { debugFn } from '../../utils/debug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/cmd-manifest-maven.mts b/src/commands/manifest/cmd-manifest-maven.mts index e9e64447fe..0b1b8c4bf4 100644 --- a/src/commands/manifest/cmd-manifest-maven.mts +++ b/src/commands/manifest/cmd-manifest-maven.mts @@ -1,8 +1,5 @@ import path from 'node:path' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { convertMavenToFacts } from './convert-maven-to-facts.mts' import { excludePathsFlag } from './manifest-flags.mts' import { parseBuildToolOpts } from './parse-build-tool-opts.mts' @@ -11,7 +8,9 @@ import constants, { SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' +import { debugFn } from '../../utils/debug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/cmd-manifest-scala.mts b/src/commands/manifest/cmd-manifest-scala.mts index c9b2152dbd..45d6750817 100644 --- a/src/commands/manifest/cmd-manifest-scala.mts +++ b/src/commands/manifest/cmd-manifest-scala.mts @@ -1,8 +1,5 @@ import path from 'node:path' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { convertSbtToFacts } from './convert-sbt-to-facts.mts' import { convertSbtToMaven } from './convert_sbt_to_maven.mts' import { excludePathsFlag } from './manifest-flags.mts' @@ -11,8 +8,10 @@ import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' +import { debugFn } from '../../utils/debug.mts' import { withTmpDir } from '../../utils/fs.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/cmd-manifest-setup.mts b/src/commands/manifest/cmd-manifest-setup.mts index 6264479250..aea37d9ef2 100644 --- a/src/commands/manifest/cmd-manifest-setup.mts +++ b/src/commands/manifest/cmd-manifest-setup.mts @@ -1,11 +1,10 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleManifestSetup } from './handle-manifest-setup.mts' import constants, { SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { assertValidExcludePaths } from '../scan/exclude-paths.mts' diff --git a/src/commands/manifest/convert-conda-to-requirements.mts b/src/commands/manifest/convert-conda-to-requirements.mts index 902d2511d5..3a9d10988c 100644 --- a/src/commands/manifest/convert-conda-to-requirements.mts +++ b/src/commands/manifest/convert-conda-to-requirements.mts @@ -1,8 +1,8 @@ import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' -import { stripAnsi } from '@socketsecurity/registry/lib/strings' +import { logger } from '../../utils/logger.mts' +import { stripAnsi } from '../../utils/strings.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/manifest/convert-dotnet-to-facts.mts b/src/commands/manifest/convert-dotnet-to-facts.mts index 79150ba4b8..d9be61551f 100644 --- a/src/commands/manifest/convert-dotnet-to-facts.mts +++ b/src/commands/manifest/convert-dotnet-to-facts.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { findDotnetEntries } from './detect-manifest-actions.mts' import { realpathOrResolved } from './discover-manifest-roots.mts' import { buildUncoveredRoot } from './generate-recursive-manifests.mts' +import { logger } from '../../utils/logger.mts' import type { EcosystemBuildConfig } from './generate-recursive-manifests.mts' import type { RunManifestFactsOutcome } from './run-manifest-facts.mts' diff --git a/src/commands/manifest/convert_gradle_to_maven.mts b/src/commands/manifest/convert_gradle_to_maven.mts index ec50691c1c..69cdcd8143 100644 --- a/src/commands/manifest/convert_gradle_to_maven.mts +++ b/src/commands/manifest/convert_gradle_to_maven.mts @@ -1,10 +1,11 @@ import fs from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants from '../../constants.mts' +import { resolveBuildToolBin } from './scripts/build-tool.mts' +import { isStandaloneSea } from '../../sea/runtime.mts' +import { logger } from '../../utils/logger.mts' +import { spawn } from '../../utils/spawn.mts' export async function convertGradleToMaven({ bin, @@ -21,9 +22,18 @@ export async function convertGradleToMaven({ }) { // TODO: Implement json/md. + let selectedBin: string + try { + selectedBin = resolveBuildToolBin('gradle', cwd, bin, isStandaloneSea()) + } catch (error) { + process.exitCode = 1 + logger.fail(error instanceof Error ? error.message : String(error)) + return + } + // Note: use resolve because the bin could be an absolute path, away from cwd // TODO: what about $PATH resolved commands? (`gradlew` without dir prefix) - const rBin = path.resolve(cwd, bin) + const rBin = path.resolve(cwd, selectedBin) const binExists = fs.existsSync(rBin) const cwdExists = fs.existsSync(cwd) @@ -64,9 +74,11 @@ export async function convertGradleToMaven({ 'pom', ] if (verbose) { - logger.log('[VERBOSE] Executing:', [bin], ', args:', commandArgs) + logger.log('[VERBOSE] Executing:', [selectedBin], ', args:', commandArgs) } - logger.log(`Converting gradle to maven from \`${bin}\` on \`${cwd}\` ...`) + logger.log( + `Converting gradle to maven from \`${selectedBin}\` on \`${cwd}\` ...`, + ) const output = await execGradle(rBin, commandArgs, cwd, verbose) if (output.code) { process.exitCode = 1 diff --git a/src/commands/manifest/convert_sbt_to_maven.mts b/src/commands/manifest/convert_sbt_to_maven.mts index eeda44768a..7d28830b70 100644 --- a/src/commands/manifest/convert_sbt_to_maven.mts +++ b/src/commands/manifest/convert_sbt_to_maven.mts @@ -1,12 +1,13 @@ import { promises as fs } from 'node:fs' import path from 'node:path' -import { safeReadFile } from '@socketsecurity/registry/lib/fs' -import { logger } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants from '../../constants.mts' +import { resolveBuildToolBin } from './scripts/build-tool.mts' +import { isStandaloneSea } from '../../sea/runtime.mts' import { getErrorCause } from '../../utils/errors.mts' +import { safeReadFile } from '../../utils/fs.mts' +import { logger } from '../../utils/logger.mts' +import { spawn } from '../../utils/spawn.mts' // Walk up from a pom path to find a `target` directory ancestor and return // its parent (the project root). Returns undefined if no `target` ancestor @@ -39,22 +40,33 @@ export async function convertSbtToMaven({ }) { // TODO: Implement json/md. + let selectedBin: string + try { + selectedBin = resolveBuildToolBin('sbt', cwd, bin, isStandaloneSea()) + } catch (error) { + process.exitCode = 1 + logger.fail(error instanceof Error ? error.message : String(error)) + return + } + const { spinner } = constants logger.group('sbt2maven:') - logger.info(`- executing: \`${bin}\``) + logger.info(`- executing: \`${selectedBin}\``) logger.info(`- src dir: \`${cwd}\``) logger.groupEnd() try { - spinner.start(`Converting sbt to maven from \`${bin}\` on \`${cwd}\`...`) + spinner.start( + `Converting sbt to maven from \`${selectedBin}\` on \`${cwd}\`...`, + ) // Run sbt with the init script we provide which should yield zero or more // pom files. We have to figure out where to store those pom files such that // we can upload them and predict them through the GitHub API. We could do a // .socket folder. We could do a socket.pom.gz with all the poms, although // I'd prefer something plain-text if it is to be committed. - const output = await spawn(bin, ['makePom', ...sbtOpts], { cwd }) + const output = await spawn(selectedBin, ['makePom', ...sbtOpts], { cwd }) spinner.stop() diff --git a/src/commands/manifest/detect-manifest-actions.mts b/src/commands/manifest/detect-manifest-actions.mts index 5cee9f39c5..118512435d 100644 --- a/src/commands/manifest/detect-manifest-actions.mts +++ b/src/commands/manifest/detect-manifest-actions.mts @@ -4,14 +4,13 @@ import { existsSync, readdirSync } from 'node:fs' import path from 'node:path' -import { debugLog } from '@socketsecurity/registry/lib/debug' - import { compareEntryFiles, isBuildFile } from './discover-manifest-roots.mts' import { ENVIRONMENT_YAML, ENVIRONMENT_YML, SOCKET_JSON, } from '../../constants.mts' +import { debugLog } from '../../utils/debug.mts' import type { SocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/discover-manifest-roots.mts b/src/commands/manifest/discover-manifest-roots.mts index 607c33e42a..b8e3bb7c16 100644 --- a/src/commands/manifest/discover-manifest-roots.mts +++ b/src/commands/manifest/discover-manifest-roots.mts @@ -3,8 +3,6 @@ import path from 'node:path' import micromatch from 'micromatch' -import { logger } from '@socketsecurity/registry/lib/logger' - import { BUILD_FILES, BUILD_TOOLS, @@ -15,6 +13,7 @@ import { import { listDotnetEntryProjects } from './scripts/run.mts' import { realpathOrResolved } from '../../utils/fs.mts' import { globWithGitIgnore } from '../../utils/glob.mts' +import { logger } from '../../utils/logger.mts' import { excludePathToScanIgnores } from '../scan/exclude-paths.mts' import type { BuildTool } from './scripts/build-tool.mts' diff --git a/src/commands/manifest/enumerate-workspaces.mts b/src/commands/manifest/enumerate-workspaces.mts index 04c79a982a..1551734a76 100644 --- a/src/commands/manifest/enumerate-workspaces.mts +++ b/src/commands/manifest/enumerate-workspaces.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { expandEnvVarRefs, formatMissingEnvVarRefs, } from './expand-env-var-refs.mts' import { enumerateWorkspaces as enumerateWorkspacesScript } from './scripts/run.mts' import { getErrorMessageOr } from '../../utils/errors.mts' +import { logger } from '../../utils/logger.mts' import type { BuildTool } from './scripts/build-tool.mts' import type { SocketFactsSbomProject } from './scripts/facts.mts' diff --git a/src/commands/manifest/generate-recursive-manifests.mts b/src/commands/manifest/generate-recursive-manifests.mts index c9a8150347..13d116c87e 100644 --- a/src/commands/manifest/generate-recursive-manifests.mts +++ b/src/commands/manifest/generate-recursive-manifests.mts @@ -1,7 +1,5 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { addCoveredBuildFiles, buildRootPath, @@ -21,6 +19,7 @@ import { import { SOCKET_JSON } from '../../constants.mts' import { loadSocketConfig } from '../../utils/config.mts' import { withTmpDir } from '../../utils/fs.mts' +import { logger } from '../../utils/logger.mts' import { readOrDefaultSocketJson, readSocketJsonCascade, diff --git a/src/commands/manifest/generate-recursive-manifests.test.mts b/src/commands/manifest/generate-recursive-manifests.test.mts index f84dc555e5..979eded5df 100644 --- a/src/commands/manifest/generate-recursive-manifests.test.mts +++ b/src/commands/manifest/generate-recursive-manifests.test.mts @@ -4,8 +4,6 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - vi.mock('../../utils/socket-json.mts', () => ({ readOrDefaultSocketJson: vi.fn(() => ({})), // Default: no per-root override found, fall back to the root config - matches @@ -25,6 +23,7 @@ import { generateRecursiveManifests } from './generate-recursive-manifests.mts' import { runManifestFacts } from './run-manifest-facts.mts' import { listDotnetEntryProjects } from './scripts/run.mts' import { testPath } from '../../../test/utils.mts' +import { logger } from '../../utils/logger.mts' import { readOrDefaultSocketJson, readSocketJsonCascade, diff --git a/src/commands/manifest/generate_auto_manifest.mts b/src/commands/manifest/generate_auto_manifest.mts index baa3294813..a45fa26736 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -1,7 +1,5 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { extractBazelToMaven } from './bazel/extract_bazel_to_maven.mts' import { convertDotnetToFacts } from './convert-dotnet-to-facts.mts' import { convertGradleToFacts } from './convert-gradle-to-facts.mts' @@ -21,6 +19,7 @@ import { SOCKET_JSON, } from '../../constants.mts' import { InputError } from '../../utils/errors.mts' +import { logger } from '../../utils/logger.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' import type { GeneratableManifests } from './detect-manifest-actions.mts' diff --git a/src/commands/manifest/generate_auto_manifest.test.mts b/src/commands/manifest/generate_auto_manifest.test.mts index e505e6cba0..f4a5e4b1b5 100644 --- a/src/commands/manifest/generate_auto_manifest.test.mts +++ b/src/commands/manifest/generate_auto_manifest.test.mts @@ -32,14 +32,13 @@ vi.mock('../../utils/socket-json.mts', () => ({ readOrDefaultSocketJson: vi.fn(() => ({})), })) -import { logger } from '@socketsecurity/registry/lib/logger' - import { extractBazelToMaven } from './bazel/extract_bazel_to_maven.mts' import { convertDotnetToFacts } from './convert-dotnet-to-facts.mts' import { convertGradleToFacts } from './convert-gradle-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' import { generateAutoManifest } from './generate_auto_manifest.mts' import { handleManifestConda } from './handle-manifest-conda.mts' +import { logger } from '../../utils/logger.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' import type { SocketJson } from '../../utils/socket-json.mts' diff --git a/src/commands/manifest/output-manifest-dynamic-sbom-inference.mts b/src/commands/manifest/output-manifest-dynamic-sbom-inference.mts index dcdfde7244..556c08398a 100644 --- a/src/commands/manifest/output-manifest-dynamic-sbom-inference.mts +++ b/src/commands/manifest/output-manifest-dynamic-sbom-inference.mts @@ -1,7 +1,6 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { buildRootPath } from './discover-manifest-roots.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { RecursiveManifestOutcome } from './generate-recursive-manifests.mts' diff --git a/src/commands/manifest/output-manifest-setup.mts b/src/commands/manifest/output-manifest-setup.mts index dff7b75c69..56e3aa5f94 100644 --- a/src/commands/manifest/output-manifest-setup.mts +++ b/src/commands/manifest/output-manifest-setup.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/manifest/output-requirements.mts b/src/commands/manifest/output-requirements.mts index 9a38131547..085cc10bb3 100644 --- a/src/commands/manifest/output-requirements.mts +++ b/src/commands/manifest/output-requirements.mts @@ -1,9 +1,8 @@ import fs from 'node:fs' -import { logger } from '@socketsecurity/registry/lib/logger' - import { REQUIREMENTS_TXT } from '../../constants.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/manifest/run-cdxgen.mts b/src/commands/manifest/run-cdxgen.mts index 20bfa22fbf..58ffb6d369 100644 --- a/src/commands/manifest/run-cdxgen.mts +++ b/src/commands/manifest/run-cdxgen.mts @@ -1,13 +1,22 @@ -import { existsSync, rmSync } from 'node:fs' +import { EventEmitter } from 'node:events' +import { existsSync, promises as fs, rmSync } from 'node:fs' import path from 'node:path' import colors from 'yoctocolors-cjs' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants, { FLAG_HELP, NPM, PNPM, YARN } from '../../constants.mts' +import { runCdxgen as runEmbeddedCdxgen } from '../../sea/adapters/cdxgen.mts' +import { + convertYarnClassicToNpmLock, + removeOwnedPackageLock, +} from '../../sea/adapters/synp.mts' +import { + assertEmbeddedToolAvailable, + isStandaloneSea, +} from '../../sea/runtime.mts' import { spawnCdxgenDlx, spawnSynpDlx } from '../../utils/dlx.mts' import { findUp } from '../../utils/fs.mts' +import { logger } from '../../utils/logger.mts' import { isYarnBerry } from '../../utils/yarn-version.mts' import type { ShadowBinResult } from '../../shadow/npm/bin.mts' @@ -63,6 +72,282 @@ function argvObjectToArray(argvObj: ArgvObject): string[] { } export async function runCdxgen(argvObj: ArgvObject): Promise { + if (isStandaloneSea()) { + const unsupported = new Set([ + 'deep', + 'evidence', + 'include-formulation', + 'include-crypto', + 'install-deps', + 'multi-project', + 'recurse', + 'server', + 'export-proto', + 'proto-bin-file', + 'profile', + 'feature-flags', + 'filter', + 'only', + 'technique', + 'standard', + 'exclude-type', + 'babel', + 'validate', + ]) + for (const key of Object.keys(argvObj)) { + if (unsupported.has(key)) { + const value = argvObj[key] + if (value !== false && value !== undefined && value !== null) { + throw new Error( + `The standalone cdxgen build does not support --${key}.`, + ) + } + } else if ( + !['_', '--', 'output', 'type', 'lifecycle', 'help'].includes(key) + ) { + throw new Error( + `The standalone cdxgen build does not support --${key}.`, + ) + } + } + if (argvObj['help']) { + throw new Error('Help output is handled before cdxgen execution.') + } + if (argvObj['lifecycle'] && argvObj['lifecycle'] !== 'pre-build') { + throw new Error( + 'The standalone cdxgen build supports only the pre-build lifecycle.', + ) + } + if ( + argvObj['type'] && + ![ + 'npm', + 'nodejs', + 'javascript', + 'js', + 'typescript', + 'ts', + 'tsx', + ].includes(String(argvObj['type']).toLowerCase()) + ) { + throw new Error('The standalone cdxgen build supports only npm projects.') + } + + const positional = Array.isArray(argvObj['_']) ? argvObj['_'] : [] + if ( + positional.length > 1 || + (positional[0] && path.resolve(String(positional[0])) !== process.cwd()) + ) { + throw new Error( + 'The standalone cdxgen build supports one project at the current working directory.', + ) + } + if (Array.isArray(argvObj['--']) && argvObj['--'].length) { + throw new Error( + 'The standalone cdxgen build does not support arguments after --.', + ) + } + + const root = process.cwd() + const packageLockPath = path.join(root, PACKAGE_LOCK_JSON) + const yarnLockPath = path.join(root, YARN_LOCK) + if ( + existsSync(path.join(root, 'npm-shrinkwrap.json')) || + existsSync(path.join(root, 'rush.json')) || + existsSync(path.join(root, PNPM_LOCK_YAML)) + ) { + throw new Error( + 'The standalone cdxgen build supports one npm or Yarn Classic project without shrinkwrap, pnpm, or Rush configuration.', + ) + } + let packageJson: { + workspaces?: unknown + workspace?: unknown + dependencies?: Record + devDependencies?: Record + optionalDependencies?: Record + peerDependencies?: Record + } + try { + packageJson = JSON.parse( + await fs.readFile(path.join(root, 'package.json'), 'utf8'), + ) as typeof packageJson + } catch { + throw new Error( + 'The standalone cdxgen build requires a valid root package.json.', + ) + } + if ( + !packageJson || + typeof packageJson !== 'object' || + Array.isArray(packageJson) + ) { + throw new Error( + 'The standalone cdxgen build requires a valid root package.json object.', + ) + } + if (packageJson.workspaces || packageJson.workspace) { + throw new Error( + 'The standalone cdxgen build does not support workspaces.', + ) + } + const localDependency = (value: unknown): boolean => + typeof value === 'string' && /^(?:file:|link:|workspace:)/i.test(value) + for (const group of [ + packageJson.dependencies, + packageJson.devDependencies, + packageJson.optionalDependencies, + packageJson.peerDependencies, + ]) { + if (Object.values(group ?? {}).some(localDependency)) { + throw new Error( + 'The standalone cdxgen build does not support local or workspace dependencies.', + ) + } + } + + const hasPackageLock = existsSync(packageLockPath) + const hasYarnLock = existsSync(yarnLockPath) + if (hasPackageLock && hasYarnLock) { + throw new Error( + 'The standalone cdxgen build does not support projects with both npm and Yarn lockfiles.', + ) + } + let generatedLock: + | Awaited> + | undefined + const abortController = new AbortController() + const onSigint = () => + abortController.abort( + Object.assign(new Error('Interrupted'), { code: 'SIGINT' }), + ) + const onSigterm = () => + abortController.abort( + Object.assign(new Error('Terminated'), { code: 'SIGTERM' }), + ) + const onSighup = () => + abortController.abort( + Object.assign(new Error('Hangup'), { code: 'SIGHUP' }), + ) + let signalHandlersInstalled = false + const installSignalHandlers = () => { + if (signalHandlersInstalled) { + return + } + signalHandlersInstalled = true + process.once('SIGINT', onSigint) + process.once('SIGTERM', onSigterm) + process.once('SIGHUP', onSighup) + } + const removeSignalHandlers = () => { + if (!signalHandlersInstalled) { + return + } + signalHandlersInstalled = false + process.removeListener('SIGINT', onSigint) + process.removeListener('SIGTERM', onSigterm) + process.removeListener('SIGHUP', onSighup) + } + if (hasPackageLock) { + let lock: { lockfileVersion?: number } + try { + lock = JSON.parse(await fs.readFile(packageLockPath, 'utf8')) as { + lockfileVersion?: number + } + } catch { + throw new Error( + 'The standalone cdxgen build requires a valid npm package-lock.json.', + ) + } + if (lock.lockfileVersion !== 3) { + throw new Error( + 'The standalone cdxgen build requires npm package-lock v3; only Yarn Classic conversion uses v1.', + ) + } + assertEmbeddedToolAvailable('cdxgen', 'manifest-cdxgen-npm-lock-v3') + } else if (hasYarnLock) { + const yarnText = await fs.readFile(yarnLockPath, 'utf8') + if ( + !/^# yarn lockfile v1(?:\r?\n|$)/m.test(yarnText) || + /^__metadata:\s*$/m.test(yarnText) || + existsSync(path.join(root, '.yarnrc.yml')) + ) { + throw new Error( + 'The standalone cdxgen build supports Yarn Classic lockfile v1 only; Yarn Berry is unavailable.', + ) + } + if (!existsSync(path.join(root, 'node_modules'))) { + throw new Error( + 'Yarn Classic conversion requires an existing node_modules directory.', + ) + } + assertEmbeddedToolAvailable('cdxgen', 'manifest-cdxgen-yarn-classic') + assertEmbeddedToolAvailable('synp') + installSignalHandlers() + try { + generatedLock = await convertYarnClassicToNpmLock( + root, + abortController.signal, + ) + } catch (error) { + removeSignalHandlers() + if (abortController.signal.aborted) { + const reason = abortController.signal.reason as { code?: unknown } + if (typeof reason?.code === 'string') { + if (isStandaloneSea()) { + await removeOwnedPackageLock(generatedLock) + const canceled = Promise.resolve({ + code: null, + signal: reason.code as NodeJS.Signals, + cancelled: true, + }) as unknown as ShadowBinResult['spawnPromise'] + Object.defineProperty(canceled, 'process', { + value: new EventEmitter(), + }) + return { spawnPromise: canceled } + } + process.kill(process.pid, reason.code as NodeJS.Signals) + } + } + throw error + } + } else { + throw new Error( + 'The standalone cdxgen build requires npm package-lock v3 or Yarn Classic lockfile v1 with node_modules.', + ) + } + + const output = + typeof argvObj['output'] === 'string' + ? argvObj['output'] + : 'socket-cdx.json' + const outputPath = output === '-' ? '-' : path.resolve(root, output) + if (generatedLock && outputPath === packageLockPath) { + await removeOwnedPackageLock(generatedLock) + removeSignalHandlers() + throw new Error( + 'The standalone cdxgen build cannot use package-lock.json as the SBOM output path.', + ) + } + installSignalHandlers() + try { + const embedded = await runEmbeddedCdxgen( + root, + outputPath, + { stdio: 'inherit', signal: abortController.signal }, + async () => { + removeSignalHandlers() + await removeOwnedPackageLock(generatedLock) + }, + ) + return embedded as ShadowBinResult + } catch (error) { + removeSignalHandlers() + await removeOwnedPackageLock(generatedLock) + throw error + } + } + const argvMutable = { __proto__: null, ...argvObj } as ArgvObject const shadowOpts: DlxOptions = { diff --git a/src/commands/manifest/run-manifest-facts.mts b/src/commands/manifest/run-manifest-facts.mts index 3470dcae78..c11f86650c 100644 --- a/src/commands/manifest/run-manifest-facts.mts +++ b/src/commands/manifest/run-manifest-facts.mts @@ -1,8 +1,6 @@ import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { buildRootPath } from './discover-manifest-roots.mts' import { expandEnvVarRefs, @@ -19,6 +17,7 @@ import { accumulateSidecar } from './scripts/sidecar.mts' import constants from '../../constants.mts' import { getErrorMessageOr } from '../../utils/errors.mts' import { realpathOrResolved } from '../../utils/fs.mts' +import { logger } from '../../utils/logger.mts' import type { BuildTool } from './scripts/build-tool.mts' import type { SocketFactsSbomProject } from './scripts/facts.mts' diff --git a/src/commands/manifest/scripts/build-tool.mts b/src/commands/manifest/scripts/build-tool.mts index 69c2c7cbcb..67f9b7f94c 100644 --- a/src/commands/manifest/scripts/build-tool.mts +++ b/src/commands/manifest/scripts/build-tool.mts @@ -1,4 +1,9 @@ -import { existsSync } from 'node:fs' +import { + accessSync, + existsSync, + constants as fsConstants, + statSync, +} from 'node:fs' import { basename, resolve } from 'node:path' import constants from '../../../constants.mts' @@ -43,6 +48,46 @@ export function buildToolLabelsFor(purlTypes: readonly PURL_Type[]): string[] { // Purl types whose packages have no namespace (NuGet ids are flat). export const NAMESPACELESS_PURL_TYPES: ReadonlySet = new Set(['nuget']) +const BUILD_TOOL_PATH_ENV: Record = { + __proto__: null, + gradle: 'SOCKET_CLI_GRADLE_PATH', + maven: 'SOCKET_CLI_MAVEN_PATH', + sbt: 'SOCKET_CLI_SBT_PATH', +} as unknown as Record + +function validatedEnvBuildToolPath( + tool: BuildTool, + projectDir: string, +): string { + const envName = BUILD_TOOL_PATH_ENV[tool] + const selected = process.env[envName] + if (!selected) { + throw new Error( + `Standalone Socket does not support ${tool} manifest execution without ${envName}: JVM runtime and toolchain assets are not bundled.`, + ) + } + const executable = resolve(projectDir, selected) + return validateExecutablePath(envName, selected, executable) +} + +function validateExecutablePath( + envName: string, + selected: string, + executable: string, +): string { + try { + if (!statSync(executable).isFile()) { + throw new Error('not a regular file') + } + accessSync(executable, fsConstants.X_OK) + return executable + } catch { + throw new Error( + `${envName} must point to a regular executable file; received '${selected}'.`, + ) + } +} + // PATH fallback when no `bin` and no project wrapper. const DEFAULT_BUILD_TOOL_BIN: Record = { __proto__: null, @@ -123,7 +168,19 @@ export function resolveBuildToolBin( tool: BuildTool, projectDir: string, bin?: string | undefined, + standalone = false, ): string { + if (standalone) { + return validatedEnvBuildToolPath(tool, projectDir) + } + const envOverride = process.env[BUILD_TOOL_PATH_ENV[tool]] + if (envOverride) { + return validateExecutablePath( + BUILD_TOOL_PATH_ENV[tool], + envOverride, + resolve(projectDir, envOverride), + ) + } if (bin) { return bin } diff --git a/src/commands/manifest/scripts/dotnet-tool/build-tool.sh b/src/commands/manifest/scripts/dotnet-tool/build-tool.sh index 0ebeb6cd23..8b65e0af87 100755 --- a/src/commands/manifest/scripts/dotnet-tool/build-tool.sh +++ b/src/commands/manifest/scripts/dotnet-tool/build-tool.sh @@ -6,7 +6,9 @@ set -euo pipefail cd "$(dirname "$0")" # Publish into a fresh dir and swap it in, so no stale file survives. staging="$(mktemp -d "${TMPDIR:-/tmp}/socket-facts-dotnet.XXXXXX")" -dotnet restore socket-facts-dotnet.csproj --locked-mode --nologo -v quiet +# Vulnerability-feed auditing runs in the dedicated security workflow; restore +# this locked build input without making the helper build depend on that feed. +dotnet restore socket-facts-dotnet.csproj --locked-mode -p:NuGetAudit=false --nologo -v quiet dotnet publish socket-facts-dotnet.csproj -c Release -o "$staging" --no-restore --nologo -v quiet rm -f "$staging"/*.pdb if [ -d publish ]; then diff --git a/src/commands/manifest/scripts/run.mts b/src/commands/manifest/scripts/run.mts index ea09c83a43..4260c44eae 100644 --- a/src/commands/manifest/scripts/run.mts +++ b/src/commands/manifest/scripts/run.mts @@ -1,15 +1,15 @@ import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { assembleFacts } from './assemble.mts' import { resolveBuildToolBin, socketFactsFileName } from './build-tool.mts' import { serializeExcludePathPatterns } from './exclude-paths-glob.mts' import { parseRecords } from './records.mts' import constants from '../../../constants.mts' +import { isStandaloneSea } from '../../../sea/runtime.mts' import { withTmpDir } from '../../../utils/fs.mts' import { getOrInsert } from '../../../utils/get-or-insert.mts' +import { spawn } from '../../../utils/spawn.mts' import { camelToKebab } from '../../../utils/strings.mts' import type { BuildTool } from './build-tool.mts' @@ -294,10 +294,15 @@ async function invokeGradle( task: string, tmpDirPrefix: string, ): Promise { + const bin = resolveBuildToolBin( + 'gradle', + opts.projectDir, + opts.bin, + isStandaloneSea(), + ) const initScript = manifestScriptsPath(initScriptFilename) return await withTmpDir(tmpDirPrefix, async tmp => { const recordsFile = path.join(tmp, 'records.tsv') - const bin = resolveBuildToolBin('gradle', opts.projectDir, opts.bin) // Disable the configuration cache: the init script's legacy // resolvedConfiguration API and shared accumulator aren't cache-safe. const args = [ @@ -348,12 +353,17 @@ async function invokeSbtIn( pluginDestFilename: string, task: string, ): Promise { + const bin = resolveBuildToolBin( + 'sbt', + opts.projectDir, + opts.bin, + isStandaloneSea(), + ) await writeSbtPlugin(globalBase, pluginSourceFilename, pluginDestFilename) const recordsFile = path.join(globalBase, 'records.tsv') // The global base is shared across sbt roots: a build that dies before // writing must not be read as the previous root's records. await fs.rm(recordsFile, { force: true }) - const bin = resolveBuildToolBin('sbt', opts.projectDir, opts.bin) // Fresh per-run global base (not ~/.sbt): sbt executes everything under // plugins/, so a shared path is a code-injection surface. BSP off for this // run. sbt also provisions the project's Scala toolchain under @@ -421,6 +431,12 @@ async function invokeMaven( coanaTask: string, tmpDirPrefix: string, ): Promise { + const bin = resolveBuildToolBin( + 'maven', + opts.projectDir, + opts.bin, + isStandaloneSea(), + ) const jarPath = manifestScriptsPath( 'maven-extension', 'coana-maven-extension.jar', @@ -432,7 +448,6 @@ async function invokeMaven( ) return await withTmpDir(tmpDirPrefix, async tmp => { const recordsFile = path.join(tmp, 'records.tsv') - const bin = resolveBuildToolBin('maven', opts.projectDir, opts.bin) // `validate` is the cheapest phase that triggers the extension; no compile // needed (analysis uses configured paths, not classes). const props = [ diff --git a/src/commands/manifest/scripts/run.test.mts b/src/commands/manifest/scripts/run.test.mts index 663e5ca4de..81b49f8ff4 100644 --- a/src/commands/manifest/scripts/run.test.mts +++ b/src/commands/manifest/scripts/run.test.mts @@ -4,15 +4,14 @@ import path from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { enumerateWorkspaces, listDotnetEntryProjects, runManifestScript, } from './run.mts' +import { spawn } from '../../../utils/spawn.mts' -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ +vi.mock('../../../utils/spawn.mts', () => ({ spawn: vi.fn(async () => ({ code: 0, stderr: '', stdout: '' })), })) // The Maven extension jar and the dotnet tool exist only in a built checkout. diff --git a/src/commands/manifest/setup-manifest-config.mts b/src/commands/manifest/setup-manifest-config.mts index 8cbd34f4f8..2aa513fdfe 100644 --- a/src/commands/manifest/setup-manifest-config.mts +++ b/src/commands/manifest/setup-manifest-config.mts @@ -1,12 +1,11 @@ import fs from 'node:fs' import path from 'node:path' -import { debugDir } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { input, select } from '@socketsecurity/registry/lib/prompts' - import { detectManifestActions } from './detect-manifest-actions.mts' import { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' +import { debugDir } from '../../utils/debug.mts' +import { logger } from '../../utils/logger.mts' +import { input, select } from '../../utils/prompts.mts' import { readSocketJsonSync, writeSocketJson, diff --git a/src/commands/manifest/setup-manifest-config.test.mts b/src/commands/manifest/setup-manifest-config.test.mts index 7138ddee3e..997237ac78 100644 --- a/src/commands/manifest/setup-manifest-config.test.mts +++ b/src/commands/manifest/setup-manifest-config.test.mts @@ -4,19 +4,18 @@ import { describe, expect, it, vi } from 'vitest' // whatever `default` was shown - the same contract inquirer honors. This is // exactly the behavior that made `askForBin` freeze a hardcoded tool // fallback (e.g. 'mvn') into socket.json when the user never typed anything. -vi.mock('@socketsecurity/registry/lib/prompts', () => ({ +vi.mock('../../utils/prompts.mts', () => ({ input: vi.fn(async ({ default: def }: { default?: string }) => def ?? ''), select: vi.fn(async ({ default: def }: { default?: string }) => def ?? ''), })) -import { input, select } from '@socketsecurity/registry/lib/prompts' - import { setupDotnet, setupGradle, setupMaven, setupSbt, } from './setup-manifest-config.mts' +import { input, select } from '../../utils/prompts.mts' describe('setup wizards', () => { it.for([ diff --git a/src/commands/manifest/setup-recursive-manifest-config.mts b/src/commands/manifest/setup-recursive-manifest-config.mts index dee44c18ff..0b9dd00244 100644 --- a/src/commands/manifest/setup-recursive-manifest-config.mts +++ b/src/commands/manifest/setup-recursive-manifest-config.mts @@ -3,9 +3,6 @@ import path from 'node:path' import micromatch from 'micromatch' -import { logger } from '@socketsecurity/registry/lib/logger' -import { select } from '@socketsecurity/registry/lib/prompts' - import { addCoveredBuildFiles, buildRootPath, @@ -28,6 +25,8 @@ import { import { SOCKET_JSON } from '../../constants.mts' import { loadSocketConfig } from '../../utils/config.mts' import { getOrInsert } from '../../utils/get-or-insert.mts' +import { logger } from '../../utils/logger.mts' +import { select } from '../../utils/prompts.mts' import { readOrDefaultSocketJson, readSocketJsonCascade, diff --git a/src/commands/manifest/setup-recursive-manifest-config.test.mts b/src/commands/manifest/setup-recursive-manifest-config.test.mts index 34ca9e0fb8..dd9bd74a65 100644 --- a/src/commands/manifest/setup-recursive-manifest-config.test.mts +++ b/src/commands/manifest/setup-recursive-manifest-config.test.mts @@ -25,7 +25,7 @@ vi.mock('./discover-manifest-roots.mts', async importOriginal => { vi.mock('./enumerate-workspaces.mts', () => ({ enumerateWorkspaces: vi.fn(), })) -vi.mock('@socketsecurity/registry/lib/prompts', () => ({ +vi.mock('../../utils/prompts.mts', () => ({ select: vi.fn(), })) vi.mock('./setup-manifest-config.mts', () => ({ @@ -40,9 +40,6 @@ vi.mock('../../utils/socket-json.mts', () => ({ writeSocketJson: vi.fn(), })) -import { logger } from '@socketsecurity/registry/lib/logger' -import { select } from '@socketsecurity/registry/lib/prompts' - import { buildRootPath, findBuildToolCandidates, @@ -58,6 +55,8 @@ import { scanBuildRoots, setupRecursiveManifestConfig, } from './setup-recursive-manifest-config.mts' +import { logger } from '../../utils/logger.mts' +import { select } from '../../utils/prompts.mts' import { readOrDefaultSocketJson, readSocketJsonCascade, diff --git a/src/commands/native-cli-behavior.native.test.mts b/src/commands/native-cli-behavior.native.test.mts new file mode 100644 index 0000000000..f643a5c24d --- /dev/null +++ b/src/commands/native-cli-behavior.native.test.mts @@ -0,0 +1,120 @@ +import { writeFile } from 'node:fs/promises' +import path from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { + captureFlow, + createNativeTerminalFixture, +} from '../../test/utils/native-cli-harness.mts' +import { + captureCheckpoint, + createRecording, + expectTerminalAnimation, +} from '../../test/utils/terminal-animation.mts' + +const binary = process.env['SOCKET_CLI_NATIVE_TEST_BINARY'] + +describe('native CLI behavior', () => { + it('reads an explicit config value', async () => { + await captureFlow({ + flow: 'behavior-config-get', + checkpointName: 'output', + args: ['config', 'get', 'defaultOrg'], + expectedText: 'native-fixture', + exitCode: 0, + env: { SOCKET_CLI_CONFIG: '{"defaultOrg":"native-fixture"}' }, + }) + }, 30_000) + + it('lists an explicit config without persisting it', async () => { + await captureFlow({ + flow: 'behavior-config-list', + checkpointName: 'output', + args: ['config', 'list'], + expectedText: 'native-fixture', + exitCode: 0, + rows: 30, + env: { SOCKET_CLI_CONFIG: '{"defaultOrg":"native-fixture"}' }, + }) + }, 30_000) + + it('rejects an unknown config key', async () => { + await captureFlow({ + flow: 'behavior-config-invalid-key', + checkpointName: 'output', + args: ['config', 'get', 'invalid-native-key'], + expectedText: 'invalid config key', + exitCode: 2, + }) + }, 30_000) + + it('shows a missing socket.json error in an isolated directory', async () => { + await captureFlow({ + flow: 'behavior-json-missing', + checkpointName: 'output', + args: ['json'], + expectedText: 'Not found:', + exitCode: 1, + }) + }, 30_000) + + it('prints a seeded socket.json fixture', async () => { + await captureFlow({ + flow: 'behavior-json-seeded', + checkpointName: 'output', + args: ['json'], + expectedText: '"projectIgnorePaths"', + exitCode: 0, + prepare: async ({ cwd }) => { + await writeFile( + `${cwd}/socket.json`, + '{"version":1,"projectIgnorePaths":["fixture/**"]}\n', + ) + }, + }) + }, 30_000) + + it('validates wrapper mode before applying the dry-run guard', async () => { + await captureFlow({ + flow: 'behavior-wrapper-on-dry-run', + checkpointName: 'output', + args: ['wrapper', 'on', '--dry-run'], + expectedText: '[DryRun]', + exitCode: 0, + }) + }, 30_000) + + it('captures the manifest setup menu and cancellation', async () => { + const fixture = await createNativeTerminalFixture(24) + await fixture.terminal.spawn([binary!, 'manifest', 'setup'], { + cwd: fixture.cwd, + env: { NO_COLOR: '', CI: '1', SOCKET_CLI_CONFIG: '{}' }, + }) + await fixture.terminal.waitFor( + 'Select ecosystem manifest generator to configure', + 30_000, + ) + await fixture.terminal.waitForStable(150, 10_000) + const menu = await captureCheckpoint(fixture.terminal, 'menu') + fixture.terminal.press('ArrowUp') + await fixture.terminal.waitForStable(150, 10_000) + const selected = await captureCheckpoint(fixture.terminal, 'exit-selected') + fixture.terminal.press('Enter') + await fixture.terminal.waitFor('User canceled', 30_000) + await expect + .poll(() => fixture.terminal.exitInfo, { timeout: 30_000 }) + .toBe('exit=0') + fixture.normalizer.flush() + const canceled = await captureCheckpoint(fixture.terminal, 'canceled') + await expectTerminalAnimation(createRecording([menu, selected, canceled]), { + flow: 'behavior-manifest-setup', + snapshotsDir: path.join(process.cwd(), 'test/native-snapshots'), + artifactsDir: + process.env['SOCKET_CLI_NATIVE_TEST_ARTIFACTS'] ?? + path.join(process.cwd(), 'build/native-test-artifacts'), + expectedSequence: ['menu', 'exit-selected', 'canceled'], + durations: [850, 850, 1100], + }) + }, 60_000) +}) diff --git a/src/commands/native-cli.native.test.mts b/src/commands/native-cli.native.test.mts new file mode 100644 index 0000000000..ec7bd1a2b4 --- /dev/null +++ b/src/commands/native-cli.native.test.mts @@ -0,0 +1,239 @@ +import path from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { + captureFlow, + createNativeTerminalFixture, +} from '../../test/utils/native-cli-harness.mts' +import { discoverNativeCommandPaths } from '../../test/utils/native-command-inventory.mts' +import { + captureCheckpoint, + createRecording, + expectTerminalAnimation, +} from '../../test/utils/terminal-animation.mts' +import { + FLAG_CONFIG, + FLAG_DRY_RUN, + FLAG_HELP, + FLAG_VERSION, +} from '../constants.mts' + +const binary = process.env['SOCKET_CLI_NATIVE_TEST_BINARY'] +if (!binary) { + throw new Error('Use pnpm test:native to run native CLI tests') +} + +const snapshotDir = path.join(process.cwd(), 'test/native-snapshots') +const artifactDir = + process.env['SOCKET_CLI_NATIVE_TEST_ARTIFACTS'] ?? + path.join(process.cwd(), 'build/native-test-artifacts') +const appVersion = process.env['SOCKET_CLI_NATIVE_TEST_VERSION'] +const nodeVersion = process.env['SOCKET_CLI_NATIVE_TEST_NODE_VERSION'] +if (!appVersion || !nodeVersion) { + throw new Error('Native test harness must provide manifest versions') +} + +const commandPaths = [ + 'analytics', + 'audit-log', + 'ci', + 'cdxgen', + 'config', + 'dependencies', + 'fix', + 'install', + 'json', + 'license', + 'login', + 'logout', + 'manifest', + 'npm', + 'npx', + 'pnpm', + 'oops', + 'optimize', + 'organization', + 'package', + 'patch', + 'raw-npm', + 'raw-npx', + 'repository', + 'scan', + 'security', + 'threat-feed', + 'uninstall', + 'wrapper', + 'yarn', + 'config auto', + 'config get', + 'config list', + 'config set', + 'config unset', + 'install completion', + 'uninstall completion', + 'manifest auto', + 'manifest bazel', + 'manifest cdxgen', + 'manifest conda', + 'manifest dotnet', + 'manifest dynamic-sbom-inference', + 'manifest gradle', + 'manifest kotlin', + 'manifest maven', + 'manifest scala', + 'manifest setup', + 'organization dependencies', + 'organization list', + 'organization quota', + 'organization policy', + 'organization policy security', + 'organization policy license', + 'package score', + 'package shallow', + 'repository create', + 'repository view', + 'repository list', + 'repository del', + 'repository update', + 'scan create', + 'scan del', + 'scan diff', + 'scan github', + 'scan list', + 'scan metadata', + 'scan reach', + 'scan report', + 'scan setup', + 'scan view', +] as const + +it('keeps the native help matrix aligned with registered commands', async () => { + expect([...commandPaths].sort()).toEqual( + (await discoverNativeCommandPaths()).sort(), + ) +}) + +for (const commandPath of commandPaths) { + const args = [...commandPath.split(' '), FLAG_HELP] + const flow = `socket-help-${commandPath.replaceAll(' ', '-')}` + it(`captures native help for ${commandPath}`, async () => { + await captureFlow({ + flow, + args, + expectedText: commandPath, + exitCode: 0, + cols: 140, + rows: 200, + }) + }, 60_000) +} + +for (const patchCommand of [ + 'apply', + 'rollback', + 'get', + 'scan', + 'list', + 'remove', + 'setup', + 'repair', +]) { + it(`captures bundled patch help for ${patchCommand}`, async () => { + await captureFlow({ + flow: `socket-patch-${patchCommand}-help`, + args: ['patch', patchCommand, FLAG_HELP], + expectedText: `Usage: socket-patch ${patchCommand}`, + exitCode: 0, + cols: 140, + rows: 200, + }) + }, 60_000) +} + +describe('native Socket CLI', () => { + it('reports the version embedded in the SEA manifest', async () => { + await captureFlow({ + flow: 'socket-version', + args: [FLAG_VERSION], + expectedText: appVersion, + exitCode: 0, + rows: 12, + checkpointName: 'version', + }) + }, 30_000) + + it('prints root help and starts the CLI from the native executable', async () => { + await captureFlow({ + flow: 'socket-help', + args: [FLAG_HELP], + expectedText: 'CLI for Socket.dev', + exitCode: 0, + rows: 200, + cols: 140, + }) + }, 30_000) + + it('runs a local dry-run command through the native executable', async () => { + await captureFlow({ + flow: 'socket-manifest-dry-run', + args: [ + 'manifest', + 'auto', + FLAG_DRY_RUN, + FLAG_CONFIG, + '{"apiToken":"fakeToken"}', + ], + expectedText: '[DryRun]', + exitCode: 0, + rows: 12, + checkpointName: 'dry-run', + env: { SOCKET_CLI_CONFIG: '{"apiToken":"fakeToken"}' }, + }) + }, 30_000) + + it('rejects unsupported cdxgen project types before helper execution', async () => { + await captureFlow({ + flow: 'socket-cdxgen-unsupported-type', + args: ['manifest', 'cdxgen', '--type', 'pnpm', FLAG_CONFIG, '{}'], + expectedText: 'supports only npm projects', + exitCode: 1, + rows: 12, + checkpointName: 'unsupported-type', + env: {}, + }) + }, 30_000) + + it('captures the scan setup selection and cancellation interaction', async () => { + const fixture = await createNativeTerminalFixture(24) + const { cwd, normalizer, terminal } = fixture + await terminal.spawn([binary!, 'scan', 'setup', FLAG_CONFIG, '{}'], { + cwd, + env: { NO_COLOR: '', CI: '1' }, + }) + await terminal.waitFor( + 'Which scan command do you want to configure?', + 30_000, + ) + await terminal.waitForStable(150, 10_000) + const menu = await captureCheckpoint(terminal, 'menu') + terminal.press('ArrowDown') + await terminal.waitForStable(150, 10_000) + const github = await captureCheckpoint(terminal, 'github-selected') + terminal.press('ArrowDown') + await terminal.waitForStable(150, 10_000) + terminal.press('Enter') + await expect + .poll(() => terminal.exitInfo, { timeout: 30_000 }) + .toBe('exit=0') + normalizer.flush() + const canceled = await captureCheckpoint(terminal, 'canceled') + await expectTerminalAnimation(createRecording([menu, github, canceled]), { + flow: 'socket-scan-setup', + snapshotsDir: snapshotDir, + artifactsDir: artifactDir, + expectedSequence: ['menu', 'github-selected', 'canceled'], + durations: [850, 850, 1100], + }) + }, 60_000) +}) diff --git a/src/commands/npm/cmd-npm.mts b/src/commands/npm/cmd-npm.mts index 86256e5f48..54d2866daf 100644 --- a/src/commands/npm/cmd-npm.mts +++ b/src/commands/npm/cmd-npm.mts @@ -1,7 +1,5 @@ import { createRequire } from 'node:module' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants, { FLAG_DRY_RUN, FLAG_HELP, @@ -10,6 +8,7 @@ import constants, { } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { filterFlags } from '../../utils/cmd.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput } from '../../utils/output-formatting.mts' import { diff --git a/src/commands/npm/socket-npm-integration.test.mts b/src/commands/npm/socket-npm-integration.test.mts index 66b755e9ae..1329143acd 100644 --- a/src/commands/npm/socket-npm-integration.test.mts +++ b/src/commands/npm/socket-npm-integration.test.mts @@ -2,18 +2,17 @@ import path from 'node:path' import { describe, expect, it } from 'vitest' -import { isDebug } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { testPath } from '../../../test/utils.mts' import constants, { FLAG_DRY_RUN, FLAG_HELP, FLAG_SILENT, } from '../../constants.mts' +import { isDebug } from '../../utils/debug.mts' +import { logger } from '../../utils/logger.mts' +import { spawn } from '../../utils/spawn.mts' -import type { SpawnError } from '@socketsecurity/registry/lib/spawn' +import type { SpawnError } from '../../utils/spawn.mts' const npmFixturesPath = path.join(testPath, 'fixtures/commands/npm') diff --git a/src/commands/npx/cmd-npx.mts b/src/commands/npx/cmd-npx.mts index c6054b6a1a..7aa45a7760 100644 --- a/src/commands/npx/cmd-npx.mts +++ b/src/commands/npx/cmd-npx.mts @@ -1,9 +1,8 @@ import { createRequire } from 'node:module' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants, { FLAG_DRY_RUN, FLAG_HELP, NPX } from '../../constants.mts' import { commonFlags } from '../../flags.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput } from '../../utils/output-formatting.mts' import { diff --git a/src/commands/oops/cmd-oops.mts b/src/commands/oops/cmd-oops.mts index 826e992ed3..14e8bd9523 100644 --- a/src/commands/oops/cmd-oops.mts +++ b/src/commands/oops/cmd-oops.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' diff --git a/src/commands/optimize/add-overrides.mts b/src/commands/optimize/add-overrides.mts index 4de4ad6ae9..4a5396526a 100644 --- a/src/commands/optimize/add-overrides.mts +++ b/src/commands/optimize/add-overrides.mts @@ -2,12 +2,6 @@ import path from 'node:path' import semver from 'semver' -import { getManifestData } from '@socketsecurity/registry' -import { hasOwn, toSortedObject } from '@socketsecurity/registry/lib/objects' -import { fetchPackageManifest } from '@socketsecurity/registry/lib/packages' -import { pEach } from '@socketsecurity/registry/lib/promises' -import { Spinner } from '@socketsecurity/registry/lib/spinner' - import { lsStdoutIncludes } from './deps-includes-by-agent.mts' import { getDependencyEntries } from './get-dependency-entries.mts' import { @@ -23,13 +17,18 @@ import { NPM, PNPM } from '../../constants.mts' import { cmdPrefixMessage } from '../../utils/cmd.mts' import { globWorkspace } from '../../utils/glob.mts' import { safeNpa } from '../../utils/npm-package-arg.mts' +import { hasOwn, toSortedObject } from '../../utils/objects.mts' +import { getManifestData } from '../../utils/packages.mts' +import { pEach } from '../../utils/promises.mts' import { getMajor } from '../../utils/semver.mts' +import { spawn } from '../../utils/spawn.mts' +import { Spinner } from '../../utils/spinner.mts' import type { GetOverridesResult } from './get-overrides-by-agent.mts' +import type { Logger } from '../../utils/logger.mts' import type { AliasResult } from '../../utils/npm-package-arg.mts' import type { EnvDetails } from '../../utils/package-environment.mts' -import type { Logger } from '@socketsecurity/registry/lib/logger' -import type { PackageJson } from '@socketsecurity/registry/lib/packages' +import type { PackageJson } from '../../utils/packages.mts' type AddOverridesOptions = { logger?: Logger | undefined @@ -48,6 +47,51 @@ type AddOverridesState = { const manifestNpmOverrides = getManifestData(NPM) +/** + * Resolve a Socket override's selected manifest version through npm itself. + * This preserves the project's npm registry, auth, and cache configuration + * without importing the registry package's Pacote-backed helper. + */ +async function fetchPackageVersion( + spec: string, + npmExecPath: string, + cwd: string, +): Promise { + const parsed = safeNpa(spec) + if (!parsed) { + return undefined + } + const npmSpec = parsed as AliasResult + const packageName = + npmSpec.type === 'alias' ? npmSpec.subSpec.name : npmSpec.name + const packageSpec = + npmSpec.type === 'alias' ? npmSpec.subSpec.rawSpec : npmSpec.rawSpec + if (!packageName || !packageSpec) { + return undefined + } + + try { + const { stdout } = await spawn( + npmExecPath, + ['view', `${packageName}@${packageSpec}`, 'version', '--json'], + { cwd, stdio: 'pipe' }, + ) + const parsedOutput: unknown = JSON.parse(stdout) + if (typeof parsedOutput === 'string') { + return parsedOutput + } + if (Array.isArray(parsedOutput)) { + const versions = parsedOutput.filter( + (version): version is string => typeof version === 'string', + ) + return semver.maxSatisfying(versions, packageSpec) ?? undefined + } + } catch { + // Pacote-backed registry.fetchPackageManifest also returns null on failure. + } + return undefined +} + export async function addOverrides( pkgEnvDetails: EnvDetails, pkgPath: string, @@ -115,7 +159,7 @@ export async function addOverrides( // Chunk package names to process them in parallel 3 at a time. await pEach( - manifestNpmOverrides, + manifestNpmOverrides ?? [], async ({ 1: data }) => { const { name: sockRegPkgName, package: origPkgName, version } = data const major = getMajor(version)! @@ -214,8 +258,11 @@ export async function addOverrides( )?.version ?? version, ) !== major ) { - const otherVersion = (await fetchPackageManifest(thisSpec)) - ?.version + const otherVersion = await fetchPackageVersion( + thisSpec, + npmExecPath, + pkgPath, + ) if (otherVersion && otherVersion !== version) { newSpec = `${sockOverridePrefix}${pin ? otherVersion : `^${getMajor(otherVersion)!}`}` } diff --git a/src/commands/optimize/apply-optimization.mts b/src/commands/optimize/apply-optimization.mts index 54d0a2923f..fd308fa383 100644 --- a/src/commands/optimize/apply-optimization.mts +++ b/src/commands/optimize/apply-optimization.mts @@ -1,9 +1,8 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { addOverrides } from './add-overrides.mts' import { CMD_NAME } from './shared.mts' import { updateLockfile } from './update-lockfile.mts' import constants from '../../constants.mts' +import { logger } from '../../utils/logger.mts' import type { CResult } from '../../types.mts' import type { EnvDetails } from '../../utils/package-environment.mts' diff --git a/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts b/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts index ff72db47db..366b981b53 100644 --- a/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts +++ b/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts @@ -4,10 +4,6 @@ import path from 'node:path' import trash from 'trash' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { JsonContent } from '@socketsecurity/registry/lib/fs' -import { readPackageJson } from '@socketsecurity/registry/lib/packages' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants, { FLAG_CONFIG, FLAG_DRY_RUN, @@ -16,6 +12,10 @@ import constants, { PNPM_LOCK_YAML, } from '../../../src/constants.mts' import { spawnSocketCli, testPath } from '../../../test/utils.mts' +import { readPackageJson } from '../../utils/packages.mts' +import { spawn } from '../../utils/spawn.mts' + +import type { JsonContent } from '../../utils/fs.mts' const fixtureBaseDir = path.join(testPath, 'fixtures/commands/optimize') const pnpm8FixtureDir = path.join(fixtureBaseDir, 'pnpm8') diff --git a/src/commands/optimize/cmd-optimize.mts b/src/commands/optimize/cmd-optimize.mts index 5dacae9029..aef60eba3d 100644 --- a/src/commands/optimize/cmd-optimize.mts +++ b/src/commands/optimize/cmd-optimize.mts @@ -1,11 +1,10 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleOptimize } from './handle-optimize.mts' import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/optimize/cmd-optimize.test.mts b/src/commands/optimize/cmd-optimize.test.mts index 13dcc47b1f..52ad890099 100644 --- a/src/commands/optimize/cmd-optimize.test.mts +++ b/src/commands/optimize/cmd-optimize.test.mts @@ -5,10 +5,6 @@ import path from 'node:path' import trash from 'trash' import { afterAll, afterEach, beforeAll, describe, expect } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' -import { readPackageJson } from '@socketsecurity/registry/lib/packages' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants, { FLAG_CONFIG, FLAG_DRY_RUN, @@ -25,6 +21,9 @@ import constants, { PNPM_LOCK_YAML, } from '../../../src/constants.mts' import { cmdit, spawnSocketCli, testPath } from '../../../test/utils.mts' +import { logger } from '../../utils/logger.mts' +import { readPackageJson } from '../../utils/packages.mts' +import { spawn } from '../../utils/spawn.mts' const fixtureBaseDir = path.join(testPath, 'fixtures/commands/optimize') const npmFixtureDir = path.join(fixtureBaseDir, NPM) diff --git a/src/commands/optimize/get-overrides-by-agent.mts b/src/commands/optimize/get-overrides-by-agent.mts index 8a14575063..08b45cff71 100644 --- a/src/commands/optimize/get-overrides-by-agent.mts +++ b/src/commands/optimize/get-overrides-by-agent.mts @@ -2,7 +2,7 @@ import constants from '../../constants.mts' import type { NpmOverrides, Overrides, PnpmOrYarnOverrides } from './types.mts' import type { Agent, EnvDetails } from '../../utils/package-environment.mts' -import type { PackageJson } from '@socketsecurity/registry/lib/packages' +import type { PackageJson } from '../../utils/packages.mts' const { BUN, diff --git a/src/commands/optimize/handle-optimize.mts b/src/commands/optimize/handle-optimize.mts index 1ff2f9433d..4d9bd9df51 100644 --- a/src/commands/optimize/handle-optimize.mts +++ b/src/commands/optimize/handle-optimize.mts @@ -1,11 +1,10 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import { applyOptimization } from './apply-optimization.mts' import { outputOptimizeResult } from './output-optimize-result.mts' import { CMD_NAME } from './shared.mts' import constants from '../../constants.mts' import { cmdPrefixMessage } from '../../utils/cmd.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' +import { logger } from '../../utils/logger.mts' import { detectAndValidatePackageEnvironment } from '../../utils/package-environment.mts' import type { OutputKind } from '../../types.mts' diff --git a/src/commands/optimize/lockfile-includes-by-agent.mts b/src/commands/optimize/lockfile-includes-by-agent.mts index e81c38d2ed..e2227b4369 100644 --- a/src/commands/optimize/lockfile-includes-by-agent.mts +++ b/src/commands/optimize/lockfile-includes-by-agent.mts @@ -1,6 +1,5 @@ -import { escapeRegExp } from '@socketsecurity/registry/lib/regexps' - import constants from '../../constants.mts' +import { escapeRegExp } from '../../utils/escape-regexp.mts' import type { EnvDetails } from '../../utils/package-environment.mts' diff --git a/src/commands/optimize/ls-by-agent.mts b/src/commands/optimize/ls-by-agent.mts index c5005dc77b..7d88ce3737 100644 --- a/src/commands/optimize/ls-by-agent.mts +++ b/src/commands/optimize/ls-by-agent.mts @@ -1,6 +1,5 @@ -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants, { FLAG_PROD } from '../../constants.mts' +import { spawn } from '../../utils/spawn.mts' import type { EnvDetails } from '../../utils/package-environment.mts' diff --git a/src/commands/optimize/output-optimize-result.mts b/src/commands/optimize/output-optimize-result.mts index 3b77998a22..70630b3a69 100644 --- a/src/commands/optimize/output-optimize-result.mts +++ b/src/commands/optimize/output-optimize-result.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' -import { pluralize } from '@socketsecurity/registry/lib/words' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' +import { pluralize } from '../../utils/words.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/optimize/update-lockfile.mts b/src/commands/optimize/update-lockfile.mts index 88fa014519..9ab527bf40 100644 --- a/src/commands/optimize/update-lockfile.mts +++ b/src/commands/optimize/update-lockfile.mts @@ -1,13 +1,12 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { Spinner } from '@socketsecurity/registry/lib/spinner' - import constants from '../../constants.mts' import { runAgentInstall } from '../../utils/agent.mts' import { cmdPrefixMessage } from '../../utils/cmd.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' +import { Spinner } from '../../utils/spinner.mts' import type { CResult } from '../../types.mts' +import type { Logger } from '../../utils/logger.mts' import type { EnvDetails } from '../../utils/package-environment.mts' -import type { Logger } from '@socketsecurity/registry/lib/logger' const { NPM_BUGGY_OVERRIDES_PATCHED_VERSION } = constants diff --git a/src/commands/optimize/update-manifest-by-agent.mts b/src/commands/optimize/update-manifest-by-agent.mts index 85137efd05..7c2abaaa6c 100644 --- a/src/commands/optimize/update-manifest-by-agent.mts +++ b/src/commands/optimize/update-manifest-by-agent.mts @@ -1,10 +1,9 @@ -import { hasKeys, isObject } from '@socketsecurity/registry/lib/objects' - import constants from '../../constants.mts' +import { hasKeys, isObject } from '../../utils/objects.mts' import type { Overrides } from './types.mts' import type { Agent } from '../../utils/package-environment.mts' -import type { EditablePackageJson } from '@socketsecurity/registry/lib/packages' +import type { EditablePackageJson } from '../../utils/packages.mts' const { BUN, diff --git a/src/commands/optimize/update-manifest-by-agent.test.mts b/src/commands/optimize/update-manifest-by-agent.test.mts index 794cfbd99f..f666e17a7d 100644 --- a/src/commands/optimize/update-manifest-by-agent.test.mts +++ b/src/commands/optimize/update-manifest-by-agent.test.mts @@ -4,10 +4,9 @@ import path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { readPackageJson } from '@socketsecurity/registry/lib/packages' - import { updateManifest } from './update-manifest-by-agent.mts' import { NPM, PNPM, YARN_BERRY, YARN_CLASSIC } from '../../constants.mts' +import { readPackageJson } from '../../utils/packages.mts' const directories: string[] = [] const overrides = { 'fixture-package': 'npm:fixture-replacement@2.0.0' } diff --git a/src/commands/organization/cmd-organization-dependencies.mts b/src/commands/organization/cmd-organization-dependencies.mts index 04e084788a..454f6ed786 100644 --- a/src/commands/organization/cmd-organization-dependencies.mts +++ b/src/commands/organization/cmd-organization-dependencies.mts @@ -1,10 +1,9 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleDependencies } from './handle-dependencies.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/organization/cmd-organization-list.mts b/src/commands/organization/cmd-organization-list.mts index 4fa02b6e95..dcfa034b23 100644 --- a/src/commands/organization/cmd-organization-list.mts +++ b/src/commands/organization/cmd-organization-list.mts @@ -1,10 +1,9 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleOrganizationList } from './handle-organization-list.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/organization/cmd-organization-policy-license.mts b/src/commands/organization/cmd-organization-policy-license.mts index 446fc9baa6..d59c9f593a 100644 --- a/src/commands/organization/cmd-organization-policy-license.mts +++ b/src/commands/organization/cmd-organization-policy-license.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleLicensePolicy } from './handle-license-policy.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/organization/cmd-organization-policy-security.mts b/src/commands/organization/cmd-organization-policy-security.mts index 1306caeb9d..f32b64a219 100644 --- a/src/commands/organization/cmd-organization-policy-security.mts +++ b/src/commands/organization/cmd-organization-policy-security.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleSecurityPolicy } from './handle-security-policy.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/organization/cmd-organization-quota.mts b/src/commands/organization/cmd-organization-quota.mts index ba679e1326..7426b2443b 100644 --- a/src/commands/organization/cmd-organization-quota.mts +++ b/src/commands/organization/cmd-organization-quota.mts @@ -1,10 +1,9 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleQuota } from './handle-quota.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' import { hasDefaultApiToken } from '../../utils/sdk.mts' diff --git a/src/commands/organization/fetch-organization-list.mts b/src/commands/organization/fetch-organization-list.mts index 9da6c09d35..525e5371ea 100644 --- a/src/commands/organization/fetch-organization-list.mts +++ b/src/commands/organization/fetch-organization-list.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleApiCall } from '../../utils/api.mts' +import { logger } from '../../utils/logger.mts' import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/organization/handle-dependencies.mts b/src/commands/organization/handle-dependencies.mts index 34142112c6..83168462c7 100644 --- a/src/commands/organization/handle-dependencies.mts +++ b/src/commands/organization/handle-dependencies.mts @@ -1,7 +1,6 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' - import { fetchDependencies } from './fetch-dependencies.mts' import { outputDependencies } from './output-dependencies.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import type { OutputKind } from '../../types.mts' diff --git a/src/commands/organization/handle-organization-list.mts b/src/commands/organization/handle-organization-list.mts index eeee21fe8b..b4b082f180 100644 --- a/src/commands/organization/handle-organization-list.mts +++ b/src/commands/organization/handle-organization-list.mts @@ -1,7 +1,6 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' - import { fetchOrganization } from './fetch-organization-list.mts' import { outputOrganizationList } from './output-organization-list.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import type { OutputKind } from '../../types.mts' diff --git a/src/commands/organization/output-dependencies.mts b/src/commands/organization/output-dependencies.mts index eb7243053c..77c7469675 100644 --- a/src/commands/organization/output-dependencies.mts +++ b/src/commands/organization/output-dependencies.mts @@ -2,9 +2,8 @@ import chalkTable from 'chalk-table' import colors from 'yoctocolors-cjs' -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/organization/output-license-policy.mts b/src/commands/organization/output-license-policy.mts index f18ef71f16..1dbd797080 100644 --- a/src/commands/organization/output-license-policy.mts +++ b/src/commands/organization/output-license-policy.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { mdTableOfPairs } from '../../utils/markdown.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' diff --git a/src/commands/organization/output-organization-list.mts b/src/commands/organization/output-organization-list.mts index 18c87c1663..4127ee0516 100644 --- a/src/commands/organization/output-organization-list.mts +++ b/src/commands/organization/output-organization-list.mts @@ -1,8 +1,7 @@ import colors from 'yoctocolors-cjs' -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { getVisibleTokenPrefix } from '../../utils/sdk.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' diff --git a/src/commands/organization/output-quota.mts b/src/commands/organization/output-quota.mts index 4f3f3661e9..2409cdda28 100644 --- a/src/commands/organization/output-quota.mts +++ b/src/commands/organization/output-quota.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/organization/output-security-policy.mts b/src/commands/organization/output-security-policy.mts index 8612a845d9..848057bf91 100644 --- a/src/commands/organization/output-security-policy.mts +++ b/src/commands/organization/output-security-policy.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { mdTableOfPairs } from '../../utils/markdown.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' diff --git a/src/commands/package/cmd-package-score.mts b/src/commands/package/cmd-package-score.mts index 216315ca4b..c34248cf28 100644 --- a/src/commands/package/cmd-package-score.mts +++ b/src/commands/package/cmd-package-score.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handlePurlDeepScore } from './handle-purl-deep-score.mts' import { parsePackageSpecifiers } from './parse-package-specifiers.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/package/cmd-package-shallow.mts b/src/commands/package/cmd-package-shallow.mts index f64c356f1f..75d3ae8617 100644 --- a/src/commands/package/cmd-package-shallow.mts +++ b/src/commands/package/cmd-package-shallow.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handlePurlsShallowScore } from './handle-purls-shallow-score.mts' import { parsePackageSpecifiers } from './parse-package-specifiers.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/package/fetch-purl-deep-score.mts b/src/commands/package/fetch-purl-deep-score.mts index 8ecb4dc2b1..cf251cabac 100644 --- a/src/commands/package/fetch-purl-deep-score.mts +++ b/src/commands/package/fetch-purl-deep-score.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { queryApiSafeJson } from '../../utils/api.mts' +import { logger } from '../../utils/logger.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/package/fetch-purls-shallow-score.mts b/src/commands/package/fetch-purls-shallow-score.mts index 2431d05527..f0f7b283a1 100644 --- a/src/commands/package/fetch-purls-shallow-score.mts +++ b/src/commands/package/fetch-purls-shallow-score.mts @@ -1,7 +1,6 @@ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleApiCall } from '../../utils/api.mts' +import { joinAnd } from '../../utils/arrays.mts' +import { logger } from '../../utils/logger.mts' import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/package/handle-purl-deep-score.mts b/src/commands/package/handle-purl-deep-score.mts index 49fa662aa3..4f8564d9c2 100644 --- a/src/commands/package/handle-purl-deep-score.mts +++ b/src/commands/package/handle-purl-deep-score.mts @@ -1,7 +1,6 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' - import { fetchPurlDeepScore } from './fetch-purl-deep-score.mts' import { outputPurlsDeepScore } from './output-purls-deep-score.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import type { OutputKind } from '../../types.mts' diff --git a/src/commands/package/handle-purls-shallow-score.mts b/src/commands/package/handle-purls-shallow-score.mts index c48e7d96f7..ce0176bc5e 100644 --- a/src/commands/package/handle-purls-shallow-score.mts +++ b/src/commands/package/handle-purls-shallow-score.mts @@ -1,7 +1,6 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' - import { fetchPurlsShallowScore } from './fetch-purls-shallow-score.mts' import { outputPurlsShallowScore } from './output-purls-shallow-score.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import type { CResult, OutputKind } from '../../types.mts' import type { SocketArtifact } from '../../utils/alert/artifact.mts' diff --git a/src/commands/package/output-purls-deep-score.mts b/src/commands/package/output-purls-deep-score.mts index 0cee85a8eb..93c4f20490 100644 --- a/src/commands/package/output-purls-deep-score.mts +++ b/src/commands/package/output-purls-deep-score.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { mdTable } from '../../utils/markdown.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' diff --git a/src/commands/package/output-purls-shallow-malware.test.mts b/src/commands/package/output-purls-shallow-malware.test.mts index 0d7a461909..421f7f1228 100644 --- a/src/commands/package/output-purls-shallow-malware.test.mts +++ b/src/commands/package/output-purls-shallow-malware.test.mts @@ -1,11 +1,21 @@ import { describe, expect, it } from 'vitest' import npmMalware from './fixtures/npm_malware.json' -import { - generateMarkdownReport, - generateTextReport, - preProcess, -} from './output-purls-shallow-score.mts' +const previousForceColor = process.env['FORCE_COLOR'] +const previousNoColor = process.env['NO_COLOR'] +process.env['FORCE_COLOR'] = '1' +delete process.env['NO_COLOR'] +const { generateMarkdownReport, generateTextReport, preProcess } = await import( + './output-purls-shallow-score.mts' +) +if (previousForceColor === undefined) { + delete process.env['FORCE_COLOR'] +} else { + process.env['FORCE_COLOR'] = previousForceColor +} +if (previousNoColor !== undefined) { + process.env['NO_COLOR'] = previousNoColor +} describe('package score output with malware detection', async () => { describe('malware and gptMalware alerts', () => { diff --git a/src/commands/package/output-purls-shallow-score.mts b/src/commands/package/output-purls-shallow-score.mts index c37b224122..a1f00089e4 100644 --- a/src/commands/package/output-purls-shallow-score.mts +++ b/src/commands/package/output-purls-shallow-score.mts @@ -1,10 +1,9 @@ import colors from 'yoctocolors-cjs' -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - +import { joinAnd } from '../../utils/arrays.mts' +import { debugFn } from '../../utils/debug.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/package/output-purls-shallow-score.test.mts b/src/commands/package/output-purls-shallow-score.test.mts index 0a8a18e671..c0b60ad18f 100644 --- a/src/commands/package/output-purls-shallow-score.test.mts +++ b/src/commands/package/output-purls-shallow-score.test.mts @@ -7,11 +7,21 @@ import nugetShallow from './fixtures/nuget_shallow.json' import pythonDupes from './fixtures/python_dupes.json' import pythonShallow from './fixtures/python_shallow.json' import rubyShallow from './fixtures/ruby_shallow.json' -import { - generateMarkdownReport, - generateTextReport, - preProcess, -} from './output-purls-shallow-score.mts' +const previousForceColor = process.env['FORCE_COLOR'] +const previousNoColor = process.env['NO_COLOR'] +process.env['FORCE_COLOR'] = '1' +delete process.env['NO_COLOR'] +const { generateMarkdownReport, generateTextReport, preProcess } = await import( + './output-purls-shallow-score.mts' +) +if (previousForceColor === undefined) { + delete process.env['FORCE_COLOR'] +} else { + process.env['FORCE_COLOR'] = previousForceColor +} +if (previousNoColor !== undefined) { + process.env['NO_COLOR'] = previousNoColor +} describe('package score output', async () => { describe('npm', () => { diff --git a/src/commands/patch/cmd-patch.mts b/src/commands/patch/cmd-patch.mts index 782f48f286..5527131118 100644 --- a/src/commands/patch/cmd-patch.mts +++ b/src/commands/patch/cmd-patch.mts @@ -1,8 +1,18 @@ import { spawnSync } from 'node:child_process' -import { existsSync } from 'node:fs' +import { + accessSync, + existsSync, + constants as fsConstants, + statSync, +} from 'node:fs' import path from 'node:path' import constants from '../../constants.mts' +import { + cleanupBundledTool, + getBundledTool, + isStandaloneSea, +} from '../../sea/runtime.mts' import type { CliCommandContext } from '../../utils/meow-with-subcommands.mts' @@ -41,6 +51,35 @@ function resolveSocketPatchBin(): string { return 'socket-patch' } +function isSelfUpdate(argv: readonly string[]): boolean { + return argv.some( + (arg, index) => + arg === '--update' || + arg === 'self-update' || + arg === 'selfupdate' || + arg === 'self_update' || + ((arg === 'update' || arg === 'upgrade') && + (index === 0 || argv[index - 1] === 'socket-patch')), + ) +} + +function validateExplicitSocketPatchPath(binPath: string): string { + const absoluteBinPath = path.resolve(binPath) + try { + const stats = statSync(absoluteBinPath) + if (!stats.isFile()) { + throw new Error('not a regular file') + } + accessSync(absoluteBinPath, fsConstants.X_OK) + } catch (cause) { + throw new Error( + `SOCKET_CLI_SOCKET_PATCH_PATH must name an existing executable file: ${absoluteBinPath}`, + { cause }, + ) + } + return absoluteBinPath +} + async function run( argv: string[] | readonly string[], _importMeta: ImportMeta, @@ -48,6 +87,27 @@ async function run( ): Promise { const { ENV } = constants + const standalone = isStandaloneSea() + if (standalone && isSelfUpdate(argv)) { + throw new Error( + 'socket-patch self-update is unavailable in the standalone Socket build because it cannot replace the bundled executable.', + ) + } + let explicitBinPath = standalone + ? ENV.SOCKET_CLI_SOCKET_PATCH_PATH + : undefined + let bundledBinPath: string | undefined + if (standalone) { + // The explicit override takes precedence; otherwise extract only the + // verified embedded payload. Neither branch consults PATH. + if (explicitBinPath) { + explicitBinPath = validateExplicitSocketPatchPath(explicitBinPath) + } else { + bundledBinPath = await getBundledTool('socket-patch') + explicitBinPath = bundledBinPath + } + } + // Build environment variables for the socket-patch binary. const spawnEnv: Record = { ...process.env, @@ -77,22 +137,32 @@ async function run( // Resolve and spawn the socket-patch Rust binary. // On Windows, node_modules/.bin shims are .cmd scripts that require shell. - const binPath = resolveSocketPatchBin() - const result = spawnSync(binPath, [...argv], { - stdio: 'inherit', - env: spawnEnv, - shell: constants.WIN32, - }) + const binPath = explicitBinPath || resolveSocketPatchBin() + try { + const result = spawnSync(binPath, [...argv], { + stdio: 'inherit', + env: spawnEnv, + shell: explicitBinPath ? false : constants.WIN32, + }) - if (result.error) { - throw result.error - } - // Propagate signal if the child was killed (e.g. SIGTERM, SIGINT). - if (result.signal) { - process.kill(process.pid, result.signal) - return - } - if (result.status !== null && result.status !== 0) { - process.exitCode = result.status + if (result.error) { + throw result.error + } + // Propagate signal if the child was killed (e.g. SIGTERM, SIGINT). + if (result.signal) { + if (bundledBinPath) { + await cleanupBundledTool(bundledBinPath) + bundledBinPath = undefined + } + process.kill(process.pid, result.signal) + return + } + if (result.status !== null && result.status !== 0) { + process.exitCode = result.status + } + } finally { + if (bundledBinPath) { + await cleanupBundledTool(bundledBinPath) + } } } diff --git a/src/commands/pnpm/cmd-pnpm.mts b/src/commands/pnpm/cmd-pnpm.mts index 5b69b30d80..240a27d48e 100644 --- a/src/commands/pnpm/cmd-pnpm.mts +++ b/src/commands/pnpm/cmd-pnpm.mts @@ -1,10 +1,9 @@ import { createRequire } from 'node:module' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants, { FLAG_DRY_RUN, FLAG_HELP, PNPM } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { filterFlags } from '../../utils/cmd.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput } from '../../utils/output-formatting.mts' import { diff --git a/src/commands/pnpm/cmd-pnpm.test.mts b/src/commands/pnpm/cmd-pnpm.test.mts index 224f0f3a53..95808b8c04 100644 --- a/src/commands/pnpm/cmd-pnpm.test.mts +++ b/src/commands/pnpm/cmd-pnpm.test.mts @@ -5,8 +5,6 @@ import path from 'node:path' import trash from 'trash' import { describe, expect, it, vi } from 'vitest' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants, { FLAG_CONFIG, FLAG_DRY_RUN, @@ -16,8 +14,9 @@ import constants, { PNPM, } from '../../../src/constants.mts' import { cmdit, spawnSocketCli } from '../../../test/utils.mts' +import { spawn } from '../../utils/spawn.mts' -import type { SpawnOptions } from '@socketsecurity/registry/lib/spawn' +import type { SpawnOptions } from '../../utils/spawn.mts' // TODO: Several exec/install tests fail due to config flag handling. describe('socket pnpm', async () => { diff --git a/src/commands/raw-npm/cmd-raw-npm.mts b/src/commands/raw-npm/cmd-raw-npm.mts index 35cb7a3c43..30c327162a 100644 --- a/src/commands/raw-npm/cmd-raw-npm.mts +++ b/src/commands/raw-npm/cmd-raw-npm.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { runRawNpm } from './run-raw-npm.mts' import constants, { FLAG_DRY_RUN, FLAG_HELP } from '../../constants.mts' import { commonFlags } from '../../flags.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import type { diff --git a/src/commands/raw-npm/run-raw-npm.mts b/src/commands/raw-npm/run-raw-npm.mts index 46153853f2..e68f315eb1 100644 --- a/src/commands/raw-npm/run-raw-npm.mts +++ b/src/commands/raw-npm/run-raw-npm.mts @@ -1,7 +1,6 @@ -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants from '../../constants.mts' import { getNpmBinPath } from '../../utils/npm-paths.mts' +import { spawn } from '../../utils/spawn.mts' export async function runRawNpm( argv: string[] | readonly string[], diff --git a/src/commands/raw-npx/cmd-raw-npx.mts b/src/commands/raw-npx/cmd-raw-npx.mts index 9b4d670d2d..209c1a0386 100644 --- a/src/commands/raw-npx/cmd-raw-npx.mts +++ b/src/commands/raw-npx/cmd-raw-npx.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { runRawNpx } from './run-raw-npx.mts' import constants, { FLAG_DRY_RUN, FLAG_HELP } from '../../constants.mts' import { commonFlags } from '../../flags.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import type { diff --git a/src/commands/raw-npx/run-raw-npx.mts b/src/commands/raw-npx/run-raw-npx.mts index 428c654b61..f072a8b1a8 100644 --- a/src/commands/raw-npx/run-raw-npx.mts +++ b/src/commands/raw-npx/run-raw-npx.mts @@ -1,7 +1,6 @@ -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants from '../../constants.mts' import { getNpxBinPath } from '../../utils/npm-paths.mts' +import { spawn } from '../../utils/spawn.mts' export async function runRawNpx( argv: string[] | readonly string[], diff --git a/src/commands/repository/cmd-repository-create.mts b/src/commands/repository/cmd-repository-create.mts index 5af857e304..666b0b7ba4 100644 --- a/src/commands/repository/cmd-repository-create.mts +++ b/src/commands/repository/cmd-repository-create.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleCreateRepo } from './handle-create-repo.mts' import constants, { V1_MIGRATION_GUIDE_URL } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/repository/cmd-repository-del.mts b/src/commands/repository/cmd-repository-del.mts index e01163806d..0fab4d1d1a 100644 --- a/src/commands/repository/cmd-repository-del.mts +++ b/src/commands/repository/cmd-repository-del.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleDeleteRepo } from './handle-delete-repo.mts' import constants, { V1_MIGRATION_GUIDE_URL } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/repository/cmd-repository-list.mts b/src/commands/repository/cmd-repository-list.mts index 811027dc67..f245c955ef 100644 --- a/src/commands/repository/cmd-repository-list.mts +++ b/src/commands/repository/cmd-repository-list.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleListRepos } from './handle-list-repos.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/repository/cmd-repository-update.mts b/src/commands/repository/cmd-repository-update.mts index 756e92bc0e..1e8058bb35 100644 --- a/src/commands/repository/cmd-repository-update.mts +++ b/src/commands/repository/cmd-repository-update.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleUpdateRepo } from './handle-update-repo.mts' import constants, { V1_MIGRATION_GUIDE_URL } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/repository/cmd-repository-view.mts b/src/commands/repository/cmd-repository-view.mts index d0385cf6df..5ca12b82d1 100644 --- a/src/commands/repository/cmd-repository-view.mts +++ b/src/commands/repository/cmd-repository-view.mts @@ -1,5 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleViewRepo } from './handle-view-repo.mts' import constants, { FLAG_JSON, @@ -10,6 +8,7 @@ import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/repository/handle-create-repo.mts b/src/commands/repository/handle-create-repo.mts index 622e2f2133..f4e2c00cbd 100644 --- a/src/commands/repository/handle-create-repo.mts +++ b/src/commands/repository/handle-create-repo.mts @@ -1,7 +1,6 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' - import { fetchCreateRepo } from './fetch-create-repo.mts' import { outputCreateRepo } from './output-create-repo.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import type { OutputKind } from '../../types.mts' diff --git a/src/commands/repository/output-create-repo.mts b/src/commands/repository/output-create-repo.mts index 638320321a..edbe649ca5 100644 --- a/src/commands/repository/output-create-repo.mts +++ b/src/commands/repository/output-create-repo.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/repository/output-delete-repo.mts b/src/commands/repository/output-delete-repo.mts index 679bdbe634..fea4753949 100644 --- a/src/commands/repository/output-delete-repo.mts +++ b/src/commands/repository/output-delete-repo.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/repository/output-list-repos.mts b/src/commands/repository/output-list-repos.mts index 8aa6428ba7..3c90ff8600 100644 --- a/src/commands/repository/output-list-repos.mts +++ b/src/commands/repository/output-list-repos.mts @@ -2,9 +2,8 @@ import chalkTable from 'chalk-table' import colors from 'yoctocolors-cjs' -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { Direction } from './types.mts' diff --git a/src/commands/repository/output-update-repo.mts b/src/commands/repository/output-update-repo.mts index dfb47e54ae..84c87074dc 100644 --- a/src/commands/repository/output-update-repo.mts +++ b/src/commands/repository/output-update-repo.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/repository/output-view-repo.mts b/src/commands/repository/output-view-repo.mts index 66f21ad401..cd66bd7328 100644 --- a/src/commands/repository/output-view-repo.mts +++ b/src/commands/repository/output-view-repo.mts @@ -2,9 +2,8 @@ import chalkTable from 'chalk-table' import colors from 'yoctocolors-cjs' -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index 5a2390b11f..502f52dd90 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -1,8 +1,6 @@ import { readdirSync } from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { assertValidExcludePaths } from './exclude-paths.mts' import { generateUvMemberSboms, @@ -39,6 +37,7 @@ import { getRepoName, gitBranch, } from '../../utils/git.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, @@ -186,7 +185,7 @@ const generalFlags: MeowFlags = { type: 'boolean', default: false, description: - 'Scan each TARGET directory as a uv project, using the versions pinned in its uv.lock or its workspace root uv.lock. Uploads a CycloneDX dependency graph per TARGET in place of manifest discovery, including all extras and dependency groups. Requires uv on PATH.', + 'Scan each TARGET directory as a uv project, using the versions pinned in its uv.lock or its workspace root uv.lock. Uploads a CycloneDX dependency graph per TARGET in place of manifest discovery, including all extras and dependency groups. Requires uv on PATH; standalone builds use SOCKET_CLI_UV_PATH.', }, } diff --git a/src/commands/scan/cmd-scan-create.test.mts b/src/commands/scan/cmd-scan-create.test.mts index 940d9e9628..3efa1bb09e 100644 --- a/src/commands/scan/cmd-scan-create.test.mts +++ b/src/commands/scan/cmd-scan-create.test.mts @@ -119,7 +119,7 @@ describe('socket scan create', async () => { --report-level Which policy level alerts should be reported (default 'error') --set-as-alerts-page When true and if this is the "default branch" then this Scan will be the one reflected on your alerts page. See help for details. Defaults to true. --tmp Set the visibility (true/false) of the scan in your dashboard. - --uv-members Scan each TARGET directory as a uv project, using the versions pinned in its uv.lock or its workspace root uv.lock. Uploads a CycloneDX dependency graph per TARGET in place of manifest discovery, including all extras and dependency groups. Requires uv on PATH. + --uv-members Scan each TARGET directory as a uv project, using the versions pinned in its uv.lock or its workspace root uv.lock. Uploads a CycloneDX dependency graph per TARGET in place of manifest discovery, including all extras and dependency groups. Requires uv on PATH; standalone builds use SOCKET_CLI_UV_PATH. --workspace The workspace in the Socket Organization that the repository is in to associate with the full scan. Reachability Options (when --reach is used) diff --git a/src/commands/scan/cmd-scan-del.mts b/src/commands/scan/cmd-scan-del.mts index d591bd50c9..3ea1b0e84b 100644 --- a/src/commands/scan/cmd-scan-del.mts +++ b/src/commands/scan/cmd-scan-del.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleDeleteScan } from './handle-delete-scan.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/scan/cmd-scan-diff.mts b/src/commands/scan/cmd-scan-diff.mts index a7aa3b217d..8c8c15fbc8 100644 --- a/src/commands/scan/cmd-scan-diff.mts +++ b/src/commands/scan/cmd-scan-diff.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleDiffScan } from './handle-diff-scan.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/scan/cmd-scan-github.mts b/src/commands/scan/cmd-scan-github.mts index c17eabcf26..631099f637 100644 --- a/src/commands/scan/cmd-scan-github.mts +++ b/src/commands/scan/cmd-scan-github.mts @@ -1,7 +1,5 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleCreateGithubScan } from './handle-create-github-scan.mts' import { outputScanGithub } from './output-scan-github.mts' import { suggestOrgSlug } from './suggest-org-slug.mts' @@ -10,6 +8,7 @@ import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/scan/cmd-scan-list.mts b/src/commands/scan/cmd-scan-list.mts index 5889c1ec7a..736ddcb534 100644 --- a/src/commands/scan/cmd-scan-list.mts +++ b/src/commands/scan/cmd-scan-list.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleListScans } from './handle-list-scans.mts' import constants, { V1_MIGRATION_GUIDE_URL } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/scan/cmd-scan-metadata.mts b/src/commands/scan/cmd-scan-metadata.mts index 4f9c87cb94..636c22dad0 100644 --- a/src/commands/scan/cmd-scan-metadata.mts +++ b/src/commands/scan/cmd-scan-metadata.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleOrgScanMetadata } from './handle-scan-metadata.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/scan/cmd-scan-reach.e2e.test.mts b/src/commands/scan/cmd-scan-reach.e2e.test.mts index d5787dac95..56ad2d3eae 100644 --- a/src/commands/scan/cmd-scan-reach.e2e.test.mts +++ b/src/commands/scan/cmd-scan-reach.e2e.test.mts @@ -5,10 +5,9 @@ import path from 'node:path' import { describe, expect } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { cmdit, spawnSocketCli, testPath } from '../../../test/utils.mts' import constants from '../../constants.mts' +import { logger } from '../../utils/logger.mts' const fixtureBaseDir = path.join(testPath, 'fixtures/commands/scan') const systemTmpDir = tmpdir() diff --git a/src/commands/scan/cmd-scan-reach.mts b/src/commands/scan/cmd-scan-reach.mts index 70d0c4f757..56847a7716 100644 --- a/src/commands/scan/cmd-scan-reach.mts +++ b/src/commands/scan/cmd-scan-reach.mts @@ -1,7 +1,5 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { assertValidExcludePaths } from './exclude-paths.mts' import { handleScanReach } from './handle-scan-reach.mts' import { resolveReachAdvisories } from './reach-advisories.mts' @@ -22,6 +20,7 @@ import { cmdFlagValueToArray } from '../../utils/cmd.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { parseReachEcosystems } from '../../utils/ecosystem.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/scan/cmd-scan-report.mts b/src/commands/scan/cmd-scan-report.mts index 5d3dc05a28..e8f4ff9685 100644 --- a/src/commands/scan/cmd-scan-report.mts +++ b/src/commands/scan/cmd-scan-report.mts @@ -1,11 +1,10 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleScanReport } from './handle-scan-report.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/scan/cmd-scan-setup.mts b/src/commands/scan/cmd-scan-setup.mts index a7e216e944..5088e9d54f 100644 --- a/src/commands/scan/cmd-scan-setup.mts +++ b/src/commands/scan/cmd-scan-setup.mts @@ -1,10 +1,9 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleScanConfig } from './handle-scan-config.mts' import constants, { SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/scan/cmd-scan-view.mts b/src/commands/scan/cmd-scan-view.mts index 348f686b30..32e9fb22d3 100644 --- a/src/commands/scan/cmd-scan-view.mts +++ b/src/commands/scan/cmd-scan-view.mts @@ -1,5 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleScanView } from './handle-scan-view.mts' import { streamScan } from './stream-scan.mts' import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts' @@ -7,6 +5,7 @@ import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/scan/create-scan-from-github.mts b/src/commands/scan/create-scan-from-github.mts index 796ae2f5ac..d44fa32777 100644 --- a/src/commands/scan/create-scan-from-github.mts +++ b/src/commands/scan/create-scan-from-github.mts @@ -9,15 +9,16 @@ import os from 'node:os' import path from 'node:path' import { pipeline } from 'node:stream/promises' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { confirm, select } from '@socketsecurity/registry/lib/prompts' - import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts' import { handleCreateNewScan } from './handle-create-new-scan.mts' import constants from '../../constants.mts' import { apiFetch } from '../../utils/api.mts' -import { debugApiRequest, debugApiResponse } from '../../utils/debug.mts' +import { + debugApiRequest, + debugApiResponse, + debugDir, + debugFn, +} from '../../utils/debug.mts' import { formatErrorWithDetail } from '../../utils/errors.mts' import { classifyGitHubResponse, @@ -25,6 +26,8 @@ import { isGitHubBlockingError, } from '../../utils/github-errors.mts' import { isReportSupportedFile } from '../../utils/glob.mts' +import { logger } from '../../utils/logger.mts' +import { confirm, select } from '../../utils/prompts.mts' import { fetchListAllRepos } from '../repository/fetch-list-all-repos.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/scan/fetch-create-org-full-scan.mts b/src/commands/scan/fetch-create-org-full-scan.mts index b16ee7f789..4a0569f4e0 100644 --- a/src/commands/scan/fetch-create-org-full-scan.mts +++ b/src/commands/scan/fetch-create-org-full-scan.mts @@ -1,10 +1,9 @@ import fs from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants from '../../constants.mts' import { handleApiCall } from '../../utils/api.mts' +import { logger } from '../../utils/logger.mts' import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/scan/fetch-diff-scan.mts b/src/commands/scan/fetch-diff-scan.mts index f4e3ec5276..a63a4ebe89 100644 --- a/src/commands/scan/fetch-diff-scan.mts +++ b/src/commands/scan/fetch-diff-scan.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { queryApiSafeJson } from '../../utils/api.mts' +import { logger } from '../../utils/logger.mts' import type { CResult } from '../../types.mts' import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' diff --git a/src/commands/scan/fetch-report-data.mts b/src/commands/scan/fetch-report-data.mts index 175dfa81e9..0bb83bb763 100644 --- a/src/commands/scan/fetch-report-data.mts +++ b/src/commands/scan/fetch-report-data.mts @@ -1,9 +1,8 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants from '../../constants.mts' import { handleApiCallNoSpinner, queryApiSafeText } from '../../utils/api.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import { formatErrorWithDetail } from '../../utils/errors.mts' +import { logger } from '../../utils/logger.mts' import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/scan/fetch-scan.mts b/src/commands/scan/fetch-scan.mts index 8df41bfd63..3de295d976 100644 --- a/src/commands/scan/fetch-scan.mts +++ b/src/commands/scan/fetch-scan.mts @@ -1,6 +1,5 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' - import { queryApiSafeTextWithStatus } from '../../utils/api.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import type { CResult } from '../../types.mts' import type { SocketArtifact } from '../../utils/alert/artifact.mts' diff --git a/src/commands/scan/fetch-supported-scan-file-names.mts b/src/commands/scan/fetch-supported-scan-file-names.mts index 4fcfc2fd75..d9df1ea3f6 100644 --- a/src/commands/scan/fetch-supported-scan-file-names.mts +++ b/src/commands/scan/fetch-supported-scan-file-names.mts @@ -4,7 +4,7 @@ import { getDefaultOrgSlug } from '../ci/fetch-default-org-slug.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { Spinner } from '../../utils/spinner.mts' import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' export type FetchSupportedScanFileNamesOptions = { diff --git a/src/commands/scan/generate-report.mts b/src/commands/scan/generate-report.mts index ecbc42fd05..09a5555d41 100644 --- a/src/commands/scan/generate-report.mts +++ b/src/commands/scan/generate-report.mts @@ -4,7 +4,7 @@ import { getSocketDevPackageOverviewUrlFromPurl } from '../../utils/socket-url.m import type { FOLD_SETTING, REPORT_LEVEL } from './types.mts' import type { CResult } from '../../types.mts' import type { SocketArtifact } from '../../utils/alert/artifact.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { Spinner } from '../../utils/spinner.mts' import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' type AlertKey = string diff --git a/src/commands/scan/generate-uv-member-sboms.mts b/src/commands/scan/generate-uv-member-sboms.mts index e01f2bc4b6..9941d626a5 100644 --- a/src/commands/scan/generate-uv-member-sboms.mts +++ b/src/commands/scan/generate-uv-member-sboms.mts @@ -1,18 +1,55 @@ -import { existsSync, promises as fs, rmSync } from 'node:fs' +import { + accessSync, + existsSync, + promises as fs, + constants as fsConstants, + rmSync, + statSync, +} from 'node:fs' import path from 'node:path' -import { isDirSync } from '@socketsecurity/registry/lib/fs' -import { logger } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants from '../../constants.mts' +import { + cleanupBundledTool, + getBundledTool, + isStandaloneSea, +} from '../../sea/runtime.mts' import { InputError, getErrorMessage } from '../../utils/errors.mts' +import { isDirSync } from '../../utils/fs.mts' +import { logger } from '../../utils/logger.mts' +import { spawn } from '../../utils/spawn.mts' import type { GeneratedScanFiles } from './handle-create-new-scan.mts' const CLEANUP_SIGNALS: NodeJS.Signals[] = ['SIGHUP', 'SIGINT', 'SIGTERM'] const UV_SBOM_FILENAME = 'socket-uv-cdx.json' +const UV_PATH_ENV = 'SOCKET_CLI_UV_PATH' + +function resolveUvExecutable(): string { + if (!isStandaloneSea()) { + return 'uv' + } + const override = process.env[UV_PATH_ENV] + if (!override) { + return '' + } + const executable = path.resolve(process.cwd(), override) + try { + accessSync( + executable, + process.platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK, + ) + if (!statSync(executable).isFile()) { + throw new Error('not a file') + } + } catch { + throw new InputError( + `${UV_PATH_ENV} must point to an executable uv binary, but '${executable}' is missing or not executable.`, + ) + } + return executable +} type Component = { name: string @@ -52,9 +89,15 @@ async function exportSbom( groups: '--all-groups' | '--no-default-groups', ): Promise { let content: string + const uvExecutable = resolveUvExecutable() + let bundledExecutable: string | undefined + let childClosed: Promise | undefined try { - const { stdout } = await spawn( - 'uv', + if (!uvExecutable) { + bundledExecutable = await getBundledTool('uv') + } + const child = spawn( + uvExecutable || bundledExecutable!, [ 'export', '--project', @@ -73,6 +116,10 @@ async function exportSbom( stdio: 'pipe', }, ) + if (child.process) { + childClosed = new Promise(resolve => child.process.once('close', resolve)) + } + const { stdout } = await child content = stdout } catch (e) { throw new InputError( @@ -81,6 +128,11 @@ async function exportSbom( ? String(e.stderr).trim() || getErrorMessage(e) : getErrorMessage(e), ) + } finally { + await childClosed + if (bundledExecutable) { + await cleanupBundledTool(bundledExecutable) + } } return parseSbom(content, memberDir) } diff --git a/src/commands/scan/generate-uv-member-sboms.test.mts b/src/commands/scan/generate-uv-member-sboms.test.mts index a8d6a4afb3..24d15be7e6 100644 --- a/src/commands/scan/generate-uv-member-sboms.test.mts +++ b/src/commands/scan/generate-uv-member-sboms.test.mts @@ -1,3 +1,4 @@ +import { EventEmitter } from 'node:events' import { existsSync, promises as fs } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' @@ -9,9 +10,20 @@ import { resolveUvMemberDirs, } from './generate-uv-member-sboms.mts' -const { mockSpawn } = vi.hoisted(() => ({ mockSpawn: vi.fn() })) +const { mockCleanupBundledTool, mockGetBundledTool, mockSea, mockSpawn } = + vi.hoisted(() => ({ + mockSpawn: vi.fn(), + mockGetBundledTool: vi.fn(), + mockCleanupBundledTool: vi.fn(), + mockSea: vi.fn(), + })) -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ spawn: mockSpawn })) +vi.mock('../../utils/spawn.mts', () => ({ spawn: mockSpawn })) +vi.mock('../../sea/runtime.mts', () => ({ + cleanupBundledTool: mockCleanupBundledTool, + getBundledTool: mockGetBundledTool, + isStandaloneSea: mockSea, +})) const sbom = JSON.stringify({ bomFormat: 'CycloneDX', @@ -28,6 +40,9 @@ describe('uv member SBOM export', () => { beforeEach(async () => { vi.clearAllMocks() + mockSea.mockReturnValue(false) + mockGetBundledTool.mockResolvedValue('/tmp/bundled-uv') + mockCleanupBundledTool.mockResolvedValue(undefined) cwd = await fs.mkdtemp(path.join(tmpdir(), 'socket-uv-unit-')) apiDir = path.join(cwd, 'packages/api') workerDir = path.join(cwd, 'packages/worker') @@ -77,6 +92,63 @@ describe('uv member SBOM export', () => { ) }) + it('cleans an extracted uv only after the child closes', async () => { + mockSea.mockReturnValue(true) + const closed: boolean[] = [] + mockSpawn.mockImplementation(() => { + const child = new EventEmitter() + const promise = Promise.resolve({ stdout: sbom }) as Promise<{ + stdout: string + }> & { process: EventEmitter } + Object.defineProperty(promise, 'process', { value: child }) + setTimeout(() => { + closed.push(true) + child.emit('close') + }, 0) + return promise + }) + mockCleanupBundledTool.mockImplementation(async () => { + expect(closed.length).toBeGreaterThan(0) + }) + + await generateUvMemberSboms([apiDir]) + expect(mockGetBundledTool).toHaveBeenCalledTimes(2) + expect(mockCleanupBundledTool).toHaveBeenCalledTimes(2) + }) + + it('does not extract or clean uv when SOCKET_CLI_UV_PATH overrides it', async () => { + mockSea.mockReturnValue(true) + const override = path.join(cwd, 'uv') + await fs.writeFile(override, 'external uv') + await fs.chmod(override, 0o700) + const previous = process.env['SOCKET_CLI_UV_PATH'] + process.env['SOCKET_CLI_UV_PATH'] = override + try { + await generateUvMemberSboms([apiDir]) + expect(mockSpawn).toHaveBeenCalledWith( + override, + expect.any(Array), + expect.any(Object), + ) + expect(mockGetBundledTool).not.toHaveBeenCalled() + expect(mockCleanupBundledTool).not.toHaveBeenCalled() + } finally { + if (previous === undefined) { + delete process.env['SOCKET_CLI_UV_PATH'] + } else { + process.env['SOCKET_CLI_UV_PATH'] = previous + } + } + }) + + it('surfaces failure to remove an extracted uv payload', async () => { + mockSea.mockReturnValue(true) + mockCleanupBundledTool.mockRejectedValueOnce(new Error('cleanup failed')) + await expect(generateUvMemberSboms([apiDir])).rejects.toThrow( + 'cleanup failed', + ) + }) + it('removes every written SBOM on cleanup', async () => { const { cleanup, files } = await generateUvMemberSboms([apiDir, workerDir]) expect(files).toEqual([ diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index a60548033a..6322f55066 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -3,10 +3,6 @@ import path from 'node:path' import micromatch from 'micromatch' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { pluralize } from '@socketsecurity/registry/lib/words' - import { applyFullExcludePaths } from './exclude-paths.mts' import { fetchCreateOrgFullScan } from './fetch-create-org-full-scan.mts' import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts' @@ -23,10 +19,13 @@ import { snapshotSocketFacts, } from '../../utils/coana.mts' import { findSocketYmlSync } from '../../utils/config.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import { withTmpDir } from '../../utils/fs.mts' +import { logger } from '../../utils/logger.mts' import { getPackageFilesForScan } from '../../utils/path-resolve.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' import { socketDocsLink } from '../../utils/terminal-link.mts' +import { pluralize } from '../../utils/words.mts' import { detectManifestActions } from '../manifest/detect-manifest-actions.mts' import { generateAutoManifest } from '../manifest/generate_auto_manifest.mts' import { buildToolsFor } from '../manifest/scripts/build-tool.mts' @@ -37,8 +36,8 @@ import type { ReachabilityOptions } from './perform-reachability-analysis.mts' import type { REPORT_LEVEL } from './types.mts' import type { OutputKind } from '../../types.mts' import type { FactsOnlyTypes } from '../../utils/coana.mts' +import type { Remap } from '../../utils/objects.mts' import type { ResolvedPathsSidecar } from '../manifest/scripts/sidecar.mts' -import type { Remap } from '@socketsecurity/registry/lib/objects' import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' // Supported-files response keys whose files count as pre-generated SBOMs: diff --git a/src/commands/scan/handle-create-new-scan.test.mts b/src/commands/scan/handle-create-new-scan.test.mts index 628711d004..f7d23a6025 100644 --- a/src/commands/scan/handle-create-new-scan.test.mts +++ b/src/commands/scan/handle-create-new-scan.test.mts @@ -10,12 +10,11 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { finalizeTier1Scan } from './finalize-tier1-scan.mts' import { handleCreateNewScan } from './handle-create-new-scan.mts' import { outputCreateNewScan } from './output-create-new-scan.mts' import { makeReachabilityOptions } from '../../../test/reachability-options.mts' +import { logger } from '../../utils/logger.mts' import type { HandleCreateNewScanConfig } from './handle-create-new-scan.mts' diff --git a/src/commands/scan/handle-scan-reach.mts b/src/commands/scan/handle-scan-reach.mts index 264ca5e500..7613a20c10 100644 --- a/src/commands/scan/handle-scan-reach.mts +++ b/src/commands/scan/handle-scan-reach.mts @@ -1,6 +1,3 @@ -import { logger } from '@socketsecurity/registry/lib/logger' -import { pluralize } from '@socketsecurity/registry/lib/words' - import { applyFullExcludePaths } from './exclude-paths.mts' import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts' import { finalizeTier1Scan } from './finalize-tier1-scan.mts' @@ -11,7 +8,9 @@ import constants from '../../constants.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { findSocketYmlSync } from '../../utils/config.mts' import { withTmpDir } from '../../utils/fs.mts' +import { logger } from '../../utils/logger.mts' import { getPackageFilesForScan } from '../../utils/path-resolve.mts' +import { pluralize } from '../../utils/words.mts' import type { ReachabilityOptions } from './perform-reachability-analysis.mts' import type { DynamicSbomInferenceResult } from './run-dynamic-sbom-inference.mts' diff --git a/src/commands/scan/handle-scan-reach.test.mts b/src/commands/scan/handle-scan-reach.test.mts index c35ce6a342..d98a4c410d 100644 --- a/src/commands/scan/handle-scan-reach.test.mts +++ b/src/commands/scan/handle-scan-reach.test.mts @@ -81,7 +81,7 @@ vi.mock('../../utils/path-resolve.mts', () => ({ getPackageFilesForScan: mockGetPackageFilesForScan, })) -vi.mock('@socketsecurity/registry/lib/logger', () => ({ +vi.mock('../../utils/logger.mts', () => ({ logger: { info: mockLoggerInfo, success: mockLoggerSuccess, diff --git a/src/commands/scan/output-create-new-scan.mts b/src/commands/scan/output-create-new-scan.mts index a708d5e5e6..e4ebd8652d 100644 --- a/src/commands/scan/output-create-new-scan.mts +++ b/src/commands/scan/output-create-new-scan.mts @@ -1,15 +1,14 @@ import open from 'open' import terminalLink from 'terminal-link' -import { logger } from '@socketsecurity/registry/lib/logger' -import { confirm } from '@socketsecurity/registry/lib/prompts' - import constants from '../../constants.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' +import { confirm } from '../../utils/prompts.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { Spinner } from '../../utils/spinner.mts' import type { FullScanResult } from '@socketsecurity/sdk' export type ReachabilityFallback = { diff --git a/src/commands/scan/output-create-new-scan.test.mts b/src/commands/scan/output-create-new-scan.test.mts index 92bd1f8a96..89b9d30852 100644 --- a/src/commands/scan/output-create-new-scan.test.mts +++ b/src/commands/scan/output-create-new-scan.test.mts @@ -11,9 +11,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { outputCreateNewScan } from './output-create-new-scan.mts' +import { logger } from '../../utils/logger.mts' import type { FullScanResult } from '@socketsecurity/sdk' diff --git a/src/commands/scan/output-delete-scan.mts b/src/commands/scan/output-delete-scan.mts index 0c4b00fdf0..e1bc3cfd15 100644 --- a/src/commands/scan/output-delete-scan.mts +++ b/src/commands/scan/output-delete-scan.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/scan/output-diff-scan.mts b/src/commands/scan/output-diff-scan.mts index 72cd494491..2fb87c7671 100644 --- a/src/commands/scan/output-diff-scan.mts +++ b/src/commands/scan/output-diff-scan.mts @@ -3,10 +3,9 @@ import util from 'node:util' import colors from 'yoctocolors-cjs' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants from '../../constants.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import { fileLink } from '../../utils/terminal-link.mts' diff --git a/src/commands/scan/output-list-scans.mts b/src/commands/scan/output-list-scans.mts index 4e677f6392..9f103e5f7e 100644 --- a/src/commands/scan/output-list-scans.mts +++ b/src/commands/scan/output-list-scans.mts @@ -2,9 +2,8 @@ import chalkTable from 'chalk-table' import colors from 'yoctocolors-cjs' -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/scan/output-scan-config-result.mts b/src/commands/scan/output-scan-config-result.mts index c60a215869..da5c213c07 100644 --- a/src/commands/scan/output-scan-config-result.mts +++ b/src/commands/scan/output-scan-config-result.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/scan/output-scan-github.mts b/src/commands/scan/output-scan-github.mts index 2a16a583ea..d21880a955 100644 --- a/src/commands/scan/output-scan-github.mts +++ b/src/commands/scan/output-scan-github.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/scan/output-scan-metadata.mts b/src/commands/scan/output-scan-metadata.mts index 47bcc8afb8..217cbe3e5e 100644 --- a/src/commands/scan/output-scan-metadata.mts +++ b/src/commands/scan/output-scan-metadata.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/scan/output-scan-reach.mts b/src/commands/scan/output-scan-reach.mts index 5b39eaa726..dc4d19a383 100644 --- a/src/commands/scan/output-scan-reach.mts +++ b/src/commands/scan/output-scan-reach.mts @@ -1,15 +1,14 @@ import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' -import { pluralize } from '@socketsecurity/registry/lib/words' - import constants from '../../constants.mts' import { extractReachabilityErrors, getFullWorkspacePath, } from '../../utils/coana.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' +import { pluralize } from '../../utils/words.mts' import type { ReachabilityAnalysisResult } from './perform-reachability-analysis.mts' import type { CResult, OutputKind } from '../../types.mts' diff --git a/src/commands/scan/output-scan-reach.test.mts b/src/commands/scan/output-scan-reach.test.mts index fcf98fe0d0..7a951dc965 100644 --- a/src/commands/scan/output-scan-reach.test.mts +++ b/src/commands/scan/output-scan-reach.test.mts @@ -18,9 +18,8 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { logger } from '@socketsecurity/registry/lib/logger' - import { outputScanReach } from './output-scan-reach.mts' +import { logger } from '../../utils/logger.mts' import type { ReachabilityAnalysisResult } from './perform-reachability-analysis.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/scan/output-scan-report.mts b/src/commands/scan/output-scan-report.mts index 35965bf96f..2c05b85ffe 100644 --- a/src/commands/scan/output-scan-report.mts +++ b/src/commands/scan/output-scan-report.mts @@ -1,15 +1,14 @@ import fs from 'node:fs/promises' -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' - import { generateReport } from './generate-report.mts' import constants, { EXT_JSON, OUTPUT_JSON, OUTPUT_TEXT, } from '../../constants.mts' +import { joinAnd } from '../../utils/arrays.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { mapToObject } from '../../utils/map-to-object.mts' import { mdTable } from '../../utils/markdown.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' diff --git a/src/commands/scan/output-scan-view.mts b/src/commands/scan/output-scan-view.mts index 55fbb4d657..a8504173bc 100644 --- a/src/commands/scan/output-scan-view.mts +++ b/src/commands/scan/output-scan-view.mts @@ -1,9 +1,8 @@ import fs from 'node:fs/promises' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants from '../../constants.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { mdTable } from '../../utils/markdown.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import { fileLink } from '../../utils/terminal-link.mts' diff --git a/src/commands/scan/perform-reachability-analysis.mts b/src/commands/scan/perform-reachability-analysis.mts index 9e57bc1a0a..64e862f8f4 100644 --- a/src/commands/scan/perform-reachability-analysis.mts +++ b/src/commands/scan/perform-reachability-analysis.mts @@ -3,8 +3,6 @@ import { promises as fs } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' - import { isOmittedReachValue } from './reachability-units.mts' import constants, { COANA_CLI_VERSION } from '../../constants.mts' import { handleApiCall } from '../../utils/api.mts' @@ -14,6 +12,7 @@ import { isReachabilityReportPath, useOnlySocketFactsArtifactsFlags, } from '../../utils/coana.mts' +import { logger } from '../../utils/logger.mts' import { hasEnterpriseOrgPlan } from '../../utils/organization.mts' import { setupSdk } from '../../utils/sdk.mts' import { socketDevLink } from '../../utils/terminal-link.mts' @@ -23,8 +22,8 @@ import { fetchOrganization } from '../organization/fetch-organization-list.mts' import type { CResult, OutputKind } from '../../types.mts' import type { FactsOnlyTypes } from '../../utils/coana.mts' import type { PURL_Type } from '../../utils/ecosystem.mts' +import type { Spinner } from '../../utils/spinner.mts' import type { ResolvedPathsSidecar } from '../manifest/scripts/sidecar.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' import type { StdioOptions } from 'node:child_process' export type ReachabilityOptions = { diff --git a/src/commands/scan/perform-reachability-analysis.test.mts b/src/commands/scan/perform-reachability-analysis.test.mts index 82add0f187..3699ca0bc5 100644 --- a/src/commands/scan/perform-reachability-analysis.test.mts +++ b/src/commands/scan/perform-reachability-analysis.test.mts @@ -89,7 +89,7 @@ vi.mock('../../constants.mts', () => ({ }, })) -vi.mock('@socketsecurity/registry/lib/logger', () => ({ +vi.mock('../../utils/logger.mts', () => ({ logger: { error: vi.fn(), info: vi.fn(), diff --git a/src/commands/scan/reach-advisories.mts b/src/commands/scan/reach-advisories.mts index 98b55587de..5bda117e30 100644 --- a/src/commands/scan/reach-advisories.mts +++ b/src/commands/scan/reach-advisories.mts @@ -1,6 +1,5 @@ -import { arrayUnique } from '@socketsecurity/registry/lib/arrays' - import { resolveAdvisoryIds } from '../../utils/advisory-ids.mts' +import { arrayUnique } from '../../utils/arrays.mts' import { CVE_FORMAT_REGEXP, GHSA_FORMAT_REGEXP, diff --git a/src/commands/scan/reachability-flags.mts b/src/commands/scan/reachability-flags.mts index 79261f967e..c92aeb169d 100644 --- a/src/commands/scan/reachability-flags.mts +++ b/src/commands/scan/reachability-flags.mts @@ -1,6 +1,5 @@ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' - import { COANA_CLI_VERSION } from '../../constants.mts' +import { joinAnd } from '../../utils/arrays.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' import { isFactsOnlyTypes } from '../../utils/coana.mts' import { diff --git a/src/commands/scan/run-dynamic-sbom-inference.mts b/src/commands/scan/run-dynamic-sbom-inference.mts index 25010c5336..ad4921d04f 100644 --- a/src/commands/scan/run-dynamic-sbom-inference.mts +++ b/src/commands/scan/run-dynamic-sbom-inference.mts @@ -1,8 +1,7 @@ -import { joinOr } from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' - +import { joinOr } from '../../utils/arrays.mts' import { isFactsOnlyTypes } from '../../utils/coana.mts' import { InputError } from '../../utils/errors.mts' +import { logger } from '../../utils/logger.mts' import { generateRecursiveManifests } from '../manifest/generate-recursive-manifests.mts' import { PURL_TYPE_BY_TOOL, diff --git a/src/commands/scan/run-dynamic-sbom-inference.test.mts b/src/commands/scan/run-dynamic-sbom-inference.test.mts index 656fa02eef..b282084f89 100644 --- a/src/commands/scan/run-dynamic-sbom-inference.test.mts +++ b/src/commands/scan/run-dynamic-sbom-inference.test.mts @@ -10,7 +10,7 @@ const { mockGenerateRecursiveManifests, mockWarn } = vi.hoisted(() => ({ mockWarn: vi.fn(), })) -vi.mock('@socketsecurity/registry/lib/logger', () => ({ +vi.mock('../../utils/logger.mts', () => ({ logger: { warn: mockWarn }, })) diff --git a/src/commands/scan/setup-scan-config.mts b/src/commands/scan/setup-scan-config.mts index 76390c66b5..9744313636 100644 --- a/src/commands/scan/setup-scan-config.mts +++ b/src/commands/scan/setup-scan-config.mts @@ -1,9 +1,6 @@ import fs from 'node:fs' import path from 'node:path' -import { logger } from '@socketsecurity/registry/lib/logger' -import { input, select } from '@socketsecurity/registry/lib/prompts' - import constants, { SOCKET_JSON } from '../../constants.mts' import { detectDefaultBranch, @@ -11,6 +8,8 @@ import { getRepoOwner, gitBranch, } from '../../utils/git.mts' +import { logger } from '../../utils/logger.mts' +import { input, select } from '../../utils/prompts.mts' import { readSocketJsonSync, writeSocketJson, diff --git a/src/commands/scan/stream-scan.mts b/src/commands/scan/stream-scan.mts index 6ff805f472..79881d1952 100644 --- a/src/commands/scan/stream-scan.mts +++ b/src/commands/scan/stream-scan.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleApiCall } from '../../utils/api.mts' +import { logger } from '../../utils/logger.mts' import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/scan/suggest-org-slug.mts b/src/commands/scan/suggest-org-slug.mts index de60f95a11..76e0287b8b 100644 --- a/src/commands/scan/suggest-org-slug.mts +++ b/src/commands/scan/suggest-org-slug.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' -import { select } from '@socketsecurity/registry/lib/prompts' - +import { logger } from '../../utils/logger.mts' +import { select } from '../../utils/prompts.mts' import { fetchOrganization } from '../organization/fetch-organization-list.mts' export async function suggestOrgSlug(): Promise { diff --git a/src/commands/scan/suggest-to-persist-orgslug.mts b/src/commands/scan/suggest-to-persist-orgslug.mts index d4235cc3c4..3f4eca974e 100644 --- a/src/commands/scan/suggest-to-persist-orgslug.mts +++ b/src/commands/scan/suggest-to-persist-orgslug.mts @@ -1,7 +1,6 @@ -import { logger } from '@socketsecurity/registry/lib/logger' -import { select } from '@socketsecurity/registry/lib/prompts' - import { getConfigValue, updateConfigValue } from '../../utils/config.mts' +import { logger } from '../../utils/logger.mts' +import { select } from '../../utils/prompts.mts' export async function suggestToPersistOrgSlug(orgSlug: string): Promise { const skipAsk = getConfigValue('skipAskToPersistDefaultOrg') diff --git a/src/commands/scan/suggest_branch_slug.mts b/src/commands/scan/suggest_branch_slug.mts index 30214f8d49..01ce4113ff 100644 --- a/src/commands/scan/suggest_branch_slug.mts +++ b/src/commands/scan/suggest_branch_slug.mts @@ -1,6 +1,6 @@ -import { select } from '@socketsecurity/registry/lib/prompts' -import { spawn } from '@socketsecurity/registry/lib/spawn' -import { stripAnsi } from '@socketsecurity/registry/lib/strings' +import { select } from '../../utils/prompts.mts' +import { spawn } from '../../utils/spawn.mts' +import { stripAnsi } from '../../utils/strings.mts' export async function suggestBranchSlug( repoDefaultBranch: string | undefined, diff --git a/src/commands/scan/suggest_target.mts b/src/commands/scan/suggest_target.mts index 5637353d2c..07db4764bb 100644 --- a/src/commands/scan/suggest_target.mts +++ b/src/commands/scan/suggest_target.mts @@ -1,4 +1,4 @@ -import { select } from '@socketsecurity/registry/lib/prompts' +import { select } from '../../utils/prompts.mts' export async function suggestTarget(): Promise { // We could prefill this with sub-dirs of the current diff --git a/src/commands/scan/warn-socket-json-only-flags.mts b/src/commands/scan/warn-socket-json-only-flags.mts index 26e2857969..91572be83e 100644 --- a/src/commands/scan/warn-socket-json-only-flags.mts +++ b/src/commands/scan/warn-socket-json-only-flags.mts @@ -1,5 +1,4 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - +import { logger } from '../../utils/logger.mts' import { camelToKebab } from '../../utils/strings.mts' import { DSI_SETUP_COMMAND } from '../manifest/scripts/resolution-report-render.mts' diff --git a/src/commands/threat-feed/cmd-threat-feed.mts b/src/commands/threat-feed/cmd-threat-feed.mts index 9fb0c69e8f..d620ddd5aa 100644 --- a/src/commands/threat-feed/cmd-threat-feed.mts +++ b/src/commands/threat-feed/cmd-threat-feed.mts @@ -1,12 +1,11 @@ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleThreatFeed } from './handle-threat-feed.mts' import constants, { NPM } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' +import { joinAnd } from '../../utils/arrays.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput, diff --git a/src/commands/threat-feed/output-threat-feed.mts b/src/commands/threat-feed/output-threat-feed.mts index 943371aaf0..aa08af32e3 100644 --- a/src/commands/threat-feed/output-threat-feed.mts +++ b/src/commands/threat-feed/output-threat-feed.mts @@ -1,9 +1,8 @@ import { createRequire } from 'node:module' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants from '../../constants.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import { msAtHome } from '../../utils/ms-at-home.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' diff --git a/src/commands/uninstall/cmd-uninstall-completion.mts b/src/commands/uninstall/cmd-uninstall-completion.mts index 9b7b3aa2f1..4b7d468131 100644 --- a/src/commands/uninstall/cmd-uninstall-completion.mts +++ b/src/commands/uninstall/cmd-uninstall-completion.mts @@ -1,8 +1,7 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { handleUninstallCompletion } from './handle-uninstall-completion.mts' import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/uninstall/output-uninstall-completion.mts b/src/commands/uninstall/output-uninstall-completion.mts index 2c26227113..0de4942005 100644 --- a/src/commands/uninstall/output-uninstall-completion.mts +++ b/src/commands/uninstall/output-uninstall-completion.mts @@ -1,6 +1,5 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' +import { logger } from '../../utils/logger.mts' import type { CResult } from '../../types.mts' diff --git a/src/commands/wrapper/add-socket-wrapper.mts b/src/commands/wrapper/add-socket-wrapper.mts index 8322c8792e..bcc1c1ab76 100644 --- a/src/commands/wrapper/add-socket-wrapper.mts +++ b/src/commands/wrapper/add-socket-wrapper.mts @@ -1,6 +1,6 @@ import fs from 'node:fs' -import { logger } from '@socketsecurity/registry/lib/logger' +import { logger } from '../../utils/logger.mts' export function addSocketWrapper(file: string): void { return fs.appendFile( diff --git a/src/commands/wrapper/check-socket-wrapper-setup.mts b/src/commands/wrapper/check-socket-wrapper-setup.mts index 9eedcd1fc8..f6f28f730c 100644 --- a/src/commands/wrapper/check-socket-wrapper-setup.mts +++ b/src/commands/wrapper/check-socket-wrapper-setup.mts @@ -1,6 +1,6 @@ import fs from 'node:fs' -import { logger } from '@socketsecurity/registry/lib/logger' +import { logger } from '../../utils/logger.mts' export function checkSocketWrapperSetup(file: string): boolean { const fileContent = fs.readFileSync(file, 'utf8') diff --git a/src/commands/wrapper/cmd-wrapper.mts b/src/commands/wrapper/cmd-wrapper.mts index c71d9bdb55..45948565f2 100644 --- a/src/commands/wrapper/cmd-wrapper.mts +++ b/src/commands/wrapper/cmd-wrapper.mts @@ -1,7 +1,5 @@ import { existsSync } from 'node:fs' -import { logger } from '@socketsecurity/registry/lib/logger' - import { addSocketWrapper } from './add-socket-wrapper.mts' import { checkSocketWrapperSetup } from './check-socket-wrapper-setup.mts' import { postinstallWrapper } from './postinstall-wrapper.mts' @@ -10,6 +8,7 @@ import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' diff --git a/src/commands/wrapper/postinstall-wrapper.mts b/src/commands/wrapper/postinstall-wrapper.mts index ab15407c3e..4097444054 100644 --- a/src/commands/wrapper/postinstall-wrapper.mts +++ b/src/commands/wrapper/postinstall-wrapper.mts @@ -1,14 +1,13 @@ import fs, { existsSync } from 'node:fs' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { confirm } from '@socketsecurity/registry/lib/prompts' - import { addSocketWrapper } from './add-socket-wrapper.mts' import { checkSocketWrapperSetup } from './check-socket-wrapper-setup.mts' import constants from '../../constants.mts' import { getBashrcDetails } from '../../utils/completion.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' import { getErrorCause } from '../../utils/errors.mts' +import { logger } from '../../utils/logger.mts' +import { confirm } from '../../utils/prompts.mts' import { updateInstalledTabCompletionScript } from '../install/setup-tab-completion.mts' export async function postinstallWrapper() { diff --git a/src/commands/wrapper/remove-socket-wrapper.mts b/src/commands/wrapper/remove-socket-wrapper.mts index a5d479925b..3306ea61df 100644 --- a/src/commands/wrapper/remove-socket-wrapper.mts +++ b/src/commands/wrapper/remove-socket-wrapper.mts @@ -1,6 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs' -import { logger } from '@socketsecurity/registry/lib/logger' +import { logger } from '../../utils/logger.mts' export function removeSocketWrapper(filepath: string): void { let content: string | undefined diff --git a/src/commands/yarn/cmd-yarn.mts b/src/commands/yarn/cmd-yarn.mts index d1389465a9..6dae88b034 100644 --- a/src/commands/yarn/cmd-yarn.mts +++ b/src/commands/yarn/cmd-yarn.mts @@ -1,10 +1,9 @@ import { createRequire } from 'node:module' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants, { FLAG_DRY_RUN, FLAG_HELP, YARN } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { filterFlags } from '../../utils/cmd.mts' +import { logger } from '../../utils/logger.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagApiRequirementsOutput } from '../../utils/output-formatting.mts' import { diff --git a/src/constants.mts b/src/constants.mts index eb31e83edf..637bfbdb98 100644 --- a/src/constants.mts +++ b/src/constants.mts @@ -1,14 +1,25 @@ -import { realpathSync } from 'node:fs' +import { readFileSync, realpathSync } from 'node:fs' import { createRequire } from 'node:module' import os from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' -import registryConstants from '@socketsecurity/registry/lib/constants' +import registryConstants, { + createConstantsObject, + getIpc, + registryConstantsAttribs, +} from './registry-constants.mts' +import { envAsBoolean, envAsNumber, envAsString } from './utils/env.mts' +import { spawnSync } from './utils/spawn-sync.mts' +import type { + RegistryConstants, + RegistryEnv, + RegistryInternals, +} from './registry-constants.mts' +import type { Remap } from './utils/objects.mts' import type { Agent } from './utils/package-environment.mts' -import type { Remap } from '@socketsecurity/registry/lib/objects' -import type { SpawnOptions } from '@socketsecurity/registry/lib/spawn' +import type { SpawnOptions } from './utils/spawn.mts' const require = createRequire(import.meta.url) const __filename = fileURLToPath(import.meta.url) @@ -24,6 +35,7 @@ const { CI, COLUMN_LIMIT, DOT_GIT_DIR, + DOT_PACKAGE_LOCK_JSON, DOT_SOCKET_DIR, EMPTY_FILE, EMPTY_VALUE, @@ -46,7 +58,6 @@ const { EXTENSIONS, EXTENSIONS_JSON, GITIGNORE, - DOT_PACKAGE_LOCK_JSON, LATEST, LICENSE, LICENSE_GLOB, @@ -72,9 +83,9 @@ const { PRE_COMMIT, README_GLOB, README_GLOB_RECURSIVE, - REGISTRY_SCOPE_DELIMITER, README_MD, REGISTRY, + REGISTRY_SCOPE_DELIMITER, RESOLUTIONS, SOCKET_GITHUB_ORG, SOCKET_IPC_HANDSHAKE, @@ -97,19 +108,8 @@ const { YARN_BERRY, YARN_CLASSIC, YARN_LOCK, - kInternalsSymbol, - [kInternalsSymbol as unknown as 'Symbol(kInternalsSymbol)']: { - attributes: registryConstantsAttribs, - createConstantsObject, - getIpc, - }, } = registryConstants -export type RegistryEnv = typeof registryConstants.ENV - -export type RegistryInternals = - (typeof registryConstants)['Symbol(kInternalsSymbol)'] - export type Sentry = any export type Internals = Remap< @@ -165,6 +165,7 @@ export type ENV = Remap< SOCKET_CLI_NO_API_TOKEN: boolean SOCKET_CLI_NPM_PATH: string SOCKET_CLI_ORG_SLUG: string + SOCKET_CLI_SOCKET_PATCH_PATH: string SOCKET_CLI_VIEW_ALL_RISKS: boolean SOCKET_PATCH_PROXY_URL: string TERM: string @@ -278,10 +279,7 @@ const SOCKET_YML = 'socket.yml' const V1_MIGRATION_GUIDE_URL = 'https://docs.socket.dev/docs/v1-migration-guide' export type Constants = Remap< - Omit< - typeof registryConstants, - 'Symbol(kInternalsSymbol)' | 'ENV' | 'ipcObject' - > & { + Omit & { readonly 'Symbol(kInternalsSymbol)': Internals readonly ALERT_TYPE_CRITICAL_CVE: typeof ALERT_TYPE_CRITICAL_CVE readonly ALERT_TYPE_CVE: typeof ALERT_TYPE_CVE @@ -421,7 +419,6 @@ export type Constants = Remap< readonly shadowYarnBinPath: string readonly socketAppDataPath: string readonly socketCachePath: string - readonly socketRegistryPath: string readonly zshRcPath: string } > @@ -444,15 +441,35 @@ function getNpmStdioPipeOptions() { const LAZY_ENV = () => { const { env } = process - const envHelpers = /*@__PURE__*/ require('@socketsecurity/registry/lib/env') - const utils = /*@__PURE__*/ require( - path.join(constants.rootPath, 'dist/utils.js'), - ) - const envAsBoolean = envHelpers.envAsBoolean - const envAsNumber = envHelpers.envAsNumber - const envAsString = envHelpers.envAsString - const getConfigValueOrUndef = utils.getConfigValueOrUndef - const readOrDefaultSocketJson = utils.readOrDefaultSocketJson + // Keep this leaf independent from dist/utils.js. That chunk imports + // constants.js, so requiring it here forms a runtime cycle that Node SEA's + // VFS loader cannot safely resolve. + const savedConfigValue = (key: string): string | undefined => { + try { + const raw = readFileSync( + path.join(constants.socketAppDataPath, 'config.json'), + 'utf8', + ) + const config = JSON.parse( + Buffer.from(raw, 'base64').toString(), + ) as Record + const value = config[key] + return typeof value === 'string' ? value : undefined + } catch { + return undefined + } + } + const socketJson = (() => { + try { + return JSON.parse( + readFileSync(path.join(process.cwd(), SOCKET_JSON), 'utf8'), + ) as { + defaults?: { scan?: { github?: { githubApiUrl?: string } } } + } + } catch { + return undefined + } + })() const GITHUB_TOKEN = envAsString(env['GITHUB_TOKEN']) const INLINED_SOCKET_CLI_PUBLISHED_BUILD = envAsBoolean( process.env['INLINED_SOCKET_CLI_PUBLISHED_BUILD'], @@ -587,7 +604,7 @@ const LAZY_ENV = () => { envAsString(env['SOCKET_CLI_API_BASE_URL']) || // TODO: Remove legacy environment variable name. envAsString(env['SOCKET_SECURITY_API_BASE_URL']) || - getConfigValueOrUndef('apiBaseUrl') || + savedConfigValue('apiBaseUrl') || API_V0_URL, // Set the proxy that all requests are routed through. // https://github.com/SocketDev/socket-cli?tab=readme-ov-file#environment-variables-for-development @@ -627,8 +644,7 @@ const LAZY_ENV = () => { // https://docs.github.com/en/rest SOCKET_CLI_GITHUB_API_URL: envAsString(env['SOCKET_CLI_GITHUB_API_URL']) || - readOrDefaultSocketJson(process.cwd())?.defaults?.scan?.github - ?.githubApiUrl || + socketJson?.defaults?.scan?.github?.githubApiUrl || 'https://api.github.com', // A classic GitHub personal access token with the "repo" scope or a // fine-grained access token with at least read/write permissions set for @@ -648,6 +664,10 @@ const LAZY_ENV = () => { envAsString(env['SOCKET_CLI_ORG_SLUG']) || // Coana CLI accepts the SOCKET_ORG_SLUG environment variable. envAsString(env['SOCKET_ORG_SLUG']), + // Explicit external socket-patch executable for standalone builds. + SOCKET_CLI_SOCKET_PATCH_PATH: envAsString( + env['SOCKET_CLI_SOCKET_PATCH_PATH'], + ), // View all risks of a Socket wrapped npm/npx run. SOCKET_CLI_VIEW_ALL_RISKS: envAsBoolean(env[SOCKET_CLI_VIEW_ALL_RISKS]), // Override the public patch API proxy URL for socket-patch. @@ -760,8 +780,6 @@ const lazyNodeMemoryFlags = () => { } const lazyNpmCachePath = () => { - const spawnHelpers = /*@__PURE__*/ require('@socketsecurity/registry/lib/spawn') - const spawnSync = spawnHelpers.spawnSync return spawnSync( constants.npmExecPath, ['config', 'get', 'cache'], @@ -770,8 +788,6 @@ const lazyNpmCachePath = () => { } const lazyNpmGlobalPrefix = () => { - const spawnHelpers = /*@__PURE__*/ require('@socketsecurity/registry/lib/spawn') - const spawnSync = spawnHelpers.spawnSync return spawnSync( constants.npmExecPath, ['prefix', '-g'], @@ -809,7 +825,14 @@ const lazyProcessEnv = () => null, ) -const lazyRootPath = () => path.join(realpathSync.native(__dirname), '..') +const lazyRootPath = () => { + // Node SEA's VFS paths are virtual and cannot be canonicalized with native + // realpath. The bootstrap publishes the archive root before loading the CLI. + const seaVfsRoot = ( + globalThis as typeof globalThis & { __socketSeaVfsRoot?: string } + ).__socketSeaVfsRoot + return seaVfsRoot ?? path.join(realpathSync.native(__dirname), '..') +} const lazyShadowBinPath = () => path.join(constants.rootPath, 'shadow-npm-bin') @@ -846,7 +869,7 @@ const lazySocketAppDataPath = (): string | undefined => { : constants.ENV.XDG_DATA_HOME if (!dataHome) { if (WIN32) { - const logger = /*@__PURE__*/ require('@socketsecurity/registry/lib/logger') + const { logger } = /*@__PURE__*/ require('./utils.js') logger.warn(`Missing %LOCALAPPDATA%.`) } else { dataHome = path.join( @@ -860,9 +883,6 @@ const lazySocketAppDataPath = (): string | undefined => { const lazySocketCachePath = () => path.join(constants.rootPath, '.cache') -const lazySocketRegistryPath = () => - path.join(constants.externalPath, '@socketsecurity/registry') - const lazyZshRcPath = () => path.join(constants.homePath, '.zshrc') const constants: Constants = createConstantsObject( @@ -998,7 +1018,6 @@ const constants: Constants = createConstantsObject( shadowYarnBinPath: undefined, socketAppDataPath: undefined, socketCachePath: undefined, - socketRegistryPath: undefined, zshRcPath: undefined, }, { @@ -1035,7 +1054,6 @@ const constants: Constants = createConstantsObject( shadowYarnBinPath: lazyShadowYarnBinPath, socketAppDataPath: lazySocketAppDataPath, socketCachePath: lazySocketCachePath, - socketRegistryPath: lazySocketRegistryPath, zshRcPath: lazyZshRcPath, }, internals: { diff --git a/src/instrument-with-sentry.mts b/src/instrument-with-sentry.mts index 3c67216b9a..7bfd3baeec 100644 --- a/src/instrument-with-sentry.mts +++ b/src/instrument-with-sentry.mts @@ -3,9 +3,8 @@ import { createRequire } from 'node:module' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants from './constants.mts' +import { logger } from './utils/logger.mts' if (constants.ENV.INLINED_SOCKET_CLI_SENTRY_BUILD) { const require = createRequire(import.meta.url) diff --git a/src/registry-constants.mts b/src/registry-constants.mts new file mode 100644 index 0000000000..711869c4b3 --- /dev/null +++ b/src/registry-constants.mts @@ -0,0 +1,433 @@ +/* + * Narrow constants facade adapted from @socketsecurity/registry 1.1.17 + * (lib/constants/index.js and the getters used by Socket CLI). + * Copyright (c) 2024 Socket Inc. Licensed under the MIT License. + * + * Keep this as a root-level, builtin-only leaf. The distribution Rollup config + * emits it separately because constants.js must not import the shared utils.js + * chunk during startup. + */ +import { existsSync } from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +type RuntimeAbortSignal = globalThis.AbortSignal +type RuntimeAbortController = { + signal: RuntimeAbortSignal + abort(reason?: unknown): void +} + +const sourceProps = { + AT_LATEST: '@latest', + BIOME_JSON: 'biome.json', + BUN: 'bun', + BUN_LOCK: 'bun.lock', + BUN_LOCKB: 'bun.lockb', + CI: 'CI', + COLUMN_LIMIT: 80, + DOT_GIT_DIR: '.git', + DOT_PACKAGE_LOCK_JSON: '.package-lock.json', + DOT_SOCKET_DIR: '.socket', + EMPTY_FILE: '/* empty */\n', + EMPTY_VALUE: '', + ESLINT_CONFIG_JS: 'eslint.config.js', + ESNEXT: 'esnext', + EXT_CJS: '.cjs', + EXT_CMD: '.cmd', + EXT_CTS: '.cts', + EXT_DTS: '.d.ts', + EXT_JS: '.js', + EXT_JSON: '.json', + EXT_LOCK: '.lock', + EXT_LOCKB: '.lockb', + EXT_MD: '.md', + EXT_MJS: '.mjs', + EXT_MTS: '.mts', + EXT_PS1: '.ps1', + EXT_YAML: '.yaml', + EXT_YML: '.yml', + EXTENSIONS: 'extensions', + EXTENSIONS_JSON: 'extensions.json', + GITIGNORE: '.gitignore', + LATEST: 'latest', + LICENSE: 'LICENSE', + LICENSE_GLOB: 'LICEN[CS]E{[.-]*,}', + LICENSE_GLOB_RECURSIVE: '**/LICEN[CS]E{[.-]*,}', + LICENSE_ORIGINAL: 'LICENSE.original', + LICENSE_ORIGINAL_GLOB: '*.original{.*,}', + LICENSE_ORIGINAL_GLOB_RECURSIVE: '**/*.original{.*,}', + LOOP_SENTINEL: 1_000_000, + MANIFEST_JSON: 'manifest.json', + MIT: 'MIT', + NODE_AUTH_TOKEN: 'NODE_AUTH_TOKEN', + NODE_ENV: 'NODE_ENV', + NODE_MODULES: 'node_modules', + NODE_MODULES_GLOB_RECURSIVE: '**/node_modules', + NPM: 'npm', + NPM_SHRINKWRAP_JSON: 'npm-shrinkwrap.json', + NPX: 'npx', + OVERRIDES: 'overrides', + PACKAGE_DEFAULT_VERSION: '1.0.0', + PACKAGE_JSON: 'package.json', + PACKAGE_LOCK_JSON: 'package-lock.json', + PNPM: 'pnpm', + PNPM_LOCK_YAML: 'pnpm-lock.yaml', + PRE_COMMIT: 'PRE_COMMIT', + README_GLOB: 'README{.*,}', + README_GLOB_RECURSIVE: '**/README{.*,}', + README_MD: 'README.md', + REGISTRY: 'registry', + REGISTRY_SCOPE_DELIMITER: '__', + RESOLUTIONS: 'resolutions', + SOCKET_GITHUB_ORG: 'SocketDev', + SOCKET_IPC_HANDSHAKE: 'SOCKET_IPC_HANDSHAKE', + SOCKET_OVERRIDE_SCOPE: '@socketoverride', + SOCKET_PUBLIC_API_TOKEN: + 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api', + SOCKET_REGISTRY_NPM_ORG: 'socketregistry', + SOCKET_REGISTRY_PACKAGE_NAME: '@socketsecurity/registry', + SOCKET_REGISTRY_REPO_NAME: 'socket-registry', + SOCKET_REGISTRY_SCOPE: '@socketregistry', + SOCKET_SECURITY_SCOPE: '@socketsecurity', + TSCONFIG_JSON: 'tsconfig.json', + UNKNOWN_ERROR: 'Unknown error', + UNKNOWN_VALUE: '', + UNLICENCED: 'UNLICENCED', + UNLICENSED: 'UNLICENSED', + UTF8: 'utf8', + VITEST: 'VITEST', + VLT: 'vlt', + VLT_LOCK_JSON: 'vlt-lock.json', + YARN: 'yarn', + YARN_BERRY: 'yarn/berry', + YARN_CLASSIC: 'yarn/classic', + YARN_LOCK: 'yarn.lock', +} as const + +const kInternalsSymbol = Symbol('@socketregistry.constants.internals') +const UNDEFINED_TOKEN = Symbol('registry-constants.undefined') +const ipcTarget: Record = Object.create(null) +let ipcReady: Promise> | undefined + +function getIpc(key?: K): Promise { + ipcReady ??= new Promise(resolve => { + const finish = () => { + process.removeListener('message', onMessage) + abortController.signal.removeEventListener('abort', finish) + resolve(Object.freeze(ipcTarget)) + } + const onMessage = (message: unknown) => { + if (message && typeof message === 'object') { + const handshake = (message as Record)[ + 'SOCKET_IPC_HANDSHAKE' + ] + if (handshake && typeof handshake === 'object') { + Object.assign(ipcTarget, handshake) + } + } + finish() + } + if (typeof process.send !== 'function') { + finish() + return + } + abortController.signal.addEventListener('abort', finish, { once: true }) + process.once('message', onMessage) + setTimeout(finish, 1_000).unref?.() + }) + return ipcReady.then(data => (key === undefined ? data : data[key])) +} + +function sortedObject>(value: T): T { + const entries = Reflect.ownKeys(value).map(key => [key, value[key]] as const) + entries.sort(([a], [b]) => String(a).localeCompare(String(b))) + return Object.fromEntries(entries) as T +} + +type ConstantAttributes< + P extends Record, + G extends Record unknown>, + I extends Record, +> = { + props: Readonly

+ getters: Readonly + internals: Readonly +} + +type ConstantInternals< + P extends Record, + G extends Record unknown>, + I extends Record, +> = I & { + readonly attributes: ConstantAttributes + readonly lazyGetterStats: Readonly<{ initialized: Set }> + readonly createConstantsObject: typeof createConstantsObject + readonly getIpc: typeof getIpc +} + +function createConstantsObject< + P extends Record, + G extends Record unknown>, + I extends Record, +>(props: P, options: { getters?: G; internals?: I } = {}) { + const attributes: ConstantAttributes = Object.freeze({ + props: Object.freeze(sortedObject({ ...props })), + getters: Object.freeze(sortedObject({ ...options.getters } as G)), + internals: Object.freeze(sortedObject({ ...options.internals } as I)), + }) + const initialized = new Set() + const internal: ConstantInternals = Object.freeze({ + get attributes() { + return attributes + }, + get lazyGetterStats() { + return Object.freeze({ initialized }) + }, + ...attributes.internals, + createConstantsObject, + getIpc, + }) as ConstantInternals + const value: Record = Object.create(null) + Object.defineProperty(value, kInternalsSymbol, { + enumerable: false, + value: internal, + }) + Object.defineProperty(value, 'kInternalsSymbol', { + enumerable: false, + value: kInternalsSymbol, + }) + Object.assign(value, attributes.props) + for (const [name, getter] of Object.entries(attributes.getters)) { + let result: unknown = UNDEFINED_TOKEN + Object.defineProperty(value, name, { + enumerable: true, + get() { + if (result === UNDEFINED_TOKEN) { + initialized.add(name) + result = getter() + } + return result + }, + }) + } + return Object.freeze(value) as P & { + readonly kInternalsSymbol: typeof kInternalsSymbol + readonly [kInternalsSymbol]: ConstantInternals + } & { + readonly [K in keyof G]: ReturnType + } +} + +const capturedEnv = process.env +const envString = (value: unknown): string => + typeof value === 'string' ? value : '' +const envBoolean = (value: unknown): boolean => { + if (typeof value === 'boolean') { + return value + } + if (typeof value !== 'string') { + return false + } + return /^(?:1|true|yes|on)$/i.test(value) +} +const home = envString(capturedEnv['HOME']) +const win32 = process.platform === 'win32' +const baseEnv = Object.freeze( + Object.assign(Object.create(null) as Record, { + APPDATA: envString(capturedEnv['APPDATA']), + CI: Object.hasOwn(capturedEnv, 'CI'), + DEBUG: envString(capturedEnv['DEBUG']), + HOME: home, + LOCALAPPDATA: envString(capturedEnv['LOCALAPPDATA']), + LOG_LEVEL: envString(capturedEnv['LOG_LEVEL']), + NODE_AUTH_TOKEN: envString(capturedEnv['NODE_AUTH_TOKEN']), + NODE_ENV: + envString(capturedEnv['NODE_ENV']).toLowerCase() === 'production' + ? 'production' + : 'development', + NODE_OPTIONS: envString(capturedEnv['NODE_OPTIONS']), + PRE_COMMIT: envBoolean(capturedEnv['PRE_COMMIT']), + SOCKET_CLI_DEBUG: + !!envString(capturedEnv['DEBUG']) || + envBoolean(capturedEnv['SOCKET_CLI_DEBUG']), + VITEST: envBoolean(capturedEnv['VITEST']), + XDG_DATA_HOME: win32 + ? '' + : envString(capturedEnv['XDG_DATA_HOME']) || + (home ? path.join(home, '.local/share') : ''), + }), +) as Readonly<{ + APPDATA: string + CI: boolean + DEBUG: string + HOME: string + LOCALAPPDATA: string + LOG_LEVEL: string + NODE_AUTH_TOKEN: string + NODE_ENV: string + NODE_OPTIONS: string + PRE_COMMIT: boolean + SOCKET_CLI_DEBUG: boolean + VITEST: boolean + XDG_DATA_HOME: string +}> + +const abortController = new AbortController() +for (const signal of ['SIGHUP', 'SIGINT', 'SIGTERM'] as const) { + try { + process.once(signal, () => abortController.abort()) + } catch { + /* unsupported signal */ + } +} +process.once('exit', () => abortController.abort()) + +function findOnPath(name: string): string { + const pathExts = win32 + ? (envString(capturedEnv['PATHEXT']) || '.EXE;.CMD;.BAT').split(';') + : [''] + for (const directory of envString(capturedEnv['PATH']).split( + path.delimiter, + )) { + if (!directory) { + continue + } + for (const extension of pathExts) { + const candidate = path.join(directory, `${name}${extension}`) + if (existsSync(candidate)) { + return candidate + } + } + } + return name +} + +function realNpm(): string { + const nodeSibling = path.join( + path.dirname(process.execPath), + win32 ? 'npm.cmd' : 'npm', + ) + if (existsSync(nodeSibling)) { + return nodeSibling + } + for (const candidate of win32 ? [] : ['/usr/local/bin/npm', '/usr/bin/npm']) { + if (existsSync(candidate)) { + return candidate + } + } + return findOnPath('npm') +} + +function realPnpm(): string { + const candidates = win32 + ? [ + path.join(envString(capturedEnv['APPDATA']), 'npm/pnpm.cmd'), + path.join(envString(capturedEnv['LOCALAPPDATA']), 'pnpm/pnpm.cmd'), + ] + : [ + '/usr/local/bin/pnpm', + '/usr/bin/pnpm', + path.join(baseEnv.XDG_DATA_HOME, 'pnpm/pnpm'), + path.join(home, '.pnpm/pnpm'), + ] + return candidates.find(existsSync) ?? findOnPath('pnpm') +} + +const nodeVersion = process.version +const versionParts = /^v?(\d+)\.(\d+)/.exec(nodeVersion) +const nodeMajor = Number(versionParts?.[1] ?? 0) +const nodeMinor = Number(versionParts?.[2] ?? 0) +const supportsPermissionFlag = + nodeMajor >= 23 || (nodeMajor === 22 && nodeMinor >= 13) +const noWarningsFlags = process.allowedNodeEnvironmentFlags.has( + '--disable-warning', +) + ? [ + '--disable-warning', + 'DeprecationWarning', + '--disable-warning', + 'ExperimentalWarning', + ] + : ['--no-warnings'] +const runtimeRoot = () => { + const seaRoot = ( + globalThis as typeof globalThis & { __socketSeaVfsRoot?: string } + ).__socketSeaVfsRoot + return ( + seaRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + ) +} +const require = createRequire(import.meta.url) + +const baseGetters = { + abortSignal: (): RuntimeAbortSignal => abortController.signal, + execPath: () => process.execPath, + maintainedNodeVersions: () => + Object.freeze( + Object.assign(['18.20.8', '20.19.1', '22.19.0', '24.8.0'], { + last: '18.20.8', + previous: '20.19.1', + current: '22.19.0', + next: '24.8.0', + }), + ), + NODE_VERSION: () => nodeVersion, + nodeNoWarningsFlags: () => Object.freeze(noWarningsFlags), + npmExecPath: realNpm, + npmRealExecPath: () => { + const npmPath = realNpm() + const nodeDirectory = path.dirname(process.execPath) + const candidates = [ + path.join(nodeDirectory, 'node_modules/npm/bin/npm-cli.js'), + path.resolve(nodeDirectory, '../lib/node_modules/npm/bin/npm-cli.js'), + path.resolve( + path.dirname(npmPath), + '../lib/node_modules/npm/bin/npm-cli.js', + ), + ] + return candidates.find(existsSync) ?? npmPath + }, + pnpmExecPath: realPnpm, + spinner: () => { + const spinnerModule = require( + path.join(runtimeRoot(), 'dist/spinner.js'), + ) as { + default?: import('./utils/spinner.mts').Spinner + defaultSpinner?: import('./utils/spinner.mts').Spinner + } + // Rollup's CommonJS output exposes this default export as `defaultSpinner`. + // Keep `default` for runtimes that preserve native ESM interop. + const spinner = spinnerModule.default ?? spinnerModule.defaultSpinner + if (!spinner) { + throw new Error('The built Socket CLI spinner export is missing') + } + return spinner + }, + SUPPORTS_NODE_PERMISSION_FLAG: () => supportsPermissionFlag, + SUPPORTS_NODE_COMPILE_CACHE_ENV_VAR: () => + nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 1), + WIN32: () => win32, + abortController: (): RuntimeAbortController => abortController, + DARWIN: () => process.platform === 'darwin', + ENV: () => baseEnv, +} + +const registryConstants = createConstantsObject(sourceProps, { + getters: baseGetters, + internals: { createConstantsObject, getIpc }, +}) + +const registryConstantsAttribs = registryConstants[kInternalsSymbol].attributes + +export { + createConstantsObject, + getIpc, + kInternalsSymbol, + registryConstantsAttribs, +} +export type RegistryConstants = typeof registryConstants +export type RegistryEnv = typeof baseEnv +export type RegistryInternals = + (typeof registryConstants)[typeof kInternalsSymbol] +export default registryConstants diff --git a/src/sea/README.md b/src/sea/README.md index 1bccbc9822..9ecb308cba 100644 --- a/src/sea/README.md +++ b/src/sea/README.md @@ -1,75 +1,47 @@ -# Socket CLI Self-Executable Application (SEA) +# Socket CLI Node 27 SEA -Build self-contained executables using Node.js SEA. +This prototype builds an executable from the root `socket` application. It embeds the application files in a Node 27 virtual file system (VFS). It does not install npm packages or download helper tools when it starts. -## Architecture +## Build -The executable is a **thin wrapper** that downloads `@socketsecurity/cli` from npm on first use. - -Contains: -- Node.js runtime -- Bootstrap code to download CLI -- No actual CLI implementation - -On first run: -1. Downloads `@socketsecurity/cli` from npm -2. Installs to `~/.socket/cli/` -3. Runs with your arguments +```sh +pnpm build:sea -- --prototype +``` -## Files +The builder uses Node `v27.0.0-alpha.0.0.0`. It verifies the Node archive against the release `SHASUMS256.txt`. Node 27 is a prerelease, so these builds are experimental. -- **bootstrap.mts** - Thin wrapper that downloads CLI from npm -- **build-sea.mts** - Build script for creating executables +Build on the target OS and architecture. The builder supports `darwin`, `linux`, and `win32`, with `x64` and `arm64` archives where Node publishes them. Cross-target builds fail. -## Building +The builder creates a private temporary copy of the distribution. It does not replace the checkout's `dist` or `external` directories. It writes the executable and manifest to `build/sea`. Use `--output-dir` to select another directory. -```bash -# Build for current platform -pnpm build:sea +The builder stages independent payloads in parallel. It waits for all staging jobs before cleanup. It builds the .NET facts helper before Rollup copies the distribution. -# Build for specific platform/arch -pnpm build:sea -- --platform=darwin --arch=arm64 +## Embedded tools -# Use specific Node version -pnpm build:sea -- --node-version=20.11.0 -``` +The build embeds the .NET facts helper, `socket-patch`, and `uv` when a verified payload exists for the target. The .NET helper still needs a compatible .NET SDK from the project or `PATH`. The scan command uses `SOCKET_CLI_UV_PATH` when set. Otherwise, it extracts embedded `uv` to a temporary directory when needed. -### Options +The build also stages `cdxgen` and `synp` APIs for darwin-arm64. The current manifest marks both features unavailable. Their source provenance and third-party notice obligations are not resolved. -- `--platform=` - Target platform (win32, darwin, linux) -- `--arch=` - Target architecture (x64, arm64) -- `--node-version=` - Node.js version (default: 20.11.0) -- `--output-dir=` - Output directory (default: dist/sea) +The build does not embed Coana, Python, or a JVM toolchain. It does not add a runtime path to download helper tools. Socket API requests and project inputs keep their existing network behavior. -## Output +## Manifest -Executables in `dist/sea/`: -- `socket-win-x64.exe` - Windows x64 -- `socket-win-arm64.exe` - Windows ARM64 -- `socket-macos-x64` - macOS Intel -- `socket-macos-arm64` - macOS Apple Silicon -- `socket-linux-x64` - Linux x64 -- `socket-linux-arm64` - Linux ARM64 +Each successful build writes a manifest beside the executable. It records the target, Socket and Node versions, file digests, bundled tools, third-party notices, and available capabilities. Read the manifest's `capabilities` and notice `obligations` before using a bundled tool. -## Usage +## Prototype status -```bash -./dist/sea/socket-macos-arm64 --version -./dist/sea/socket-macos-arm64 scan . -./dist/sea/socket-macos-arm64 [options] -``` +Use the executable's `--version` output and the adjacent manifest's recorded Socket and Node versions to identify a build. Artifact size, digest, signature status, bundled tools, and capabilities depend on the build inputs and target; inspect the generated manifest for those values. The prototype remains experimental. Production signing, notarization, release provenance, and target qualification are incomplete. -First run downloads CLI from npm. Subsequent runs use cached version. +## Native terminal tests -## How It Works +Use Node 26.13.0 and build the prototype on the host OS before running its native tests: -1. **First Run**: Downloads `@socketsecurity/cli` from npm to `~/.socket/cli/` -2. **Subsequent Runs**: Uses cached CLI -3. **Requirements**: System Node.js required to run downloaded CLI +```sh +pnpm build:sea -- --prototype +SOCKET_CLI_NATIVE_TEST_ARTIFACTS=build/native-test-artifacts/run-1 pnpm test:native src/commands/native-cli.native.test.mts src/commands/native-cli-behavior.native.test.mts +node scripts/test/terminal-report.mts --artifacts build/native-test-artifacts/run-1 --output build/native-test-artifacts/run-1/index.html +``` -## Notes +Choose a fresh run directory each time; it stores checkpoint PNG/SVG files, the animation, and signing receipt. On macOS, configured Developer ID credentials sign the staged binary; CI exposes signing secrets only on trusted pushes. -- Small binary contains only bootstrap code -- CLI downloaded on first use -- Cached at `~/.socket/cli/` -- Requires Node.js installed on system \ No newline at end of file +Terminal images trim unused rows while preserving internal spacing. Each animation uses a shared height across its checkpoints. diff --git a/src/sea/adapters/cdxgen-worker.mjs b/src/sea/adapters/cdxgen-worker.mjs new file mode 100644 index 0000000000..d6874dd97e --- /dev/null +++ b/src/sea/adapters/cdxgen-worker.mjs @@ -0,0 +1,519 @@ +import childProcess from 'node:child_process' +import dgram from 'node:dgram' +import { readFile, writeFile } from 'node:fs/promises' +import http from 'node:http' +import http2 from 'node:http2' +import https from 'node:https' +import { syncBuiltinESMExports } from 'node:module' +import net from 'node:net' +import path from 'node:path' +import tls from 'node:tls' +import { fileURLToPath } from 'node:url' +import util from 'node:util' +import { workerData } from 'node:worker_threads' + +// eslint-disable-next-line import-x/no-unresolved, n/no-missing-import -- The worker path resolves in its staged SEA VFS payload. +import { isObjectObject } from './utils/is-object-object.mjs' + +const ipc = workerData?.socketIpc +if ( + !ipc || + typeof ipc.projectPath !== 'string' || + typeof ipc.sourceRoot !== 'string' || + typeof ipc.outputPath !== 'string' +) { + throw new Error('Invalid Socket cdxgen Worker request') +} + +// cdxgen probes host tools while importing its API. A nonempty allowlist +// prevents those probes from spawning commands; the restricted npm API path +// does not need any subprocesses. +process.env.CDXGEN_PLUGINS_DIR = path.join( + path.dirname(fileURLToPath(import.meta.url)), + 'plugins-empty', +) +process.env.CDXGEN_ALLOWED_COMMANDS = 'socket-sea-no-commands-allowed' + +const denied = () => { + throw new Error( + 'Standalone cdxgen cannot start subprocesses or access the network', + ) +} +for (const name of [ + 'spawn', + 'spawnSync', + 'exec', + 'execSync', + 'execFile', + 'execFileSync', + 'fork', +]) { + childProcess[name] = denied +} +for (const module of [http, https]) { + module.request = denied + module.get = denied +} +net.connect = denied +net.createConnection = denied +net.Socket.prototype.connect = denied +http2.connect = denied +dgram.createSocket = denied +tls.connect = denied +tls.TLSSocket.prototype.connect = denied +globalThis.fetch = denied +syncBuiltinESMExports() + +const isolatedRoot = path.resolve(ipc.projectPath) +const originalRoot = path.resolve(ipc.sourceRoot) +const packageJson = JSON.parse( + await readFile(path.join(isolatedRoot, 'package.json'), 'utf8'), +) +const packageLock = JSON.parse( + await readFile(path.join(isolatedRoot, 'package-lock.json'), 'utf8'), +) +if (!isObjectObject(packageJson)) { + throw new Error('Invalid package.json in cdxgen project') +} +if (!isObjectObject(packageLock)) { + throw new Error('Malformed npm package-lock.json') +} +if (![1, 3].includes(packageLock.lockfileVersion)) { + throw new Error( + 'The standalone cdxgen API supports package-lock versions 1 and 3 only', + ) +} +if (packageJson.workspaces || packageJson.workspace) { + throw new Error('The standalone cdxgen API does not support workspaces') +} +const hasLocalDependency = value => + typeof value === 'string' && /^(?:file:|link:|workspace:)/i.test(value) +const validPackageName = name => { + if (typeof name !== 'string' || !name || name.includes('\\')) { + return false + } + const parts = name.split('/') + if (parts.some(part => !part || part === '.' || part === '..')) { + return false + } + return name.startsWith('@') + ? parts.length === 2 && parts[0].length > 1 + : parts.length === 1 +} +for (const group of [ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', +]) { + if (packageJson[group] !== undefined && !isObjectObject(packageJson[group])) { + throw new Error(`Invalid ${group} in package.json`) + } + if ( + Object.keys(packageJson[group] || {}).some(name => !validPackageName(name)) + ) { + throw new Error(`Invalid package name in package.json ${group}`) + } + if (Object.values(packageJson[group] || {}).some(hasLocalDependency)) { + throw new Error( + 'The standalone cdxgen API does not support local or workspace dependencies', + ) + } +} +const inspectV1Dependencies = ( + dependencies, + dependencyPath = 'dependencies', +) => { + if (dependencies === undefined) { + return + } + if (!isObjectObject(dependencies)) { + throw new Error(`Malformed package-lock v1 ${dependencyPath}`) + } + for (const [name, dependency] of Object.entries(dependencies)) { + if (!validPackageName(name)) { + throw new Error(`Invalid package-lock v1 dependency name '${name}'`) + } + if (!isObjectObject(dependency)) { + throw new Error(`Malformed package-lock v1 dependency '${name}'`) + } + if (typeof dependency.version !== 'string' || !dependency.version) { + throw new Error(`Malformed package-lock v1 version for '${name}'`) + } + for (const metadataKey of [ + '_socketOptionalDependencies', + '_socketPeerDependencies', + ]) { + if ( + dependency[metadataKey] !== undefined && + (!Array.isArray(dependency[metadataKey]) || + dependency[metadataKey].some(item => typeof item !== 'string')) + ) { + throw new Error(`Malformed Synp dependency metadata for '${name}'`) + } + } + if ( + hasLocalDependency(dependency.version) || + hasLocalDependency(dependency.resolved) || + dependency.link + ) { + throw new Error( + 'The standalone cdxgen API does not support local or linked lockfile entries', + ) + } + inspectV1Dependencies( + dependency.dependencies, + `${dependencyPath}.${name}.dependencies`, + ) + } +} +const checkRootDependencyGroup = ( + group, + lockDependencies, + groupName, + optional, + optionalNames = new Set(), +) => { + if (group === undefined) { + return + } + if (!isObjectObject(group)) { + throw new Error(`Malformed package.json ${groupName}`) + } + for (const name of Object.keys(group)) { + if ( + !( + isObjectObject(lockDependencies) && + Object.hasOwn(lockDependencies, name) + ) && + !optional && + !optionalNames.has(name) + ) { + throw new Error( + `package-lock is missing required root ${groupName} '${name}'`, + ) + } + } +} + +if (packageLock.lockfileVersion === 1) { + const rootDependencies = packageLock.dependencies ?? {} + if (!isObjectObject(rootDependencies)) { + throw new Error('Malformed package-lock v1 dependencies map') + } + inspectV1Dependencies(rootDependencies) + checkRootDependencyGroup( + packageJson.dependencies, + rootDependencies, + 'dependencies', + false, + new Set(Object.keys(packageJson.optionalDependencies || {})), + ) + checkRootDependencyGroup( + packageJson.devDependencies, + rootDependencies, + 'devDependencies', + false, + ) + checkRootDependencyGroup( + packageJson.optionalDependencies, + rootDependencies, + 'optionalDependencies', + true, + ) + checkRootDependencyGroup( + packageJson.peerDependencies, + rootDependencies, + 'peerDependencies', + true, + ) + + const resolveV1Dependency = (name, ownDependencies, ancestorMaps) => + Boolean( + (isObjectObject(ownDependencies) && + Object.hasOwn(ownDependencies, name)) || + ancestorMaps.some( + map => isObjectObject(map) && Object.hasOwn(map, name), + ), + ) + const validateV1Graph = (dependencies, ancestorMaps = []) => { + for (const [packageName, dependency] of Object.entries( + dependencies || {}, + )) { + const requires = dependency.requires + if (requires !== undefined && !isObjectObject(requires)) { + throw new Error( + `Malformed package-lock v1 requires map for '${packageName}'`, + ) + } + if ( + Object.values(requires || {}).some(value => typeof value !== 'string') + ) { + throw new Error( + `Malformed package-lock v1 requirement spec for '${packageName}'`, + ) + } + const optional = new Set(dependency._socketOptionalDependencies || []) + const peers = new Set(dependency._socketPeerDependencies || []) + const visibleMaps = [dependencies, ...ancestorMaps] + for (const requiredName of Object.keys(requires || {})) { + if (!validPackageName(requiredName)) { + throw new Error( + `Invalid package-lock v1 requirement name '${requiredName}'`, + ) + } + if ( + !resolveV1Dependency( + requiredName, + dependency.dependencies, + visibleMaps, + ) && + !optional.has(requiredName) && + !Object.hasOwn(dependency.optionalDependencies || {}, requiredName) && + !peers.has(requiredName) + ) { + throw new Error( + `package-lock v1 is missing required '${requiredName}' for '${packageName}'`, + ) + } + } + validateV1Graph(dependency.dependencies, visibleMaps) + } + } + validateV1Graph(rootDependencies) +} else { + const packages = packageLock.packages + if ( + !isObjectObject(packages) || + !Object.hasOwn(packages, '') || + !isObjectObject(packages['']) + ) { + throw new Error( + 'Malformed package-lock v3 packages map or root package record', + ) + } + for (const [packagePath, dependency] of Object.entries(packages)) { + if (!isObjectObject(dependency)) { + throw new Error(`Malformed package-lock v3 record '${packagePath}'`) + } + if (packagePath) { + const parts = packagePath.split('/') + if ( + !packagePath.startsWith('node_modules/') || + packagePath.startsWith('/') || + packagePath.includes('\\') || + parts.some(part => !part || part === '.' || part === '..') + ) { + throw new Error(`Invalid package-lock v3 package path '${packagePath}'`) + } + if (typeof dependency.version !== 'string' || !dependency.version) { + throw new Error(`Missing package-lock v3 version for '${packagePath}'`) + } + if ( + hasLocalDependency(dependency.version) || + hasLocalDependency(dependency.resolved) || + dependency.link + ) { + throw new Error( + 'The standalone cdxgen API does not support local or linked lockfile entries', + ) + } + } + } + + const resolveV3Dependency = (name, packagePath) => { + let current = packagePath + while (current) { + const candidate = `${current}/node_modules/${name}` + if (Object.hasOwn(packages, candidate)) { + return true + } + const parentMarker = current.lastIndexOf('/node_modules/') + current = parentMarker < 0 ? '' : current.slice(0, parentMarker) + } + return Object.hasOwn(packages, `node_modules/${name}`) + } + const validateV3DependencyGroup = ( + group, + packagePath, + groupName, + optional, + optionalNames = new Set(), + ) => { + if (group === undefined) { + return + } + if (!isObjectObject(group)) { + throw new Error( + `Malformed package-lock v3 ${groupName} map at '${packagePath || ''}'`, + ) + } + for (const name of Object.keys(group)) { + if ( + !resolveV3Dependency(name, packagePath) && + !optional && + !optionalNames.has(name) + ) { + throw new Error( + `package-lock v3 is missing required '${name}' at '${packagePath || ''}'`, + ) + } + } + } + const rootOptionalNames = new Set( + Object.keys(packageJson.optionalDependencies || {}), + ) + const validateV3RootGroup = (group, groupName, optional) => { + if (group === undefined) { + return + } + if (!isObjectObject(group)) { + throw new Error(`Malformed package.json ${groupName}`) + } + for (const name of Object.keys(group)) { + if ( + !resolveV3Dependency(name, '') && + !optional && + !(groupName === 'dependencies' && rootOptionalNames.has(name)) + ) { + throw new Error( + `package-lock v3 is missing required root ${groupName} '${name}'`, + ) + } + } + } + validateV3RootGroup(packageJson.dependencies, 'dependencies', false) + validateV3RootGroup(packageJson.devDependencies, 'devDependencies', false) + validateV3RootGroup( + packageJson.optionalDependencies, + 'optionalDependencies', + true, + ) + validateV3RootGroup(packageJson.peerDependencies, 'peerDependencies', true) + for (const [packagePath, dependency] of Object.entries(packages)) { + validateV3DependencyGroup( + dependency.dependencies, + packagePath, + 'dependencies', + false, + new Set(Object.keys(dependency.optionalDependencies || {})), + ) + validateV3DependencyGroup( + dependency.devDependencies, + packagePath, + 'devDependencies', + packagePath !== '', + ) + validateV3DependencyGroup( + dependency.optionalDependencies, + packagePath, + 'optionalDependencies', + true, + ) + validateV3DependencyGroup( + dependency.peerDependencies, + packagePath, + 'peerDependencies', + true, + ) + } +} +// eslint-disable-next-line import-x/no-unresolved, n/no-missing-import -- The package is staged beside this worker in the SEA VFS. +const { createNodejsBom } = await import('./lib/cli/index.js') + +let incompleteNotice = '' +const originalConsole = new Map() +for (const method of ['log', 'warn', 'error']) { + const original = console[method] + originalConsole.set(method, original) + console[method] = (...args) => { + const message = util.format(...args) + if ( + message.includes('Unable to parse') && + message.includes('The resulting SBOM would be incomplete.') + ) { + incompleteNotice += message + } + original.apply(console, args) + } +} +let bom +try { + bom = await createNodejsBom(isolatedRoot, { + projectType: ['npm'], + installDeps: false, + noBabel: true, + recurse: false, + deep: false, + evidence: false, + includeCrypto: false, + includeFormulation: false, + lifecycle: 'pre-build', + specVersion: 1.6, + output: undefined, + }) +} finally { + for (const [method, original] of originalConsole) { + console[method] = original + } +} +if (incompleteNotice) { + throw new Error( + 'cdxgen could not parse the package-lock; refusing an incomplete SBOM', + ) +} +if (!bom?.bomJson) { + throw new Error('cdxgen did not produce an npm BOM') +} +const rewriteTemporaryPath = sourceValue => { + const sourcePath = path.resolve( + path.isAbsolute(sourceValue) + ? sourceValue + : path.join(isolatedRoot, sourceValue), + ) + if ( + sourcePath === isolatedRoot || + sourcePath.startsWith(`${isolatedRoot}${path.sep}`) + ) { + return path.join(originalRoot, path.relative(isolatedRoot, sourcePath)) + } + return sourceValue +} +const rewriteSourcePaths = (value, inEvidence = false) => { + if (!value || typeof value !== 'object') { + return + } + if (Array.isArray(value)) { + for (const child of value) { + rewriteSourcePaths(child, inEvidence) + } + return + } + if (value.name === 'SrcFile' && typeof value.value === 'string') { + value.value = rewriteTemporaryPath(value.value) + } else if ( + value.name === 'cdx:npm:binPaths' && + typeof value.value === 'string' + ) { + value.value = value.value.split(', ').map(rewriteTemporaryPath).join(', ') + } + const evidence = inEvidence || Object.hasOwn(value, 'identity') + for (const [key, child] of Object.entries(value)) { + if ( + evidence && + ['value', 'concludedValue', 'location'].includes(key) && + typeof child === 'string' + ) { + value[key] = rewriteTemporaryPath(child) + } else { + rewriteSourcePaths(child, evidence || key === 'evidence') + } + } +} +rewriteSourcePaths(bom.bomJson) +const serialized = `${JSON.stringify(bom.bomJson, null, 2)}\n` +if (ipc.outputPath === '-') { + process.stdout.write(serialized) +} else { + await writeFile(ipc.outputPath, serialized, { encoding: 'utf8' }) +} diff --git a/src/sea/adapters/cdxgen.mts b/src/sea/adapters/cdxgen.mts new file mode 100644 index 0000000000..08f7da0326 --- /dev/null +++ b/src/sea/adapters/cdxgen.mts @@ -0,0 +1,95 @@ +import { EventEmitter } from 'node:events' +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { runEmbeddedJsTool } from '../runtime.mts' + +import type { DlxOptions } from '../../utils/dlx.mts' +import type { EmbeddedToolRunResult } from '../runtime.mts' + +export async function runCdxgen( + projectPath: string, + outputPath: string, + options?: DlxOptions, + beforeExit?: (() => Promise) | undefined, +): Promise<{ + spawnPromise: Promise & { process: EventEmitter } +}> { + const isolatedRoot = await fs.mkdtemp( + path.join(os.tmpdir(), 'socket-cdxgen-api-'), + ) + try { + const packageJson = JSON.parse( + await fs.readFile(path.join(projectPath, 'package.json'), 'utf8'), + ) as Record + if (typeof packageJson['name'] !== 'string' || !packageJson['name']) { + // cdxgen otherwise uses the directory basename as its fallback project + // name; retain the user's project identity while scanning the temp root. + packageJson['name'] = path.basename(path.resolve(projectPath)) + } + await Promise.all([ + fs.writeFile( + path.join(isolatedRoot, 'package.json'), + `${JSON.stringify(packageJson)}\n`, + ), + fs.copyFile( + path.join(projectPath, 'package-lock.json'), + path.join(isolatedRoot, 'package-lock.json'), + ), + ]) + } catch (error) { + await fs.rm(isolatedRoot, { recursive: true, force: true }) + throw error + } + + const process = new EventEmitter() + const cleanTemp = async () => + await fs.rm(isolatedRoot, { recursive: true, force: true }) + const embeddedPromise = runEmbeddedJsTool('cdxgen', [], { + cwd: options?.cwd, + env: { CDXGEN_PLUGINS_DIR: 'plugins-empty' }, + inheritEnv: false, + signal: options?.signal, + stdio: options?.stdio, + ipc: { + projectPath: isolatedRoot, + sourceRoot: path.resolve(projectPath), + outputPath, + }, + }) + const finalize = async ( + code: number | null, + signal: NodeJS.Signals | null, + ) => { + const cleanupResults = await Promise.allSettled([ + cleanTemp(), + Promise.resolve().then(async () => await beforeExit?.()), + ]) + const cleanupErrors = cleanupResults.flatMap(result => + result.status === 'rejected' ? [result.reason] : [], + ) + const failure = + cleanupErrors.length === 0 + ? undefined + : cleanupErrors.length === 1 + ? cleanupErrors[0] + : new AggregateError(cleanupErrors, 'cdxgen cleanup failed') + process.emit('exit', failure ? 1 : code, failure ? null : signal) + if (failure) { + throw failure + } + } + const spawnPromise = embeddedPromise.then( + async result => { + await finalize(result.code, result.signal) + return result + }, + async error => { + await finalize(1, null) + throw error + }, + ) as Promise & { process: EventEmitter } + Object.defineProperty(spawnPromise, 'process', { value: process }) + return { spawnPromise } +} diff --git a/src/sea/adapters/synp-worker.mjs b/src/sea/adapters/synp-worker.mjs new file mode 100644 index 0000000000..92319afdcb --- /dev/null +++ b/src/sea/adapters/synp-worker.mjs @@ -0,0 +1,191 @@ +/* eslint-disable no-await-in-loop -- Nested lockfile annotations are written in traversal order. */ + +import { createHash } from 'node:crypto' +import { open, readFile, stat } from 'node:fs/promises' +import { createRequire } from 'node:module' +import path from 'node:path' +import { workerData } from 'node:worker_threads' + +import { isObjectObject } from '../../utils/is-object-object.mjs' + +const require = createRequire(import.meta.url) + +async function main() { + const ipc = workerData && workerData.socketIpc + if ( + !ipc || + typeof ipc.projectPath !== 'string' || + typeof ipc.outputPath !== 'string' || + typeof ipc.tempDev !== 'string' || + typeof ipc.tempIno !== 'string' || + path.dirname(path.resolve(ipc.outputPath)) !== + path.resolve(ipc.projectPath) || + !/^\.socket-cdxgen-lock-[a-f\d-]+\.tmp$/i.test( + path.basename(ipc.outputPath), + ) + ) { + throw new Error('Invalid Socket Synp Worker request') + } + + const projectPath = path.resolve(ipc.projectPath) + const outputPath = path.resolve(ipc.outputPath) + const yarnLockPath = path.join(projectPath, 'yarn.lock') + const yarnLock = await readFile(yarnLockPath, 'utf8') + if (!/^# yarn lockfile v1(?:\r?\n|$)/m.test(yarnLock)) { + throw new Error('Embedded Synp supports Yarn Classic lockfile v1 only') + } + + const packageJson = JSON.parse( + await readFile(path.join(projectPath, 'package.json'), 'utf8'), + ) + if (packageJson.workspaces) { + throw new Error('Embedded Synp does not support workspaces') + } + if ( + Object.values({ + ...packageJson.dependencies, + ...packageJson.devDependencies, + ...packageJson.optionalDependencies, + }).some( + value => typeof value === 'string' && value.startsWith('workspace:'), + ) + ) { + throw new Error( + 'Embedded Synp does not support Yarn workspace dependencies', + ) + } + const nodeModules = await stat(path.join(projectPath, 'node_modules')) + if (!nodeModules.isDirectory()) { + throw new Error('Embedded Synp requires an existing node_modules directory') + } + + const synp = require('synp') + const converted = JSON.parse(synp.yarnToNpm(projectPath, false)) + if (converted.lockfileVersion !== 1) { + throw new Error('Synp returned an unsupported package-lock format') + } + const annotateDependencies = async (dependencies, modulesPath) => { + if (dependencies !== undefined && !isObjectObject(dependencies)) { + throw new Error('Synp returned a malformed dependencies map') + } + for (const [name, entry] of Object.entries(dependencies || {})) { + const packagePath = path.join(modulesPath, ...name.split('/')) + let manifest + try { + manifest = JSON.parse( + await readFile(path.join(packagePath, 'package.json'), 'utf8'), + ) + } catch { + throw new Error( + `Synp lock entry '${name}' has no matching installed package manifest`, + ) + } + entry._socketOptionalDependencies = Object.keys( + manifest.optionalDependencies || {}, + ) + entry._socketPeerDependencies = Object.keys( + manifest.peerDependencies || {}, + ) + await annotateDependencies( + entry.dependencies, + path.join(packagePath, 'node_modules'), + ) + } + } + await annotateDependencies( + converted.dependencies, + path.join(projectPath, 'node_modules'), + ) + const findDependency = (name, ownDependencies, ancestorMaps) => { + if ( + isObjectObject(ownDependencies) && + Object.hasOwn(ownDependencies, name) + ) { + return ownDependencies[name] + } + for (const map of ancestorMaps) { + if (isObjectObject(map) && Object.hasOwn(map, name)) { + return map[name] + } + } + } + const collectDependencies = (name, entry, ancestorMaps, collected) => { + if (!entry || collected.has(entry)) { + return + } + collected.add(entry) + if (entry.requires !== undefined && !isObjectObject(entry.requires)) { + throw new Error(`Synp returned a malformed requires map for '${name}'`) + } + const visibleMaps = [entry.dependencies, ...ancestorMaps] + const dependencyNames = new Set([ + ...Object.keys(entry.requires || {}), + ...(entry._socketOptionalDependencies || []), + ]) + for (const dependencyName of dependencyNames) { + collectDependencies( + dependencyName, + findDependency(dependencyName, entry.dependencies, visibleMaps), + visibleMaps, + collected, + ) + } + } + const productionEntries = new Set() + for (const name of [ + ...Object.keys(packageJson.dependencies || {}), + ...Object.keys(packageJson.optionalDependencies || {}), + ]) { + collectDependencies( + name, + findDependency(name, converted.dependencies, []), + [converted.dependencies], + productionEntries, + ) + } + const developmentEntries = new Set() + for (const name of Object.keys(packageJson.devDependencies || {})) { + collectDependencies( + name, + findDependency(name, converted.dependencies, []), + [converted.dependencies], + developmentEntries, + ) + } + for (const entry of productionEntries) { + entry.dev = false + } + for (const entry of developmentEntries) { + if (!productionEntries.has(entry)) { + entry.dev = true + } + } + const bytes = Buffer.from(`${JSON.stringify(converted, null, 2)}\n`) + const digest = createHash('sha256').update(bytes).digest('hex') + let handle + try { + handle = await open(outputPath, 'r+') + const createdStat = await handle.stat() + if ( + String(createdStat.dev) !== ipc.tempDev || + String(createdStat.ino) !== ipc.tempIno + ) { + throw new Error( + 'Synp temporary output ownership changed before conversion', + ) + } + await handle.truncate(0) + await handle.writeFile(bytes) + await handle.sync() + process.stdout.write( + `${JSON.stringify({ dev: String(createdStat.dev), ino: String(createdStat.ino), size: bytes.length, sha256: digest })}\n`, + ) + } finally { + await handle?.close() + } +} + +main().catch(error => { + process.stderr.write(`${error && error.stack ? error.stack : error}\n`) + process.exitCode = 1 +}) diff --git a/src/sea/adapters/synp.mts b/src/sea/adapters/synp.mts new file mode 100644 index 0000000000..be553709fc --- /dev/null +++ b/src/sea/adapters/synp.mts @@ -0,0 +1,166 @@ +import { createHash, randomUUID } from 'node:crypto' +import { promises as fs } from 'node:fs' +import path from 'node:path' + +import { assertEmbeddedToolAvailable, runEmbeddedJsTool } from '../runtime.mts' + +export interface OwnedPackageLock { + path: string + dev: string + ino: string + size: number + sha256: string +} + +interface SynpWorkerResult { + dev: string + ino: string + size: number + sha256: string +} + +async function removeIfOwned( + filename: string, + identity: Pick, +): Promise { + try { + const stat = await fs.lstat(filename) + if ( + String(stat.dev) === identity.dev && + String(stat.ino) === identity.ino + ) { + await fs.unlink(filename) + } + } catch (error) { + if ( + !(error instanceof Error && 'code' in error && error.code === 'ENOENT') + ) { + throw error + } + } +} + +export async function removeOwnedPackageLock( + owner: OwnedPackageLock | undefined, +): Promise { + if (!owner) { + return + } + try { + const [stat, contents] = await Promise.all([ + fs.lstat(owner.path), + fs.readFile(owner.path), + ]) + const digest = createHash('sha256').update(contents).digest('hex') + if ( + String(stat.dev) === owner.dev && + String(stat.ino) === owner.ino && + stat.size === owner.size && + digest === owner.sha256 + ) { + await fs.unlink(owner.path) + } + } catch (error) { + if ( + !(error instanceof Error && 'code' in error && error.code === 'ENOENT') + ) { + throw error + } + } +} + +export async function convertYarnClassicToNpmLock( + projectPath: string, + signal?: AbortSignal, +): Promise { + assertEmbeddedToolAvailable('synp') + const root = path.resolve(projectPath) + const packageLockPath = path.join(root, 'package-lock.json') + const temporaryPath = path.join( + root, + `.socket-cdxgen-lock-${randomUUID()}.tmp`, + ) + const placeholder = await fs.open(temporaryPath, 'wx', 0o600) + const placeholderStat = await placeholder.stat() + await placeholder.close() + const placeholderIdentity = { + dev: String(placeholderStat.dev), + ino: String(placeholderStat.ino), + } + let packageLockOwner: OwnedPackageLock | undefined + + try { + const result = await runEmbeddedJsTool('synp', [], { + cwd: root, + inheritEnv: false, + env: {}, + signal, + stdio: 'pipe', + ipc: { + projectPath: root, + outputPath: temporaryPath, + tempDev: placeholderIdentity.dev, + tempIno: placeholderIdentity.ino, + }, + }) + if (result.code !== 0 || result.cancelled) { + throw Object.assign( + new Error( + result.stderr?.trim() || 'Embedded Synp failed to convert yarn.lock.', + ), + { code: result.signal ?? undefined }, + ) + } + + let converted: SynpWorkerResult + try { + converted = JSON.parse(result.stdout ?? '') as SynpWorkerResult + } catch { + throw new Error('Embedded Synp returned invalid ownership metadata.') + } + if ( + converted.dev !== placeholderIdentity.dev || + converted.ino !== placeholderIdentity.ino || + !Number.isSafeInteger(converted.size) || + converted.size <= 0 || + !/^[a-f0-9]{64}$/.test(converted.sha256) + ) { + throw new Error('Embedded Synp returned invalid ownership metadata.') + } + + const [tempStat, tempContents] = await Promise.all([ + fs.lstat(temporaryPath), + fs.readFile(temporaryPath), + ]) + const tempDigest = createHash('sha256').update(tempContents).digest('hex') + if ( + String(tempStat.dev) !== converted.dev || + String(tempStat.ino) !== converted.ino || + tempStat.size !== converted.size || + tempDigest !== converted.sha256 || + JSON.parse(tempContents.toString('utf8')).lockfileVersion !== 1 + ) { + throw new Error( + 'Embedded Synp output failed ownership or format verification.', + ) + } + + // link() creates the destination atomically and fails if another process + // created package-lock.json after our initial absence check. + await fs.link(temporaryPath, packageLockPath) + packageLockOwner = { + path: packageLockPath, + dev: converted.dev, + ino: converted.ino, + size: converted.size, + sha256: converted.sha256, + } + await fs.unlink(temporaryPath) + return packageLockOwner + } catch (error) { + await removeOwnedPackageLock(packageLockOwner) + throw error + } finally { + await removeIfOwned(temporaryPath, placeholderIdentity) + } +} diff --git a/src/sea/archive-paths.mts b/src/sea/archive-paths.mts new file mode 100644 index 0000000000..d31e92f1d3 --- /dev/null +++ b/src/sea/archive-paths.mts @@ -0,0 +1,19 @@ +export function seaArchivePaths({ + hasCdxgen, + hasSynp, + hasUv, + rootFiles, +}: { + hasCdxgen: boolean + hasSynp: boolean + hasUv: boolean + rootFiles: string[] +}): string[] { + return [ + 'dist', + 'external', + ...rootFiles, + 'resources', + ...(hasCdxgen || hasSynp || hasUv ? ['tools'] : []), + ] +} diff --git a/src/sea/bootstrap.mts b/src/sea/bootstrap.mts index 42d7992268..b0870940b0 100644 --- a/src/sea/bootstrap.mts +++ b/src/sea/bootstrap.mts @@ -1,198 +1,7 @@ -/** - * Ultra-thin bootstrap wrapper for Socket CLI SEA. - * - * This is the THINNEST possible wrapper that: - * - Downloads @socketsecurity/cli from npm on first use - * - Executes it with user's arguments - * - Has NO external dependencies except Node.js built-ins - */ - -import { spawn } from 'node:child_process' -import crypto from 'node:crypto' -import { existsSync, promises as fs } from 'node:fs' -import os from 'node:os' -import path from 'node:path' - -// Minimal constants. -const SOCKET_HOME = path.join(os.homedir(), '.socket') -const SOCKET_CLI_DIR = path.join(SOCKET_HOME, 'cli') -const SOCKET_CLI_PACKAGE = '@socketsecurity/cli' -const NPM_REGISTRY = 'https://registry.npmjs.org' - -async function getLatestVersion(): Promise { - const response = await fetch(`${NPM_REGISTRY}/${SOCKET_CLI_PACKAGE}/latest`) - if (!response.ok) { - throw new Error(`Failed to fetch package info: ${response.statusText}`) - } - const data = (await response.json()) as { version: string } - return data.version -} - -async function downloadPackage(version: string): Promise { - console.error(`Downloading ${SOCKET_CLI_PACKAGE}@${version} from npm...`) - - const tarballUrl = `${NPM_REGISTRY}/${SOCKET_CLI_PACKAGE}/-/cli-${version}.tgz` - const response = await fetch(tarballUrl) - - if (!response.ok) { - throw new Error(`Failed to download package: ${response.statusText}`) - } - - const tempDir = path.join( - SOCKET_HOME, - 'tmp', - crypto.createHash('sha256').update(`${version}`).digest('hex'), - ) - await fs.mkdir(tempDir, { recursive: true }) - - try { - const tarballPath = path.join(tempDir, 'package.tgz') - const buffer = Buffer.from(await response.arrayBuffer()) - await fs.writeFile(tarballPath, buffer) - - // Extract tarball using tar command. - await new Promise((resolve, reject) => { - const child = spawn('tar', ['-xzf', tarballPath, '-C', tempDir], { - stdio: 'ignore', - }) - child.on('exit', code => - code === 0 ? resolve() : reject(new Error('tar failed')), - ) - }) - - const packageDir = path.join(tempDir, 'package') - - if (existsSync(SOCKET_CLI_DIR)) { - await fs.rm(SOCKET_CLI_DIR, { recursive: true, force: true }) - } - - await fs.rename(packageDir, SOCKET_CLI_DIR) - - // Install dependencies. - console.error('Installing dependencies...') - await new Promise((resolve, reject) => { - const child = spawn( - 'npm', - ['install', '--production', '--no-save', '--no-audit', '--no-fund'], - { - cwd: SOCKET_CLI_DIR, - stdio: 'inherit', - }, - ) - child.on('exit', code => - code === 0 ? resolve() : reject(new Error('npm install failed')), - ) - }) - - console.error('Socket CLI downloaded successfully!') - } finally { - await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}) - } -} - -async function getInstalledVersion(): Promise { - const packageJsonPath = path.join(SOCKET_CLI_DIR, 'package.json') - - if (!existsSync(packageJsonPath)) { - return null - } - - try { - const content = await fs.readFile(packageJsonPath, 'utf8') - const packageJson = JSON.parse(content) as { version: string } - return packageJson.version - } catch { - return null - } -} - -async function main(): Promise { - try { - await fs.mkdir(SOCKET_HOME, { recursive: true }) - - const installedVersion = await getInstalledVersion() - - if (!installedVersion) { - console.error('First run detected. Downloading Socket CLI from npm...') - const latestVersion = await getLatestVersion() - await downloadPackage(latestVersion) - } - - // Find CLI entry point. - const packageJsonPath = path.join(SOCKET_CLI_DIR, 'package.json') - const content = await fs.readFile(packageJsonPath, 'utf8') - const packageJson = JSON.parse(content) as { - bin?: Record | string - } - - let cliPath: string - if (typeof packageJson.bin === 'string') { - cliPath = path.join(SOCKET_CLI_DIR, packageJson.bin) - } else if (packageJson.bin?.['socket']) { - cliPath = path.join(SOCKET_CLI_DIR, packageJson.bin['socket']) - } else { - cliPath = path.join(SOCKET_CLI_DIR, 'dist', 'cli.js') - } - - // Forward all arguments to the CLI. - const args = process.argv.slice(2) - - // The SEA contains Node.js runtime, so we need to find a way to execute - // the downloaded CLI. Since require() won't work in SEA context for external - // modules, we need to spawn using system Node.js if available. - - // Try using 'node' from PATH first. - const nodeCmd = 'node' - let useSystemNode = true - - // Quick check if node exists. - try { - const testChild = spawn('node', ['--version'], { stdio: 'pipe' }) - await new Promise(resolve => { - testChild.on('error', () => { - useSystemNode = false - resolve() - }) - testChild.on('exit', code => { - if (code !== 0) { - useSystemNode = false - } - resolve() - }) - }) - } catch { - useSystemNode = false - } - - if (!useSystemNode) { - console.error('Error: Node.js is required to run Socket CLI') - console.error( - 'The SEA wrapper has downloaded the CLI but needs Node.js to execute it.', - ) - console.error('Please install Node.js from https://nodejs.org/') - // eslint-disable-next-line n/no-process-exit - process.exit(1) - } - - // Spawn with system Node.js. - const child = spawn(nodeCmd, [cliPath, ...args], { - stdio: 'inherit', - env: process.env, - }) - - child.on('exit', code => { - // eslint-disable-next-line n/no-process-exit - process.exit(code || 0) - }) - } catch (error) { - console.error('Socket CLI bootstrap error:', error) - // eslint-disable-next-line n/no-process-exit - process.exit(1) - } -} - -main().catch(error => { - console.error('Fatal error:', error) - // eslint-disable-next-line n/no-process-exit - process.exit(1) -}) +/** Load the complete root Socket distribution relative to the SEA VFS main. */ +// Node places the VFS main at its mount root. Expose that stable root before +// loading Socket so native payload adapters can read embedded files by path. +;( + globalThis as typeof globalThis & { __socketSeaVfsRoot?: string } +).__socketSeaVfsRoot = __dirname +require('./dist/cli.js') diff --git a/src/sea/build-lock.mts b/src/sea/build-lock.mts new file mode 100644 index 0000000000..7bc6861b2a --- /dev/null +++ b/src/sea/build-lock.mts @@ -0,0 +1,165 @@ +import { promises as fs } from 'node:fs' +import path from 'node:path' + +/** Acquire an exclusive directory lock. Existing locks are never removed as stale. */ +export async function acquireBuildLock( + lockPath: string, +): Promise<() => Promise> { + await fs.mkdir(path.dirname(lockPath), { recursive: true }) + try { + await fs.mkdir(lockPath) + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'EEXIST') { + throw new Error(`SEA build lock is already held: ${lockPath}`) + } + throw error + } + let released = false + return async () => { + if (released) { + return + } + released = true + await fs.rmdir(lockPath) + } +} + +/** Replace an executable and its manifest as one rollback-safe publication. */ +export async function publishArtifactPair({ + operations = {}, + outputBinary, + outputManifest, + stagedBinary, + stagedManifest, + transactionDirectory, +}: { + stagedBinary: string + stagedManifest: string + outputBinary: string + outputManifest: string + transactionDirectory: string + operations?: { + rename?: typeof fs.rename + rm?: typeof fs.rm + } +}): Promise { + const rename = operations.rename ?? fs.rename + const remove = operations.rm ?? fs.rm + const nextBinary = path.join( + transactionDirectory, + path.basename(outputBinary), + ) + const nextManifest = path.join( + transactionDirectory, + path.basename(outputManifest), + ) + const backupBinary = path.join( + transactionDirectory, + `${path.basename(outputBinary)}.previous`, + ) + const backupManifest = path.join( + transactionDirectory, + `${path.basename(outputManifest)}.previous`, + ) + let backedUpBinary = false + let backedUpManifest = false + let installedBinary = false + let installedManifest = false + let committed = false + let retainedBackups = false + await fs.mkdir(transactionDirectory) + try { + await fs.copyFile(stagedBinary, nextBinary) + await fs.copyFile(stagedManifest, nextManifest) + await fs.chmod(nextBinary, (await fs.stat(stagedBinary)).mode & 0o7777) + const exists = async (filename: string): Promise => { + try { + await fs.lstat(filename) + return true + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return false + } + throw error + } + } + const hadBinary = await exists(outputBinary) + const hadManifest = await exists(outputManifest) + if (hadBinary) { + await rename(outputBinary, backupBinary) + backedUpBinary = true + } + if (hadManifest) { + await rename(outputManifest, backupManifest) + backedUpManifest = true + } + await rename(nextBinary, outputBinary) + installedBinary = true + await rename(nextManifest, outputManifest) + installedManifest = true + committed = true + } catch (error) { + const rollbackErrors: unknown[] = [] + if (installedBinary) { + try { + await remove(outputBinary, { force: true }) + } catch (rollbackError) { + rollbackErrors.push(rollbackError) + } + } + if (installedManifest) { + try { + await remove(outputManifest, { force: true }) + } catch (rollbackError) { + rollbackErrors.push(rollbackError) + } + } + if (backedUpBinary) { + try { + await rename(backupBinary, outputBinary) + backedUpBinary = false + } catch (rollbackError) { + rollbackErrors.push(rollbackError) + } + } + if (backedUpManifest) { + try { + await rename(backupManifest, outputManifest) + backedUpManifest = false + } catch (rollbackError) { + rollbackErrors.push(rollbackError) + } + } + if (rollbackErrors.length) { + retainedBackups = backedUpBinary || backedUpManifest + throw new AggregateError( + [error, ...rollbackErrors], + `SEA artifact publication failed and rollback was incomplete; recoverable backups remain in ${transactionDirectory}`, + ) + } + throw error + } finally { + await remove(nextBinary, { force: true }).catch(() => undefined) + await remove(nextManifest, { force: true }).catch(() => undefined) + if (committed) { + let cleanupFailed = false + try { + await remove(backupBinary, { force: true }) + } catch { + cleanupFailed = true + } + try { + await remove(backupManifest, { force: true }) + } catch { + cleanupFailed = true + } + retainedBackups = cleanupFailed + } + if (!retainedBackups) { + await remove(transactionDirectory, { + recursive: true, + force: true, + }).catch(() => undefined) + } + } +} diff --git a/src/sea/build-sea.mts b/src/sea/build-sea.mts index be5e314ce2..ec0e3d27bb 100644 --- a/src/sea/build-sea.mts +++ b/src/sea/build-sea.mts @@ -1,522 +1,1339 @@ #!/usr/bin/env node -/** - * Build script for creating self-executable Socket CLI applications. - * Uses Node.js Single Executable Application (SEA) feature. - * - * IMPORTANT: This builds a THIN WRAPPER that downloads @socketsecurity/cli on first use. - * The binary contains only: - * - Node.js runtime - * - Bootstrap code to download/execute @socketsecurity/cli - * - No actual CLI implementation - * - * The real Socket CLI code lives in the @socketsecurity/cli npm package, - * which is downloaded from npm registry on first run. - * - * Supported platforms: - * - Windows (x64, arm64) - * - macOS (x64, arm64) - * - Linux (x64, arm64) - * - * Usage: - * - Build all platforms: npm run build:sea - * - Build specific platform: npm run build:sea -- --platform=darwin --arch=x64 - * - Use advanced bootstrap: npm run build:sea -- --advanced - */ - -import crypto from 'node:crypto' +/* eslint-disable no-await-in-loop -- Target builds mutate shared staging and output paths. */ +/** Build a self-contained root Socket CLI using the pinned Node 27 SEA/VFS APIs. */ +import { createHash, randomUUID } from 'node:crypto' import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import path from 'node:path' -import url from 'node:url' - -import trash from 'trash' - -import { spawn } from '@socketsecurity/registry/lib/spawn' - -const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) - -interface BuildTarget { - platform: NodeJS.Platform - arch: string - nodeVersion: string - outputName: string +import { fileURLToPath, pathToFileURL } from 'node:url' +import { gunzipSync } from 'node:zlib' + +import { parse as parseYaml } from 'yaml' + +import { seaArchivePaths } from './archive-paths.mts' +import { acquireBuildLock, publishArtifactPair } from './build-lock.mts' +import { createSeaCapabilities } from './capabilities.mts' +import { stageSynp } from './stage-synp.mts' +import { stageUv } from './stage-uv.mts' + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const ROOT = path.resolve(HERE, '../..') +const NODE_VERSION = '27.0.0-alpha.0.0.0' +// `pnpm build:dist:src` starts by deleting `dist/`. Keep SEA artifacts outside +// that managed distribution tree so an ordinary package build cannot erase +// the output of a preceding SEA build. +const DEFAULT_OUT = path.join(ROOT, 'build/sea') +const TARGETS = [ + ['darwin', 'arm64', 'darwin-arm64'], + ['darwin', 'x64', 'darwin-x64'], + ['linux', 'arm64', 'linux-arm64'], + ['linux', 'x64', 'linux-x64'], + ['win32', 'arm64', 'win-arm64'], + ['win32', 'x64', 'win-x64'], +] as const + +function sha256(data: Buffer): string { + return createHash('sha256').update(data).digest('hex') } -interface BuildOptions { - platform?: NodeJS.Platform - arch?: string - nodeVersion?: string - outputDir?: string +async function digestFile(filename: string): Promise { + return sha256(await fs.readFile(filename)) } -// Supported Node.js versions for SEA. -// Node v24+ has better SEA support and smaller binary sizes. -// const SUPPORTED_NODE_VERSIONS = ['20.11.0', '22.0.0', '24.8.0'] - -// Default Node.js version for SEA. -// Using v20 which has stable SEA support. -const DEFAULT_NODE_VERSION = '20.11.0' - -// Build targets for different platforms. -const BUILD_TARGETS: BuildTarget[] = [ - { - platform: 'win32', - arch: 'x64', - nodeVersion: DEFAULT_NODE_VERSION, - outputName: 'socket-win-x64.exe', - }, - { - platform: 'win32', - arch: 'arm64', - nodeVersion: DEFAULT_NODE_VERSION, - outputName: 'socket-win-arm64.exe', - }, - { - platform: 'darwin', - arch: 'x64', - nodeVersion: DEFAULT_NODE_VERSION, - outputName: 'socket-macos-x64', - }, - { - platform: 'darwin', - arch: 'arm64', - nodeVersion: DEFAULT_NODE_VERSION, - outputName: 'socket-macos-arm64', - }, - { - platform: 'linux', - arch: 'x64', - nodeVersion: DEFAULT_NODE_VERSION, - outputName: 'socket-linux-x64', - }, - { - platform: 'linux', - arch: 'arm64', - nodeVersion: DEFAULT_NODE_VERSION, - outputName: 'socket-linux-arm64', - }, -] - -/** - * Download Node.js binary for a specific platform. - */ -async function downloadNodeBinary( - version: string, - platform: NodeJS.Platform, - arch: string, -): Promise { - const nodeDir = path.join(os.homedir(), '.socket', 'node-binaries') - const platformArch = `${platform}-${arch}` - const nodeFilename = platform === 'win32' ? 'node.exe' : 'node' - const nodePath = path.join(nodeDir, `v${version}`, platformArch, nodeFilename) - - // Check if already downloaded. - if (existsSync(nodePath)) { - console.log(`Using cached Node.js ${version} for ${platformArch}`) - return nodePath - } - - // Construct download URL. - const baseUrl = 'https://nodejs.org/download/release' - const archMap: Record = { - x64: 'x64', - arm64: 'arm64', - ia32: 'x86', - } - const platformMap: Record = { - darwin: 'darwin', - linux: 'linux', - win32: 'win', - } - - const nodePlatform = platformMap[platform] - const nodeArch = archMap[arch] - const tarName = `node-v${version}-${nodePlatform}-${nodeArch}` - const extension = platform === 'win32' ? '.zip' : '.tar.gz' - const downloadUrl = `${baseUrl}/v${version}/${tarName}${extension}` - - console.log(`Downloading Node.js ${version} for ${platformArch}...`) - console.log(`URL: ${downloadUrl}`) - - // Download the archive. - const response = await fetch(downloadUrl) - if (!response.ok) { - throw new Error(`Failed to download Node.js: ${response.statusText}`) - } - - // Create temp directory. - const tempDir = path.join( - nodeDir, - 'tmp', - crypto.createHash('sha256').update(downloadUrl).digest('hex'), - ) - await fs.mkdir(tempDir, { recursive: true }) - - try { - // Save archive. - const archivePath = path.join(tempDir, `node${extension}`) - const buffer = Buffer.from(await response.arrayBuffer()) - await fs.writeFile(archivePath, buffer) - - // Extract archive. - if (platform === 'win32') { - // For Windows binaries, use unzip if available, otherwise skip. - // Note: We're building cross-platform, so we may be on macOS/Linux building for Windows. - if (process.platform === 'win32') { - // On Windows, use PowerShell. - await spawn( - 'powershell', - [ - '-Command', - `Expand-Archive -Path '${archivePath}' -DestinationPath '${tempDir}'`, - ], - { stdio: 'ignore' }, - ) - } else { - // On Unix building for Windows, try unzip. - await spawn('unzip', ['-q', archivePath, '-d', tempDir], { - stdio: 'ignore', +async function digestTree(directory: string): Promise { + const entries: Array<{ + path: string + type: 'file' | 'symlink' + mode: number + sha256?: string + target?: string + }> = [] + const visit = async (current: string): Promise => { + const names = (await fs.readdir(current)).sort() + for (const name of names) { + const fullPath = path.join(current, name) + const relativePath = path + .relative(directory, fullPath) + .split(path.sep) + .join('/') + const stat = await fs.lstat(fullPath) + if (stat.isDirectory()) { + await visit(fullPath) + } else if (stat.isFile()) { + entries.push({ + path: relativePath, + type: 'file', + mode: stat.mode & 0o7777, + sha256: await digestFile(fullPath), }) + } else if (stat.isSymbolicLink()) { + entries.push({ + path: relativePath, + type: 'symlink', + mode: stat.mode & 0o7777, + target: await fs.readlink(fullPath), + }) + } else { + throw new Error(`Unsupported non-file SEA payload entry: ${fullPath}`) } - } else { - // Use tar for Unix systems. - await spawn('tar', ['-xzf', archivePath, '-C', tempDir], { - stdio: 'ignore', - }) } + } + await visit(directory) + if (entries.length === 0) { + throw new Error(`Required SEA payload directory is empty: ${directory}`) + } + return sha256(Buffer.from(JSON.stringify(entries))) +} - // Find and move the Node binary. - const extractedDir = path.join(tempDir, tarName) - const extractedBinary = path.join( - extractedDir, - platform === 'win32' ? 'node.exe' : 'bin/node', - ) - - // Ensure target directory exists. - const targetDir = path.dirname(nodePath) - await fs.mkdir(targetDir, { recursive: true }) - - // Move binary to final location. - await fs.copyFile(extractedBinary, nodePath) - - // Make executable on Unix. - if (platform !== 'win32') { - await fs.chmod(nodePath, 0o755) +async function listThirdPartyArtifacts( + directory: string, + vfsPrefix: string, +): Promise<{ + inventories: Array<{ path: string; sha256: string }> + documents: Array<{ path: string; sha256: string }> +}> { + const inventories: Array<{ path: string; sha256: string }> = [] + const documents: Array<{ path: string; sha256: string }> = [] + const visit = async (current: string): Promise => { + for (const name of (await fs.readdir(current)).sort()) { + const fullPath = path.join(current, name) + const stat = await fs.lstat(fullPath) + if (stat.isDirectory()) { + await visit(fullPath) + } else if (stat.isFile()) { + const relative = path + .relative(directory, fullPath) + .split(path.sep) + .join('/') + const entry = { + path: `${vfsPrefix}/${relative}`, + sha256: await digestFile(fullPath), + } + if (/^THIRD-PARTY-NOTICES(?:-[A-Za-z0-9._-]+)?\.json$/.test(name)) { + inventories.push(entry) + } else if (relative.includes('third-party-notices/')) { + documents.push(entry) + } + } } + } + await visit(directory) + inventories.sort((left, right) => left.path.localeCompare(right.path)) + documents.sort((left, right) => left.path.localeCompare(right.path)) + return { inventories, documents } +} - console.log(`Downloaded Node.js ${version} for ${platformArch}`) - return nodePath - } finally { - // Clean up temp directory using trash. - await trash(tempDir).catch(() => {}) +async function normalizeTreeTimestamps(directory: string): Promise { + const fixedTimestamp = Date.UTC(1980, 0, 1) / 1000 + const visit = async (current: string): Promise => { + const names = (await fs.readdir(current)).sort() + for (const name of names) { + const fullPath = path.join(current, name) + const stat = await fs.lstat(fullPath) + if (stat.isDirectory()) { + await visit(fullPath) + } else if (!stat.isFile()) { + throw new Error(`Unsupported cdxgen payload entry: ${fullPath}`) + } + await fs.utimes(fullPath, fixedTimestamp, fixedTimestamp) + } + await fs.utimes(current, fixedTimestamp, fixedTimestamp) } + await visit(directory) } -/** - * Generate SEA configuration. - */ -async function generateSeaConfig( - entryPoint: string, - outputPath: string, -): Promise { - const configPath = path.join(path.dirname(outputPath), 'sea-config.json') - const blobPath = path.join(path.dirname(outputPath), 'sea-blob.blob') - - const config = { - main: entryPoint, - output: blobPath, - disableExperimentalSEAWarning: true, - useSnapshot: false, // Disable for compatibility. - useCodeCache: true, // Enable code cache for optimization. - assets: {}, // No assets to minimize size. - } - - await fs.writeFile(configPath, JSON.stringify(config, null, 2)) - return configPath +function integrityMatches(bytes: Buffer, integrity: string): boolean { + const [algorithm, encoded] = integrity.split('-', 2) + if (algorithm !== 'sha512' || !encoded) { + return false + } + return createHash('sha512').update(bytes).digest('base64') === encoded } -/** - * Build SEA blob. - */ -async function buildSeaBlob( - nodeBinary: string, - configPath: string, -): Promise { - const config = JSON.parse(await fs.readFile(configPath, 'utf8')) - const blobPath = config.output - - console.log('Generating SEA blob...') - - // Generate the blob using the Node binary. - const spawnPromise = spawn( - nodeBinary, - ['--experimental-sea-config', configPath], - { stdio: 'inherit' }, +export function findLockedPackageIntegrity( + lock: string, + packageName: string, + version: string, +): string | undefined { + const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const match = lock.match( + new RegExp( + `^ '${escapedName}@${escapedVersion}':\\r?\\n resolution: \\{integrity: (sha512-[^}]+)\\}`, + 'm', + ), ) + return match?.[1] +} - const result = await spawnPromise - if ( - result && - typeof result === 'object' && - 'exitCode' in result && - result['exitCode'] !== 0 - ) { +function readTarEntries( + archive: Buffer, + binaryEntry: string, +): Map { + const tar = gunzipSync(archive) + const files = new Map() + for (let offset = 0; offset + 512 <= tar.length; ) { + const header = tar.subarray(offset, offset + 512) + if (header.every(byte => byte === 0)) { + break + } + const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '') + const prefix = header + .subarray(345, 500) + .toString('utf8') + .replace(/\0.*$/, '') + const entryName = prefix ? `${prefix}/${name}` : name + const type = header[156] + if (type !== 0 && type !== 48) { + throw new Error(`Unsupported socket-patch tar entry type at ${entryName}`) + } + const sizeText = header + .subarray(124, 136) + .toString('ascii') + .replace(/\0.*$/, '') + .trim() + if (!/^[0-7]+$/.test(sizeText)) { + throw new Error(`Malformed socket-patch tar entry size at ${entryName}`) + } + const size = Number.parseInt(sizeText, 8) + const checksumText = header + .subarray(148, 156) + .toString('ascii') + .replace(/\0.*$/, '') + .trim() + const expectedChecksum = Number.parseInt(checksumText, 8) + let checksum = 0 + for (let index = 0; index < 512; index += 1) { + checksum += index >= 148 && index < 156 ? 32 : header[index]! + } + if (!Number.isFinite(expectedChecksum) || checksum !== expectedChecksum) { + throw new Error( + `Invalid socket-patch tar header checksum at ${entryName}`, + ) + } + const dataStart = offset + 512 + const dataEnd = dataStart + size + if (dataEnd > tar.length) { + throw new Error(`Truncated socket-patch tar entry at ${entryName}`) + } + if (entryName !== binaryEntry && entryName !== 'package/package.json') { + throw new Error(`Unexpected socket-patch tar entry: ${entryName}`) + } + if (files.has(entryName)) { + throw new Error(`Duplicate socket-patch tar entry: ${entryName}`) + } + files.set(entryName, Buffer.from(tar.subarray(dataStart, dataEnd))) + offset = dataStart + Math.ceil(size / 512) * 512 + } + if (!files.has(binaryEntry) || !files.has('package/package.json')) { throw new Error( - `Failed to generate SEA blob: exit code ${result['exitCode']}`, + 'socket-patch package tarball is missing its executable or package metadata', ) } - - return blobPath + return files } -/** - * Inject SEA blob into Node binary. - */ -async function injectSeaBlob( - nodeBinary: string, - blobPath: string, - outputPath: string, -): Promise { - console.log('Creating self-executable...') - - // Copy the Node binary. - await fs.copyFile(nodeBinary, outputPath) - - if (process.platform === 'darwin') { - // On macOS, remove signature before injection. - console.log('Removing signature...') - await spawn('codesign', ['--remove-signature', outputPath], { - stdio: 'inherit', - }) - - // Inject with macOS-specific flags. - console.log('Injecting SEA blob...') - await spawn( - 'pnpm', - [ - 'exec', - 'postject', - outputPath, - 'NODE_SEA_BLOB', - blobPath, - '--sentinel-fuse', - 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2', - '--macho-segment-name', - 'NODE_SEA', - ], - { stdio: 'inherit' }, +function validateNativeBinary( + bytes: Buffer, + platform: string, + arch: string, +): void { + let valid = false + if (platform === 'darwin') { + const magic = bytes.readUInt32LE(0) + const cpu = bytes.readUInt32LE(4) + valid = + (magic === 0xfeedfacf || magic === 0xcffaedfe) && + ((arch === 'arm64' && (cpu === 0x0100000c || cpu === 0x0c000001)) || + (arch === 'x64' && (cpu === 0x01000007 || cpu === 0x07000001))) + } else if (platform === 'linux') { + const machine = + bytes[5] === 2 ? bytes.readUInt16BE(18) : bytes.readUInt16LE(18) + valid = + bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) && + ((arch === 'arm64' && machine === 183) || + (arch === 'x64' && machine === 62)) + } else if ( + platform === 'win32' && + bytes.length >= 64 && + bytes.toString('ascii', 0, 2) === 'MZ' + ) { + const peOffset = bytes.readUInt32LE(0x3c) + valid = + peOffset + 6 <= bytes.length && + bytes.toString('ascii', peOffset, peOffset + 4) === 'PE\0\0' && + ((arch === 'arm64' && bytes.readUInt16LE(peOffset + 4) === 0xaa64) || + (arch === 'x64' && bytes.readUInt16LE(peOffset + 4) === 0x8664)) + } + if (!valid) { + throw new Error( + `socket-patch binary does not match target ${platform}-${arch}`, ) + } +} - // Re-sign the binary. - console.log('Re-signing binary...') - await spawn('codesign', ['--sign', '-', outputPath], { - stdio: 'inherit', - }) - } else if (process.platform === 'win32') { - // Windows injection. - await spawn( - 'pnpm', - [ - 'exec', - 'postject', - outputPath, - 'NODE_SEA_BLOB', - blobPath, - '--sentinel-fuse', - 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2', - ], - { stdio: 'inherit' }, +async function stageSocketPatch( + target: readonly [string, string, string], + stage: string, + version: string, + parentLicense: string, +): Promise< + | { packageName: string; integrity: string; digest: string; license: string } + | undefined +> { + const [platform, arch] = target + const packagePlatform = platform === 'win32' ? 'win32' : platform + const packageName = `@socketsecurity/socket-patch-${packagePlatform}-${arch}` + const lock = await fs.readFile(path.join(ROOT, 'pnpm-lock.yaml'), 'utf8') + const integrity = findLockedPackageIntegrity(lock, packageName, version) + if (!integrity) { + return undefined + } + const tarball = await download( + `https://registry.npmjs.org/${packageName}/-/${packageName.slice(packageName.lastIndexOf('/') + 1)}-${version}.tgz`, + ) + if (!integrityMatches(tarball, integrity)) { + throw new Error( + `${packageName}@${version} failed pnpm lockfile integrity verification`, ) - console.log('Note: Windows binary may need signing for distribution') - } else { - // Linux injection. - await spawn( - 'pnpm', - [ - 'exec', - 'postject', - outputPath, - 'NODE_SEA_BLOB', - blobPath, - '--sentinel-fuse', - 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2', - ], - { stdio: 'inherit' }, + } + const executableName = + platform === 'win32' ? 'socket-patch.exe' : 'socket-patch' + const binaryEntry = `package/${executableName}` + const entries = readTarEntries(tarball, binaryEntry) + const metadata = JSON.parse( + entries.get('package/package.json')!.toString('utf8'), + ) as { name?: string; version?: string; license?: string } + if (metadata.name !== packageName || metadata.version !== version) { + throw new Error( + `socket-patch package metadata mismatch for ${packageName}@${version}`, ) } + if (typeof metadata.license !== 'string' || metadata.license.trim() === '') { + throw new Error( + `${packageName}@${version} package metadata does not declare a license`, + ) + } + if (metadata.license !== parentLicense) { + throw new Error( + `${packageName}@${version} license ${metadata.license} does not match parent package license ${parentLicense}`, + ) + } + const binary = entries.get(binaryEntry)! + validateNativeBinary(binary, platform, arch) + const payloadDirectory = path.join(stage, 'resources') + await fs.mkdir(payloadDirectory, { recursive: true }) + await fs.writeFile(path.join(payloadDirectory, executableName), binary, { + mode: 0o755, + flag: 'wx', + }) + await fs.copyFile( + path.join(HERE, 'licenses/socket-patch-MIT.txt'), + path.join(payloadDirectory, 'socket-patch-LICENSE.txt'), + ) + return { + packageName, + integrity, + digest: sha256(binary), + license: metadata.license, + } } -/** - * Build a single target. - */ -async function buildTarget( - target: BuildTarget, - options: BuildOptions, -): Promise { - const { outputDir = path.join(__dirname, '../../dist/sea') } = options - - console.log( - `\nBuilding thin wrapper for ${target.platform}-${target.arch}...`, - ) - console.log('(Actual CLI will be downloaded from npm on first use)') +type StagedCdxgenApi = { + packageName: string + version: string + declaredLockIntegrity: string + license: string + digest: string + sourceInventorySha256: string + sourceModuleCount: number + noticePackageCount: number + licenseTextCount: number + noticeTextCount: number + obligationCount: number + noticeDocuments: Array<{ path: string; sha256: string }> +} - // Use the thin bootstrap for minimal size. - const tsEntryPoint = path.join(__dirname, 'bootstrap.mts') +async function stageCdxgenApi( + target: readonly [string, string, string], + stage: string, + version: string, +): Promise { + // Only darwin-arm64 has been qualified with the Node 27 VFS Worker smoke. + if (target[0] !== 'darwin' || target[1] !== 'arm64') { + return undefined + } - // Ensure output directory exists. - await fs.mkdir(outputDir, { recursive: true }) + const packageName = '@cyclonedx/cdxgen' + const packageRoot = path.join(ROOT, 'node_modules/@cyclonedx/cdxgen') + const packagePath = path.join(packageRoot, 'package.json') + if (!existsSync(packagePath)) { + throw new Error( + `Pinned ${packageName}@${version} is not installed at ${packagePath}`, + ) + } + const metadata = JSON.parse(await fs.readFile(packagePath, 'utf8')) as { + name?: string + version?: string + license?: string + } + if (metadata.name !== packageName || metadata.version !== version) { + throw new Error( + `Installed cdxgen package metadata does not match ${packageName}@${version}`, + ) + } + if (typeof metadata.license !== 'string' || metadata.license.trim() === '') { + throw new Error( + `${packageName}@${version} package.json does not declare a license`, + ) + } - // Build the bootstrap with Rollup to CommonJS for SEA. - const entryPoint = path.join(outputDir, 'bootstrap.cjs') - console.log('Building bootstrap...') + const lock = await fs.readFile(path.join(ROOT, 'pnpm-lock.yaml'), 'utf8') + const integrity = findLockedPackageIntegrity(lock, packageName, version) + if (!integrity) { + throw new Error( + `pnpm-lock.yaml has no integrity for ${packageName}@${version}`, + ) + } - // Set environment variables for the rollup config. - process.env['SEA_BOOTSTRAP'] = tsEntryPoint - process.env['SEA_OUTPUT'] = entryPoint + const payloadRoot = path.join(stage, 'tools/cdxgen') + await fs.mkdir(payloadRoot, { recursive: true }) + await run( + 'pnpm', + ['exec', 'rollup', '-c', '.config/rollup.cdxgen-sea.config.mjs'], + { + SEA_CDXGEN_INPUT: path.join(packageRoot, 'lib/cli/index.js'), + SEA_CDXGEN_OUTPUT: payloadRoot, + }, + ) + const entrypoint = path.join(payloadRoot, 'lib/cli/index.js') + const entryStat = await fs.stat(entrypoint).catch(() => undefined) + if (!entryStat?.isFile() || entryStat.size === 0) { + throw new Error('Rollup did not produce the embedded cdxgen API entrypoint') + } + const noticeInventoryPath = path.join(payloadRoot, 'THIRD-PARTY-NOTICES.json') + const noticeInventoryStat = await fs + .stat(noticeInventoryPath) + .catch(() => undefined) + if (!noticeInventoryStat?.isFile() || noticeInventoryStat.size === 0) { + throw new Error( + 'Rollup did not produce the cdxgen third-party notice inventory', + ) + } + const noticeInventory = JSON.parse( + await fs.readFile(noticeInventoryPath, 'utf8'), + ) as { + packages?: Array<{ + licenseFiles?: Array<{ path?: unknown; sha256?: unknown }> + noticeFiles?: Array<{ path?: unknown; sha256?: unknown }> + }> + obligations?: unknown[] + } + if ( + !Array.isArray(noticeInventory.packages) || + !Array.isArray(noticeInventory.obligations) + ) { + throw new Error( + 'The cdxgen third-party notice inventory has an invalid structure', + ) + } + const noticeDocuments = noticeInventory.packages + .flatMap(item => [ + ...(item.licenseFiles ?? []), + ...(item.noticeFiles ?? []), + ]) + .filter( + (item): item is { path: string; sha256: string } => + typeof item.path === 'string' && typeof item.sha256 === 'string', + ) + .map(item => ({ path: item.path, sha256: item.sha256 })) + .sort((left, right) => left.path.localeCompare(right.path)) + const sourceInventoryPath = path.join( + payloadRoot, + 'ROLLUP-SOURCE-INVENTORY.json', + ) + const sourceInventoryBytes = await fs + .readFile(sourceInventoryPath) + .catch(() => undefined) + if (!sourceInventoryBytes) { + throw new Error('Rollup did not produce the cdxgen source hash inventory') + } + const sourceInventory = JSON.parse(sourceInventoryBytes.toString('utf8')) as { + verification?: string + modules?: unknown[] + } + if ( + sourceInventory.verification !== 'unverified' || + !Array.isArray(sourceInventory.modules) + ) { + throw new Error( + 'The cdxgen source hash inventory has an invalid or unexpectedly verified structure', + ) + } - await spawn('pnpm', ['run', 'build:sea:internal:bootstrap'], { - stdio: 'inherit', + const dataDirectory = path.join(packageRoot, 'data') + if (!existsSync(dataDirectory)) { + throw new Error( + `Required cdxgen data directory is missing: ${dataDirectory}`, + ) + } + await fs.cp(dataDirectory, path.join(payloadRoot, 'data'), { + recursive: true, }) - - // Download Node.js binary for target platform. - const nodeBinary = await downloadNodeBinary( - target.nodeVersion, - target.platform, - target.arch, + await fs.copyFile(packagePath, path.join(payloadRoot, 'package.json')) + const licensePath = path.join(packageRoot, 'LICENSE') + if (!existsSync(licensePath)) { + throw new Error(`Required cdxgen license file is missing: ${licensePath}`) + } + await fs.copyFile(licensePath, path.join(payloadRoot, 'LICENSE')) + await fs.copyFile( + path.join(HERE, 'adapters/cdxgen-worker.mjs'), + path.join(payloadRoot, 'sea-worker.mjs'), ) + const workerUtils = path.join(payloadRoot, 'utils') + await fs.mkdir(workerUtils, { recursive: true }) + await fs.copyFile( + path.join(ROOT, 'src/utils/is-object-object.mjs'), + path.join(workerUtils, 'is-object-object.mjs'), + ) + // Keep a concrete VFS path for cdxgen's plugin-directory override. The + // marker does not install or expose any plugin executable. + const pluginsDirectory = path.join(payloadRoot, 'plugins-empty') + await fs.mkdir(pluginsDirectory, { recursive: true }) + await fs.writeFile(path.join(pluginsDirectory, '.socket-empty'), '') + + // ZIP stores file timestamps, so normalize the complete cdxgen closure to + // keep its archive representation stable across otherwise identical builds. + await normalizeTreeTimestamps(payloadRoot) + const digest = await digestTree(payloadRoot) + return { + packageName, + version, + declaredLockIntegrity: integrity, + license: metadata.license, + digest, + sourceInventorySha256: sha256(sourceInventoryBytes), + sourceModuleCount: sourceInventory.modules.length, + noticePackageCount: noticeInventory.packages.length, + licenseTextCount: noticeInventory.packages.reduce( + (count, item) => count + (item.licenseFiles?.length ?? 0), + 0, + ), + noticeTextCount: noticeInventory.packages.reduce( + (count, item) => count + (item.noticeFiles?.length ?? 0), + 0, + ), + obligationCount: noticeInventory.obligations.length, + noticeDocuments, + } +} - // Generate output path. - const outputPath = path.join(outputDir, target.outputName) - await fs.mkdir(outputDir, { recursive: true }) - - // Generate SEA configuration. - const configPath = await generateSeaConfig(entryPoint, outputPath) +async function download(url: string): Promise { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Download failed (${response.status}): ${url}`) + } + return Buffer.from(await response.arrayBuffer()) +} - try { - // Build SEA blob using the downloaded Node binary. - const blobPath = await buildSeaBlob(nodeBinary, configPath) +async function pinnedNode(target: string): Promise<{ + executable: string + licenseFile: string + temporaryDirectory?: string +}> { + const base = `https://nodejs.org/dist/v${NODE_VERSION}` + const platform = target.startsWith('win-') ? 'win' : target.split('-')[0] + const arch = target.split('-')[1] + const ext = platform === 'win' ? 'zip' : 'tar.gz' + const archive = `node-v${NODE_VERSION}-${platform}-${arch}.${ext}` + const [sums, bytes] = await Promise.all([ + download(`${base}/SHASUMS256.txt`), + download(`${base}/${archive}`), + ]) + const line = sums + .toString('utf8') + .split(/\r?\n/) + .find(item => item.trimEnd().endsWith(` ${archive}`)) + const expected = line?.trim().split(/\s+/)[0] + if (!expected || sha256(bytes) !== expected) { + throw new Error( + `Node ${NODE_VERSION} ${target} failed official SHASUMS256.txt verification`, + ) + } - // Inject blob into Node binary. - await injectSeaBlob(nodeBinary, blobPath, outputPath) + // The archive hash is part of the cache identity. Builds never mutate or + // clean a shared extraction tree; each extracts into an owned temp dir and + // atomically promotes the complete result. A cache that exists but fails + // full-tree validation is left untouched; this build uses its own verified + // extraction instead of racing another process to repair shared state. + const cache = path.join( + os.homedir(), + '.cache/socket-sea/node', + NODE_VERSION, + target, + expected, + ) + const executableRelative = platform === 'win' ? 'node.exe' : 'bin/node' + const executable = path.join(cache, executableRelative) - // Make executable on Unix. - if (target.platform !== 'win32') { - await fs.chmod(outputPath, 0o755) + const cacheParent = path.dirname(cache) + await fs.mkdir(cacheParent, { recursive: true }) + const owned = await fs.mkdtemp( + path.join(cacheParent, `.extract-${process.pid}-`), + ) + let preserveOwned = false + try { + const archivePath = path.join(owned, archive) + await fs.writeFile(archivePath, bytes) + const extracted = path.join(owned, 'unpacked') + await fs.mkdir(extracted) + if (platform === 'win') { + const ps = `Expand-Archive -LiteralPath '${archivePath.replaceAll("'", "''")}' -DestinationPath '${extracted.replaceAll("'", "''")}' -Force` + await run('powershell', ['-NoProfile', '-Command', ps]) + const extractedRoot = path.join( + extracted, + `node-v${NODE_VERSION}-win-${arch}`, + ) + await fs.copyFile( + path.join(extractedRoot, 'node.exe'), + path.join(owned, 'node.exe'), + ) + await fs.copyFile( + path.join(extractedRoot, 'LICENSE'), + path.join(owned, 'LICENSE'), + ) + } else { + await run('tar', [ + '-xzf', + archivePath, + '-C', + extracted, + '--strip-components=1', + ]) + await fs.copyFile( + path.join(extracted, 'LICENSE'), + path.join(owned, 'LICENSE'), + ) + for (const directory of ['bin', 'include', 'lib']) { + await fs + .rename(path.join(extracted, directory), path.join(owned, directory)) + .catch(error => { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error + } + }) + } } - - console.log(`✓ Built ${target.outputName}`) - - // Clean up temporary files using trash. - const filesToClean = [ - blobPath, - entryPoint.endsWith('.compiled.mjs') ? entryPoint : null, - entryPoint.endsWith('.mjs') && !entryPoint.endsWith('.compiled.mjs') - ? entryPoint - : null, - ].filter(Boolean) as string[] - - if (filesToClean.length > 0) { - await trash(filesToClean).catch(() => {}) + await fs.rm(extracted, { recursive: true, force: true }) + await fs.rm(archivePath, { force: true }) + if (!existsSync(path.join(owned, executableRelative))) { + throw new Error( + `Verified Node archive did not contain expected executable: ${path.join(owned, executableRelative)}`, + ) + } + const nodeLicense = path.join(owned, 'LICENSE') + if (!(await fs.stat(nodeLicense).catch(() => undefined))?.isFile()) { + throw new Error( + 'The verified Node archive did not contain its root LICENSE file', + ) } - } finally { - // Clean up config. - await trash(configPath).catch(() => {}) - } -} -/** - * Parse command-line arguments. - */ -function parseArgs(): BuildOptions { - const args = process.argv.slice(2) - const options: BuildOptions = {} - - for (const arg of args) { - if (arg.startsWith('--platform=')) { - const platform = arg.split('=')[1] - if (platform) { - options.platform = platform as NodeJS.Platform + // Hash the full extracted runtime group against this freshly downloaded + // and officially verified archive before reusing any warm cache entry. + const verifiedTree = await digestTree(owned) + const cachedStat = await fs.lstat(cache).catch(error => { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return undefined + } + throw error + }) + if (cachedStat) { + if ( + cachedStat.isDirectory() && + !cachedStat.isSymbolicLink() && + (await digestTree(cache).catch(() => '')) === verifiedTree + ) { + return { executable, licenseFile: path.join(cache, 'LICENSE') } } - } else if (arg.startsWith('--arch=')) { - const arch = arg.split('=')[1] - if (arch) { - options.arch = arch + preserveOwned = true + return { + executable: path.join(owned, executableRelative), + licenseFile: nodeLicense, + temporaryDirectory: owned, } - } else if (arg.startsWith('--node-version=')) { - const nodeVersion = arg.split('=')[1] - if (nodeVersion) { - options.nodeVersion = nodeVersion + } + try { + await fs.rename(owned, cache) + } catch (error) { + // A concurrent build may have promoted first. Accept it only if its + // complete runtime tree matches the verified extraction owned here. + const code = (error as NodeJS.ErrnoException).code + if (code !== 'EEXIST' && code !== 'ENOTEMPTY') { + throw error + } + const concurrentStat = await fs.lstat(cache).catch(() => undefined) + if ( + concurrentStat?.isDirectory() && + !concurrentStat.isSymbolicLink() && + (await digestTree(cache).catch(() => '')) === verifiedTree + ) { + return { executable, licenseFile: path.join(cache, 'LICENSE') } } - } else if (arg.startsWith('--output-dir=')) { - const outputDir = arg.split('=')[1] - if (outputDir) { - options.outputDir = outputDir + preserveOwned = true + return { + executable: path.join(owned, executableRelative), + licenseFile: nodeLicense, + temporaryDirectory: owned, } } + } finally { + if (!preserveOwned) { + await fs.rm(owned, { recursive: true, force: true }) + } } + if (!existsSync(executable)) { + throw new Error( + `Verified Node archive did not produce expected executable: ${executable}`, + ) + } + return { executable, licenseFile: path.join(cache, 'LICENSE') } +} - return options +async function run( + command: string, + args: string[], + env?: NodeJS.ProcessEnv, + cwd = ROOT, +): Promise { + const { spawn } = await import('node:child_process') + await new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + stdio: 'inherit', + env: { ...process.env, ...env }, + }) + child.once('error', reject) + child.once('close', code => + code === 0 + ? resolve() + : reject(new Error(`${command} exited with ${code}`)), + ) + }) +} + +function parseArgs(): { + platform?: string + arch?: string + outputDir: string + prototype: boolean +} { + const result: { + platform?: string + arch?: string + outputDir: string + prototype: boolean + } = { outputDir: DEFAULT_OUT, prototype: false } + for (const arg of process.argv.slice(2)) { + if (arg === '--') { + continue + } + const [key, ...rest] = arg.replace(/^--/, '').split('=') + const value = rest.join('=') + if (key === 'prototype' && value === '') { + result.prototype = true + } else if (key === 'platform') { + result.platform = value + } else if (key === 'arch') { + result.arch = value + } else if (key === 'output-dir') { + result.outputDir = path.resolve(value) + } else if (key === 'node-version') { + if (value !== NODE_VERSION) { + throw new Error(`Node version is pinned to ${NODE_VERSION}`) + } + } else { + throw new Error(`Unknown build option: ${arg}`) + } + } + return result } -/** - * Main build function. - */ async function main(): Promise { const options = parseArgs() - - console.log('Socket CLI Self-Executable Builder') - console.log('====================================') - console.log( - 'Building THIN WRAPPER that downloads @socketsecurity/cli on first use', + if (!options.prototype) { + throw new Error( + 'This is an experimental core-only prototype, not a release builder. Pass --prototype explicitly.', + ) + } + const target = TARGETS.find( + ([platform, arch]) => + (options.platform ?? process.platform) === platform && + (options.arch ?? process.arch) === arch, ) - - // Filter targets based on options. - let targets = BUILD_TARGETS - - if (options.platform) { - targets = targets.filter(t => t.platform === options.platform) - } else { - // If no platform specified, only build for current platform to avoid cross-platform issues. - targets = targets.filter(t => t.platform === process.platform) + if (!target) { + throw new Error( + 'No matching supported target; pass --platform and --arch explicitly for cross builds', + ) } - - if (options.arch) { - targets = targets.filter(t => t.arch === options.arch) - } else if (!options.platform) { - // If no arch specified and building for current platform, use current arch. - targets = targets.filter(t => t.arch === process.arch) + const [, , triple] = target + if (target[0] !== process.platform || target[1] !== process.arch) { + throw new Error( + `SEA prototype builds require a native host (${process.platform}-${process.arch}); requested ${triple}`, + ) } - - if (options.nodeVersion) { - targets = targets.map(t => ({ - ...t, - nodeVersion: options.nodeVersion || t.nodeVersion, - })) + const packageJson = JSON.parse( + await fs.readFile(path.join(ROOT, 'package.json'), 'utf8'), + ) as { + version?: string + dependencies?: Record + devDependencies?: Record } - - if (!targets.length) { - throw new Error('No build targets match the specified criteria') + const socketPatchVersion = + packageJson.dependencies?.['@socketsecurity/socket-patch'] + if ( + !socketPatchVersion || + !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test( + socketPatchVersion, + ) + ) { + throw new Error( + '@socketsecurity/socket-patch must have an exact semver version in package.json to build the SEA payload', + ) } - - // Build each target. - for (const target of targets) { - // eslint-disable-next-line no-await-in-loop - await buildTarget(target, options) + const socketPatchPackage = JSON.parse( + await fs.readFile( + path.join(ROOT, 'node_modules/@socketsecurity/socket-patch/package.json'), + 'utf8', + ), + ) as { + name?: string + version?: string + license?: string } + if ( + socketPatchPackage.name !== '@socketsecurity/socket-patch' || + socketPatchPackage.version !== socketPatchVersion + ) { + throw new Error( + `Installed @socketsecurity/socket-patch metadata does not match pinned version ${socketPatchVersion}`, + ) + } + const socketPatchLicense = socketPatchPackage.license + if ( + typeof socketPatchLicense !== 'string' || + socketPatchLicense.trim() === '' + ) { + throw new Error( + `Pinned @socketsecurity/socket-patch@${socketPatchVersion} package.json does not declare a license`, + ) + } + const out = path.resolve(options.outputDir) + await fs.mkdir(out, { recursive: true }) + const releaseOutputLock = await acquireBuildLock( + path.join(out, '.socket-sea.lock'), + ) + let releaseDotnetLock: (() => Promise) | undefined + try { + releaseDotnetLock = await acquireBuildLock( + path.join(ROOT, '.cache/socket-sea-dotnet.lock'), + ) + } catch (error) { + await releaseOutputLock() + throw error + } + let nodeRuntime: Awaited> | undefined + let node = '' + let stageDirectory: string | undefined + try { + node = (nodeRuntime = await pinnedNode(triple)).executable + const stage = (stageDirectory = await fs.mkdtemp( + path.join(os.tmpdir(), `socket-sea-${process.pid}-`), + )) + const stageDist = path.join(stage, 'dist') + const stageExternal = path.join(stage, 'external') + await fs.mkdir(stageDist, { recursive: true }) + await fs.mkdir(stageExternal, { recursive: true }) + await fs.mkdir(path.join(stage, 'resources'), { recursive: true }) + + const cdxgenVersion = packageJson.devDependencies?.['@cyclonedx/cdxgen'] + if ( + !cdxgenVersion || + !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(cdxgenVersion) + ) { + throw new Error( + '@cyclonedx/cdxgen must have an exact version in devDependencies to build the SEA payload', + ) + } + const synpVersion = packageJson.devDependencies?.['synp'] + const prepResults = await Promise.allSettled([ + // The 1.14.x distribution includes this first-party helper. Build it + // before Rollup copies manifest-scripts into the private distribution. + // Its runtime still uses the user's .NET SDK; NuGet is build-time only. + run('pnpm', ['run', 'build:dotnet-tool']), + stageSocketPatch(target, stage, socketPatchVersion, socketPatchLicense), + (async () => { + const cdxgenApi = await stageCdxgenApi(target, stage, cdxgenVersion) + if ( + cdxgenApi && + (!synpVersion || + !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(synpVersion)) + ) { + throw new Error( + 'synp must have an exact version in devDependencies to build the Yarn Classic SEA payload', + ) + } + const synpPayload = cdxgenApi + ? await stageSynp(stage, synpVersion!) + : undefined + return { cdxgenApi, synpPayload } + })(), + stageUv(stage, target[0], target[1]), + ]) + const prepErrors = prepResults.flatMap(result => + result.status === 'rejected' ? [result.reason] : [], + ) + if (prepErrors.length > 0) { + throw new AggregateError( + prepErrors, + 'One or more independent SEA payload preparation steps failed', + ) + } + const socketPatch = ( + prepResults[1] as PromiseFulfilledResult< + Awaited> + > + ).value + const { cdxgenApi, synpPayload } = ( + prepResults[2] as PromiseFulfilledResult<{ + cdxgenApi: Awaited> + synpPayload: Awaited> | undefined + }> + ).value + const uvPayload = ( + prepResults[3] as PromiseFulfilledResult< + Awaited> + > + ).value + const socketPatchHashFile = socketPatch + ? path.join(stage, 'resources', 'socket-patch.sha256') + : undefined + if (socketPatch && socketPatchHashFile) { + await fs.writeFile(socketPatchHashFile, `${socketPatch.digest}\n`) + } - console.log('\n✅ Build complete!') - console.log(`Output directory: ${options.outputDir || 'dist/sea'}`) - console.log('\nNOTE: These binaries are thin wrappers that will download') - console.log('@socketsecurity/cli from npm on first run.') + // Rollup depends on the completed helper and copies it into stageDist. + // Keep it ordered after preparation, while the independent downloads and + // helper build above are allowed to overlap. + await run( + 'pnpm', + [ + 'exec', + 'dotenvx', + '-q', + 'run', + '-f', + '.env.local', + '--', + 'rollup', + '-c', + '.config/rollup.dist.config.mjs', + ], + { + SOCKET_SEA_BUILD: 'true', + SOCKET_SEA_DIST_DIR: stageDist, + SOCKET_SEA_EXTERNAL_DIR: stageExternal, + INLINED_SOCKET_CLI_LEGACY_BUILD: 'false', + INLINED_SOCKET_CLI_SENTRY_BUILD: 'false', + INLINED_SOCKET_CLI_PUBLISHED_BUILD: 'true', + }, + ) + const synpLock = synpPayload + ? (parseYaml( + await fs.readFile(path.join(ROOT, 'pnpm-lock.yaml'), 'utf8'), + ) as { + packages?: Record< + string, + { resolution?: { integrity?: unknown } } | undefined + > + }) + : undefined + const synpLocator = synpPayload ? `synp@${synpPayload.version}` : undefined + const synpLockIntegrity = synpLocator + ? synpLock?.packages?.[synpLocator]?.resolution?.integrity + : undefined + if ( + synpPayload && + (typeof synpLockIntegrity !== 'string' || + !synpLockIntegrity.startsWith('sha512-')) + ) { + throw new Error( + `pnpm-lock.yaml has no integrity for synp@${synpPayload.version}`, + ) + } + const archive = path.join(stage, 'socket-vfs.zip') + const bootstrap = path.join(stage, 'bootstrap.cjs') + const seaConfig = path.join(stage, 'sea-config.json') + const rootFiles = [ + 'package.json', + 'requirements.json', + 'translations.json', + 'LICENSE', + 'node-runtime-LICENSE', + ] + for (const filename of rootFiles) { + const source = + filename === 'node-runtime-LICENSE' + ? nodeRuntime.licenseFile + : path.join(ROOT, filename) + if (!existsSync(source)) { + throw new Error(`Required SEA VFS runtime file is missing: ${source}`) + } + await fs.copyFile(source, path.join(stage, filename)) + } + const requiredPayloads = ['dist/cli.js', 'external'] + for (const filename of requiredPayloads) { + const fullPath = path.join(stage, filename) + const stat = await fs.stat(fullPath).catch(() => undefined) + if ( + !stat || + (filename === 'dist/cli.js' + ? !stat.isFile() || stat.size === 0 + : !stat.isDirectory()) + ) { + throw new Error( + `Required SEA payload is missing or malformed: ${filename}`, + ) + } + } + const dotnetToolDirectory = path.join( + stageDist, + 'manifest-scripts/dotnet-tool', + ) + const dotnetToolEntry = path.join( + dotnetToolDirectory, + 'socket-facts-dotnet.dll', + ) + const dotnetToolStat = await fs.stat(dotnetToolEntry).catch(() => undefined) + if (!dotnetToolStat?.isFile() || dotnetToolStat.size === 0) { + throw new Error( + 'The .NET facts helper was not copied into the SEA distribution', + ) + } + const capabilities = createSeaCapabilities({ + triple, + hasCdxgen: Boolean(cdxgenApi), + hasSynp: Boolean(synpPayload), + hasSocketPatch: Boolean(socketPatch), + hasUv: Boolean(uvPayload), + socketPatchVersion, + }) + await fs.writeFile( + path.join(stage, 'resources/capabilities.json'), + `${JSON.stringify(capabilities, null, 2)}\n`, + ) + // VFS archives and named SEA assets are mutually exclusive in Node 27. + // Native tools belong in the archive too, then are read from the VFS and + // materialized only at invocation time. + const archivePaths = seaArchivePaths({ + hasCdxgen: Boolean(cdxgenApi), + hasSynp: Boolean(synpPayload), + hasUv: Boolean(uvPayload), + rootFiles, + }) + if (process.platform === 'win32') { + const quotePowerShell = (value: string) => + `'${value.replaceAll("'", "''")}'` + const sourceList = archivePaths + .map(filename => quotePowerShell(path.join(stage, filename))) + .join(', ') + await run( + 'powershell', + [ + '-NoProfile', + '-Command', + `Compress-Archive -LiteralPath @(${sourceList}) -DestinationPath ${quotePowerShell(archive)} -Force`, + ], + undefined, + stage, + ) + } else { + await run('zip', ['-q', '-r', archive, ...archivePaths], undefined, stage) + } + const archiveStat = await fs.stat(archive).catch(() => undefined) + if (!archiveStat?.isFile() || archiveStat.size === 0) { + throw new Error('SEA VFS archive was not created or is empty') + } + await run( + 'pnpm', + ['exec', 'rollup', '-c', '.config/rollup.sea.config.mjs'], + { + SEA_BOOTSTRAP: path.join(HERE, 'bootstrap.mts'), + SEA_OUTPUT: bootstrap, + }, + ) + + const config = { + main: bootstrap, + output: path.join( + stage, + triple.startsWith('win-') ? `socket-${triple}.exe` : `socket-${triple}`, + ), + disableExperimentalSEAWarning: true, + useVfs: true, + vfsArchive: archive, + execArgv: ['--no-global-search-paths'], + execArgvExtension: 'none', + } + await fs.writeFile(seaConfig, JSON.stringify(config, null, 2)) + await run(node, ['--build-sea', seaConfig]) + const output = config.output + if (!triple.startsWith('win-')) { + await fs.chmod(output, 0o755) + } + if (triple.startsWith('darwin-')) { + await run('codesign', ['--sign', '-', output]) + } + + const appVersion = packageJson.version ?? 'unknown' + const payloads: Array> = [ + { + id: 'node-runtime', + version: NODE_VERSION, + file: path.basename(node), + sha256: await digestFile(node), + license: 'MIT', + notices: [ + { + type: 'license', + value: 'MIT', + path: 'node-runtime-LICENSE', + sha256: await digestFile(path.join(stage, 'node-runtime-LICENSE')), + }, + ], + }, + { + id: 'socket-vfs', + version: appVersion, + file: path.basename(output), + sha256: await digestFile(output), + }, + { + id: 'socket-vfs-archive', + version: appVersion, + file: 'embedded-vfs-archive', + sha256: await digestFile(archive), + }, + { + id: 'socket-dist', + version: appVersion, + file: 'dist/', + sha256: await digestTree(path.join(stage, 'dist')), + }, + { + id: 'socket-dotnet-facts-helper', + version: appVersion, + file: 'dist/manifest-scripts/dotnet-tool/', + sha256: await digestTree( + path.join(stage, 'dist/manifest-scripts/dotnet-tool'), + ), + }, + { + id: 'external-dependencies', + version: appVersion, + file: 'external/', + sha256: await digestTree(path.join(stage, 'external')), + }, + ...(await Promise.all( + rootFiles.map(async filename => ({ + id: `runtime-file:${filename}`, + version: appVersion, + file: filename, + sha256: await digestFile(path.join(stage, filename)), + })), + )), + ] + if (socketPatch) { + payloads.push({ + id: 'socket-patch', + version: socketPatchVersion, + package: socketPatch.packageName, + integrity: socketPatch.integrity, + license: socketPatch.license, + notices: [ + { + type: 'license', + value: socketPatch.license, + path: 'resources/socket-patch-LICENSE.txt', + }, + ], + file: `SEA asset: socket-patch`, + source: `resources/${triple.startsWith('win-') ? 'socket-patch.exe' : 'socket-patch'}`, + digestFile: 'resources/socket-patch.sha256', + sha256: socketPatch.digest, + }) + } + if (cdxgenApi) { + payloads.push({ + id: 'cdxgen-api', + version: cdxgenApi.version, + package: cdxgenApi.packageName, + declaredLockIntegrity: cdxgenApi.declaredLockIntegrity, + sourceVerification: 'unverified', + sourceVerificationReason: + 'The frozen lockfile SRI is recorded as declared metadata. Installed Rollup module hashes are provided separately and are not authenticated against the locked tarballs or generated bundle bytes.', + sourceInventory: { + file: 'tools/cdxgen/ROLLUP-SOURCE-INVENTORY.json', + sha256: cdxgenApi.sourceInventorySha256, + moduleCount: cdxgenApi.sourceModuleCount, + }, + license: cdxgenApi.license, + notices: [ + { + type: 'license', + value: cdxgenApi.license, + path: 'tools/cdxgen/LICENSE', + }, + { + type: 'third-party-notice-inventory', + path: 'tools/cdxgen/THIRD-PARTY-NOTICES.json', + }, + ], + thirdPartyNotices: { + inventory: 'tools/cdxgen/THIRD-PARTY-NOTICES.json', + documents: cdxgenApi.noticeDocuments, + packageCount: cdxgenApi.noticePackageCount, + licenseTextCount: cdxgenApi.licenseTextCount, + noticeTextCount: cdxgenApi.noticeTextCount, + flaggedObligationCount: cdxgenApi.obligationCount, + }, + file: 'tools/cdxgen/', + entrypoint: 'tools/cdxgen/sea-worker.mjs', + sha256: cdxgenApi.digest, + }) + } + if (synpPayload && typeof synpLockIntegrity === 'string') { + payloads.push({ + id: 'synp-api', + version: synpPayload.version, + package: synpPayload.packageName, + declaredLockIntegrity: synpLockIntegrity, + sourceVerification: 'unverified', + sourceVerificationReason: + 'The frozen lockfile SRI and staged payload digest are recorded. No per-module source inventory is emitted, and neither value authenticates installed or transformed source bytes against the locked tarball.', + license: synpPayload.license, + notices: [ + { + type: 'license', + value: synpPayload.license, + path: 'tools/synp/LICENSE', + }, + { + type: 'third-party-notice-inventory', + path: 'tools/synp/THIRD-PARTY-NOTICES.json', + }, + ], + thirdPartyNotices: { + inventory: 'tools/synp/THIRD-PARTY-NOTICES.json', + inventorySha256: synpPayload.noticeInventorySha256, + documents: synpPayload.noticeDocuments, + packageCount: synpPayload.noticePackageCount, + flaggedObligationCount: synpPayload.obligationCount, + }, + file: 'tools/synp/', + entrypoint: 'tools/synp/bin/synp.js', + sha256: synpPayload.integrity, + }) + } + if (uvPayload) { + payloads.push({ + id: 'uv', + package: uvPayload.packageName, + version: uvPayload.version, + target: uvPayload.target, + source: uvPayload.sourceUrl, + archiveSha256: uvPayload.declaredArchiveSha256, + binary: uvPayload.binaryPath, + sha256: uvPayload.binarySha256, + size: uvPayload.binarySize, + license: uvPayload.license, + notices: uvPayload.noticeDocuments, + noticeInventory: { + path: uvPayload.noticeInventoryPath, + sha256: uvPayload.noticeInventorySha256, + }, + noticeObligations: uvPayload.noticeObligations, + }) + } + const manifest = { + schemaVersion: 1, + target: { os: target[0], arch: target[1], triple }, + application: { name: 'socket', version: appVersion }, + payloads, + thirdPartyNotices: { + rootDistribution: await listThirdPartyArtifacts( + path.join(stage, 'dist'), + 'dist', + ), + externalDependencies: await listThirdPartyArtifacts( + path.join(stage, 'external'), + 'external', + ), + }, + runtimeRequirements: [ + { + capability: 'manifest-dotnet-facts', + executable: 'dotnet', + resolution: 'socket manifest dotnet --bin or PATH', + requirement: + 'A .NET SDK compatible with the selected project; the Socket facts helper DLL is embedded, but the SDK/runtime is user-provided.', + }, + ], + capabilities, + } + const manifestStage = path.join(stage, 'artifact.manifest.json') + await fs.writeFile(manifestStage, `${JSON.stringify(manifest, null, 2)}\n`) + await publishArtifactPair({ + stagedBinary: output, + stagedManifest: manifestStage, + outputBinary: path.join(out, path.basename(output)), + outputManifest: path.join(out, `socket-${triple}.manifest.json`), + transactionDirectory: path.join( + out, + `.socket-sea-publish-${randomUUID()}`, + ), + }) + console.log( + `Built ${path.join(out, path.basename(output))} with Node v${NODE_VERSION} (${triple})`, + ) + } finally { + try { + if (stageDirectory) { + await fs.rm(stageDirectory, { recursive: true, force: true }) + } + if (nodeRuntime?.temporaryDirectory) { + await fs.rm(nodeRuntime.temporaryDirectory, { + recursive: true, + force: true, + }) + } + } finally { + try { + await releaseDotnetLock() + } finally { + await releaseOutputLock() + } + } + } } -// Run if executed directly. -if (import.meta.url === `file://${process.argv[1]}`) { +if ( + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).href +) { main().catch(error => { - console.error('Build failed:', error) - // eslint-disable-next-line n/no-process-exit - process.exit(1) + console.error(error) + process.exitCode = 1 }) } -export { buildTarget, downloadNodeBinary, main } +export { main, pinnedNode } diff --git a/src/sea/build-sea.test.mts b/src/sea/build-sea.test.mts new file mode 100644 index 0000000000..06e7e20cec --- /dev/null +++ b/src/sea/build-sea.test.mts @@ -0,0 +1,257 @@ +import { execFile } from 'node:child_process' +import { promises as fs } from 'node:fs' +import { + mkdir, + mkdtemp, + readFile, + readdir, + rm, + stat, + writeFile, +} from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' + +import { describe, expect, it } from 'vitest' + +import { seaArchivePaths } from './archive-paths.mts' +import { acquireBuildLock, publishArtifactPair } from './build-lock.mts' +import { findLockedPackageIntegrity } from './build-sea.mts' +import { createSeaCapabilities } from './capabilities.mts' + +const execFileAsync = promisify(execFile) +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') + +describe('SEA lockfile metadata', () => { + it('finds the pinned Windows socket-patch integrity in LF and CRLF lockfiles', async () => { + const lock = ( + await readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8') + ).replace(/\r\n/g, '\n') + const packageName = '@socketsecurity/socket-patch-win32-x64' + const integrity = findLockedPackageIntegrity(lock, packageName, '2.0.0') + + expect(integrity).toMatch(/^sha512-/) + expect( + findLockedPackageIntegrity( + lock.replace(/\r?\n/g, '\r\n'), + packageName, + '2.0.0', + ), + ).toBe(integrity) + expect(findLockedPackageIntegrity(lock, packageName, '99.99.99')).toBe( + undefined, + ) + }) +}) + +describe('SEA artifact publication', () => { + it('fails fast on a concurrent output lock and releases it for later builds', async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'sea-lock-test-')) + const lockPath = path.join(directory, 'output.lock') + const release = await acquireBuildLock(lockPath) + await expect(acquireBuildLock(lockPath)).rejects.toThrow() + await release() + const releaseAgain = await acquireBuildLock(lockPath) + await releaseAgain() + await rm(directory, { recursive: true, force: true }) + }) + + it('preserves an existing binary and manifest when staging the replacement fails', async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'sea-publish-test-')) + const stagedBinary = path.join(directory, 'new-binary') + const stagedManifest = path.join(directory, 'missing-manifest') + const outputBinary = path.join(directory, 'socket') + const outputManifest = path.join(directory, 'socket.manifest.json') + await writeFile(stagedBinary, 'new executable') + await writeFile(outputBinary, 'old executable') + await writeFile(outputManifest, 'old manifest') + await expect( + publishArtifactPair({ + stagedBinary, + stagedManifest, + outputBinary, + outputManifest, + transactionDirectory: path.join(directory, 'transaction'), + }), + ).rejects.toThrow() + await expect(readFile(outputBinary, 'utf8')).resolves.toBe('old executable') + await expect(readFile(outputManifest, 'utf8')).resolves.toBe('old manifest') + await rm(directory, { recursive: true, force: true }) + }) + + it('rolls back the binary when manifest promotion fails', async () => { + const directory = await mkdtemp( + path.join(os.tmpdir(), 'sea-promotion-test-'), + ) + const stagedBinary = path.join(directory, 'new-binary') + const stagedManifest = path.join(directory, 'new-manifest') + const outputBinary = path.join(directory, 'socket') + const outputManifest = path.join(directory, 'socket.manifest.json') + const transactionDirectory = path.join(directory, 'transaction') + await writeFile(stagedBinary, 'new executable') + await writeFile(stagedManifest, 'new manifest') + await writeFile(outputBinary, 'old executable') + await writeFile(outputManifest, 'old manifest') + let failed = false + await expect( + publishArtifactPair({ + stagedBinary, + stagedManifest, + outputBinary, + outputManifest, + transactionDirectory, + operations: { + rename: async (source, destination) => { + if (destination === outputManifest && !failed) { + failed = true + throw new Error('injected manifest promotion failure') + } + return await fs.rename(source, destination) + }, + }, + }), + ).rejects.toThrow('injected manifest promotion failure') + await expect(readFile(outputBinary, 'utf8')).resolves.toBe('old executable') + await expect(readFile(outputManifest, 'utf8')).resolves.toBe('old manifest') + await rm(directory, { recursive: true, force: true }) + }) + + it('keeps a recoverable previous pair when backup cleanup fails after promotion', async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'sea-backup-test-')) + const stagedBinary = path.join(directory, 'new-binary') + const stagedManifest = path.join(directory, 'new-manifest') + const outputBinary = path.join(directory, 'socket') + const outputManifest = path.join(directory, 'socket.manifest.json') + const transactionDirectory = path.join(directory, 'transaction') + await writeFile(stagedBinary, 'new executable') + await writeFile(stagedManifest, 'new manifest') + await writeFile(outputBinary, 'old executable') + await writeFile(outputManifest, 'old manifest') + await publishArtifactPair({ + stagedBinary, + stagedManifest, + outputBinary, + outputManifest, + transactionDirectory, + operations: { + rm: async (filename, options) => { + if (String(filename).includes('.previous')) { + throw new Error('injected backup cleanup failure') + } + return await fs.rm(filename, options) + }, + }, + }) + await expect(readFile(outputBinary, 'utf8')).resolves.toBe('new executable') + await expect(readFile(outputManifest, 'utf8')).resolves.toBe('new manifest') + await expect(readdir(transactionDirectory)).resolves.toEqual( + expect.arrayContaining([ + 'socket.previous', + 'socket.manifest.json.previous', + ]), + ) + await rm(directory, { recursive: true, force: true }) + }) +}) + +describe('SEA capability policy', () => { + it('keeps optional tools unavailable unless their payloads are present', () => { + expect( + createSeaCapabilities({ + triple: 'linux-x64', + hasCdxgen: false, + hasSynp: false, + hasSocketPatch: false, + hasUv: false, + }).available, + ).toEqual(['socket-core', 'manifest-dotnet-facts']) + }) +}) + +describe('SEA archive paths', () => { + it('archives staged native tools through their shared tools parent', () => { + const paths = seaArchivePaths({ + hasCdxgen: true, + hasSynp: true, + hasUv: true, + rootFiles: ['package.json'], + }) + expect(paths.filter(item => item === 'tools')).toHaveLength(1) + expect(paths).not.toContain('tools/cdxgen') + expect(paths).not.toContain('tools/synp') + expect(paths).not.toContain('tools/uv') + }) + + it.skipIf(process.platform !== 'win32')( + 'keeps nested tools paths in a real Compress-Archive round trip', + async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'sea-zip-test-')) + const stage = path.join(directory, 'stage') + const archive = path.join(directory, 'tools.zip') + const expanded = path.join(directory, 'expanded') + const script = path.join(directory, 'archive-fixture.ps1') + await mkdir(path.join(stage, 'tools/uv'), { recursive: true }) + await writeFile(path.join(stage, 'tools/uv/uv.exe'), 'fixture executable') + await writeFile( + script, + [ + 'param([string]$Stage, [string]$Archive, [string]$Expanded)', + "$ErrorActionPreference = 'Stop'", + "Compress-Archive -LiteralPath (Join-Path $Stage 'tools') -DestinationPath $Archive -Force", + 'Expand-Archive -LiteralPath $Archive -DestinationPath $Expanded -Force', + "if (-not (Test-Path (Join-Path $Expanded 'tools/uv/uv.exe'))) { throw 'Nested uv executable was flattened' }", + ].join('\n'), + ) + try { + await execFileAsync( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Bypass', + '-File', + script, + stage, + archive, + expanded, + ], + { timeout: 30_000 }, + ) + expect( + (await stat(path.join(expanded, 'tools/uv/uv.exe'))).isFile(), + ).toBe(true) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }, + 30_000, + ) +}) + +describe('SEA bootstrap Rollup config', () => { + it('compiles the TypeScript bootstrap through the real Rollup configuration', async () => { + const directory = await mkdtemp( + path.join(os.tmpdir(), 'sea-bootstrap-test-'), + ) + const output = path.join(directory, 'bootstrap.cjs') + try { + await execFileAsync( + process.execPath, + [ + path.join(root, 'node_modules/rollup/dist/bin/rollup'), + '-c', + path.join(root, '.config/rollup.sea.config.mjs'), + '--file', + output, + ], + { timeout: 30_000 }, + ) + expect((await stat(output)).size).toBeGreaterThan(0) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }, 30_000) +}) diff --git a/src/sea/capabilities.mts b/src/sea/capabilities.mts new file mode 100644 index 0000000000..8c3d276461 --- /dev/null +++ b/src/sea/capabilities.mts @@ -0,0 +1,95 @@ +export type SeaCapability = { id: string } | { id: string; reason: string } + +export type SeaCapabilities = { + available: string[] + unavailable: Array<{ id: string; reason: string }> +} + +export function createSeaCapabilities({ + hasCdxgen, + hasSocketPatch, + hasSynp, + hasUv, + socketPatchVersion, + triple, +}: { + triple: string + hasCdxgen: boolean + hasSynp: boolean + hasSocketPatch: boolean + hasUv: boolean + socketPatchVersion?: string +}): SeaCapabilities { + return { + available: [ + 'socket-core', + 'manifest-dotnet-facts', + ...(hasSocketPatch ? ['socket-patch'] : []), + ...(hasUv ? ['uv'] : []), + ], + unavailable: [ + ...(hasCdxgen + ? [ + { + id: 'manifest-cdxgen-npm-lock-v3', + reason: + 'The restricted npm lock v3 path passed local command smoke, but installed Rollup module bytes are not authenticated against the frozen package tarballs or generated bundle. See the explicitly unverified source inventory.', + }, + { + id: 'cdxgen-other-modes', + reason: + 'Only one npm package-lock.json v3 project is implemented; other project types and modes are not included.', + }, + { + id: 'manifest-cdxgen-yarn-classic', + reason: + 'Synp v1.9.14 API conversion and cdxgen package-lock v1 API smoke passed, but Synp Rollup provenance remains unauthenticated and its third-party notice inventory has missing-license-text obligations.', + }, + ] + : [ + { + id: 'cdxgen', + reason: `No cdxgen API payload is embedded for ${triple}; only darwin-arm64 has a qualified VFS bundle.`, + }, + ]), + { + id: 'synp', + reason: hasSynp + ? 'The Synp API is bundled for Yarn Classic, but source provenance remains unverified and its third-party notice inventory has missing-license-text obligations.' + : 'No Synp API payload is embedded because no compatible cdxgen VFS API payload is available for this target.', + }, + { + id: 'coana', + reason: + 'No Coana executable or verified payload is embedded in this core SEA build.', + }, + ...(!hasSocketPatch + ? [ + { + id: 'socket-patch', + reason: `No compatible locked socket-patch package was validated for ${triple}${socketPatchVersion ? ` at ${socketPatchVersion}` : ''}.`, + }, + ] + : []), + { + id: 'python', + reason: + 'No relocatable Python runtime and standard library are embedded in this core SEA build.', + }, + { + id: 'jvm-toolchain', + reason: + 'No JVM runtime or toolchain payload is embedded in this core SEA build.', + }, + ...(!hasSocketPatch + ? [ + { + id: 'native-assets', + reason: + 'No separately verified native tool assets are embedded in this core SEA build.', + }, + ] + : []), + ], + } +} diff --git a/src/sea/licenses/cdxgen-third-party/boolbase-1.0.0-ISC.txt b/src/sea/licenses/cdxgen-third-party/boolbase-1.0.0-ISC.txt new file mode 100644 index 0000000000..a689aae96e --- /dev/null +++ b/src/sea/licenses/cdxgen-third-party/boolbase-1.0.0-ISC.txt @@ -0,0 +1,13 @@ +Copyright (c) 2014-2015, Felix Boehm + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/sea/licenses/cdxgen-third-party/err-code-2.0.3-MIT.txt b/src/sea/licenses/cdxgen-third-party/err-code-2.0.3-MIT.txt new file mode 100644 index 0000000000..f5b714c6bc --- /dev/null +++ b/src/sea/licenses/cdxgen-third-party/err-code-2.0.3-MIT.txt @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) IndigoUnited + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/sea/licenses/cdxgen-third-party/err-code-2.0.3-README.md b/src/sea/licenses/cdxgen-third-party/err-code-2.0.3-README.md new file mode 100644 index 0000000000..5afdab00c9 --- /dev/null +++ b/src/sea/licenses/cdxgen-third-party/err-code-2.0.3-README.md @@ -0,0 +1,70 @@ +# err-code + +[![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url] [![Greenkeeper badge][greenkeeper-image]][greenkeeper-url] + +[npm-url]:https://npmjs.org/package/err-code +[downloads-image]:http://img.shields.io/npm/dm/err-code.svg +[npm-image]:http://img.shields.io/npm/v/err-code.svg +[travis-url]:https://travis-ci.org/IndigoUnited/js-err-code +[travis-image]:http://img.shields.io/travis/IndigoUnited/js-err-code/master.svg +[david-dm-url]:https://david-dm.org/IndigoUnited/js-err-code +[david-dm-image]:https://img.shields.io/david/IndigoUnited/js-err-code.svg +[david-dm-dev-url]:https://david-dm.org/IndigoUnited/js-err-code?type=dev +[david-dm-dev-image]:https://img.shields.io/david/dev/IndigoUnited/js-err-code.svg +[greenkeeper-image]:https://badges.greenkeeper.io/IndigoUnited/js-err-code.svg +[greenkeeper-url]:https://greenkeeper.io/ + +Create new error instances with a code and additional properties. + + +## Installation + +```console +$ npm install err-code +// or +$ bower install err-code +``` + +The browser file is named index.umd.js which supports CommonJS, AMD and globals (errCode). + + +## Why + +I find myself doing this repeatedly: + +```js +var err = new Error('My message'); +err.code = 'SOMECODE'; +err.detail = 'Additional information about the error'; +throw err; +``` + + +## Usage + +Simple usage. + +```js +var errcode = require('err-code'); + +// fill error with message + code +throw errcode(new Error('My message'), 'ESOMECODE'); +// fill error with message + code + props +throw errcode(new Error('My message'), 'ESOMECODE', { detail: 'Additional information about the error' }); +// fill error with message + props +throw errcode(new Error('My message'), { detail: 'Additional information about the error' }); +``` + +## Pre-existing fields + +If the passed `Error` already has a `.code` field, or fields specified in the third argument to `errcode` they will be overwritten, unless the fields are read only or otherwise throw during assignment in which case a new object will be created that shares a prototype chain with the original `Error`. The `.stack` and `.message` properties will be carried over from the original error and `.code` or any passed properties will be set on it. + + +## Tests + +`$ npm test` + + +## License + +Released under the [MIT License](http://www.opensource.org/licenses/mit-license.php). diff --git a/src/sea/licenses/cdxgen-third-party/imurmurhash-0.1.4-MIT.txt b/src/sea/licenses/cdxgen-third-party/imurmurhash-0.1.4-MIT.txt new file mode 100644 index 0000000000..3976989301 --- /dev/null +++ b/src/sea/licenses/cdxgen-third-party/imurmurhash-0.1.4-MIT.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Gary Court, Jens Taylor + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/sea/licenses/cdxgen-third-party/spdx-exceptions-2.5.0-CC-BY-3.0.txt b/src/sea/licenses/cdxgen-third-party/spdx-exceptions-2.5.0-CC-BY-3.0.txt new file mode 100644 index 0000000000..1a16e05564 --- /dev/null +++ b/src/sea/licenses/cdxgen-third-party/spdx-exceptions-2.5.0-CC-BY-3.0.txt @@ -0,0 +1,319 @@ +Creative Commons Legal Code + +Attribution 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined above) for the purposes of this + License. + c. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + d. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + e. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + f. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + g. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + h. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + i. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(b), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(b), as requested. + b. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Section 3(b), in the case of an Adaptation, + a credit identifying the use of the Work in the Adaptation (e.g., + "French translation of the Work by Original Author," or "Screenplay + based on original Work by Original Author"). The credit required by + this Section 4 (b) may be implemented in any reasonable manner; + provided, however, that in the case of a Adaptation or Collection, at + a minimum such credit will appear, if a credit for all contributing + authors of the Adaptation or Collection appears, then as part of these + credits and in a manner at least as prominent as the credits for the + other contributing authors. For the avoidance of doubt, You may only + use the credit required by this Section for the purpose of attribution + in the manner set out above and, by exercising Your rights under this + License, You may not implicitly or explicitly assert or imply any + connection with, sponsorship or endorsement by the Original Author, + Licensor and/or Attribution Parties, as appropriate, of You or Your + use of the Work, without the separate, express prior written + permission of the Original Author, Licensor and/or Attribution + Parties. + c. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of this License. + + Creative Commons may be contacted at https://creativecommons.org/. diff --git a/src/sea/licenses/cdxgen-third-party/spdx-exceptions-2.5.0-README.md b/src/sea/licenses/cdxgen-third-party/spdx-exceptions-2.5.0-README.md new file mode 100644 index 0000000000..6c927ecc69 --- /dev/null +++ b/src/sea/licenses/cdxgen-third-party/spdx-exceptions-2.5.0-README.md @@ -0,0 +1,36 @@ +The package exports an array of strings. Each string is an identifier +for a license exception under the [Software Package Data Exchange +(SPDX)][SPDX] software license metadata standard. + +[SPDX]: https://spdx.org + +## Copyright and Licensing + +### SPDX + +"SPDX" is a federally registered United States trademark of The Linux +Foundation Corporation. + +From version 2.0 of the [SPDX] specification: + +> Copyright © 2010-2015 Linux Foundation and its Contributors. Licensed +> under the Creative Commons Attribution License 3.0 Unported. All other +> rights are expressly reserved. + +The Linux Foundation and the SPDX working groups are good people. Only +they decide what "SPDX" means, as a standard and otherwise. I respect +their work and their rights. You should, too. + +### This Package + +> I created this package by copying exception identifiers out of the +> SPDX specification. That work was mechanical, routine, and required no +> creativity whatsoever. - Kyle Mitchell, package author + +United States users concerned about intellectual property may wish to +discuss the following Supreme Court decisions with their attorneys: + +- _Baker v. Selden_, 101 U.S. 99 (1879) + +- _Feist Publications, Inc., v. Rural Telephone Service Co._, + 499 U.S. 340 (1991) diff --git a/src/sea/licenses/cdxgen-third-party/spdx-license-ids-3.0.22-CC0-1.0.txt b/src/sea/licenses/cdxgen-third-party/spdx-license-ids-3.0.22-CC0-1.0.txt new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/src/sea/licenses/cdxgen-third-party/spdx-license-ids-3.0.22-CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/src/sea/licenses/cdxgen-third-party/spdx-license-ids-3.0.22-README.md b/src/sea/licenses/cdxgen-third-party/spdx-license-ids-3.0.22-README.md new file mode 100644 index 0000000000..ba650628ae --- /dev/null +++ b/src/sea/licenses/cdxgen-third-party/spdx-license-ids-3.0.22-README.md @@ -0,0 +1,51 @@ +# spdx-license-ids + +[![npm version](https://img.shields.io/npm/v/spdx-license-ids.svg)](https://www.npmjs.com/package/spdx-license-ids) + +A list of [SPDX license](https://spdx.org/licenses/) identifiers + +## Installation + +[Download JSON directly](https://raw.githubusercontent.com/jslicense/spdx-license-ids/main/index.json), or [use](https://docs.npmjs.com/cli/install) [npm](https://docs.npmjs.com/about-npm/): + +``` +npm install spdx-license-ids +``` + +## [Node.js](https://nodejs.org/) API + +### require('spdx-license-ids') + +Type: `string[]` + +All license IDs except for the currently deprecated ones. + +```javascript +const ids = require('spdx-license-ids'); +//=> ['0BSD', 'AAL', 'ADSL', 'AFL-1.1', 'AFL-1.2', 'AFL-2.0', 'AFL-2.1', 'AFL-3.0', 'AGPL-1.0-only', ...] + +ids.includes('BSD-3-Clause'); //=> true +ids.includes('CC-BY-1.0'); //=> true + +ids.includes('GPL-3.0'); //=> false +``` + +### require('spdx-license-ids/deprecated') + +Type: `string[]` + +Deprecated license IDs. + +```javascript +const deprecatedIds = require('spdx-license-ids/deprecated'); +//=> ['AGPL-1.0', 'AGPL-3.0', 'GFDL-1.1', 'GFDL-1.2', 'GFDL-1.3', 'GPL-1.0', 'GPL-2.0', ...] + +deprecatedIds.includes('BSD-3-Clause'); //=> false +deprecatedIds.includes('CC-BY-1.0'); //=> false + +deprecatedIds.includes('GPL-3.0'); //=> true +``` + +## License + +[Creative Commons Zero v1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/deed) diff --git a/src/sea/licenses/core-third-party/@npmcli-agent-3.0.0-README.md b/src/sea/licenses/core-third-party/@npmcli-agent-3.0.0-README.md new file mode 100644 index 0000000000..a3a9105270 --- /dev/null +++ b/src/sea/licenses/core-third-party/@npmcli-agent-3.0.0-README.md @@ -0,0 +1,40 @@ +## @npmcli/agent + +A pair of Agent implementations for nodejs that provide consistent keep-alives, granular timeouts, dns caching, and proxy support. + +### Usage + +```js +const { getAgent, HttpAgent } = require('@npmcli/agent') +const fetch = require('minipass-fetch') + +const main = async () => { + // if you know what agent you need, you can create one directly + const agent = new HttpAgent(agentOptions) + // or you can use the getAgent helper, it will determine and create an Agent + // instance for you as well as reuse that agent for new requests as appropriate + const agent = getAgent('https://registry.npmjs.org/npm', agentOptions) + // minipass-fetch is just an example, this will work for any http client that + // supports node's Agents + const res = await fetch('https://registry.npmjs.org/npm', { agent }) +} + +main() +``` + +### Options + +All options supported by the node Agent implementations are supported here, see [the docs](https://nodejs.org/api/http.html#new-agentoptions) for those. + +Options that have been added by this module include: + +- `family`: what tcp family to use, can be `4` for IPv4, `6` for IPv6 or `0` for both. +- `proxy`: a URL to a supported proxy, currently supports `HTTP CONNECT` based http/https proxies as well as socks4 and 5. +- `dns`: configuration for the built-in dns cache + - `ttl`: how long (in milliseconds) to keep cached dns entries, defaults to `5 * 60 * 100 (5 minutes)` + - `lookup`: optional function to override how dns lookups are performed, defaults to `require('dns').lookup` +- `timeouts`: a set of granular timeouts, all default to `0` + - `connection`: time between initiating connection and actually connecting + - `idle`: time between data packets (if a top level `timeout` is provided, it will be copied here) + - `response`: time between sending a request and receiving a response + - `transfer`: time between starting to receive a request and consuming the response fully diff --git a/src/sea/licenses/core-third-party/@npmcli-agent-4.0.0-README.md b/src/sea/licenses/core-third-party/@npmcli-agent-4.0.0-README.md new file mode 100644 index 0000000000..a3a9105270 --- /dev/null +++ b/src/sea/licenses/core-third-party/@npmcli-agent-4.0.0-README.md @@ -0,0 +1,40 @@ +## @npmcli/agent + +A pair of Agent implementations for nodejs that provide consistent keep-alives, granular timeouts, dns caching, and proxy support. + +### Usage + +```js +const { getAgent, HttpAgent } = require('@npmcli/agent') +const fetch = require('minipass-fetch') + +const main = async () => { + // if you know what agent you need, you can create one directly + const agent = new HttpAgent(agentOptions) + // or you can use the getAgent helper, it will determine and create an Agent + // instance for you as well as reuse that agent for new requests as appropriate + const agent = getAgent('https://registry.npmjs.org/npm', agentOptions) + // minipass-fetch is just an example, this will work for any http client that + // supports node's Agents + const res = await fetch('https://registry.npmjs.org/npm', { agent }) +} + +main() +``` + +### Options + +All options supported by the node Agent implementations are supported here, see [the docs](https://nodejs.org/api/http.html#new-agentoptions) for those. + +Options that have been added by this module include: + +- `family`: what tcp family to use, can be `4` for IPv4, `6` for IPv6 or `0` for both. +- `proxy`: a URL to a supported proxy, currently supports `HTTP CONNECT` based http/https proxies as well as socks4 and 5. +- `dns`: configuration for the built-in dns cache + - `ttl`: how long (in milliseconds) to keep cached dns entries, defaults to `5 * 60 * 100 (5 minutes)` + - `lookup`: optional function to override how dns lookups are performed, defaults to `require('dns').lookup` +- `timeouts`: a set of granular timeouts, all default to `0` + - `connection`: time between initiating connection and actually connecting + - `idle`: time between data packets (if a top level `timeout` is provided, it will be copied here) + - `response`: time between sending a request and receiving a response + - `transfer`: time between starting to receive a request and consuming the response fully diff --git a/src/sea/licenses/core-third-party/ansi-term-0.0.2-ISC.txt b/src/sea/licenses/core-third-party/ansi-term-0.0.2-ISC.txt new file mode 100644 index 0000000000..0bdc810a98 --- /dev/null +++ b/src/sea/licenses/core-third-party/ansi-term-0.0.2-ISC.txt @@ -0,0 +1,7 @@ +ISC License + +Copyright Yaron Naveh + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/sea/licenses/core-third-party/drawille-canvas-blessed-contrib-0.1.3-MIT.txt b/src/sea/licenses/core-third-party/drawille-canvas-blessed-contrib-0.1.3-MIT.txt new file mode 100644 index 0000000000..eb1b988c14 --- /dev/null +++ b/src/sea/licenses/core-third-party/drawille-canvas-blessed-contrib-0.1.3-MIT.txt @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) Bence Dányi + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/sea/licenses/core-third-party/drawille-canvas-blessed-contrib-0.1.3-README.md b/src/sea/licenses/core-third-party/drawille-canvas-blessed-contrib-0.1.3-README.md new file mode 100644 index 0000000000..0fddad5211 --- /dev/null +++ b/src/sea/licenses/core-third-party/drawille-canvas-blessed-contrib-0.1.3-README.md @@ -0,0 +1,81 @@ +# drawille-canvas + +![anim](anim.gif) + +HTML5 Canvas API for [`drawille`](https://github.com/madbence/node-drawille). + +## Install + +``` +$ npm install drawille-canvas +``` + +In node `v0.10` it's a bit slow, with node `v0.11` it runs very smoothly. + +## Usage + +See [example](example.js), check out in the console (`node example`) or in the [browser](http://madbence.github.io/node-drawille-canvas/) (`example.html`). + +## API + +### new Canvas(width, height) + +Creates a new `Canvas` with the given dimensions. +For method details, see [`CanvasRenderingContext2D` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) + +### c.fillRect(x, y, w, h) + +Fills the given area of the canvas. + +### c.clearRect(x, y, w, h) + +Clears the given area of the canvas. + +### c.save() + +Save the current transformation matrix on the stack. + +### c.restore() + +Restore the last transformation matrix. + +### c.translate(x, y) + +Translate the current transformation matrix. + +### c.rotate(a) + +Rotate the current transformation matrix with the given angles. + +### c.scale(x, y) + +Scale the current transformation matrix. + +### c.beginPath() + +Start a new path. + +### c.closePath() + +Close the current path. + +### c.moveTo(x, y) + +Moves the starting point of the next segment to the given coordinates. + +### c.lineTo(x, y) + +Connects the starting point with the given point with a straight line. + +### c.stroke() + +Stroke the current path. + +## TODO + +- add `.fill` to fill paths +- add `.arc`, `.arcTo` + +## License + +MIT diff --git a/src/sea/licenses/core-third-party/npmcli-agent-3.0.0-ISC.txt b/src/sea/licenses/core-third-party/npmcli-agent-3.0.0-ISC.txt new file mode 100644 index 0000000000..ede5b2b0cb --- /dev/null +++ b/src/sea/licenses/core-third-party/npmcli-agent-3.0.0-ISC.txt @@ -0,0 +1,7 @@ +ISC License + +Copyright GitHub Inc. + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/sea/licenses/core-third-party/npmcli-agent-4.0.0-ISC.txt b/src/sea/licenses/core-third-party/npmcli-agent-4.0.0-ISC.txt new file mode 100644 index 0000000000..ede5b2b0cb --- /dev/null +++ b/src/sea/licenses/core-third-party/npmcli-agent-4.0.0-ISC.txt @@ -0,0 +1,7 @@ +ISC License + +Copyright GitHub Inc. + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/sea/licenses/core-third-party/sigstore-verify-3.1.1-Apache-2.0.txt b/src/sea/licenses/core-third-party/sigstore-verify-3.1.1-Apache-2.0.txt new file mode 100644 index 0000000000..e9e7c1679a --- /dev/null +++ b/src/sea/licenses/core-third-party/sigstore-verify-3.1.1-Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 The Sigstore Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/sea/licenses/core-third-party/sigstore-verify-3.1.1-README.md b/src/sea/licenses/core-third-party/sigstore-verify-3.1.1-README.md new file mode 100644 index 0000000000..2f50943ab1 --- /dev/null +++ b/src/sea/licenses/core-third-party/sigstore-verify-3.1.1-README.md @@ -0,0 +1,9 @@ +# @sigstore/verify · [![npm version](https://img.shields.io/npm/v/@sigstore/verify.svg?style=flat)](https://www.npmjs.com/package/@sigstore/verify) [![CI Status](https://github.com/sigstore/sigstore-js/workflows/CI/badge.svg)](https://github.com/sigstore/sigstore-js/actions/workflows/ci.yml) [![Smoke Test Status](https://github.com/sigstore/sigstore-js/workflows/smoke-test/badge.svg)](https://github.com/sigstore/sigstore-js/actions/workflows/smoke-test.yml) + +A library for verifying [Sigstore][1] signatures. + +## Prerequisites + +- Node.js version ^20.17.0 || >=22.9.0 + +[1]: https://www.sigstore.dev diff --git a/src/sea/licenses/core-third-party/x256-0.0.2-MIT.txt b/src/sea/licenses/core-third-party/x256-0.0.2-MIT.txt new file mode 100644 index 0000000000..a57c6861a8 --- /dev/null +++ b/src/sea/licenses/core-third-party/x256-0.0.2-MIT.txt @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/sea/licenses/core-third-party/x256-0.0.2-readme.markdown b/src/sea/licenses/core-third-party/x256-0.0.2-readme.markdown new file mode 100644 index 0000000000..bc415bdebe --- /dev/null +++ b/src/sea/licenses/core-third-party/x256-0.0.2-readme.markdown @@ -0,0 +1,83 @@ +# x256 + +return the nearest +[xterm 256 color code](http://www.frexx.de/xterm-256-notes/) +for rgb inputs. + +[![testling badge](https://ci.testling.com/substack/node-x256.png)](https://ci.testling.com/substack/node-x256) + +[![build status](https://secure.travis-ci.org/substack/node-x256.png)](http://travis-ci.org/substack/node-x256) + +# example + +You can just print the index: + +``` js +var x256 = require('x256'); +var ix = x256(220,40,150); +console.log(ix); +``` + +output: + +``` +162 +``` + +or you can use raw ansi escape codes: + +``` js +var x256 = require('x256'); +var ix = x256(220,40,150); +console.log('\x1b[38;5;' + ix + 'mBEEEEEP'); +``` + +output: + +![x256 raw beep](http://substack.net/images/screenshots/x256_raw_beep.png) + +or you can use [charm](https://github.com/substack/node-charm): + +``` js +var x256 = require('x256'); +var charm = require('charm')(process.stdout); + +var ix = x256(220,40,150); +charm.foreground(ix).write('beep boop'); +``` + +output: + +![x256 charm beep boop](http://substack.net/images/screenshots/x256_charm_beep_boop.png) + +# methods + +``` +var x256 = require('x256') +``` + +## x256(red, green, blue) + +Return the nearest xterm 256 color code for the 24-bit `[red, green, blue]` +values. + +`red`, `green`, and `blue` should each be integers from 0 through 255, +inclusive. + +# attributes + +## x256.colors + +array of `[red,green,blue]` 24-bit color arrays indexed by xterm 256 color code + +# install + +With [npm](http://npmjs.org) do: + +``` +npm install x256 +``` + +# license + +MIT diff --git a/src/sea/licenses/socket-patch-MIT.txt b/src/sea/licenses/socket-patch-MIT.txt new file mode 100644 index 0000000000..47776098d2 --- /dev/null +++ b/src/sea/licenses/socket-patch-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Socket + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/sea/rollup-provenance.mjs b/src/sea/rollup-provenance.mjs new file mode 100644 index 0000000000..b7186dee0d --- /dev/null +++ b/src/sea/rollup-provenance.mjs @@ -0,0 +1,811 @@ +import { createHash } from 'node:crypto' +/* eslint-disable no-await-in-loop -- Inventory traversal and hashing must remain deterministic. */ +import { promises as fs } from 'node:fs' +import path from 'node:path' +import url from 'node:url' + +import { parse as parseYaml } from 'yaml' + +const legalFilenamePattern = + /^(?:license|licence|copying|copyright|notice|patents?)(?:[._ -].*)?$/i + +const curatedLegalDocuments = { + '@npmcli/agent@3.0.0': [ + { + filename: 'npmcli-agent-3.0.0-ISC.txt', + kind: 'license', + sourcePath: + 'ISC terms (npm package metadata declares ISC; npm tarball and pinned upstream commit omit LICENSE)', + source: 'https://opensource.org/license/isc-license-txt/', + attributionSource: 'https://registry.npmjs.org/@npmcli/agent/3.0.0', + attributionInferredFrom: + 'Copyright holder is inferred from the npm author field (GitHub Inc.); no published copyright notice or year was found.', + }, + { + filename: '@npmcli-agent-3.0.0-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm tarball; lock SRI verified)', + source: 'https://registry.npmjs.org/@npmcli/agent/3.0.0', + }, + ], + '@npmcli/agent@4.0.0': [ + { + filename: 'npmcli-agent-4.0.0-ISC.txt', + kind: 'license', + sourcePath: + 'ISC terms (npm package metadata declares ISC; npm tarball and pinned upstream commit omit LICENSE)', + source: 'https://opensource.org/license/isc-license-txt/', + attributionSource: 'https://registry.npmjs.org/@npmcli/agent/4.0.0', + attributionInferredFrom: + 'Copyright holder is inferred from the npm author field (GitHub Inc.); no published copyright notice or year was found.', + }, + { + filename: '@npmcli-agent-4.0.0-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm tarball; lock SRI verified)', + source: 'https://registry.npmjs.org/@npmcli/agent/4.0.0', + }, + ], + '@sigstore/verify@3.1.1': [ + { + filename: 'sigstore-verify-3.1.1-Apache-2.0.txt', + kind: 'license', + sourcePath: 'LICENSE (upstream sigstore-js root file at package gitHead)', + source: + 'https://github.com/sigstore/sigstore-js/blob/c1dc7d4778a450787fc72b083f2490ad02b714c6/LICENSE', + }, + { + filename: 'sigstore-verify-3.1.1-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm tarball; lock SRI verified)', + source: 'https://registry.npmjs.org/@sigstore/verify/3.1.1', + }, + ], + 'x256@0.0.2': [ + { + filename: 'x256-0.0.2-MIT.txt', + kind: 'license', + sourcePath: + 'MIT terms (npm README declares MIT; npm tarball and pinned upstream commit omit LICENSE)', + source: + 'https://raw.githubusercontent.com/spdx/license-list-data/v3.26.0/text/MIT.txt', + attributionSource: 'https://registry.npmjs.org/x256/0.0.2', + attributionInferredFrom: + 'Copyright holder is inferred from the npm author field (James Halliday); no published copyright notice or year was found.', + }, + { + filename: 'x256-0.0.2-readme.markdown', + kind: 'notice', + sourcePath: 'readme.markdown (published npm tarball; lock SRI verified)', + source: 'https://registry.npmjs.org/x256/0.0.2', + }, + ], + 'ansi-term@0.0.2': [ + { + filename: 'ansi-term-0.0.2-ISC.txt', + kind: 'license', + sourcePath: + 'ISC terms (npm package metadata declares ISC; npm tarball and upstream repository omit LICENSE)', + source: 'https://opensource.org/license/isc-license-txt/', + attributionSource: 'https://registry.npmjs.org/ansi-term/0.0.2', + attributionInferredFrom: + 'Copyright holder is inferred from the npm author field (Yaron Naveh); no published copyright notice or year was found.', + }, + ], + 'drawille-canvas-blessed-contrib@0.1.3': [ + { + filename: 'drawille-canvas-blessed-contrib-0.1.3-MIT.txt', + kind: 'license', + sourcePath: + 'MIT terms (npm README declares MIT; npm tarball and pinned upstream commit omit LICENSE)', + source: + 'https://raw.githubusercontent.com/spdx/license-list-data/v3.26.0/text/MIT.txt', + attributionSource: + 'https://registry.npmjs.org/drawille-canvas-blessed-contrib/0.1.3', + attributionInferredFrom: + 'Copyright holder is inferred from the npm author field (Bence Dányi); no published copyright notice or year was found.', + }, + { + filename: 'drawille-canvas-blessed-contrib-0.1.3-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm tarball; lock SRI verified)', + source: + 'https://registry.npmjs.org/drawille-canvas-blessed-contrib/0.1.3', + }, + ], + 'err-code@2.0.3': [ + { + filename: 'err-code-2.0.3-MIT.txt', + kind: 'license', + sourcePath: 'MIT text (package README declares MIT)', + source: + 'https://raw.githubusercontent.com/spdx/license-list-data/v3.26.0/text/MIT.txt', + attributionSource: 'https://registry.npmjs.org/err-code/2.0.3', + attributionInferredFrom: + 'npm author field (IndigoUnited); no package copyright line or LICENSE file is published.', + }, + { + filename: 'err-code-2.0.3-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm tarball; lock SRI verified)', + source: 'https://registry.npmjs.org/err-code/2.0.3', + }, + ], + 'imurmurhash@0.1.4': [ + { + filename: 'imurmurhash-0.1.4-MIT.txt', + kind: 'license', + sourcePath: 'LICENSE (upstream file; omitted from npm tarball)', + source: 'https://github.com/jensyt/imurmurhash-js/blob/0.1.4/LICENSE', + }, + ], + 'spdx-exceptions@2.5.0': [ + { + filename: 'spdx-exceptions-2.5.0-CC-BY-3.0.txt', + kind: 'license', + sourcePath: 'CC-BY-3.0 legal code', + source: + 'https://raw.githubusercontent.com/spdx/license-list-data/v3.26.0/text/CC-BY-3.0.txt', + }, + { + filename: 'spdx-exceptions-2.5.0-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm tarball; lock SRI verified)', + source: 'https://registry.npmjs.org/spdx-exceptions/2.5.0', + }, + ], + 'spdx-license-ids@3.0.22': [ + { + filename: 'spdx-license-ids-3.0.22-CC0-1.0.txt', + kind: 'license', + sourcePath: 'CC0-1.0 legal code', + source: + 'https://raw.githubusercontent.com/spdx/license-list-data/v3.26.0/text/CC0-1.0.txt', + }, + { + filename: 'spdx-license-ids-3.0.22-README.md', + kind: 'notice', + sourcePath: 'README.md (published npm tarball; lock SRI verified)', + source: 'https://registry.npmjs.org/spdx-license-ids/3.0.22', + }, + ], +} + +const coreThirdPartyPackageIds = new Set([ + '@npmcli/agent@3.0.0', + '@npmcli/agent@4.0.0', + '@sigstore/verify@3.1.1', + 'x256@0.0.2', + 'ansi-term@0.0.2', + 'drawille-canvas-blessed-contrib@0.1.3', +]) + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex') +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0 +} + +function stableValue(value) { + if (Array.isArray(value)) { + return value.map(stableValue) + } + if (!value || typeof value !== 'object') { + return value + } + return Object.fromEntries( + Object.keys(value) + .sort(compareText) + .map(key => [key, stableValue(value[key])]), + ) +} + +function stableJson(value) { + return `${JSON.stringify(stableValue(value), null, 2)}\n` +} + +function sourcePathFromId(id) { + const withoutQuery = id.split('?')[0] + if (withoutQuery.startsWith('file:')) { + return url.fileURLToPath(withoutQuery) + } + return withoutQuery +} + +async function findPackageRoot(modulePath) { + let current = path.dirname(modulePath) + while (current !== path.dirname(current)) { + const packageJsonPath = path.join(current, 'package.json') + try { + const metadata = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) + if ( + typeof metadata.name === 'string' && + typeof metadata.version === 'string' + ) { + return { directory: current, metadata } + } + } catch (error) { + if (error?.code !== 'ENOENT' && !(error instanceof SyntaxError)) { + throw error + } + } + if (path.basename(current) === 'node_modules') { + break + } + current = path.dirname(current) + } +} + +async function findLegalFiles(packageRoot) { + const files = [] + const skippedLinks = [] + const visit = async (directory, relativeDirectory = '') => { + for (const entry of ( + await fs.readdir(directory, { withFileTypes: true }) + ).sort((left, right) => compareText(left.name, right.name))) { + if (entry.name === 'node_modules' || entry.name === '.git') { + continue + } + const relativePath = path.posix.join( + relativeDirectory.split(path.sep).join('/'), + entry.name, + ) + const fullPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + await visit(fullPath, path.join(relativeDirectory, entry.name)) + } else if (entry.isSymbolicLink()) { + if (legalFilenamePattern.test(entry.name)) { + skippedLinks.push(relativePath) + } + } else if (entry.isFile() && legalFilenamePattern.test(entry.name)) { + files.push({ fullPath, relativePath }) + } + } + } + await visit(packageRoot) + return { files, skippedLinks } +} + +function normalizedInstalledInstance(packageRoot, root) { + const parts = path.resolve(packageRoot).split(path.sep).filter(Boolean) + const pnpmIndex = parts.lastIndexOf('.pnpm') + if (pnpmIndex >= 0) { + return `pnpm-virtual-store/${parts.slice(pnpmIndex + 1).join('/')}` + } + const storeIndex = parts.lastIndexOf('store') + if (storeIndex >= 0 && parts[storeIndex + 1]?.startsWith('v')) { + return `pnpm-content-store/${parts.slice(storeIndex + 2).join('/')}` + } + const relative = path.relative(root, packageRoot).split(path.sep).join('/') + if (relative && !relative.startsWith('../')) { + return `repository/${relative}` + } + return `external-store/${path.basename(packageRoot)}` +} + +function lockContextFor(lock, packageName, version, root, packageRoot) { + const packageKey = `${packageName}@${version}` + const packageEntry = lock.packages?.[packageKey] + const snapshotCandidates = Object.keys(lock.snapshots ?? {}) + .filter(key => key === packageKey || key.startsWith(`${packageKey}(`)) + .sort(compareText) + const patch = lock.patchedDependencies?.[packageKey] + const resolution = packageEntry?.resolution ?? null + const { integrity: declaredIntegrity, ...otherResolutionFields } = + resolution ?? {} + const output = { + packageResolutionLocator: packageEntry ? packageKey : null, + snapshotLocators: snapshotCandidates, + snapshotAttribution: + snapshotCandidates.length > 1 + ? 'ambiguous-unverified' + : snapshotCandidates.length === 1 + ? 'single-candidate-unverified-not-bound-to-installed-instance' + : 'no-lock-snapshot-candidate', + normalizedInstalledInstance: normalizedInstalledInstance(packageRoot, root), + declaredLockIntegrity: declaredIntegrity ?? null, + declaredResolutionContext: otherResolutionFields, + patch: null, + } + if (patch) { + const patchPath = path.join(root, patch.path) + output.patch = { + path: patch.path, + declaredPatchHash: patch.hash ?? null, + patchFileSha256: null, + patchFileHashMatchesLock: false, + appliedSourceVerification: 'unverified', + } + return fs.readFile(patchPath).then( + bytes => { + output.patch.patchFileSha256 = sha256(bytes) + output.patch.patchFileHashMatchesLock = + output.patch.patchFileSha256 === patch.hash + return output + }, + error => { + if (error?.code === 'ENOENT') { + return output + } + throw error + }, + ) + } + return Promise.resolve(output) +} + +function packageSlug(packageName, version, lockContext) { + const identity = `${packageName}@${version}\0${lockContext.normalizedInstalledInstance}\0${lockContext.snapshotLocators.join('|')}` + return `${packageName.replace(/[^A-Za-z0-9._-]+/g, '_')}-${version}-${sha256(Buffer.from(identity)).slice(0, 12)}` +} + +async function collectPackageRecords(ids, { lock, root }) { + const packages = new Map() + const unmappedModules = [] + const seen = new Set() + for (const id of ids) { + if (typeof id !== 'string' || id.startsWith('\0')) { + continue + } + const rawPath = sourcePathFromId(id) + if ( + !path.isAbsolute(rawPath) || + !rawPath.split(path.sep).includes('node_modules') + ) { + continue + } + let modulePath + try { + modulePath = await fs.realpath(rawPath) + } catch (error) { + if (error?.code === 'ENOENT') { + continue + } + throw error + } + if (seen.has(modulePath)) { + continue + } + seen.add(modulePath) + const packageInfo = await findPackageRoot(modulePath) + const moduleBytes = await fs.readFile(modulePath) + if (!packageInfo) { + unmappedModules.push({ + path: `unmapped/${path.basename(modulePath)}-${sha256(moduleBytes).slice(0, 12)}`, + sha256: sha256(moduleBytes), + verification: 'unverified', + verificationReason: + 'No package metadata root was found for this included Rollup module.', + }) + continue + } + const { directory, metadata } = packageInfo + const lockContext = await lockContextFor( + lock, + metadata.name, + metadata.version, + root, + directory, + ) + const sourceFile = path + .relative(directory, modulePath) + .split(path.sep) + .join('/') + const instanceKey = `${metadata.name}@${metadata.version}\0${lockContext.normalizedInstalledInstance}\0${lockContext.snapshotLocators.join('|')}` + let record = packages.get(instanceKey) + if (!record) { + record = { + name: metadata.name, + version: metadata.version, + packageRoot: directory, + instanceKey, + license: metadata.license ?? metadata.licenses ?? null, + lockContext, + modules: [], + legalDocuments: [], + obligations: [], + } + packages.set(instanceKey, record) + } + record.modules.push({ + packageInstance: lockContext.normalizedInstalledInstance, + path: `${lockContext.normalizedInstalledInstance}/${sourceFile}`, + packagePath: sourceFile, + sha256: sha256(moduleBytes), + verification: 'unverified', + verificationReason: + 'Hash identifies installed module bytes; declared lock SRI does not authenticate installed, patched, or Rollup-generated bytes.', + }) + } + for (const record of packages.values()) { + record.modules.sort((left, right) => compareText(left.path, right.path)) + const { files, skippedLinks } = await findLegalFiles(record.packageRoot) + const slug = packageSlug(record.name, record.version, record.lockContext) + const stagedPaths = new Set() + for (const file of files) { + const bytes = await fs.readFile(file.fullPath) + const digest = sha256(bytes) + const relative = file.relativePath + .split('/') + .map(segment => segment.replace(/[^A-Za-z0-9._-]+/g, '_')) + .join('/') + const outputPath = `third-party-notices/${slug}/${relative}.${digest.slice(0, 12)}` + if (stagedPaths.has(outputPath)) { + continue + } + stagedPaths.add(outputPath) + record.legalDocuments.push({ + packagePath: file.relativePath, + outputPath, + sha256: digest, + type: /^(?:notice|copyright|patents?)/i.test( + path.posix.basename(file.relativePath), + ) + ? 'notice' + : 'license', + contents: bytes, + }) + } + if (!record.legalDocuments.some(document => document.type === 'license')) { + for (const document of curatedLegalDocuments[ + `${record.name}@${record.version}` + ] ?? []) { + const packageId = `${record.name}@${record.version}` + const legalDirectory = coreThirdPartyPackageIds.has(packageId) + ? 'core-third-party' + : 'cdxgen-third-party' + const curatedPath = path.join( + root, + 'src/sea/licenses', + legalDirectory, + document.filename, + ) + const contents = await fs.readFile(curatedPath) + const digest = sha256(contents) + const outputPath = `third-party-notices/${slug}/${document.filename}.${digest.slice(0, 12)}` + record.legalDocuments.push({ + packagePath: document.sourcePath, + outputPath, + sha256: digest, + type: document.kind, + contents, + source: document.source, + ...(document.attributionSource + ? { attributionSource: document.attributionSource } + : {}), + ...(document.attributionInferredFrom + ? { attributionInferredFrom: document.attributionInferredFrom } + : {}), + }) + if (document.attributionInferredFrom) { + record.obligations.push({ + code: 'inferred-copyright-attribution', + message: document.attributionInferredFrom, + }) + } + } + } + record.legalDocuments.sort((left, right) => + compareText(left.outputPath, right.outputPath), + ) + if (!record.legalDocuments.some(document => document.type === 'license')) { + record.obligations.push({ + code: 'missing-license-text', + message: 'No license text file was found in the included package tree.', + }) + } + if (skippedLinks.length) { + record.obligations.push({ + code: 'unresolved-license-symlink', + paths: [...new Set(skippedLinks)].sort(compareText), + message: + 'A license or notice symlink was not followed when collecting package texts.', + }) + } + } + return { + packages: [...packages.values()].sort((left, right) => + compareText( + `${left.name}@${left.version}\0${left.instanceKey}`, + `${right.name}@${right.version}\0${right.instanceKey}`, + ), + ), + unmappedModules: unmappedModules.sort((left, right) => + compareText(left.path, right.path), + ), + } +} + +function serializeInventory(records, { bundleId, lockfilePath }) { + return { + schemaVersion: 1, + generator: 'rollup-included-module-source-hashes', + bundle: bundleId, + lockfile: { path: lockfilePath, lockfileVersion: records.lockfileVersion }, + sourceAuthentication: 'unverified', + sourceAuthenticationReason: + 'Per-file hashes identify observed installed source. Declared lock SRI authenticates the registry tarball only and is not proof of installed bytes, patch application, or generated Rollup output.', + packageCount: records.packages.length, + moduleCount: + records.packages.reduce( + (count, record) => count + record.modules.length, + 0, + ) + records.unmappedModules.length, + packages: records.packages.map(record => ({ + name: record.name, + version: record.version, + license: record.license, + lockContext: record.lockContext, + modules: record.modules, + legalDocuments: record.legalDocuments.map( + ({ contents: _contents, ...document }) => document, + ), + obligations: record.obligations, + })), + unmappedModules: records.unmappedModules, + obligations: [ + ...records.packages.flatMap(record => + record.obligations.map(item => ({ + package: `${record.name}@${record.version}`, + ...item, + })), + ), + ...(records.unmappedModules.length + ? [ + { + code: 'unmapped-rollup-modules', + count: records.unmappedModules.length, + message: + 'Included Rollup modules could not be mapped to package metadata.', + }, + ] + : []), + ], + } +} + +async function readLock(root, lockfilePath) { + const lock = parseYaml( + await fs.readFile(path.join(root, lockfilePath), 'utf8'), + ) + if (!lock || typeof lock.packages !== 'object' || !lock.packages) { + throw new Error( + `Lockfile does not contain a packages graph: ${lockfilePath}`, + ) + } + return lock +} + +async function stageRecords( + records, + { bundleId, lockfilePath, manifestName, outputRoot }, +) { + const docsDir = path.join(outputRoot, 'third-party-notices') + await fs.mkdir(docsDir, { recursive: true }) + const outputDocuments = [] + for (const record of records.packages) { + for (const document of record.legalDocuments) { + const destination = path.join(outputRoot, document.outputPath) + await fs.mkdir(path.dirname(destination), { recursive: true }) + await fs.writeFile(destination, document.contents) + outputDocuments.push({ + package: `${record.name}@${record.version}`, + path: document.outputPath, + sha256: document.sha256, + }) + } + } + const inventory = serializeInventory( + { ...records, lockfileVersion: records.lockfileVersion }, + { bundleId, lockfilePath }, + ) + inventory.outputDocuments = outputDocuments.sort((left, right) => + compareText(left.path, right.path), + ) + const manifestPath = path.join(outputRoot, manifestName) + await fs.writeFile(manifestPath, stableJson(inventory)) + inventory.inventoryFile = { + path: manifestName, + sha256: sha256(Buffer.from(stableJson(inventory))), + } + return inventory +} + +export function includedModulesInventoryPlugin({ + bundleId = 'core-rollup', + inventoryFile = 'THIRD-PARTY-NOTICES.json', + lockfilePath = 'pnpm-lock.yaml', + mode = 'emit', + outputRoot, + root, +} = {}) { + const resolvedRoot = path.resolve(root) + let generated + return { + name: `sea-provenance-${bundleId}`, + async generateBundle(_options, bundle) { + const moduleIds = new Set() + for (const output of Object.values(bundle)) { + if (output.type === 'chunk') { + for (const id of Object.keys(output.modules)) { + moduleIds.add(id) + } + } + } + const lock = await readLock(resolvedRoot, lockfilePath) + const records = await collectPackageRecords(moduleIds, { + root: resolvedRoot, + lock, + }) + records.lockfileVersion = lock.lockfileVersion + const inventory = serializeInventory(records, { bundleId, lockfilePath }) + const assets = records.packages.flatMap(record => record.legalDocuments) + if (mode === 'emit') { + for (const document of assets) { + this.emitFile({ + type: 'asset', + fileName: document.outputPath, + source: document.contents, + }) + } + this.emitFile({ + type: 'asset', + fileName: inventoryFile, + source: stableJson(inventory), + }) + } else { + generated = { records, inventory, assets } + } + generated ??= { records, inventory, assets } + }, + async writeBundle(outputOptions) { + if (mode !== 'stage' || !generated) { + return + } + const destination = + outputRoot ?? + path.resolve( + outputOptions.dir ?? path.dirname(outputOptions.file ?? resolvedRoot), + ) + await stageRecords(generated.records, { + outputRoot: destination, + manifestName: inventoryFile, + bundleId, + lockfilePath, + }) + }, + } +} + +export async function writeCopiedPackageInventory({ + bundleId, + inventoryFile, + lockfilePath = 'pnpm-lock.yaml', + outputPackageRoot, + outputRoot, + packageName: _packageName, + packageRoot, + root, +}) { + const resolvedRoot = path.resolve(root) + const lock = await readLock(resolvedRoot, lockfilePath) + const packageJsonPath = path.join(packageRoot, 'package.json') + const metadata = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) + const resolvedPackageRoot = await fs.realpath(packageRoot) + const lockContext = await lockContextFor( + lock, + metadata.name, + metadata.version, + resolvedRoot, + resolvedPackageRoot, + ) + const { files, skippedLinks } = await findLegalFiles(packageRoot) + const slug = packageSlug(metadata.name, metadata.version, lockContext) + const legalDocuments = [] + for (const file of files) { + const contents = await fs.readFile(file.fullPath) + const digest = sha256(contents) + const relative = file.relativePath + .split('/') + .map(segment => segment.replace(/[^A-Za-z0-9._-]+/g, '_')) + .join('/') + const outputPath = `third-party-notices/${slug}/${relative}.${digest.slice(0, 12)}` + const destination = path.join(outputRoot, outputPath) + await fs.mkdir(path.dirname(destination), { recursive: true }) + await fs.writeFile(destination, contents) + legalDocuments.push({ + packagePath: file.relativePath, + outputPath, + sha256: digest, + type: /^(?:notice|copyright|patents?)/i.test( + path.posix.basename(file.relativePath), + ) + ? 'notice' + : 'license', + }) + } + const filesInOutput = [] + const visit = async (directory, relativeDirectory = '') => { + for (const entry of ( + await fs.readdir(directory, { withFileTypes: true }) + ).sort((a, b) => compareText(a.name, b.name))) { + const absolute = path.join(directory, entry.name) + const relativePath = path.posix.join(relativeDirectory, entry.name) + if (entry.isDirectory()) { + await visit(absolute, relativePath) + } else if (entry.isFile()) { + const bytes = await fs.readFile(absolute) + const sourcePath = path.join(packageRoot, ...relativePath.split('/')) + const sourceBytes = await fs.readFile(sourcePath).catch(error => { + if (error?.code === 'ENOENT') { + return undefined + } + throw error + }) + filesInOutput.push({ + path: relativePath, + outputSha256: sha256(bytes), + ...(sourceBytes + ? { sourceSha256: sha256(sourceBytes) } + : { sourceSha256: null }), + verification: 'unverified', + }) + } + } + } + await visit(outputPackageRoot) + const obligations = [] + if (!legalDocuments.some(document => document.type === 'license')) { + obligations.push({ + code: 'missing-license-text', + message: 'No license text file was found in the copied package source.', + }) + } + if (skippedLinks.length) { + obligations.push({ + code: 'unresolved-license-symlink', + paths: skippedLinks.sort(compareText), + }) + } + const inventory = { + schemaVersion: 1, + generator: 'sea-copied-package-source-hashes', + bundle: bundleId, + lockfile: { path: lockfilePath, lockfileVersion: lock.lockfileVersion }, + sourceAuthentication: 'unverified', + sourceAuthenticationReason: + 'Hashes identify installed package bytes and copied output. Declared lock SRI is not authentication of installed or patched files.', + packageCount: 1, + moduleCount: filesInOutput.filter(file => /\.m?js$/i.test(file.path)) + .length, + packages: [ + { + name: metadata.name, + version: metadata.version, + license: metadata.license ?? metadata.licenses ?? null, + lockContext, + files: filesInOutput, + legalDocuments, + obligations, + }, + ], + unmappedModules: [], + obligations: obligations.map(item => ({ + package: `${metadata.name}@${metadata.version}`, + ...item, + })), + outputDocuments: legalDocuments.map(document => ({ + package: `${metadata.name}@${metadata.version}`, + path: document.outputPath, + sha256: document.sha256, + })), + } + const bytes = Buffer.from(stableJson(inventory)) + await fs.writeFile(path.join(outputRoot, inventoryFile), bytes) + return { + ...inventory, + inventoryFile: { path: inventoryFile, sha256: sha256(bytes) }, + } +} diff --git a/src/sea/runtime.mts b/src/sea/runtime.mts new file mode 100644 index 0000000000..58e3c6e99d --- /dev/null +++ b/src/sea/runtime.mts @@ -0,0 +1,465 @@ +import { createHash } from 'node:crypto' +import { existsSync, promises as fs, readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import os from 'node:os' +import path from 'node:path' +import { Readable, type Writable } from 'node:stream' +import { fileURLToPath } from 'node:url' +import { Worker } from 'node:worker_threads' + +import type { StdioOptions } from 'node:child_process' + +/** + * SEA runtime adapters. Node 27 VFS Workers share the parent PID, so these are + * for allowlisted JavaScript tools that are compatible with Worker threads; + * native helpers use getBundledTool() and a real child process instead. + */ +export interface EmbeddedToolRunOptions { + cwd?: string | URL | undefined + env?: NodeJS.ProcessEnv | undefined + /** True (default) merges process.env with env; false uses only defined env entries. */ + inheritEnv?: boolean | undefined + /** Explicit Socket metadata for the Worker; this is workerData, not process IPC. */ + ipc?: Readonly> | undefined + stdio?: StdioOptions | undefined + stdin?: + | string + | Buffer + | NodeJS.ReadableStream + | AsyncIterable + | undefined + stdout?: Writable | undefined + stderr?: Writable | undefined + signal?: AbortSignal | undefined +} + +export interface EmbeddedToolRunResult { + code: number | null + signal: NodeJS.Signals | null + cancelled?: boolean + stdout?: string + stderr?: string +} + +const sea = (() => { + try { + return createRequire(import.meta.url)('node:sea') as { + isSea?: () => boolean + getAsset?: (key: string) => ArrayBuffer | Uint8Array + } + } catch { + // The regular Socket CLI supports Node releases predating `node:sea`. + return {} + } +})() + +const extractedToolDirectories = new Map() +type SeaGlobal = typeof globalThis & { __socketSeaVfsRoot?: string } + +// These are the only JS entrypoints the Worker adapter may launch. The paths +// are staged under the archive root by a later helper-payload build slice; +// absent files remain unavailable and fail closed here. +const embeddedJsToolEntrypoints = Object.freeze({ + cdxgen: 'tools/cdxgen/sea-worker.mjs', + synp: 'tools/synp/bin/synp.js', +}) + +const embeddedNativeToolPaths = Object.freeze({ + uv: (platform: string) => + `tools/uv/${platform === 'win32' ? 'uv.exe' : 'uv'}`, +}) + +function getVfsRoot(): string { + const root = (globalThis as SeaGlobal).__socketSeaVfsRoot + if (!root) { + throw new Error('The standalone Socket VFS root is not available') + } + return root +} + +function signalFromAbortReason(reason: unknown): NodeJS.Signals | null { + const code = + reason !== null && typeof reason === 'object' + ? (reason as { code?: unknown }).code + : undefined + return typeof code === 'string' && + [ + 'SIGABRT', + 'SIGALRM', + 'SIGBUS', + 'SIGCHLD', + 'SIGCONT', + 'SIGFPE', + 'SIGHUP', + 'SIGILL', + 'SIGINT', + 'SIGIO', + 'SIGIOT', + 'SIGKILL', + 'SIGPIPE', + 'SIGPOLL', + 'SIGPROF', + 'SIGPWR', + 'SIGQUIT', + 'SIGSEGV', + 'SIGSTKFLT', + 'SIGSTOP', + 'SIGSYS', + 'SIGTERM', + 'SIGTRAP', + 'SIGTSTP', + 'SIGTTIN', + 'SIGTTOU', + 'SIGURG', + 'SIGUSR1', + 'SIGUSR2', + 'SIGVTALRM', + 'SIGWINCH', + 'SIGXCPU', + 'SIGXFSZ', + ].includes(code) + ? (code as NodeJS.Signals) + : null +} + +export function isStandaloneSea(): boolean { + return typeof sea.isSea === 'function' && sea.isSea() +} + +export function getEmbeddedAsset(id: string): Buffer { + if (!isStandaloneSea()) { + throw new Error( + `Embedded asset '${id}' is available only in the standalone Socket build`, + ) + } + const files: Record = { + 'socket-patch': `resources/${process.platform === 'win32' ? 'socket-patch.exe' : 'socket-patch'}`, + 'socket-patch-sha256': 'resources/socket-patch.sha256', + } + const relative = files[id] + const root = (globalThis as SeaGlobal).__socketSeaVfsRoot + if (!relative || !root) { + throw new Error( + `Standalone Socket asset '${id}' is missing from this build`, + ) + } + try { + return readFileSync(path.join(root, relative)) + } catch { + throw new Error( + `Standalone Socket asset '${id}' is missing from this build`, + ) + } +} + +/** Fail closed unless the build manifest explicitly advertises this tool. */ +export function assertEmbeddedToolAvailable( + id: 'cdxgen' | 'synp', + capability?: 'manifest-cdxgen-npm-lock-v3' | 'manifest-cdxgen-yarn-classic', +): void { + if (!isStandaloneSea()) { + return + } + const root = (globalThis as SeaGlobal).__socketSeaVfsRoot + let capabilities: unknown + try { + if (!root) { + throw new Error('missing VFS root') + } + capabilities = JSON.parse( + readFileSync(path.join(root, 'resources/capabilities.json'), 'utf8'), + ) + } catch { + throw new Error( + `Embedded ${id} is unavailable because tool capabilities are missing or invalid in this build.`, + ) + } + if ( + capabilities === null || + typeof capabilities !== 'object' || + Array.isArray(capabilities) || + !Array.isArray((capabilities as { available?: unknown }).available) || + !Array.isArray((capabilities as { unavailable?: unknown }).unavailable) + ) { + throw new Error( + `Embedded ${id} is unavailable because tool capabilities are missing or invalid in this build.`, + ) + } + const manifest = capabilities as { + available: unknown[] + unavailable: unknown[] + } + const required = capability + ? [capability] + : id === 'cdxgen' + ? ['manifest-cdxgen-npm-lock-v3', 'manifest-cdxgen-yarn-classic'] + : ['synp'] + if (required.some(tool => manifest.available.includes(tool))) { + return + } + const unavailableIds = id === 'cdxgen' ? [...required, id] : [id] + const reasons = manifest.unavailable.flatMap(entry => { + if (entry === null || typeof entry !== 'object') { + return [] + } + const { id: unavailableId, reason } = entry as { + id?: unknown + reason?: unknown + } + return unavailableIds.includes(unavailableId as string) && + typeof reason === 'string' + ? [reason] + : [] + }) + const reason = reasons.length ? ` ${[...new Set(reasons)].join(' ')}` : '' + throw new Error( + `Embedded ${id} is unavailable in this standalone build.${reason}`, + ) +} + +export async function getBundledTool(id: string): Promise { + if (id !== 'socket-patch' && id !== 'uv') { + throw new Error( + `Bundled tool '${id}' is not configured in this SEA foundation`, + ) + } + if (!isStandaloneSea()) { + throw new Error( + `Bundled tool '${id}' is available only in the standalone Socket build`, + ) + } + + if (id === 'uv') { + const relative = embeddedNativeToolPaths.uv(process.platform) + const source = path.join(getVfsRoot(), relative) + const binary = await fs.readFile(source) + const expectedDigest = await fs.readFile( + path.join(getVfsRoot(), 'tools/uv/uv.sha256'), + 'utf8', + ) + const actualDigest = createHash('sha256').update(binary).digest('hex') + if ( + !/^[a-f0-9]{64}$/.test(expectedDigest.trim()) || + actualDigest !== expectedDigest.trim() + ) { + throw new Error('Embedded uv payload failed SHA-256 verification') + } + const directory = await fs.mkdtemp( + path.join(os.tmpdir(), 'socket-cli-sea-'), + ) + const filename = path.join(directory, path.basename(relative)) + try { + await fs.writeFile(filename, binary, { flag: 'wx', mode: 0o700 }) + if (process.platform !== 'win32') { + await fs.chmod(filename, 0o700) + } + extractedToolDirectories.set(filename, directory) + return filename + } catch (error) { + await fs.rm(directory, { recursive: true, force: true }) + throw error + } + } + + const binary = getEmbeddedAsset('socket-patch') + const expectedDigest = getEmbeddedAsset('socket-patch-sha256') + .toString('utf8') + .trim() + const actualDigest = createHash('sha256').update(binary).digest('hex') + if ( + !/^[a-f0-9]{64}$/.test(expectedDigest) || + actualDigest !== expectedDigest + ) { + throw new Error('Embedded socket-patch payload failed SHA-256 verification') + } + + const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'socket-cli-sea-')) + const filename = path.join( + directory, + process.platform === 'win32' ? 'socket-patch.exe' : 'socket-patch', + ) + try { + await fs.writeFile(filename, binary, { flag: 'wx', mode: 0o700 }) + if (process.platform !== 'win32') { + await fs.chmod(filename, 0o700) + } + extractedToolDirectories.set(filename, directory) + return filename + } catch (error) { + await fs.rm(directory, { recursive: true, force: true }) + throw error + } +} + +export async function cleanupBundledTool(filename: string): Promise { + const directory = extractedToolDirectories.get(filename) + if (!directory) { + return + } + extractedToolDirectories.delete(filename) + await fs.rm(directory, { recursive: true, force: true }) +} + +export async function runEmbeddedJsTool( + id: string, + args: readonly string[], + options: EmbeddedToolRunOptions = {}, +): Promise { + if (!isStandaloneSea()) { + throw new Error( + `Embedded JavaScript tool '${id}' is available only in the standalone Socket build`, + ) + } + if (!Object.hasOwn(embeddedJsToolEntrypoints, id)) { + throw new Error(`Embedded JavaScript tool '${id}' is not allowlisted`) + } + if (id === 'cdxgen' || id === 'synp') { + assertEmbeddedToolAvailable(id) + } + if (options.signal?.aborted) { + return { + code: null, + signal: signalFromAbortReason(options.signal.reason), + cancelled: true, + } + } + + // Worker threads inherit cwd and Node does not support changing it per + // Worker. Reject a different request rather than process.chdir() in the + // shared parent process. + const requestedCwd = + options.cwd instanceof URL ? fileURLToPath(options.cwd) : options.cwd + if (requestedCwd && path.resolve(requestedCwd) !== process.cwd()) { + throw new Error( + `Embedded JavaScript tool '${id}' cannot change cwd in a VFS Worker; ` + + 'run Socket from the target project directory.', + ) + } + + const entrypoint = path.join( + getVfsRoot(), + embeddedJsToolEntrypoints[id as keyof typeof embeddedJsToolEntrypoints], + ) + if (!existsSync(entrypoint)) { + throw new Error( + `Embedded JavaScript tool '${id}' is unavailable because its VFS entrypoint is not bundled for this target.`, + ) + } + + const requestedStdio = options.stdio ?? 'inherit' + if ( + requestedStdio !== 'inherit' && + requestedStdio !== 'ignore' && + requestedStdio !== 'pipe' + ) { + throw new Error( + `Embedded JavaScript tool '${id}' supports only inherit, ignore, or pipe stdio in a VFS Worker.`, + ) + } + const stdio = requestedStdio + const wantsStreams = stdio !== 'ignore' + const pipeInput = options.stdin + const captureStdout: Buffer[] = [] + const captureStderr: Buffer[] = [] + const worker = new Worker(entrypoint, { + argv: [...args], + env: + options.inheritEnv === false + ? Object.fromEntries( + Object.entries(options.env ?? {}).filter( + (entry): entry is [string, string] => entry[1] !== undefined, + ), + ) + : { ...process.env, ...options.env }, + execArgv: [], + workerData: { socketIpc: options.ipc }, + stdin: wantsStreams, + // Always capture Worker stdio. In ignore mode we drain it below so it + // cannot inherit the CLI's streams or stall on a full pipe. + stdout: true, + stderr: true, + }) + + if (wantsStreams && worker.stdin) { + if (pipeInput !== undefined) { + const input = + typeof pipeInput === 'string' || Buffer.isBuffer(pipeInput) + ? Readable.from([pipeInput]) + : Symbol.asyncIterator in Object(pipeInput) + ? Readable.from(pipeInput as AsyncIterable) + : (pipeInput as NodeJS.ReadableStream) + input.pipe(worker.stdin) + } else if (stdio === 'inherit') { + process.stdin.pipe(worker.stdin) + } else { + worker.stdin.end() + } + } + + const outputTarget = (kind: 'stdout' | 'stderr'): Writable | undefined => { + const explicit = options[kind] + if (explicit) { + return explicit + } + if (stdio === 'inherit') { + return kind === 'stdout' ? process.stdout : process.stderr + } + return undefined + } + const stdoutTarget = outputTarget('stdout') + const stderrTarget = outputTarget('stderr') + if (!wantsStreams) { + worker.stdout?.resume() + worker.stderr?.resume() + } + if (wantsStreams && worker.stdout) { + if (stdoutTarget) { + worker.stdout.pipe(stdoutTarget, { end: false }) + } else if (stdio === 'pipe') { + worker.stdout.on('data', chunk => captureStdout.push(Buffer.from(chunk))) + } + } + if (wantsStreams && worker.stderr) { + if (stderrTarget) { + worker.stderr.pipe(stderrTarget, { end: false }) + } else if (stdio === 'pipe') { + worker.stderr.on('data', chunk => captureStderr.push(Buffer.from(chunk))) + } + } + + let cancelled = false + const onAbort = () => { + cancelled = true + void worker.terminate() + } + options.signal?.addEventListener('abort', onAbort, { once: true }) + + try { + const code = await new Promise((resolve, reject) => { + worker.once('error', reject) + worker.once('exit', resolve) + }) + return { + code: cancelled ? null : code, + signal: cancelled ? signalFromAbortReason(options.signal?.reason) : null, + ...(cancelled ? { cancelled: true } : {}), + ...(stdio === 'pipe' + ? { + stdout: Buffer.concat(captureStdout).toString(), + stderr: Buffer.concat(captureStderr).toString(), + } + : {}), + } + } finally { + options.signal?.removeEventListener('abort', onAbort) + if (stdio === 'inherit' && !pipeInput && worker.stdin) { + process.stdin.unpipe(worker.stdin) + } + if (worker.stdout && stdoutTarget) { + worker.stdout.unpipe(stdoutTarget) + } + if (worker.stderr && stderrTarget) { + worker.stderr.unpipe(stderrTarget) + } + } +} diff --git a/src/sea/runtime.test.mts b/src/sea/runtime.test.mts new file mode 100644 index 0000000000..9cfe599ce5 --- /dev/null +++ b/src/sea/runtime.test.mts @@ -0,0 +1,97 @@ +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +const { mockWorker } = vi.hoisted(() => ({ mockWorker: vi.fn() })) + +function mockCreateRequire(): (specifier: string) => unknown { + return (specifier: string) => + specifier === 'node:sea' ? { isSea: () => true } : undefined +} + +vi.mock('node:module', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + createRequire: mockCreateRequire, + } +}) +vi.mock('node:worker_threads', () => ({ Worker: mockWorker })) + +import { assertEmbeddedToolAvailable, runEmbeddedJsTool } from './runtime.mts' + +type SeaGlobal = typeof globalThis & { __socketSeaVfsRoot?: string } + +describe('embedded SEA tool capabilities', () => { + let root: string | undefined + const seaGlobal = globalThis as SeaGlobal + const oldRoot = seaGlobal.__socketSeaVfsRoot + + afterEach(async () => { + if (root) { + await fs.rm(root, { recursive: true, force: true }) + } + root = undefined + if (oldRoot === undefined) { + delete seaGlobal.__socketSeaVfsRoot + } else { + seaGlobal.__socketSeaVfsRoot = oldRoot + } + mockWorker.mockClear() + }) + + async function setup(manifest?: unknown): Promise { + root = await fs.mkdtemp(path.join(os.tmpdir(), 'socket-sea-capabilities-')) + seaGlobal.__socketSeaVfsRoot = root + if (manifest !== undefined) { + await fs.mkdir(path.join(root, 'resources'), { recursive: true }) + await fs.writeFile( + path.join(root, 'resources/capabilities.json'), + JSON.stringify(manifest), + ) + } + } + + it('fails closed with the recorded reason even when the entrypoint exists', async () => { + await setup({ + available: [], + unavailable: [ + { + id: 'manifest-cdxgen-npm-lock-v3', + reason: 'provenance is unverified', + }, + ], + }) + const entrypoint = path.join(root!, 'tools/cdxgen/sea-worker.mjs') + await fs.mkdir(path.dirname(entrypoint), { recursive: true }) + await fs.writeFile(entrypoint, '') + + expect(() => assertEmbeddedToolAvailable('cdxgen')).toThrow( + /provenance is unverified/, + ) + await expect(runEmbeddedJsTool('cdxgen', [])).rejects.toThrow( + /provenance is unverified/, + ) + expect(mockWorker).not.toHaveBeenCalled() + }) + + it.each([undefined, { available: ['synp'] }, { available: 'cdxgen' }])( + 'rejects missing or malformed capabilities: %j', + async manifest => { + await setup(manifest) + expect(() => assertEmbeddedToolAvailable('cdxgen')).toThrow( + /capabilities are missing or invalid/, + ) + }, + ) + + it('allows the matching stable capability identifier', async () => { + await setup({ + available: ['manifest-cdxgen-yarn-classic'], + unavailable: [], + }) + expect(() => assertEmbeddedToolAvailable('cdxgen')).not.toThrow() + }) +}) diff --git a/src/sea/stage-synp.mts b/src/sea/stage-synp.mts new file mode 100644 index 0000000000..96624f5e0d --- /dev/null +++ b/src/sea/stage-synp.mts @@ -0,0 +1,161 @@ +import { execFileSync } from 'node:child_process' +/* eslint-disable no-await-in-loop -- Package staging shares a mutable VFS tree. */ +import { createHash } from 'node:crypto' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const ROOT = path.resolve(HERE, '../..') +const CONFIG = path.join(ROOT, '.config/rollup.synp-sea.config.mjs') +const EXPECTED_PACKAGE = 'synp' + +function sha256(bytes: Uint8Array): string { + return createHash('sha256').update(bytes).digest('hex') +} + +async function listFiles(directory: string): Promise { + const files: string[] = [] + const visit = async (current: string): Promise => { + for (const entry of await fs.readdir(current, { withFileTypes: true })) { + const filename = path.join(current, entry.name) + if (entry.isDirectory()) { + await visit(filename) + } else if (entry.isFile()) { + files.push(filename) + } else { + throw new Error(`Unsupported Synp payload entry: ${filename}`) + } + } + } + await visit(directory) + return files.sort() +} + +async function normalizeDirectoryTimes(directory: string): Promise { + const fixedTime = new Date('1980-01-01T00:00:00.000Z') + for (const entry of await fs.readdir(directory, { withFileTypes: true })) { + if (entry.isDirectory()) { + await normalizeDirectoryTimes(path.join(directory, entry.name)) + } + } + await fs.utimes(directory, fixedTime, fixedTime) +} + +/** Stage the pinned Synp API, its bundled dependency closure, and licenses. */ +export async function stageSynp( + stageRoot: string, + expectedVersion: string, +): Promise<{ + packageName: string + version: string + license: string + integrity: string + fileCount: number + noticePackageCount: number + obligationCount: number + noticeInventorySha256: string + noticeDocuments: Array<{ path: string; sha256: string }> +}> { + const packageRoot = path.join(ROOT, 'node_modules/synp') + const metadata = JSON.parse( + await fs.readFile(path.join(packageRoot, 'package.json'), 'utf8'), + ) as { name?: string; version?: string; license?: string } + if ( + metadata.name !== EXPECTED_PACKAGE || + metadata.version !== expectedVersion || + typeof metadata.license !== 'string' + ) { + throw new Error( + `Installed Synp metadata does not match ${EXPECTED_PACKAGE}@${expectedVersion}`, + ) + } + const outputRoot = path.join(stageRoot, 'tools/synp') + await fs.rm(outputRoot, { recursive: true, force: true }) + await fs.mkdir(outputRoot, { recursive: true }) + execFileSync( + process.execPath, + [path.join(ROOT, 'node_modules/rollup/dist/bin/rollup'), '-c', CONFIG], + { + cwd: ROOT, + env: { + ...process.env, + SEA_SYNP_INPUT: path.join(HERE, 'adapters/synp-worker.mjs'), + SEA_SYNP_OUTPUT: outputRoot, + }, + stdio: 'inherit', + }, + ) + await Promise.all([ + fs.copyFile( + path.join(packageRoot, 'LICENSE.md'), + path.join(outputRoot, 'LICENSE'), + ), + fs.writeFile( + path.join(outputRoot, 'package.json'), + `${JSON.stringify( + { + name: metadata.name, + version: metadata.version, + license: metadata.license, + type: 'commonjs', + main: 'bin/synp.js', + }, + null, + 2, + )}\n`, + ), + ]) + + const workerPath = path.join(outputRoot, 'bin/synp.js') + const workerStat = await fs.stat(workerPath).catch(() => undefined) + if (!workerStat?.isFile() || workerStat.size === 0) { + throw new Error( + 'Rollup did not produce the embedded Synp Worker entrypoint', + ) + } + + const files = await listFiles(outputRoot) + const entries: Array<{ path: string; sha256: string; size: number }> = [] + const fixedTime = new Date('1980-01-01T00:00:00.000Z') + for (const filename of files) { + const bytes = await fs.readFile(filename) + await fs.utimes(filename, fixedTime, fixedTime) + entries.push({ + path: path.relative(stageRoot, filename).split(path.sep).join('/'), + sha256: sha256(bytes), + size: bytes.length, + }) + } + await normalizeDirectoryTimes(outputRoot) + const inventoryPath = path.join(outputRoot, 'THIRD-PARTY-NOTICES.json') + const notice = JSON.parse(await fs.readFile(inventoryPath, 'utf8')) as { + packages?: Array<{ + licenseFiles?: Array<{ path?: unknown; sha256?: unknown }> + noticeFiles?: Array<{ path?: unknown; sha256?: unknown }> + }> + obligations?: unknown[] + } + const noticeDocuments = (notice.packages ?? []) + .flatMap(record => [ + ...(record.licenseFiles ?? []), + ...(record.noticeFiles ?? []), + ]) + .filter( + (record): record is { path: string; sha256: string } => + typeof record.path === 'string' && typeof record.sha256 === 'string', + ) + .map(record => ({ path: record.path, sha256: record.sha256 })) + .sort((left, right) => left.path.localeCompare(right.path)) + return { + packageName: metadata.name, + version: metadata.version, + license: metadata.license, + integrity: sha256(Buffer.from(JSON.stringify(entries))), + fileCount: entries.length, + noticePackageCount: notice.packages?.length ?? 0, + obligationCount: notice.obligations?.length ?? 0, + noticeInventorySha256: await sha256(await fs.readFile(inventoryPath)), + noticeDocuments, + } +} diff --git a/src/sea/stage-uv.mts b/src/sea/stage-uv.mts new file mode 100644 index 0000000000..6a0eb9c274 --- /dev/null +++ b/src/sea/stage-uv.mts @@ -0,0 +1,559 @@ +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +/* eslint-disable no-await-in-loop -- Package staging shares a mutable VFS tree. */ +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { gunzipSync, inflateRawSync } from 'node:zlib' + +import pins from './uv-pins.json' with { type: 'json' } + +const UV_VERSION = pins.version +const MAX_ARCHIVE_BYTES = 40 * 1024 * 1024 +const MAX_EXPANDED_BYTES = 128 * 1024 * 1024 + +type Platform = 'darwin' | 'linux' | 'win32' +type Architecture = 'arm64' | 'x64' +type Target = `${Platform}-${Architecture}` + +type UvTargetPin = { + archive: string + sha256: string + executable: string + binary: string +} + +type UvPins = { + package: string + version: string + license: string + repository: string + releaseBaseUrl: string + licenseBaseUrl: string + licenseFiles: Array<{ name: string; sha256: string }> + targets: Record +} + +const uvPins = pins as UvPins + +export type StagedUv = { + packageName: string + version: string + target: Target + license: string + archive: string + sourceUrl: string + declaredArchiveSha256: string + binaryPath: string + binarySha256: string + binarySize: number + licenseFiles: Array<{ path: string; sha256: string; size: number }> + noticeInventoryPath: string + noticeInventorySha256: string + noticeDocuments: Array<{ path: string; sha256: string }> + noticeObligations: string[] +} + +function sha256(bytes: Uint8Array): string { + return createHash('sha256').update(bytes).digest('hex') +} + +function unsupportedTarget(target: string): Error { + return new Error( + `Bundled uv is unavailable for ${target}; supported targets are ${Object.keys(uvPins.targets).join(', ')}.`, + ) +} + +function targetFor(platform: string, arch: string): Target { + const target = `${platform}-${arch}` + if (!Object.hasOwn(uvPins.targets, target)) { + throw unsupportedTarget(target) + } + return target as Target +} + +async function fetchPinnedBytes(url: string, limit: number): Promise { + const response = await fetch(url, { + redirect: 'follow', + signal: AbortSignal.timeout(120_000), + }) + if (response.status !== 200) { + throw new Error( + `Could not download pinned uv input ${url}: HTTP ${response.status}`, + ) + } + const declaredLength = Number(response.headers.get('content-length')) + if (Number.isFinite(declaredLength) && declaredLength > limit) { + throw new Error(`Pinned uv input exceeds the ${limit}-byte limit: ${url}`) + } + if (!response.body) { + throw new Error(`Pinned uv input has no response body: ${url}`) + } + const reader = response.body.getReader() + const chunks: Buffer[] = [] + let total = 0 + try { + for (;;) { + const { done, value } = await reader.read() + if (done) { + break + } + total += value.byteLength + if (total > limit) { + await reader.cancel() + throw new Error( + `Pinned uv input exceeds the ${limit}-byte limit: ${url}`, + ) + } + chunks.push(Buffer.from(value)) + } + } finally { + reader.releaseLock() + } + return Buffer.concat(chunks, total) +} + +function validateTarPath(name: string, expected: string): void { + const normalized = name.replace(/\\/g, '/') + if ( + normalized.startsWith('/') || + /^[a-zA-Z]:/.test(normalized) || + normalized.split('/').some(part => part === '..') + ) { + throw new Error(`uv archive contains an unsafe path: ${name}`) + } + if ( + normalized !== expected && + normalized !== `${expected.slice(0, expected.lastIndexOf('/') + 1)}` && + normalized !== `${expected.slice(0, expected.lastIndexOf('/'))}/uvx` + ) { + throw new Error(`uv archive contains an unexpected entry: ${name}`) + } +} + +function readTarExecutable(archive: Buffer, expected: string): Buffer { + let decompressed: Buffer + try { + decompressed = gunzipSync(archive, { maxOutputLength: MAX_EXPANDED_BYTES }) + } catch (error) { + throw new Error('Pinned uv tarball is not a valid bounded gzip archive', { + cause: error, + }) + } + + let offset = 0 + let executable: Buffer | undefined + while (offset + 512 <= decompressed.length) { + const header = decompressed.subarray(offset, offset + 512) + if (header.every(byte => byte === 0)) { + break + } + + const storedChecksum = Number.parseInt( + header.toString('ascii', 148, 156).trim(), + 8, + ) + let actualChecksum = 0 + for (let i = 0; i < 512; i += 1) { + actualChecksum += i >= 148 && i < 156 ? 32 : header[i]! + } + if (!Number.isFinite(storedChecksum) || storedChecksum !== actualChecksum) { + throw new Error( + 'Pinned uv tarball contains an invalid tar header checksum', + ) + } + + const prefix = header.toString('utf8', 345, 500).replace(/\0.*$/, '') + const base = header.toString('utf8', 0, 100).replace(/\0.*$/, '') + const name = prefix ? `${prefix}/${base}` : base + validateTarPath(name, expected) + const type = String.fromCharCode(header[156] ?? 0) + const size = Number.parseInt(header.toString('ascii', 124, 136).trim(), 8) + if (!Number.isSafeInteger(size) || size < 0 || size > MAX_EXPANDED_BYTES) { + throw new Error(`uv archive entry has an invalid size: ${name}`) + } + const contentStart = offset + 512 + const contentEnd = contentStart + size + if (contentEnd > decompressed.length) { + throw new Error(`uv archive entry is truncated: ${name}`) + } + if (name === expected) { + if (executable || (type !== '0' && type !== '\0')) { + throw new Error( + 'uv archive executable is duplicated or is not a regular file', + ) + } + executable = Buffer.from(decompressed.subarray(contentStart, contentEnd)) + } else if ( + name !== `${expected.slice(0, expected.lastIndexOf('/'))}/uvx` && + type !== '5' + ) { + throw new Error(`uv archive contains an unsupported entry type: ${name}`) + } else if ( + name === `${expected.slice(0, expected.lastIndexOf('/'))}/uvx` && + type !== '0' && + type !== '\0' + ) { + throw new Error(`uv archive uvx entry is not a regular file: ${name}`) + } + offset = contentStart + Math.ceil(size / 512) * 512 + } + if (!executable?.length) { + throw new Error( + `Pinned uv tarball is missing its expected executable ${expected}`, + ) + } + return executable +} + +const crcTable = (() => { + const table = new Uint32Array(256) + for (let i = 0; i < table.length; i += 1) { + let value = i + for (let bit = 0; bit < 8; bit += 1) { + value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1 + } + table[i] = value >>> 0 + } + return table +})() + +function crc32(bytes: Uint8Array): number { + let crc = 0xffffffff + for (const byte of bytes) { + crc = crcTable[(crc ^ byte) & 0xff]! ^ (crc >>> 8) + } + return (crc ^ 0xffffffff) >>> 0 +} + +function readZipExecutable(archive: Buffer, expected: string): Buffer { + const eocdSignature = 0x06054b50 + let eocdOffset = -1 + const minimumOffset = Math.max(0, archive.length - 65_557) + for (let offset = archive.length - 22; offset >= minimumOffset; offset -= 1) { + if (archive.readUInt32LE(offset) === eocdSignature) { + eocdOffset = offset + break + } + } + if (eocdOffset < 0) { + throw new Error('Pinned uv ZIP has no end-of-directory record') + } + const disk = archive.readUInt16LE(eocdOffset + 4) + const centralDisk = archive.readUInt16LE(eocdOffset + 6) + const diskEntries = archive.readUInt16LE(eocdOffset + 8) + const entries = archive.readUInt16LE(eocdOffset + 10) + const centralSize = archive.readUInt32LE(eocdOffset + 12) + const centralOffset = archive.readUInt32LE(eocdOffset + 16) + const commentLength = archive.readUInt16LE(eocdOffset + 20) + if ( + disk !== 0 || + centralDisk !== 0 || + diskEntries !== entries || + entries === 0xffff || + centralSize === 0xffffffff || + centralOffset === 0xffffffff || + eocdOffset + 22 + commentLength !== archive.length || + centralOffset + centralSize > eocdOffset + ) { + throw new Error('Pinned uv ZIP uses an unsupported or malformed ZIP layout') + } + + let cursor = centralOffset + let executable: Buffer | undefined + for (let index = 0; index < entries; index += 1) { + if ( + cursor + 46 > archive.length || + archive.readUInt32LE(cursor) !== 0x02014b50 + ) { + throw new Error('Pinned uv ZIP has a malformed central directory') + } + const flags = archive.readUInt16LE(cursor + 8) + const method = archive.readUInt16LE(cursor + 10) + const expectedCrc = archive.readUInt32LE(cursor + 16) + const compressedSize = archive.readUInt32LE(cursor + 20) + const uncompressedSize = archive.readUInt32LE(cursor + 24) + const nameLength = archive.readUInt16LE(cursor + 28) + const extraLength = archive.readUInt16LE(cursor + 30) + const entryCommentLength = archive.readUInt16LE(cursor + 32) + const diskStart = archive.readUInt16LE(cursor + 34) + const externalAttributes = archive.readUInt32LE(cursor + 38) + const localOffset = archive.readUInt32LE(cursor + 42) + const nameStart = cursor + 46 + const name = archive.toString('utf8', nameStart, nameStart + nameLength) + if ( + !name || + name.startsWith('/') || + /^[a-zA-Z]:/.test(name) || + name + .replace(/\\/g, '/') + .split('/') + .some(part => part === '..') + ) { + throw new Error(`uv ZIP contains an unsafe path: ${name}`) + } + if (name !== expected && name !== 'uvw.exe' && name !== 'uvx.exe') { + throw new Error(`uv ZIP contains an unexpected entry: ${name}`) + } + if ( + (flags & 1) !== 0 || + (method !== 0 && method !== 8) || + diskStart !== 0 || + compressedSize === 0xffffffff || + uncompressedSize === 0xffffffff || + uncompressedSize > MAX_EXPANDED_BYTES + ) { + throw new Error(`uv ZIP entry uses an unsupported encoding: ${name}`) + } + const unixMode = externalAttributes >>> 16 + if ((unixMode & 0xf000) === 0xa000) { + throw new Error(`uv ZIP contains a symbolic link: ${name}`) + } + if ((unixMode & 0xf000) !== 0 && (unixMode & 0xf000) !== 0x8000) { + throw new Error(`uv ZIP contains a non-regular file: ${name}`) + } + if (name === expected) { + if (executable) { + throw new Error('uv ZIP executable is duplicated') + } + if ( + localOffset + 30 > archive.length || + archive.readUInt32LE(localOffset) !== 0x04034b50 + ) { + throw new Error('uv ZIP executable has a malformed local header') + } + const localNameLength = archive.readUInt16LE(localOffset + 26) + const localExtraLength = archive.readUInt16LE(localOffset + 28) + const localFlags = archive.readUInt16LE(localOffset + 6) + const localMethod = archive.readUInt16LE(localOffset + 8) + const localName = archive.toString( + 'utf8', + localOffset + 30, + localOffset + 30 + localNameLength, + ) + if ( + localName !== name || + localFlags !== flags || + localMethod !== method + ) { + throw new Error('uv ZIP local and central entry headers differ') + } + const dataStart = localOffset + 30 + localNameLength + localExtraLength + const dataEnd = dataStart + compressedSize + if (dataEnd > centralOffset) { + throw new Error('uv ZIP executable data is out of bounds') + } + const compressed = archive.subarray(dataStart, dataEnd) + try { + executable = + method === 0 + ? Buffer.from(compressed) + : inflateRawSync(compressed, { + maxOutputLength: MAX_EXPANDED_BYTES, + }) + } catch (error) { + throw new Error('uv ZIP executable could not be decompressed safely', { + cause: error, + }) + } + if ( + executable.length !== uncompressedSize || + crc32(executable) !== expectedCrc + ) { + throw new Error( + 'uv ZIP executable size or CRC does not match its directory record', + ) + } + } + cursor = nameStart + nameLength + extraLength + entryCommentLength + } + if (cursor !== centralOffset + centralSize) { + throw new Error('Pinned uv ZIP central directory size does not match') + } + if (!executable?.length) { + throw new Error(`Pinned uv ZIP is missing ${expected}`) + } + return executable +} + +function validateBinary(bytes: Buffer, target: Target): void { + const [platform, arch] = target.split('-') as [Platform, Architecture] + let valid = false + if ( + platform === 'win32' && + bytes.length >= 64 && + bytes.toString('ascii', 0, 2) === 'MZ' + ) { + const peOffset = bytes.readUInt32LE(0x3c) + valid = + peOffset + 6 <= bytes.length && + bytes.toString('ascii', peOffset, peOffset + 4) === 'PE\0\0' && + ((arch === 'arm64' && bytes.readUInt16LE(peOffset + 4) === 0xaa64) || + (arch === 'x64' && bytes.readUInt16LE(peOffset + 4) === 0x8664)) + } else if ( + platform === 'linux' && + bytes.length >= 20 && + bytes[0] === 0x7f && + bytes.toString('ascii', 1, 4) === 'ELF' + ) { + const machine = bytes.readUInt16LE(18) + valid = + (arch === 'arm64' && machine === 183) || + (arch === 'x64' && machine === 62) + } else if (platform === 'darwin' && bytes.length >= 8) { + const magic = bytes.readUInt32LE(0) + const cpu = bytes.readUInt32LE(4) + valid = + (magic === 0xfeedfacf && + ((arch === 'arm64' && cpu === 0x0100000c) || + (arch === 'x64' && cpu === 0x01000007))) || + (magic === 0xcffaedfe && + ((arch === 'arm64' && bytes.readUInt32BE(4) === 0x0100000c) || + (arch === 'x64' && bytes.readUInt32BE(4) === 0x01000007))) + } + if (!valid) { + throw new Error(`Downloaded uv executable does not match target ${target}`) + } +} + +function validateUvVersion(filename: string): void { + let output: string + try { + output = execFileSync(filename, ['--version'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 15_000, + windowsHide: true, + env: { ...process.env, UV_NO_UPDATE_CHECK: '1' }, + }) + } catch (error) { + throw new Error( + `Pinned uv ${UV_VERSION} failed its build-time version probe`, + { cause: error }, + ) + } + const [command, version] = output.trim().split(/\s+/) + if (command !== 'uv' || version !== UV_VERSION) { + throw new Error( + `Pinned uv version mismatch: expected ${UV_VERSION}, received ${output.trim()}`, + ) + } +} + +/** + * Fetch and stage the exact upstream uv executable for one native SEA target. + * Downloads happen only during the build. The runtime receives the extracted, + * verified executable and does not acquire uv or Python. + */ +export async function stageUv( + stageRoot: string, + platform: string = process.platform, + arch: string = process.arch, +): Promise { + if (platform !== process.platform || arch !== process.arch) { + throw new Error( + `uv staging requires a native build host (${process.platform}-${process.arch}); requested ${platform}-${arch}.`, + ) + } + const target = targetFor(platform, arch) + const pin = uvPins.targets[target] + const sourceUrl = `${uvPins.releaseBaseUrl}/${pin.archive}` + const archive = await fetchPinnedBytes(sourceUrl, MAX_ARCHIVE_BYTES) + const actualArchiveSha256 = sha256(archive) + if (actualArchiveSha256 !== pin.sha256) { + throw new Error( + `uv ${UV_VERSION} archive checksum mismatch for ${target}: expected ${pin.sha256}, got ${actualArchiveSha256}`, + ) + } + + const executable = pin.archive.endsWith('.zip') + ? readZipExecutable(archive, pin.executable) + : readTarExecutable(archive, pin.executable) + validateBinary(executable, target) + + const payloadRoot = path.join(stageRoot, 'tools/uv') + await fs.rm(payloadRoot, { recursive: true, force: true }) + await fs.mkdir(payloadRoot, { recursive: true }) + const binaryPath = path.join(payloadRoot, pin.binary) + await fs.writeFile(binaryPath, executable, { + flag: 'wx', + mode: 0o755, + }) + if (platform !== 'win32') { + await fs.chmod(binaryPath, 0o755) + } + validateUvVersion(binaryPath) + const binaryDigest = sha256(executable) + await fs.writeFile(path.join(payloadRoot, 'uv.sha256'), `${binaryDigest}\n`, { + flag: 'wx', + }) + + const licenseFiles: StagedUv['licenseFiles'] = [] + for (const license of uvPins.licenseFiles) { + const sourceUrl = `${uvPins.licenseBaseUrl}/${license.name}` + const bytes = await fetchPinnedBytes(sourceUrl, 256 * 1024) + const digest = sha256(bytes) + if (digest !== license.sha256) { + throw new Error( + `uv ${UV_VERSION} license checksum mismatch for ${license.name}: expected ${license.sha256}, got ${digest}`, + ) + } + const destination = path.join(payloadRoot, license.name) + await fs.writeFile(destination, bytes, { flag: 'wx' }) + licenseFiles.push({ + path: path.relative(stageRoot, destination).split(path.sep).join('/'), + sha256: digest, + size: bytes.length, + }) + } + + // uv is dual-licensed under MIT OR Apache-2.0. Its release archive does not + // ship a separate transitive-crate notice file. Record that limitation as a + // review obligation instead of silently claiming a complete notice set. + const noticeObligations = [ + 'The uv release archive has no generated transitive-crate license inventory; review Cargo dependency license notices before qualifying redistribution.', + ] + const noticeDocuments = licenseFiles.map(({ path, sha256 }) => ({ + path, + sha256, + })) + const noticeInventory = { + schemaVersion: 1, + package: uvPins.package, + version: UV_VERSION, + license: uvPins.license, + source: uvPins.repository, + sourceTag: UV_VERSION, + target, + archive: pin.archive, + archiveUrl: sourceUrl, + archiveSha256: actualArchiveSha256, + licenseDocuments: noticeDocuments, + obligations: noticeObligations, + } + const noticeInventoryPath = path.join(payloadRoot, 'THIRD-PARTY-NOTICES.json') + const noticeInventoryBytes = Buffer.from( + `${JSON.stringify(noticeInventory, null, 2)}\n`, + ) + await fs.writeFile(noticeInventoryPath, noticeInventoryBytes, { flag: 'wx' }) + + return { + packageName: uvPins.package, + version: UV_VERSION, + target, + license: uvPins.license, + archive: pin.archive, + sourceUrl, + declaredArchiveSha256: pin.sha256, + binaryPath: path.relative(stageRoot, binaryPath).split(path.sep).join('/'), + binarySha256: binaryDigest, + binarySize: executable.length, + licenseFiles, + noticeInventoryPath: path + .relative(stageRoot, noticeInventoryPath) + .split(path.sep) + .join('/'), + noticeInventorySha256: sha256(noticeInventoryBytes), + noticeDocuments, + noticeObligations, + } +} diff --git a/src/sea/uv-pins.json b/src/sea/uv-pins.json new file mode 100644 index 0000000000..eb7ae82473 --- /dev/null +++ b/src/sea/uv-pins.json @@ -0,0 +1,56 @@ +{ + "package": "uv", + "version": "0.12.15", + "license": "MIT OR Apache-2.0", + "repository": "https://github.com/astral-sh/uv", + "releaseBaseUrl": "https://github.com/astral-sh/uv/releases/download/0.12.15", + "licenseBaseUrl": "https://raw.githubusercontent.com/astral-sh/uv/0.12.15", + "licenseFiles": [ + { + "name": "LICENSE-MIT", + "sha256": "860e3d7a86b84e6a7012c7a635fc64df475cebc6cce34dfeb73a5982ec58176c" + }, + { + "name": "LICENSE-APACHE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ], + "targets": { + "darwin-arm64": { + "archive": "uv-aarch64-apple-darwin.tar.gz", + "sha256": "dc304b9ed1b24174572290fba60ac3f6fe63c73a671f0439e62a91375841964d", + "executable": "uv-aarch64-apple-darwin/uv", + "binary": "uv" + }, + "darwin-x64": { + "archive": "uv-x86_64-apple-darwin.tar.gz", + "sha256": "e9ca61775532368fe518ab03e7a354c7ecab8ccb3c7d941c775fcc4a362b801b", + "executable": "uv-x86_64-apple-darwin/uv", + "binary": "uv" + }, + "linux-arm64": { + "archive": "uv-aarch64-unknown-linux-gnu.tar.gz", + "sha256": "0e9a3499b0587d449c9ff684c0160da607826e4af1cee220bc87f378702d3e08", + "executable": "uv-aarch64-unknown-linux-gnu/uv", + "binary": "uv" + }, + "linux-x64": { + "archive": "uv-x86_64-unknown-linux-gnu.tar.gz", + "sha256": "f97935763c04be3e692460a7aaeaaab8fc3b78fcf8b389da820b38ae7423a638", + "executable": "uv-x86_64-unknown-linux-gnu/uv", + "binary": "uv" + }, + "win32-arm64": { + "archive": "uv-aarch64-pc-windows-msvc.zip", + "sha256": "a37c8e96cb1260488c8510b64c848533a3a82a2fdf9e905de7c2700ceebf6437", + "executable": "uv.exe", + "binary": "uv.exe" + }, + "win32-x64": { + "archive": "uv-x86_64-pc-windows-msvc.zip", + "sha256": "477bd99a84e34891f2bd4c9152ddeb74e971accccbc59c0f0301f11f08a32d46", + "executable": "uv.exe", + "binary": "uv.exe" + } + } +} diff --git a/src/shadow/common.mts b/src/shadow/common.mts index 2de423cfd6..3038348ea2 100644 --- a/src/shadow/common.mts +++ b/src/shadow/common.mts @@ -1,18 +1,16 @@ import { fileURLToPath } from 'node:url' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { readPackageJson } from '@socketsecurity/registry/lib/packages' - import constants, { FLAG_DRY_RUN, PACKAGE_JSON } from '../constants.mts' import { getAlertsMapFromPurls } from '../utils/alerts-map.mts' import { isAddCommand } from '../utils/cmd.mts' -import { debugScan } from '../utils/debug.mts' +import { debugDir, debugFn, debugScan } from '../utils/debug.mts' +import { logger } from '../utils/logger.mts' import { safeNpmSpecToPurl } from '../utils/npm-spec.mts' +import { readPackageJson } from '../utils/packages.mts' import { logAlertsMap } from '../utils/socket-package-alert.mts' import type { AlertsByPurl } from '../utils/socket-package-alert.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { Spinner } from '../utils/spinner.mts' function extractPackagePurlsFromCommandArgs( rawArgs: string[] | readonly string[], @@ -105,6 +103,9 @@ async function extractPackagePurlsFromPackageJson( try { const pkgJson = await readPackageJson(cwd) + if (!pkgJson) { + return packagePurls + } const allDeps = { ...pkgJson.dependencies, diff --git a/src/shadow/npm-base.mts b/src/shadow/npm-base.mts index 0a9b9ac9a9..0b836e2a58 100644 --- a/src/shadow/npm-base.mts +++ b/src/shadow/npm-base.mts @@ -1,15 +1,5 @@ import { fileURLToPath } from 'node:url' -import { - isNpmAuditFlag, - isNpmLoglevelFlag, - isNpmNodeOptionsFlag, - isNpmProgressFlag, -} from '@socketsecurity/registry/lib/agent' -import { isDebug } from '@socketsecurity/registry/lib/debug' -import { getOwn } from '@socketsecurity/registry/lib/objects' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { ensureIpcInStdio } from './stdio-ipc.mts' import constants, { FLAG_LOGLEVEL, @@ -17,17 +7,22 @@ import constants, { NPM, NPX, } from '../constants.mts' +import { + isNpmAuditFlag, + isNpmLoglevelFlag, + isNpmNodeOptionsFlag, + isNpmProgressFlag, +} from '../utils/agent.mts' import { cmdFlagsToString } from '../utils/cmd.mts' +import { isDebug } from '../utils/debug.mts' import { findUp } from '../utils/fs.mts' +import { getOwn } from '../utils/objects.mts' import { getPublicApiToken } from '../utils/sdk.mts' import { installNpmLinks, installNpxLinks } from '../utils/shadow-links.mts' +import { spawn } from '../utils/spawn.mts' import type { IpcObject } from '../constants.mts' -import type { - SpawnExtra, - SpawnOptions, - SpawnResult, -} from '@socketsecurity/registry/lib/spawn' +import type { SpawnExtra, SpawnOptions, SpawnResult } from '../utils/spawn.mts' export type ShadowBinOptions = SpawnOptions & { ipc?: IpcObject | undefined diff --git a/src/shadow/npm/arborist-helpers.mts b/src/shadow/npm/arborist-helpers.mts index 0b8ce73628..2695947a48 100644 --- a/src/shadow/npm/arborist-helpers.mts +++ b/src/shadow/npm/arborist-helpers.mts @@ -1,19 +1,18 @@ -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { getOwn } from '@socketsecurity/registry/lib/objects' -import { parseUrl } from '@socketsecurity/registry/lib/url' - import constants from '../../constants.mts' import { DiffAction } from './arborist/types.mts' import { getAlertsMapFromPurls } from '../../utils/alerts-map.mts' +import { debugFn } from '../../utils/debug.mts' import { toFilterConfig } from '../../utils/filter-config.mts' +import { getOwn } from '../../utils/objects.mts' import { idToNpmPurl } from '../../utils/spec.mts' +import { parseUrl } from '../../utils/url.mts' import type { ArboristInstance, Diff, NodeClass } from './arborist/types.mts' import type { AlertFilter, AlertsByPurl, } from '../../utils/socket-package-alert.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { Spinner } from '../../utils/spinner.mts' function getUrlOrigin(input: string): string { // TODO: URL.parse is available in Node 22.1.0. We can use it when we drop Node 18. diff --git a/src/shadow/npm/arborist/lib/arborist/index.mts b/src/shadow/npm/arborist/lib/arborist/index.mts index 0b2aab09c5..3f046e5bfa 100755 --- a/src/shadow/npm/arborist/lib/arborist/index.mts +++ b/src/shadow/npm/arborist/lib/arborist/index.mts @@ -1,10 +1,9 @@ // @ts-ignore import UntypedArborist from '@npmcli/arborist/lib/arborist/index.js' -import { logger } from '@socketsecurity/registry/lib/logger' - import constants, { NODE_MODULES, NPX } from '../../../../../constants.mts' import { findUp } from '../../../../../utils/fs.mts' +import { logger } from '../../../../../utils/logger.mts' import { logAlertsMap } from '../../../../../utils/socket-package-alert.mts' import { getAlertsMapFromArborist, diff --git a/src/shadow/npm/bin.mts b/src/shadow/npm/bin.mts index e4ae3f635f..cc1eda8722 100755 --- a/src/shadow/npm/bin.mts +++ b/src/shadow/npm/bin.mts @@ -1,8 +1,8 @@ import { NPM } from '../../constants.mts' import shadowNpmBase from '../npm-base.mts' +import type { SpawnExtra } from '../../utils/spawn.mts' import type { ShadowBinOptions, ShadowBinResult } from '../npm-base.mts' -import type { SpawnExtra } from '@socketsecurity/registry/lib/spawn' export type { ShadowBinOptions, ShadowBinResult } diff --git a/src/shadow/npm/bin.test.mts b/src/shadow/npm/bin.test.mts index e544c0d2cf..19ac93a07f 100644 --- a/src/shadow/npm/bin.test.mts +++ b/src/shadow/npm/bin.test.mts @@ -11,35 +11,44 @@ vi.mock('../../utils/shadow-links.mts', () => ({ installNpmLinks: mockInstallNpmLinks, })) -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ +vi.mock('../../utils/spawn.mts', () => ({ spawn: mockSpawn, })) -vi.mock('../../utils/fs.mts', () => ({ +vi.mock('../../utils/fs.mts', async importOriginal => ({ + ...(await importOriginal()), findUp: mockFindUp, })) vi.mock('../../constants.mts', async importOriginal => { const actual = (await importOriginal()) as Record + const mockDefault = { + ...actual?.default, + shadowBinPath: '/mock/shadow-bin', + shadowNpmInjectPath: '/mock/inject.js', + execPath: '/usr/bin/node', + npmGlobalPrefix: '/usr/local', + npmCachePath: '/home/.npm', + nodeNoWarningsFlags: [], + nodeDebugFlags: [], + nodeHardenFlags: [], + nodeMemoryFlags: [], + processEnv: {}, + ENV: { INLINED_SOCKET_CLI_SENTRY_BUILD: false }, + SUPPORTS_NODE_PERMISSION_FLAG: false, + } + const internalKey = actual?.default?.kInternalsSymbol + if (internalKey) { + Object.defineProperty(mockDefault, 'kInternalsSymbol', { + value: internalKey, + }) + Object.defineProperty(mockDefault, internalKey, { + value: actual.default[internalKey], + }) + } return { ...actual, - default: { - ...actual?.default, - shadowBinPath: '/mock/shadow-bin', - shadowNpmInjectPath: '/mock/inject.js', - execPath: '/usr/bin/node', - npmGlobalPrefix: '/usr/local', - npmCachePath: '/home/.npm', - nodeNoWarningsFlags: [], - nodeDebugFlags: [], - nodeHardenFlags: [], - nodeMemoryFlags: [], - processEnv: {}, - ENV: { - INLINED_SOCKET_CLI_SENTRY_BUILD: false, - }, - SUPPORTS_NODE_PERMISSION_FLAG: false, - }, + default: mockDefault, } }) diff --git a/src/shadow/npm/install.mts b/src/shadow/npm/install.mts index 728ccd3ea0..eed5b9319e 100644 --- a/src/shadow/npm/install.mts +++ b/src/shadow/npm/install.mts @@ -1,19 +1,18 @@ +import constants, { FLAG_LOGLEVEL, NPM } from '../../constants.mts' import { isNpmAuditFlag, isNpmFundFlag, isNpmLoglevelFlag, isNpmProgressFlag, - resolveBinPathSync, -} from '@socketsecurity/registry/lib/agent' -import { isDebug } from '@socketsecurity/registry/lib/debug' -import { getOwn, isObject } from '@socketsecurity/registry/lib/objects' -import { spawn } from '@socketsecurity/registry/lib/spawn' - -import constants, { FLAG_LOGLEVEL, NPM } from '../../constants.mts' +} from '../../utils/agent.mts' +import { resolveBinPathSync } from '../../utils/bin.mts' +import { isDebug } from '../../utils/debug.mts' import { getNpmBinPath } from '../../utils/npm-paths.mts' +import { getOwn, isObject } from '../../utils/objects.mts' +import { spawn } from '../../utils/spawn.mts' -import type { SpawnResult } from '@socketsecurity/registry/lib/spawn' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { SpawnResult } from '../../utils/spawn.mts' +import type { Spinner } from '../../utils/spinner.mts' type SpawnOption = Exclude[2], undefined> diff --git a/src/shadow/npm/paths.mts b/src/shadow/npm/paths.mts index 59b46d440e..530a096a20 100644 --- a/src/shadow/npm/paths.mts +++ b/src/shadow/npm/paths.mts @@ -1,9 +1,8 @@ import path from 'node:path' -import { normalizePath } from '@socketsecurity/registry/lib/path' - import constants from '../../constants.mts' import { getNpmRequire } from '../../utils/npm-paths.mts' +import { normalizePath } from '../../utils/path.mts' let _arboristPkgPath: string | undefined export function getArboristPackagePath() { diff --git a/src/shadow/npx/bin.mts b/src/shadow/npx/bin.mts index 9015c611e5..b7e3af601d 100644 --- a/src/shadow/npx/bin.mts +++ b/src/shadow/npx/bin.mts @@ -1,8 +1,8 @@ import { NPX } from '../../constants.mts' import shadowNpmBase from '../npm-base.mts' +import type { SpawnExtra } from '../../utils/spawn.mts' import type { ShadowBinOptions, ShadowBinResult } from '../npm-base.mts' -import type { SpawnExtra } from '@socketsecurity/registry/lib/spawn' export type { ShadowBinOptions, ShadowBinResult } diff --git a/src/shadow/npx/bin.test.mts b/src/shadow/npx/bin.test.mts index 08b0ad88bc..d89b09315d 100644 --- a/src/shadow/npx/bin.test.mts +++ b/src/shadow/npx/bin.test.mts @@ -11,35 +11,44 @@ vi.mock('../../utils/shadow-links.mts', () => ({ installNpxLinks: mockInstallNpxLinks, })) -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ +vi.mock('../../utils/spawn.mts', () => ({ spawn: mockSpawn, })) -vi.mock('../../utils/fs.mts', () => ({ +vi.mock('../../utils/fs.mts', async importOriginal => ({ + ...(await importOriginal()), findUp: mockFindUp, })) vi.mock('../../constants.mts', async importOriginal => { const actual = (await importOriginal()) as Record + const mockDefault = { + ...actual?.default, + shadowBinPath: '/mock/shadow-bin', + shadowNpmInjectPath: '/mock/inject.js', + execPath: '/usr/bin/node', + npmGlobalPrefix: '/usr/local', + npmCachePath: '/usr/local/.npm', + nodeNoWarningsFlags: [], + nodeDebugFlags: [], + nodeHardenFlags: [], + nodeMemoryFlags: [], + processEnv: {}, + ENV: { INLINED_SOCKET_CLI_SENTRY_BUILD: false }, + SUPPORTS_NODE_PERMISSION_FLAG: false, + } + const internalKey = actual?.default?.kInternalsSymbol + if (internalKey) { + Object.defineProperty(mockDefault, 'kInternalsSymbol', { + value: internalKey, + }) + Object.defineProperty(mockDefault, internalKey, { + value: actual.default[internalKey], + }) + } return { ...actual, - default: { - ...actual?.default, - shadowBinPath: '/mock/shadow-bin', - shadowNpmInjectPath: '/mock/inject.js', - execPath: '/usr/bin/node', - npmGlobalPrefix: '/usr/local', - npmCachePath: '/usr/local/.npm', - nodeNoWarningsFlags: [], - nodeDebugFlags: [], - nodeHardenFlags: [], - nodeMemoryFlags: [], - processEnv: {}, - ENV: { - INLINED_SOCKET_CLI_SENTRY_BUILD: false, - }, - SUPPORTS_NODE_PERMISSION_FLAG: false, - }, + default: mockDefault, } }) diff --git a/src/shadow/pnpm/bin.mts b/src/shadow/pnpm/bin.mts index ffae846f5f..c06d63fbfc 100644 --- a/src/shadow/pnpm/bin.mts +++ b/src/shadow/pnpm/bin.mts @@ -2,10 +2,6 @@ import { existsSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants, { FLAG_DRY_RUN, PNPM, @@ -17,20 +13,23 @@ import { isAddCommand, isPnpmLockfileScanCommand, } from '../../utils/cmd.mts' +import { debugDir, debugFn } from '../../utils/debug.mts' +import { logger } from '../../utils/logger.mts' import { parsePnpmLockfile, readPnpmLockfile } from '../../utils/pnpm.mts' import { getPublicApiToken } from '../../utils/sdk.mts' import { installPnpmLinks } from '../../utils/shadow-links.mts' import { logAlertsMap } from '../../utils/socket-package-alert.mts' +import { spawn } from '../../utils/spawn.mts' import { scanPackagesAndLogAlerts } from '../common.mts' import { ensureIpcInStdio } from '../stdio-ipc.mts' import type { IpcObject } from '../../constants.mts' -import type { DlxFlags } from '../common.mts' import type { SpawnExtra, SpawnOptions, SpawnResult, -} from '@socketsecurity/registry/lib/spawn' +} from '../../utils/spawn.mts' +import type { DlxFlags } from '../common.mts' export type ShadowPnpmOptions = SpawnOptions & { ipc?: IpcObject | undefined diff --git a/src/shadow/pnpm/bin.test.mts b/src/shadow/pnpm/bin.test.mts index 6bc70dc087..e07ec2d96c 100644 --- a/src/shadow/pnpm/bin.test.mts +++ b/src/shadow/pnpm/bin.test.mts @@ -45,32 +45,42 @@ vi.mock('../../utils/shadow-links.mts', () => ({ installPnpmLinks: mockInstallPnpmLinks, })) -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ +vi.mock('../../utils/spawn.mts', () => ({ spawn: mockSpawn, })) vi.mock('../../constants.mts', async importOriginal => { const actual = (await importOriginal()) as Record + const mockDefault = { + ...actual?.default, + shadowBinPath: '/mock/shadow-bin', + ENV: new Proxy( + {}, + { + get(_target, prop) { + if (prop === 'SOCKET_CLI_ACCEPT_RISKS') { + return process.env.SOCKET_CLI_ACCEPT_RISKS || '' + } + if (prop === 'SOCKET_CLI_VIEW_ALL_RISKS') { + return process.env.SOCKET_CLI_VIEW_ALL_RISKS || '' + } + return '' + }, + }, + ), + } + const internalKey = actual?.default?.kInternalsSymbol + if (internalKey) { + Object.defineProperty(mockDefault, 'kInternalsSymbol', { + value: internalKey, + }) + Object.defineProperty(mockDefault, internalKey, { + value: actual.default[internalKey], + }) + } return { ...actual, - default: { - ...actual?.default, - shadowBinPath: '/mock/shadow-bin', - ENV: new Proxy( - {}, - { - get(_target, prop) { - if (prop === 'SOCKET_CLI_ACCEPT_RISKS') { - return process.env.SOCKET_CLI_ACCEPT_RISKS || '' - } - if (prop === 'SOCKET_CLI_VIEW_ALL_RISKS') { - return process.env.SOCKET_CLI_VIEW_ALL_RISKS || '' - } - return '' - }, - }, - ), - }, + default: mockDefault, } }) diff --git a/src/shadow/yarn/bin.mts b/src/shadow/yarn/bin.mts index b0b9b9e2e7..0d6ca56b0b 100644 --- a/src/shadow/yarn/bin.mts +++ b/src/shadow/yarn/bin.mts @@ -1,22 +1,21 @@ import { fileURLToPath } from 'node:url' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants, { YARN } from '../../constants.mts' import { cmdFlagsToString } from '../../utils/cmd.mts' +import { debugFn } from '../../utils/debug.mts' import { getPublicApiToken } from '../../utils/sdk.mts' import { installYarnLinks } from '../../utils/shadow-links.mts' +import { spawn } from '../../utils/spawn.mts' import { scanPackagesAndLogAlerts } from '../common.mts' import { ensureIpcInStdio } from '../stdio-ipc.mts' import type { IpcObject } from '../../constants.mts' -import type { DlxFlags } from '../common.mts' import type { SpawnExtra, SpawnOptions, SpawnResult, -} from '@socketsecurity/registry/lib/spawn' +} from '../../utils/spawn.mts' +import type { DlxFlags } from '../common.mts' export type ShadowYarnOptions = SpawnOptions & { ipc?: IpcObject | undefined diff --git a/src/shadow/yarn/bin.test.mts b/src/shadow/yarn/bin.test.mts index 2473585529..335fd3d5ff 100644 --- a/src/shadow/yarn/bin.test.mts +++ b/src/shadow/yarn/bin.test.mts @@ -8,7 +8,7 @@ import { FLAG_DRY_RUN } from '../../constants.mts' // Mock readPackageJson from registry const mockReadPackageJson = vi.hoisted(() => vi.fn()) -vi.mock('@socketsecurity/registry/lib/packages', async importOriginal => { +vi.mock('../../utils/packages.mts', async importOriginal => { const actual = (await importOriginal()) as Record return { ...actual, @@ -34,32 +34,42 @@ vi.mock('../../utils/shadow-links.mts', () => ({ installYarnLinks: mockInstallYarnLinks, })) -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ +vi.mock('../../utils/spawn.mts', () => ({ spawn: mockSpawn, })) vi.mock('../../constants.mts', async importOriginal => { const actual = (await importOriginal()) as Record + const mockDefault = { + ...actual?.default, + shadowBinPath: '/mock/shadow-bin', + ENV: new Proxy( + {}, + { + get(_target, prop) { + if (prop === 'SOCKET_CLI_ACCEPT_RISKS') { + return process.env.SOCKET_CLI_ACCEPT_RISKS || '' + } + if (prop === 'SOCKET_CLI_VIEW_ALL_RISKS') { + return process.env.SOCKET_CLI_VIEW_ALL_RISKS || '' + } + return '' + }, + }, + ), + } + const internalKey = actual?.default?.kInternalsSymbol + if (internalKey) { + Object.defineProperty(mockDefault, 'kInternalsSymbol', { + value: internalKey, + }) + Object.defineProperty(mockDefault, internalKey, { + value: actual.default[internalKey], + }) + } return { ...actual, - default: { - ...actual?.default, - shadowBinPath: '/mock/shadow-bin', - ENV: new Proxy( - {}, - { - get(_target, prop) { - if (prop === 'SOCKET_CLI_ACCEPT_RISKS') { - return process.env.SOCKET_CLI_ACCEPT_RISKS || '' - } - if (prop === 'SOCKET_CLI_VIEW_ALL_RISKS') { - return process.env.SOCKET_CLI_VIEW_ALL_RISKS || '' - } - return '' - }, - }, - ), - }, + default: mockDefault, } }) diff --git a/src/utils/advisory-ids.mts b/src/utils/advisory-ids.mts index c50425d121..b5ae1f68e0 100644 --- a/src/utils/advisory-ids.mts +++ b/src/utils/advisory-ids.mts @@ -1,11 +1,10 @@ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' - +import { joinAnd } from './arrays.mts' import { CVE_FORMAT_REGEXP, GHSA_FORMAT_REGEXP, convertCveToGhsa, } from './cve-to-ghsa.mts' +import { logger } from './logger.mts' import { convertPurlToGhsas } from './purl-to-ghsa.mts' type ResolveAdvisoryIdsOptions = { diff --git a/src/utils/agent.mts b/src/utils/agent.mts index 5569b4480e..038070f03c 100644 --- a/src/utils/agent.mts +++ b/src/utils/agent.mts @@ -1,92 +1,76 @@ /** - * Package manager agent utilities for Socket CLI. - * Manages package installation via different package managers. - * - * Key Functions: - * - runAgentInstall: Execute package installation with detected agent - * - * Supported Agents: - * - npm: Node Package Manager - * - pnpm: Fast, disk space efficient package manager - * - yarn: Alternative package manager - * - * Features: - * - Automatic agent detection - * - Shadow installation for security scanning - * - Spinner support for progress indication + * Minimal npm argument predicates copied from @socketsecurity/registry 1.1.17 + * lib/agent.js. Copyright (c) 2024 Socket Inc. Licensed under MIT. */ -import { getOwn } from '@socketsecurity/registry/lib/objects' -import { spawn } from '@socketsecurity/registry/lib/spawn' -import { Spinner } from '@socketsecurity/registry/lib/spinner' - import constants, { NPM, PNPM } from '../constants.mts' -import { cmdFlagsToString } from './cmd.mts' -import { shadowNpmInstall } from '../shadow/npm/install.mts' +import { spawn } from './spawn.mts' import type { EnvDetails } from './package-environment.mts' +import type { SpawnExtra, SpawnOptions, SpawnResult } from './spawn.mts' -type SpawnOption = Exclude[2], undefined> - -export type AgentInstallOptions = SpawnOption & { - args?: string[] | readonly string[] | undefined - spinner?: Spinner | undefined -} +const auditFlags = new Set(['--audit', '--no-audit']) +const fundFlags = new Set(['--fund', '--no-fund']) +const logFlags = new Set([ + '--loglevel', + '-d', + '--dd', + '--ddd', + '-q', + '--quiet', + '-s', + '--silent', +]) +const progressFlags = new Set(['--progress', '--no-progress']) -export type AgentSpawnResult = ReturnType +export const isNpmAuditFlag = (arg: string): boolean => auditFlags.has(arg) +export const isNpmFundFlag = (arg: string): boolean => fundFlags.has(arg) +export const isNpmLoglevelFlag = (arg: string): boolean => + arg.startsWith('--loglevel=') || logFlags.has(arg) +export const isNpmNodeOptionsFlag = (arg: string): boolean => + arg.startsWith('--node-options=') +export const isNpmProgressFlag = (arg: string): boolean => + progressFlags.has(arg) +/** Run the detected package manager's install command with Socket's safe defaults. */ export function runAgentInstall( pkgEnvDetails: EnvDetails, - options?: AgentInstallOptions | undefined, -): AgentSpawnResult { + options: SpawnOptions & { args?: string[] | readonly string[] } = {}, +): SpawnResult { const { agent, agentExecPath, pkgPath } = pkgEnvDetails + const { args = [], spinner, ...spawnOptions } = options const isNpm = agent === NPM const isPnpm = agent === PNPM - // All package managers support the "install" command. - if (isNpm) { - return shadowNpmInstall({ - agentExecPath, - cwd: pkgPath, - ...options, - }) - } - const { - args = [], - spinner, - ...spawnOpts - } = { __proto__: null, ...options } as AgentInstallOptions const skipNodeHardenFlags = isPnpm && pkgEnvDetails.agentVersion.major < 11 - // In CI mode, pnpm uses --frozen-lockfile by default, which prevents lockfile updates. - // We need to explicitly disable it when updating the lockfile with overrides. - // Also add --config.confirmModulesPurge=false to avoid interactive prompts. - const installArgs = isPnpm - ? [ - 'install', - '--config.confirmModulesPurge=false', - '--no-frozen-lockfile', - ...args, - ] - : ['install', ...args] - + const installArgs = isNpm + ? ['install', '--no-audit', '--no-fund', ...args] + : isPnpm + ? [ + 'install', + '--config.confirmModulesPurge=false', + '--no-frozen-lockfile', + ...args, + ] + : ['install', ...args] + const nodeOptions = [ + process.env['NODE_OPTIONS'], + ...(skipNodeHardenFlags ? [] : constants.nodeHardenFlags), + ...constants.nodeNoWarningsFlags, + '--disable-sigusr1', + ] + .filter(Boolean) + .join(' ') return spawn(agentExecPath, installArgs, { cwd: pkgPath, - // On Windows, package managers are often .cmd files that require shell execution. - // The spawn function from @socketsecurity/registry will handle this properly - // when shell is true. shell: constants.WIN32, spinner, stdio: 'inherit', - ...spawnOpts, + ...spawnOptions, env: { ...process.env, - ...constants.processEnv, - // Set CI for pnpm to ensure non-interactive mode and consistent behavior. ...(isPnpm ? { CI: '1' } : {}), - NODE_OPTIONS: cmdFlagsToString([ - ...(skipNodeHardenFlags ? [] : constants.nodeHardenFlags), - ...constants.nodeNoWarningsFlags, - ]), - ...getOwn(spawnOpts, 'env'), + NODE_OPTIONS: nodeOptions, + ...spawnOptions.env, }, }) } diff --git a/src/utils/alert/artifact.mts b/src/utils/alert/artifact.mts index 0b4f77ead0..da65c6c22c 100755 --- a/src/utils/alert/artifact.mts +++ b/src/utils/alert/artifact.mts @@ -1,6 +1,6 @@ import constants from '../../constants.mts' -import type { Remap } from '@socketsecurity/registry/lib/objects' +import type { Remap } from '../objects.mts' import type { ALERT_ACTION, ALERT_TYPE, diff --git a/src/utils/alert/severity.mts b/src/utils/alert/severity.mts index 1facfb07a6..9e34e266f8 100644 --- a/src/utils/alert/severity.mts +++ b/src/utils/alert/severity.mts @@ -1,5 +1,4 @@ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' - +import { joinAnd } from '../arrays.mjs' import { createEnum, pick } from '../objects.mts' import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' diff --git a/src/utils/alerts-map.mts b/src/utils/alerts-map.mts index dfa9f229e2..55310e3b6b 100644 --- a/src/utils/alerts-map.mts +++ b/src/utils/alerts-map.mts @@ -18,22 +18,21 @@ * - Respects filter configurations from socket.yml */ -import { arrayUnique } from '@socketsecurity/registry/lib/arrays' -import { debugDir } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { getOwn } from '@socketsecurity/registry/lib/objects' -import { isNonEmptyString } from '@socketsecurity/registry/lib/strings' - +import { arrayUnique } from './arrays.mts' import { findSocketYmlSync } from './config.mts' +import { debugDir } from './debug.mts' import { toFilterConfig } from './filter-config.mts' +import { logger } from './logger.mts' +import { getOwn } from './objects.mts' import { extractPurlsFromPnpmLockfile } from './pnpm.mts' import { setupSdk } from './sdk.mts' import { addArtifactToAlertsMap } from './socket-package-alert.mts' +import { isNonEmptyString } from './strings.mts' import type { CompactSocketArtifact } from './alert/artifact.mts' import type { AlertFilter, AlertsByPurl } from './socket-package-alert.mts' +import type { Spinner } from './spinner.mts' import type { LockfileObject } from '@pnpm/lockfile.fs' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' export type GetAlertsMapFromPnpmLockfileOptions = { apiToken?: string | undefined diff --git a/src/utils/api.mts b/src/utils/api.mts index a27c88e613..13a25e8072 100644 --- a/src/utils/api.mts +++ b/src/utils/api.mts @@ -25,12 +25,22 @@ import { ReadableStream } from 'node:stream/web' import { HttpsProxyAgent } from 'hpagent' import { messageWithCauses } from 'pony-cause' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { isNonEmptyString } from '@socketsecurity/registry/lib/strings' - import { getConfigValueOrUndef } from './config.mts' -import { debugApiRequest, debugApiResponse } from './debug.mts' +import { + debugApiRequest, + debugApiResponse, + debugDir, + debugFn, +} from './debug.mts' +import { logger } from './logger.mts' +import { getRequirements, getRequirementsKey } from './requirements.mts' +import { + getCliUserAgent, + getDefaultApiToken, + getDefaultProxyUrl, + getExtraCaCerts, +} from './sdk.mts' +import { isNonEmptyString } from './strings.mts' import constants, { CONFIG_KEY_API_BASE_URL, EMPTY_VALUE, @@ -40,16 +50,9 @@ import constants, { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_UNAUTHORIZED, } from '../constants.mts' -import { getRequirements, getRequirementsKey } from './requirements.mts' -import { - getCliUserAgent, - getDefaultApiToken, - getDefaultProxyUrl, - getExtraCaCerts, -} from './sdk.mts' import type { CResult } from '../types.mts' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { Spinner } from './spinner.mts' const MAX_REDIRECTS = 20 const NO_ERROR_MESSAGE = 'No error message returned' diff --git a/src/utils/api.test.mts b/src/utils/api.test.mts index 05882e275c..601863ba21 100644 --- a/src/utils/api.test.mts +++ b/src/utils/api.test.mts @@ -104,6 +104,8 @@ vi.mock('./config.mts', () => ({ vi.mock('./debug.mts', () => ({ debugApiRequest: vi.fn(), debugApiResponse: vi.fn(), + debugFn: vi.fn(), + debugDir: vi.fn(), })) // Mock requirements. diff --git a/src/utils/arrays.mts b/src/utils/arrays.mts new file mode 100644 index 0000000000..443a606bb6 --- /dev/null +++ b/src/utils/arrays.mts @@ -0,0 +1,30 @@ +/** + * Narrowly copied from `@socketsecurity/registry` 1.1.17 `lib/arrays.js`. + * Copyright (c) Socket Inc. Licensed under the MIT License. + */ + +let conjunctionFormatter: Intl.ListFormat | undefined +let disjunctionFormatter: Intl.ListFormat | undefined + +/** Join values with the English conjunction "and". */ +export function joinAnd(values: readonly string[]): string { + conjunctionFormatter ??= new Intl.ListFormat('en', { + style: 'long', + type: 'conjunction', + }) + return conjunctionFormatter.format(values) +} + +/** Return an array containing each value once, preserving its first occurrence. */ +export function arrayUnique(values: readonly T[]): T[] { + return [...new Set(values)] +} + +/** Join values with the English disjunction "or". */ +export function joinOr(values: readonly string[]): string { + disjunctionFormatter ??= new Intl.ListFormat('en', { + style: 'long', + type: 'disjunction', + }) + return disjunctionFormatter.format(values) +} diff --git a/src/utils/bin.mts b/src/utils/bin.mts new file mode 100644 index 0000000000..c13b673328 --- /dev/null +++ b/src/utils/bin.mts @@ -0,0 +1,142 @@ +/** Minimal binary lookup/resolution adapted from @socketsecurity/registry 1.1.17 lib/bin.js (MIT). */ + +import { existsSync, readFileSync, realpathSync } from 'node:fs' +import path from 'node:path' + +import which from 'which' + +const normalize = (value: string) => value.replaceAll('\\', '/') + +export function resolveBinPathSync(input: string): string { + const binPath = normalize(input) + const realBinPath = realpathSync.native(binPath) + const ext = path.extname(binPath).toLowerCase() + const basename = path.basename(binPath, path.extname(binPath)) + if (process.platform === 'win32') { + if (!['', '.cmd', '.ps1'].includes(ext)) { + return realBinPath + } + const source = readFileSync(realBinPath, 'utf8') + let relPath: string | undefined + if (basename === 'npm' || basename === 'npx') { + const cli = basename === 'npm' ? 'NPM' : 'NPX' + relPath = + ext === '.cmd' + ? new RegExp(`(?<="${cli}_CLI_JS=%~dp0\\\\).*(?=")`).exec(source)?.[0] + : ext === '.ps1' + ? new RegExp(`(?<=\\$${cli}_CLI_JS="\\$PSScriptRoot/).*(?=")`).exec( + source, + )?.[0] + : new RegExp(`(?<=${cli}_CLI_JS="\\$CLI_BASEDIR/).*(?=")`).exec( + source, + )?.[0] + const quickPath = path.join( + path.dirname(realBinPath), + `node_modules/npm/bin/${basename}-cli.js`, + ) + if (existsSync(quickPath)) { + return realpathSync.native(quickPath) + } + } else if (basename === 'pnpm' || basename === 'yarn') { + relPath = + ext === '.cmd' + ? (/(?<=node\s+")%~dp0\\([^"]+)(?="\s+%\*)/.exec(source)?.[1] ?? + /(?<="%dp0%\\).*(?=" %\*\r\n)/.exec(source)?.[0]) + : ext === '.ps1' + ? /(?<="\$basedir\/).*(?=" \$args\n)/.exec(source)?.[0] + : /(?<="\$basedir\/)[^"]+(?="\s+"\$@")/.exec(source)?.[0] + } else if (ext === '.cmd') { + relPath = /(?<="%dp0%\\).*(?=" %\*\r\n)/.exec(source)?.[0] + } + if (relPath) { + return realpathSync.native( + normalize(path.resolve(path.dirname(realBinPath), relPath)), + ) + } + throw Object.assign(new Error(`not resolved: ${binPath}`), { + code: 'ENOENT', + }) + } + + if (ext === '') { + const source = readFileSync(realBinPath, 'utf8') + let relPath: string | undefined + if (basename === 'npm' || basename === 'npx') { + relPath = + basename === 'npm' + ? /(?<=NPM_CLI_JS="\$CLI_BASEDIR\/).*(?=")/.exec(source)?.[0] + : /(?<=NPX_CLI_JS="\$CLI_BASEDIR\/).*(?=")/.exec(source)?.[0] + } else if (basename === 'pnpm' || basename === 'yarn') { + relPath = /(?<="\$basedir\/)[^"]+(?="\s+"\$@")/.exec(source)?.[0] + } + if (relPath) { + return realpathSync.native( + normalize(path.resolve(path.dirname(realBinPath), relPath)), + ) + } + } + return realBinPath +} + +type LookupOptions = { + all?: boolean + nothrow?: boolean + path?: string + pathExt?: string +} + +export function whichBin( + name: string, + options: LookupOptions & { all: true; nothrow: true }, +): Promise +export function whichBin( + name: string, + options: LookupOptions & { nothrow: true; all?: false }, +): Promise +export function whichBin(name: string, options?: LookupOptions): Promise +export async function whichBin( + name: string, + options: LookupOptions = {}, +): Promise { + try { + const result = await which(name, options as any) + if (options['all']) { + const paths = Array.isArray(result) ? result : [result] + return paths.map(resolveBinPathSync) + } + return resolveBinPathSync(result as string) + } catch (error) { + if (options['nothrow']) { + return null + } + throw error + } +} + +export function whichBinSync( + name: string, + options: LookupOptions & { all: true; nothrow: true }, +): string[] | null +export function whichBinSync( + name: string, + options: LookupOptions & { nothrow: true; all?: false }, +): string | null +export function whichBinSync(name: string, options?: LookupOptions): string +export function whichBinSync( + name: string, + options: LookupOptions = {}, +): string | string[] | null { + try { + const result = which.sync(name, options as any) + if (options['all']) { + const paths = Array.isArray(result) ? result : [result] + return paths.map(resolveBinPathSync) + } + return resolveBinPathSync(result as string) + } catch (error) { + if (options['nothrow']) { + return null + } + throw error + } +} diff --git a/src/utils/check-input.mts b/src/utils/check-input.mts index 4f9fcee0ad..3edbd5e94a 100644 --- a/src/utils/check-input.mts +++ b/src/utils/check-input.mts @@ -1,10 +1,9 @@ import colors from 'yoctocolors-cjs' -import { LOG_SYMBOLS, logger } from '@socketsecurity/registry/lib/logger' -import { stripAnsi } from '@socketsecurity/registry/lib/strings' - import { failMsgWithBadge } from './fail-msg-with-badge.mts' +import { LOG_SYMBOLS, logger } from './logger.mts' import { serializeResultJson } from './serialize-result-json.mts' +import { stripAnsi } from './strings.mts' import type { OutputKind } from '../types.mts' diff --git a/src/utils/coana-release.mts b/src/utils/coana-release.mts index 14e7354b27..7a76e6b550 100644 --- a/src/utils/coana-release.mts +++ b/src/utils/coana-release.mts @@ -20,10 +20,9 @@ import { pipeline } from 'node:stream/promises' import semver from 'semver' import { x as extractTar } from 'tar' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - +import { debugDir, debugFn } from './debug.mts' +import { logger } from './logger.mts' +import { spawn } from './spawn.mts' import { getDefaultOrgSlug } from '../commands/ci/fetch-default-org-slug.mts' import constants, { COANA_CLI_VERSION, @@ -37,10 +36,11 @@ import { getDefaultApiToken, getDefaultProxyUrl, } from './sdk.mts' +import { isStandaloneSea } from '../sea/runtime.mts' import type { CResult } from '../types.mts' -import type { SpawnOptions } from '@socketsecurity/registry/lib/spawn' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { SpawnOptions } from './spawn.mts' +import type { Spinner } from './spinner.mts' type CoanaArtifacts = { cli: CoanaComponent @@ -409,6 +409,30 @@ export async function spawnCoana( ...options, } as CoanaSpawnOptions + const localPath = process.env['SOCKET_CLI_COANA_LOCAL_PATH'] + const standaloneSea = isStandaloneSea() + if (standaloneSea && !localPath) { + return { + ok: false, + message: 'Coana is unavailable in the standalone Socket build', + cause: + 'Set SOCKET_CLI_COANA_LOCAL_PATH to a native Coana executable to run Coana from a standalone build.', + } + } + if ( + standaloneSea && + localPath && + (localPath.endsWith('.js') || localPath.endsWith('.mjs')) + ) { + return { + ok: false, + message: + 'Cannot launch a JavaScript Coana CLI from the standalone Socket build', + cause: + 'SOCKET_CLI_COANA_LOCAL_PATH must name a native Coana executable; the standalone JavaScript Worker adapter is not configured.', + } + } + const mixinsEnv: Record = { SOCKET_CLI_VERSION: constants.ENV.INLINED_SOCKET_CLI_VERSION, // Forwarded to the Coana CLI so it can append our product token to its @@ -439,7 +463,7 @@ export async function spawnCoana( ...spawnEnv, }) - let scriptPath = process.env['SOCKET_CLI_COANA_LOCAL_PATH'] + let scriptPath = localPath if (!scriptPath) { const coanaDirCResult = await getCoanaCliDir( coanaVersion || COANA_CLI_VERSION, diff --git a/src/utils/coana-release.test.mts b/src/utils/coana-release.test.mts index f7454f0392..b7bb2b727b 100644 --- a/src/utils/coana-release.test.mts +++ b/src/utils/coana-release.test.mts @@ -21,8 +21,7 @@ import path from 'node:path' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { spawn } from '@socketsecurity/registry/lib/spawn' - +import { spawn } from './spawn.mts' import constants from '../constants.mts' import { spawnCoana } from './coana-release.mts' import { getCliUserAgent } from './sdk.mts' diff --git a/src/utils/coana.mts b/src/utils/coana.mts index c27f4cae34..3c478f5e44 100644 --- a/src/utils/coana.mts +++ b/src/utils/coana.mts @@ -25,9 +25,8 @@ import path from 'node:path' import { pipeline } from 'node:stream/promises' import { createBrotliCompress } from 'node:zlib' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { readJsonSync } from '@socketsecurity/registry/lib/fs' - +import { debugDir, debugFn } from './debug.mts' +import { readJsonSync } from './fs.mts' import constants from '../constants.mts' import type { PURL_Type } from './ecosystem.mts' diff --git a/src/utils/config.mts b/src/utils/config.mts index 57486404a1..db4bd0e63e 100644 --- a/src/utils/config.mts +++ b/src/utils/config.mts @@ -26,13 +26,13 @@ import { mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' import config from '@socketsecurity/config' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { safeReadFileSync } from '@socketsecurity/registry/lib/fs' -import { logger } from '@socketsecurity/registry/lib/logger' -import { naturalCompare } from '@socketsecurity/registry/lib/sorts' -import { debugConfig } from './debug.mts' +import { debugConfig, debugDir, debugFn } from './debug.mts' import { getEditableJsonClass } from './editable-json.mts' +import { getErrorCause } from './errors.mts' +import { safeReadFileSync } from './fs.mts' +import { logger } from './logger.mts' +import { naturalCompare } from './sorts.mts' import constants, { CONFIG_KEY_API_BASE_URL, CONFIG_KEY_API_PROXY, @@ -43,7 +43,6 @@ import constants, { SOCKET_YAML, SOCKET_YML, } from '../constants.mts' -import { getErrorCause } from './errors.mts' import type { CResult } from '../types.mts' import type { SocketYml } from '@socketsecurity/config' @@ -106,7 +105,12 @@ function getConfigValues(): LocalConfig { try { Object.assign( _cachedConfig, - JSON.parse(Buffer.from(raw, 'base64').toString()), + JSON.parse( + Buffer.from( + Buffer.isBuffer(raw) ? raw.toString('utf8') : raw, + 'base64', + ).toString(), + ), ) debugConfig(configFilePath, true) } catch (e) { diff --git a/src/utils/debug.mts b/src/utils/debug.mts index b7a4de3690..d1fa8e168c 100644 --- a/src/utils/debug.mts +++ b/src/utils/debug.mts @@ -1,35 +1,185 @@ -/** - * Debug utilities for Socket CLI. - * Provides structured debugging with categorized levels and helpers. - * - * Debug Categories: - * DEFAULT (shown with SOCKET_CLI_DEBUG=1): - * - 'error': Critical errors that prevent operation - * - 'warn': Important warnings that may affect behavior - * - 'notice': Notable events and state changes - * - 'silly': Very verbose debugging info - * - * OPT-IN ONLY (require explicit DEBUG='category' even with SOCKET_CLI_DEBUG=1): - * - 'inspect': Detailed object inspection (DEBUG='inspect' or DEBUG='*') - * - 'stdio': Command execution logs (DEBUG='stdio' or DEBUG='*') - * - * These opt-in categories are intentionally excluded from default debug output - * to reduce noise. Enable them explicitly when needed for deep debugging. +/* + * Adapted narrowly from @socketsecurity/registry 1.1.17, lib/debug.js. + * Copyright (c) 2024 Socket Inc. Licensed under the MIT License. */ -import { debugDir, debugFn, isDebug } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' +import util from 'node:util' import constants from '../constants.mts' +import { logger } from './logger.mts' +import { hasOwn } from './objects.mts' -/** - * Debug an API request start. - * Logs essential info without exposing sensitive data. - */ +type SpinnerLike = { isSpinning: boolean; start(): unknown; stop(): unknown } +type DebugOptions = { namespaces?: string; spinner?: SpinnerLike } +type NamespaceArg = string | DebugOptions + +function extractOptions(value: NamespaceArg): DebugOptions { + return typeof value === 'string' ? { namespaces: value } : value +} + +function matches(pattern: string, name: string): boolean { + if (pattern === '*') { + return true + } + const escaped = pattern + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*') + return new RegExp(`^${escaped}$`).test(name) +} + +function isEnabled(namespaces?: string): boolean { + if (!namespaces || namespaces === '*') { + return true + } + const configured = process.env['DEBUG'] ?? '' + const patterns = configured.split(/[\s,]+/).filter(Boolean) + const requested = namespaces.split(/[\s,]+/).filter(Boolean) + const included = patterns.filter(pattern => !pattern.startsWith('-')) + const excluded = patterns + .filter(pattern => pattern.startsWith('-')) + .map(pattern => pattern.slice(1)) + const enabled = (name: string): boolean => { + const isExcluded = excluded.some(pattern => matches(pattern, name)) + const defaultEnabled = + !constants.ENV.DEBUG && + constants.ENV.SOCKET_CLI_DEBUG && + (name === 'error' || name === 'notice') + return ( + (included.some(pattern => matches(pattern, name)) || defaultEnabled) && + !isExcluded + ) + } + const names = requested.filter(name => !name.startsWith('-')) + const skips = requested + .filter(name => name.startsWith('-')) + .map(name => name.slice(1)) + return ( + (!names.length || names.some(enabled)) && + skips.every(name => !enabled(name)) + ) +} + +const defaultInspectOptions = Object.keys(process.env) + .filter(key => /^debug_/i.test(key)) + .reduce>((options, key) => { + const prop = key + .slice(6) + .toLowerCase() + .replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase()) + const value = process.env[key] ?? '' + if (/^(yes|on|true|enabled)$/i.test(value)) { + options[prop] = true + } else if (/^(no|off|false|disabled)$/i.test(value)) { + options[prop] = false + } else if (value === 'null') { + options[prop] = null + } else { + options[prop] = Number(value) + } + return options + }, {}) as util.InspectOptions + +function withSpinner(options: DebugOptions, callback: () => void): void { + const spinner = options.spinner ?? (constants.spinner as SpinnerLike) + const wasSpinning = spinner.isSpinning + spinner.stop() + callback() + if (wasSpinning) { + spinner.start() + } +} + +export function isDebug(namespaces?: string): boolean { + return constants.ENV.SOCKET_CLI_DEBUG && isEnabled(namespaces) +} + +export function debugDir( + namespacesOrOpts: NamespaceArg, + obj: unknown, + inspectOptions?: util.InspectOptions, +): void { + const options = extractOptions(namespacesOrOpts) + if (!isEnabled(options.namespaces)) { + return + } + withSpinner(options, () => + logger.dir(obj, inspectOptions ?? defaultInspectOptions), + ) +} + +let pointingTriangle: string | undefined +export function debugFn( + namespacesOrOpts: NamespaceArg, + ...args: unknown[] +): void { + const options = extractOptions(namespacesOrOpts) + if (!isEnabled(options.namespaces)) { + return + } + const stack = new Error().stack ?? '' + let lineCount = 0 + let lineStart = 0 + let name = 'anonymous' + for (let i = 0; i < stack.length; i += 1) { + if (stack.charCodeAt(i) === 10) { + lineCount += 1 + if (lineCount < 4) { + lineStart = i + 1 + } else { + const line = stack.slice(lineStart, i).trimStart() + const match = /(?<=^at\s+).*?(?=\s+\(|$)/.exec(line)?.[0] + if (match) { + name = match.replace(/^(?:async|bound|get|new|set)\s+/, '') + if (name.startsWith('Object.')) { + const afterDot = name.slice(7) + if (!hasOwn(Object, afterDot)) { + name = afterDot + } + } + } + break + } + } + } + pointingTriangle ??= isUnicodeSupported() ? '▸' : '>' + const text = args[0] + const logArgs = + typeof text === 'string' + ? [ + applyLinePrefix( + `${name ? `${name} ${pointingTriangle} ` : ''}${text}`, + '[DEBUG] ', + ), + ...args.slice(1), + ] + : args + withSpinner(options, () => Reflect.apply(logger.info, logger, logArgs)) +} + +function applyLinePrefix(value: string, prefix: string): string { + return prefix.length + ? `${prefix}${value.includes('\n') ? value.replace(/\n/g, `\n${prefix}`) : value}` + : value +} + +export function debugLog( + namespacesOrOpts: NamespaceArg, + ...args: unknown[] +): void { + const options = extractOptions(namespacesOrOpts) + if (!isEnabled(options.namespaces)) { + return + } + withSpinner(options, () => + logger.info(util.formatWithOptions(defaultInspectOptions, ...args)), + ) +} + +/** Structured CLI debug helpers retained from the existing Socket CLI module. */ export function debugApiRequest( method: string, endpoint: string, - timeout?: number | undefined, + timeout?: number, ): void { if (constants.ENV.SOCKET_CLI_DEBUG) { const timeoutStr = timeout !== undefined ? ` (timeout: ${timeout}ms)` : '' @@ -39,25 +189,21 @@ export function debugApiRequest( } } -/** - * Debug an API response end. - * Logs essential info without exposing sensitive data. - */ export function debugApiResponse( method: string, endpoint: string, - status?: number | undefined, - error?: unknown | undefined, - duration?: number | undefined, - headers?: Record | undefined, + status?: number, + error?: unknown, + duration?: number, + headers?: Record, ): void { if (!constants.ENV.SOCKET_CLI_DEBUG) { return } - + const durationStr = duration !== undefined ? ` (${duration}ms)` : '' if (error) { logger.fail( - `[DEBUG] ${new Date().toISOString()} request error: ${method} ${endpoint} - ${error instanceof Error ? error.message : 'Unknown error'}${duration !== undefined ? ` (${duration}ms)` : ''}`, + `[DEBUG] ${new Date().toISOString()} request error: ${method} ${endpoint} - ${error instanceof Error ? error.message : 'Unknown error'}${durationStr}`, ) if (headers) { logger.info( @@ -65,7 +211,6 @@ export function debugApiResponse( ) } } else { - const durationStr = duration !== undefined ? ` (${duration}ms)` : '' logger.info( `[DEBUG] ${new Date().toISOString()} request ended: ${method} ${endpoint}: HTTP ${status}${durationStr}`, ) @@ -77,14 +222,10 @@ export function debugApiResponse( } } -/** - * Debug file operation. - * Logs file operations with appropriate level. - */ export function debugFileOp( operation: 'read' | 'write' | 'delete' | 'create', filepath: string, - error?: unknown | undefined, + error?: unknown, ): void { if (error) { debugDir('warn', { @@ -97,14 +238,10 @@ export function debugFileOp( } } -/** - * Debug package scanning. - * Provides insight into security scanning. - */ export function debugScan( phase: 'start' | 'progress' | 'complete' | 'error', - packageCount?: number | undefined, - details?: unknown | undefined, + packageCount?: number, + details?: unknown, ): void { switch (phase) { case 'start': @@ -124,21 +261,15 @@ export function debugScan( ) break case 'error': - debugDir('error', { - phase: 'scan_error', - details, - }) + debugDir('error', { phase: 'scan_error', details }) break } } -/** - * Debug configuration loading. - */ export function debugConfig( source: string, found: boolean, - error?: unknown | undefined, + error?: unknown, ): void { if (error) { debugDir('warn', { @@ -152,29 +283,38 @@ export function debugConfig( } } -/** - * Debug git operations. - * Only logs important git operations, not every command. - */ export function debugGit( operation: string, success: boolean, - details?: Record | undefined, + details?: Record, ): void { if (!success) { - debugDir('warn', { - git_op: operation, - ...details, - }) + debugDir('warn', { git_op: operation, ...details }) } else if ( (isDebug('notice') && operation.includes('push')) || operation.includes('commit') ) { - // Only log important operations like push and commit. debugFn('notice', `Git ${operation} succeeded`) } else if (isDebug('silly')) { debugFn('silly', `Git ${operation}`) } } -export { debugDir, debugFn, isDebug } +function isUnicodeSupported(): boolean { + if (process.platform !== 'win32') { + return process.env['TERM'] !== 'linux' + } + const { env } = process + return !!( + env['WT_SESSION'] || + env['TERMINUS_SUBLIME'] || + env['ConEmuTask'] === '{cmd::Cmder}' || + env['TERM_PROGRAM'] === 'Terminus-Sublime' || + env['TERM_PROGRAM'] === 'vscode' || + env['TERM'] === 'xterm-256color' || + env['TERM'] === 'alacritty' || + env['TERM'] === 'rxvt-unicode' || + env['TERM'] === 'rxvt-unicode-256color' || + env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm' + ) +} diff --git a/src/utils/determine-org-slug.mts b/src/utils/determine-org-slug.mts index 608c5cc98e..b4de0dc520 100644 --- a/src/utils/determine-org-slug.mts +++ b/src/utils/determine-org-slug.mts @@ -1,5 +1,4 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - +import { logger } from './logger.mts' import { CONFIG_KEY_DEFAULT_ORG, V1_MIGRATION_GUIDE_URL, diff --git a/src/utils/dlx-binary.mts b/src/utils/dlx-binary.mts index 9b55fc9fa2..188a816a6b 100644 --- a/src/utils/dlx-binary.mts +++ b/src/utils/dlx-binary.mts @@ -27,17 +27,13 @@ import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import path from 'node:path' -import { readJson } from '@socketsecurity/registry/lib/fs' -import { spawn } from '@socketsecurity/registry/lib/spawn' - -import constants from '../constants.mts' import { apiFetch } from './api.mts' import { InputError } from './errors.mts' +import { readJson } from './fs.mts' +import { spawn } from './spawn.mts' +import constants from '../constants.mts' -import type { - SpawnExtra, - SpawnOptions, -} from '@socketsecurity/registry/lib/spawn' +import type { SpawnExtra, SpawnOptions } from './spawn.mts' export interface DlxBinaryOptions { /** URL to download the binary from. */ diff --git a/src/utils/dlx-detection.mts b/src/utils/dlx-detection.mts index 8182c24d5b..cfa95ba8c1 100644 --- a/src/utils/dlx-detection.mts +++ b/src/utils/dlx-detection.mts @@ -24,8 +24,7 @@ import path from 'node:path' -import { normalizePath } from '@socketsecurity/registry/lib/path' - +import { normalizePath } from './path.mts' import constants from '../constants.mts' /** diff --git a/src/utils/dlx.mts b/src/utils/dlx.mts index 2c84b34f2f..10239dc98e 100644 --- a/src/utils/dlx.mts +++ b/src/utils/dlx.mts @@ -19,8 +19,7 @@ import { createRequire } from 'node:module' -import { getOwn } from '@socketsecurity/registry/lib/objects' - +import { getOwn } from './objects.mts' import constants, { FLAG_QUIET, FLAG_SILENT, @@ -30,9 +29,10 @@ import constants, { } from '../constants.mts' import { findUp } from './fs.mts' import { isYarnBerry } from './yarn-version.mts' +import { isStandaloneSea } from '../sea/runtime.mts' +import type { SpawnExtra } from './spawn.mts' import type { ShadowBinOptions, ShadowBinResult } from '../shadow/npm-base.mts' -import type { SpawnExtra } from '@socketsecurity/registry/lib/spawn' const require = createRequire(import.meta.url) @@ -58,6 +58,15 @@ export async function spawnDlx( options?: DlxOptions | undefined, spawnExtra?: SpawnExtra | undefined, ): Promise { + // Standalone builds must never turn a missing embedded helper into a + // package-manager install. Keep this guard ahead of lockfile detection and + // shadow-bin loading so every generic dlx caller fails closed consistently. + if (isStandaloneSea()) { + throw new Error( + `Cannot run '${packageSpec.name}' from a standalone Socket CLI: this helper is not bundled for this target, and runtime package installation is disabled.`, + ) + } + const { agent, silent = false, ...shadowOptions } = options ?? {} let finalShadowOptions = shadowOptions diff --git a/src/utils/ecosystem.mts b/src/utils/ecosystem.mts index ccef4d4772..4e5ef499b6 100644 --- a/src/utils/ecosystem.mts +++ b/src/utils/ecosystem.mts @@ -22,28 +22,16 @@ * - Ensures type safety for ecosystem operations */ -import { arrayUnique, joinAnd } from '@socketsecurity/registry/lib/arrays' - +import { arrayUnique, joinAnd } from './arrays.mts' import { NPM } from '../constants.mts' import { InputError } from './errors.mts' -import type { EcosystemString } from '@socketsecurity/registry' import type { components } from '@socketsecurity/sdk/types/api' export type PURL_Type = components['schemas']['SocketPURL_Type'] type ExpectNever = T -// Temporarily commented out due to dependency version mismatch. -// SDK has "alpm" but registry's EcosystemString doesn't yet. -// type MissingInEcosystemString = Exclude -type ExtraInEcosystemString = Exclude - -// export type _Check_EcosystemString_has_all_purl_types = -// ExpectNever -export type _Check_EcosystemString_has_no_extras = - ExpectNever - export const ALL_ECOSYSTEMS = [ 'alpm', 'apk', diff --git a/src/utils/env.mts b/src/utils/env.mts new file mode 100644 index 0000000000..94909c8571 --- /dev/null +++ b/src/utils/env.mts @@ -0,0 +1,59 @@ +/* + * Adapted from @socketsecurity/registry 1.1.17, lib/env.js. + * Copyright (c) 2024 Socket Inc. Licensed under the MIT License. + */ + +const NumberCtor = Number +const { isFinite: NumberIsFinite, parseInt: NumberParseInt } = NumberCtor +const StringCtor = String + +/** + * Convert an environment variable value to a boolean. + * @param value The value to convert. + * @param defaultValue Default value when input is null/undefined. + * @returns The boolean representation of the value. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function envAsBoolean(value: unknown, defaultValue = false): boolean { + if (typeof value === 'string') { + const trimmed = value.trim() + return trimmed === '1' || trimmed.toLowerCase() === 'true' + } + if (value === null || value === undefined) { + return !!defaultValue + } + return !!value +} + +/** + * Convert an environment variable value to a number. + * @param value The value to convert. + * @param defaultValue Default value when conversion fails. + * @returns The numeric representation of the value. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function envAsNumber(value: unknown, defaultValue = 0): number { + const numOrNaN = NumberParseInt(value as string, 10) + const numMayBeNegZero = NumberIsFinite(numOrNaN) + ? numOrNaN + : NumberCtor(defaultValue) + // Ensure -0 is treated as 0. + return numMayBeNegZero || 0 +} + +/** + * Convert an environment variable value to a trimmed string. + * @param value The value to convert. + * @param defaultValue Default value when input is null/undefined. + * @returns The trimmed string representation of the value. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function envAsString(value: unknown, defaultValue = ''): string { + if (typeof value === 'string') { + return value.trim() + } + if (value === null || value === undefined) { + return defaultValue === '' ? defaultValue : StringCtor(defaultValue).trim() + } + return StringCtor(value).trim() +} diff --git a/src/utils/errors.mts b/src/utils/errors.mts index db79931d00..dc1c51c58b 100644 --- a/src/utils/errors.mts +++ b/src/utils/errors.mts @@ -20,8 +20,7 @@ import { setTimeout as wait } from 'node:timers/promises' -import { debugFn } from '@socketsecurity/registry/lib/debug' - +import { debugFn } from './debug.mts' import constants, { UNKNOWN_ERROR } from '../constants.mts' const { diff --git a/src/utils/escape-regexp.mts b/src/utils/escape-regexp.mts new file mode 100644 index 0000000000..9ab3c45c61 --- /dev/null +++ b/src/utils/escape-regexp.mts @@ -0,0 +1,10 @@ +/** + * Escape regular-expression syntax characters in a literal string. + * + * Copied narrowly from `@socketsecurity/registry` 1.1.17 + * (`registry/lib/regexps.js`, MIT) for Socket CLI ownership. Keep the upstream + * behavior for hyphens so this remains compatible with Unicode RegExp modes. + */ +export function escapeRegExp(value: string): string { + return value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') +} diff --git a/src/utils/filter-config.mts b/src/utils/filter-config.mts index a5325f618c..afc4131a4f 100644 --- a/src/utils/filter-config.mts +++ b/src/utils/filter-config.mts @@ -11,7 +11,7 @@ * - Validates boolean and array values */ -import { isObject } from '@socketsecurity/registry/lib/objects' +import { isObject } from './objects.mts' export type FilterConfig = { [key: string]: boolean | string[] diff --git a/src/utils/fs.mts b/src/utils/fs.mts index 2297920107..ad99c1f429 100644 --- a/src/utils/fs.mts +++ b/src/utils/fs.mts @@ -17,11 +17,203 @@ * - Searching for specific files in parent directories */ -import { promises as fs } from 'node:fs' +import { existsSync, promises as fs, readFileSync, statSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import constants from '../constants.mts' +import { stripBom } from './strings.mts' + +// Adapted from @socketsecurity/registry/lib/fs, MIT License. +// Copyright (c) 2024 Socket Inc +export type JsonContent = Record + +type ReadFileOptions = { + encoding?: BufferEncoding | null | undefined + flag?: string | undefined + signal?: AbortSignal | undefined +} + +type ReadJsonOptions = { + encoding?: BufferEncoding + flag?: string + signal?: AbortSignal + throws?: boolean + reviver?: Parameters[1] +} + +type WriteJsonOptions = { + EOL?: string + finalEOL?: boolean + replacer?: Parameters[1] + spaces?: Parameters[2] + encoding?: BufferEncoding + flag?: string + mode?: number | string + signal?: AbortSignal +} + +function parseJson( + content: string | Buffer, + filepath: string, + reviver: ReadJsonOptions['reviver'], + throws: boolean, +): JsonContent | null { + try { + const jsonText = Buffer.isBuffer(content) + ? content.toString('utf8') + : content + return JSON.parse(stripBom(jsonText), reviver) as JsonContent + } catch (error) { + if (throws) { + if (error && typeof filepath === 'string') { + const parseError = error as Error + parseError.message = `${filepath}: ${parseError.message}` + } + throw error + } + return null + } +} + +export async function readJson( + filepath: string, + options?: ReadJsonOptions | BufferEncoding, +): Promise { + if (typeof options === 'string') { + options = { encoding: options } + } + const { reviver, throws, ...fsOptions } = { __proto__: null, ...options } + const shouldThrow = throws === undefined || !!throws + let content: string | Buffer + try { + content = await fs.readFile(filepath, { encoding: 'utf8', ...fsOptions }) + } catch (error) { + if (shouldThrow) { + throw error + } + return null + } + return parseJson(content, filepath, reviver, shouldThrow) +} + +export function readJsonSync( + filepath: string, + options?: ReadJsonOptions | BufferEncoding, +): JsonContent | null { + if (typeof options === 'string') { + options = { encoding: options } + } + const { reviver, throws, ...fsOptions } = { __proto__: null, ...options } + const shouldThrow = throws === undefined || !!throws + let content: string | Buffer + try { + content = readFileSync(filepath, { encoding: 'utf8', ...fsOptions }) + } catch (error) { + if (shouldThrow) { + throw error + } + return null + } + return parseJson(content, filepath, reviver, shouldThrow) +} + +export function safeStatsSync( + filepath: import('node:fs').PathLike, + options?: import('node:fs').StatSyncOptions, +): import('node:fs').Stats | import('node:fs').BigIntStats | undefined { + try { + return statSync(filepath, { throwIfNoEntry: false, ...options }) + } catch {} + return undefined +} + +export async function writeJson( + filepath: string, + jsonContent: JsonContent, + options?: WriteJsonOptions | BufferEncoding, +): Promise { + if (typeof options === 'string') { + options = { encoding: options } + } + const { EOL, finalEOL, replacer, spaces, ...fsOptions } = { + __proto__: null, + ...options, + } + const eol = EOL ?? '\n' + const jsonString = `${JSON.stringify(jsonContent, replacer, spaces).replace(/\n/g, eol)}${finalEOL === false ? '' : eol}` + await fs.writeFile(filepath, jsonString, { encoding: 'utf8', ...fsOptions }) +} + +export async function readFileBinary( + filepath: string, + options?: ReadFileOptions, +): Promise { + return (await fs.readFile(filepath, { + signal: constants.abortSignal, + ...options, + // `encoding: 'binary'` returns a latin1 string in Node, despite the + // registry helper's Buffer declaration. Preserve the declared binary + // contract and let Node return the bytes as a Buffer. + encoding: null, + })) as Buffer +} + +export async function readFileUtf8( + filepath: string, + options?: ReadFileOptions, +): Promise { + return await fs.readFile(filepath, { + signal: constants.abortSignal, + ...options, + encoding: 'utf8', + }) +} + +export async function safeReadFile( + filepath: string, + options?: ReadFileOptions | string, +): Promise { + try { + const readOptions = typeof options === 'string' ? {} : options + const encoding: BufferEncoding | null = + (typeof options === 'string' + ? (options as BufferEncoding) + : options?.encoding) ?? 'utf8' + return await fs.readFile(filepath, { + ...readOptions, + signal: constants.abortSignal, + encoding, + }) + } catch {} + return undefined +} + +export function safeReadFileSync( + filepath: import('node:fs').PathOrFileDescriptor, + options?: ReadFileOptions | string, +): string | Buffer | undefined { + try { + const readOptions = typeof options === 'string' ? {} : options + const encoding: BufferEncoding | null = + (typeof options === 'string' + ? (options as BufferEncoding) + : options?.encoding) ?? 'utf8' + return readFileSync(filepath, { + ...readOptions, + encoding, + }) + } catch {} + return undefined +} + +export function isDirSync(filepath: import('node:fs').PathLike): boolean { + try { + return existsSync(filepath) && statSync(filepath).isDirectory() + } catch { + return false + } +} export type FindUpOptions = { cwd?: string | undefined diff --git a/src/utils/git.mts b/src/utils/git.mts index 83d50552d2..abc03122d7 100644 --- a/src/utils/git.mts +++ b/src/utils/git.mts @@ -26,17 +26,15 @@ * - gitBranch: Get current branch or commit hash */ -import { debugDir, debugFn, isDebug } from '@socketsecurity/registry/lib/debug' -import { envAsString } from '@socketsecurity/registry/lib/env' -import { normalizePath } from '@socketsecurity/registry/lib/path' -import { isSpawnError, spawn } from '@socketsecurity/registry/lib/spawn' - -import constants, { FLAG_QUIET } from '../constants.mts' -import { debugGit } from './debug.mts' +import { debugDir, debugFn, debugGit, isDebug } from './debug.mts' +import { envAsString } from './env.mts' import { extractName, extractOwner } from './extract-names.mts' +import { normalizePath } from './path.mts' +import { isSpawnError, spawn } from './spawn.mts' +import constants, { FLAG_QUIET } from '../constants.mts' import type { CResult } from '../types.mts' -import type { SpawnOptions } from '@socketsecurity/registry/lib/spawn' +import type { SpawnOptions } from './spawn.mts' // Listed in order of check preference. const COMMON_DEFAULT_BRANCH_NAMES = [ diff --git a/src/utils/git.test.mts b/src/utils/git.test.mts index d4b1f83b2d..cc295e2c02 100644 --- a/src/utils/git.test.mts +++ b/src/utils/git.test.mts @@ -4,8 +4,6 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { spawn } from '@socketsecurity/registry/lib/spawn' - import { getCiBranch, gitBranch, @@ -13,6 +11,7 @@ import { gitUnstagedModifiedFiles, gitUntrackedFiles, } from './git.mts' +import { spawn } from './spawn.mts' // GitHub Actions sets these in its own runs, so they have to be cleared for // the tests to exercise anything other than the CI job they run inside. diff --git a/src/utils/github-errors.mts b/src/utils/github-errors.mts index 0597dab339..02bc6197a5 100644 --- a/src/utils/github-errors.mts +++ b/src/utils/github-errors.mts @@ -19,14 +19,12 @@ import process from 'node:process' import { setTimeout as sleep } from 'node:timers/promises' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { envAsNumber } from '@socketsecurity/registry/lib/env' -import { logger } from '@socketsecurity/registry/lib/logger' -import { pRetry } from '@socketsecurity/registry/lib/promises' - import { apiFetch } from './api.mts' -import { debugApiRequest, debugApiResponse } from './debug.mts' +import { debugApiRequest, debugApiResponse, debugFn } from './debug.mts' +import { envAsNumber } from './env.mts' import { formatErrorWithDetail } from './errors.mts' +import { logger } from './logger.mts' +import { pRetry } from './promises.mts' import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_INTERNAL_SERVER_ERROR, diff --git a/src/utils/github.mts b/src/utils/github.mts index f3c2470708..c7a5c44246 100644 --- a/src/utils/github.mts +++ b/src/utils/github.mts @@ -33,21 +33,15 @@ import { } from '@octokit/graphql' import { Octokit } from '@octokit/rest' -import { debugDir, debugFn, isDebug } from '@socketsecurity/registry/lib/debug' -import { - readJson, - safeStatsSync, - writeJson, -} from '@socketsecurity/registry/lib/fs' -import { spawn } from '@socketsecurity/registry/lib/spawn' -import { parseUrl } from '@socketsecurity/registry/lib/url' - +import { debugDir, debugFn, isDebug } from './debug.mts' import { formatErrorWithDetail } from './errors.mts' +import { type JsonContent, readJson, safeStatsSync, writeJson } from './fs.mts' +import { spawn } from './spawn.mts' +import { parseUrl } from './url.mts' import constants from '../constants.mts' +import type { SpawnOptions } from './spawn.mts' import type { components } from '@octokit/openapi-types' -import type { JsonContent } from '@socketsecurity/registry/lib/fs' -import type { SpawnOptions } from '@socketsecurity/registry/lib/spawn' export type Pr = components['schemas']['pull-request'] @@ -59,9 +53,9 @@ async function readCache( const cacheJsonPath = path.join(constants.githubCachePath, `${key}.json`) const stat = safeStatsSync(cacheJsonPath) if (stat) { - const isExpired = Date.now() - stat.mtimeMs > ttlMs + const isExpired = Date.now() - Number(stat.mtimeMs) > ttlMs if (!isExpired) { - return await readJson(cacheJsonPath) + return (await readJson(cacheJsonPath)) ?? undefined } } return undefined diff --git a/src/utils/glob-content-dedup.test.mts b/src/utils/glob-content-dedup.test.mts index 566a3ffd28..72e43a620b 100644 --- a/src/utils/glob-content-dedup.test.mts +++ b/src/utils/glob-content-dedup.test.mts @@ -5,9 +5,8 @@ import v8 from 'node:v8' import { describe, expect, it } from 'vitest' -import { normalizePath } from '@socketsecurity/registry/lib/path' - import { globWithGitIgnore } from './glob.mts' +import { normalizePath } from './path.mts' function codeSpaceMb(): number { const spaces = v8.getHeapSpaceStatistics() diff --git a/src/utils/glob.mts b/src/utils/glob.mts index c4be9b93fc..05d4c03ab6 100644 --- a/src/utils/glob.mts +++ b/src/utils/glob.mts @@ -5,13 +5,12 @@ import ignore from 'ignore' import micromatch from 'micromatch' import { parse as yamlParse } from 'yaml' -import { isDirSync, safeReadFile } from '@socketsecurity/registry/lib/fs' -import { defaultIgnore } from '@socketsecurity/registry/lib/globs' -import { readPackageJson } from '@socketsecurity/registry/lib/packages' -import { normalizePath } from '@socketsecurity/registry/lib/path' -import { transform } from '@socketsecurity/registry/lib/streams' -import { isNonEmptyString } from '@socketsecurity/registry/lib/strings' - +import { isDirSync, safeReadFile } from './fs.mts' +import { defaultIgnore } from './globs.mts' +import { readPackageJson } from './packages.mts' +import { normalizePath } from './path.mts' +import { transform } from './streams.mts' +import { isNonEmptyString } from './strings.mts' import { NODE_MODULES, PNPM } from '../constants.mts' import type { Agent } from './package-environment.mts' @@ -59,7 +58,7 @@ async function getWorkspaceGlobs( const yml = await safeReadFile(workspacePath) if (yml) { try { - workspacePatterns = yamlParse(yml)?.packages + workspacePatterns = yamlParse(yml.toString())?.packages } catch {} } } else { @@ -104,6 +103,19 @@ function ignoreFileToGlobPatterns( return ignoreFileLinesToGlobPatterns(content.split(/\r?\n/), filepath, cwd) } +function escapeMinimatchGroupStarts(pattern: string): string { + let escaped = '' + let previousWasOddBackslash = false + for (const char of pattern) { + if ((char === '{' || char === '(') && !previousWasOddBackslash) { + escaped += '\\' + } + escaped += char + previousWasOddBackslash = char === '\\' ? !previousWasOddBackslash : false + } + return escaped +} + // Based on `@eslint/compat` convertIgnorePatternToMinimatch. // Apache v2.0 licensed // Copyright Nicholas C. Zakas @@ -135,11 +147,9 @@ function ignorePatternToMinimatch(pattern: string): string { // For example, gitignore pattern `src/{a,b}.js` ignores file `src/{a,b}.js`. // But, the same minimatch pattern `src/{a,b}.js` ignores files `src/a.js` and `src/b.js`. // Minimatch pattern `src/\{a,b}.js` is equivalent to gitignore pattern `src/{a,b}.js`. - const escapedPatternWithoutLeadingSlash = - patternWithoutLeadingSlash.replaceAll( - /(?=((?:\\.|[^{(])*))\1([{(])/guy, - '$1\\$2', - ) + const escapedPatternWithoutLeadingSlash = escapeMinimatchGroupStarts( + patternWithoutLeadingSlash, + ) const matchInsideSuffix = patternToTest.endsWith('/**') ? '/*' : '' return `${negatedPrefix}${matchEverywherePrefix}${escapedPatternWithoutLeadingSlash}${matchInsideSuffix}` } @@ -364,8 +374,16 @@ export async function globWithGitIgnore( suppressErrors: true, }, ) - for await (const found of transform( - discoveryStream, + for await (const found of transform< + string, + { + content: string + dir: string + patterns: string[] + venv: boolean + } + >( + discoveryStream as unknown as AsyncIterable, async (filepath: string) => { const dirRel = normalizePath(path.relative(cwd, path.dirname(filepath))) if (path.basename(filepath) === PYVENV_CFG) { @@ -379,7 +397,7 @@ export async function globWithGitIgnore( venv: true, } } - const content = (await safeReadFile(filepath)) ?? '' + const content = ((await safeReadFile(filepath)) ?? '').toString() return { content, dir: dirRel, @@ -506,7 +524,7 @@ export async function globWorkspace( absolute: true, cwd, dot: true, - ignore: defaultIgnore, + ignore: [...defaultIgnore], }) : [] } diff --git a/src/utils/glob.test.mts b/src/utils/glob.test.mts index 8967093e5d..6b5a8940ed 100644 --- a/src/utils/glob.test.mts +++ b/src/utils/glob.test.mts @@ -1,29 +1,21 @@ import { chmodSync, - existsSync, mkdirSync, mkdtempSync, - readdirSync, rmSync, writeFileSync, } from 'node:fs' -import { tmpdir } from 'node:os' +import os from 'node:os' import path from 'node:path' -import { fileURLToPath } from 'node:url' -import mockFs from 'mock-fs' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { normalizePath } from '@socketsecurity/registry/lib/path' - -import { NODE_MODULES } from '../constants.mjs' import { createSupportedFilesFilter, globWithGitIgnore, pathsToGlobPatterns, } from './glob.mts' - -import type FileSystem from 'mock-fs/lib/filesystem' +import { normalizePath } from './path.mts' // Filter functions defined at module scope to satisfy linting rules. function filterJsonFiles(filepath: string): boolean { @@ -34,65 +26,34 @@ function filterTsFiles(filepath: string): boolean { return filepath.endsWith('.ts') } -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) +let mockFixturePath = '' -const rootNmPath = path.join(__dirname, '../..', NODE_MODULES) -const mockFixturePath = normalizePath(path.join(__dirname, 'glob-mock')) -const mockNmPath = normalizePath(rootNmPath) - -// Remove broken symlinks in node_modules before loading to prevent mock-fs errors. -function cleanupBrokenSymlinks(dirPath: string): void { - try { - if (!existsSync(dirPath)) { - return - } - const entries = readdirSync(dirPath, { withFileTypes: true }) - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name) - try { - if (entry.isSymbolicLink() && !existsSync(fullPath)) { - // Symlink exists but target does not, remove it. - rmSync(fullPath, { force: true }) - } else if (entry.isDirectory()) { - // Recursively check subdirectories. - cleanupBrokenSymlinks(fullPath) - } - } catch { - // Ignore errors for individual entries. +function mockTestFs(config: Record): void { + const writeEntry = (filename: string, value: unknown): void => { + if (value && typeof value === 'object' && !Buffer.isBuffer(value)) { + mkdirSync(filename, { recursive: true }) + for (const [name, child] of Object.entries(value)) { + writeEntry(path.join(filename, name), child) } + return } - } catch { - // If we cannot read the directory, skip cleanup. + mkdirSync(path.dirname(filename), { recursive: true }) + writeFileSync(filename, value as string | Uint8Array) } -} - -// Clean up broken symlinks before loading node_modules. -cleanupBrokenSymlinks(rootNmPath) - -// Load node_modules with error handling for any remaining issues. -const mockedNmCallback = (() => { - try { - return mockFs.load(rootNmPath) - } catch (e) { - // If loading fails due to broken symlinks or missing files, return empty mock. - console.warn( - `Warning: Failed to load node_modules for mock-fs: ${e instanceof Error ? e.message : String(e)}`, - ) - return {} + for (const [filename, value] of Object.entries(config)) { + writeEntry(filename, value) } -})() - -function mockTestFs(config: FileSystem.DirectoryItems) { - return mockFs({ - ...config, - [mockNmPath]: mockedNmCallback, - }) } describe('glob utilities', () => { afterEach(() => { - mockFs.restore() + rmSync(mockFixturePath, { recursive: true, force: true }) + }) + + beforeEach(() => { + mockFixturePath = normalizePath( + mkdtempSync(path.join(os.tmpdir(), 'glob-test-')), + ) }) describe('globWithGitIgnore()', () => { @@ -129,6 +90,23 @@ describe('glob utilities', () => { ]) }) + it('treats gitignore braces and parentheses literally', async () => { + mockTestFs({ + [`${mockFixturePath}/.gitignore`]: 'literal{a,b}.txt\nliteral(a).txt', + [`${mockFixturePath}/literal{a,b}.txt`]: '', + [`${mockFixturePath}/literal(a).txt`]: '', + [`${mockFixturePath}/literal-a.txt`]: '', + }) + + const results = await globWithGitIgnore(['**/*.txt'], { + cwd: mockFixturePath, + }) + + expect(results.map(normalizePath).sort()).toEqual([ + `${mockFixturePath}/literal-a.txt`, + ]) + }) + it('should handle negated patterns in .gitignore', async () => { mockTestFs({ [`${mockFixturePath}/.gitignore`]: 'ignored/**\n!ignored/keep.json', @@ -150,7 +128,7 @@ describe('glob utilities', () => { it('should apply filter function during streaming to reduce memory', async () => { // Create a mock filesystem with many files. - const files: FileSystem.DirectoryItems = {} + const files: Record = {} const fileCount = 100 for (let i = 0; i < fileCount; i += 1) { files[`${mockFixturePath}/file${i}.txt`] = 'content' @@ -292,7 +270,7 @@ describe('glob utilities', () => { it.skipIf(skipUnreadableDirTest)( 'skips an unreadable directory instead of throwing EACCES', async () => { - const realTmp = mkdtempSync(path.join(tmpdir(), 'socket-glob-perm-')) + const realTmp = mkdtempSync(path.join(os.tmpdir(), 'socket-glob-perm-')) const unreadable = path.join(realTmp, 'data/postgres/pgdata') try { mkdirSync(unreadable, { recursive: true }) diff --git a/src/utils/globs.mts b/src/utils/globs.mts new file mode 100644 index 0000000000..730aa9a88f --- /dev/null +++ b/src/utils/globs.mts @@ -0,0 +1,31 @@ +/** + * Package-file ignore defaults copied from @socketsecurity/registry 1.1.17 + * lib/globs.js (MIT; adapted npm-packlist defaults and Socket additions). + */ +export const defaultIgnore = Object.freeze([ + '**/.git', + '**/.npmrc', + '**/node_modules', + '**/.DS_Store', + '**/.gitignore', + '**/.hg', + '**/.lock-wscript', + '**/.npmignore', + '**/.svn', + '**/.wafpickle-*', + '**/.*.swp', + '**/._*/**', + '**/archived-packages/**', + '**/build/config.gypi', + '**/CVS', + '**/npm-debug.log', + '**/*.orig', + '**/.env', + '**/.eslintcache', + '**/.nvm', + '**/.tap', + '**/.vscode', + '**/*.tsbuildinfo', + '**/Thumbs.db', + '**/bower_components', +]) diff --git a/src/utils/is-object-object.d.mts b/src/utils/is-object-object.d.mts new file mode 100644 index 0000000000..c87d3086b6 --- /dev/null +++ b/src/utils/is-object-object.d.mts @@ -0,0 +1 @@ +export function isObjectObject(value: unknown): value is object diff --git a/src/utils/is-object-object.mjs b/src/utils/is-object-object.mjs new file mode 100644 index 0000000000..a710d853f9 --- /dev/null +++ b/src/utils/is-object-object.mjs @@ -0,0 +1,4 @@ +/** Return true for non-null object values that are not arrays. */ +export function isObjectObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) +} diff --git a/src/utils/lockfile.mts b/src/utils/lockfile.mts index 15af7fcc24..8cab4d10fd 100644 --- a/src/utils/lockfile.mts +++ b/src/utils/lockfile.mts @@ -1,6 +1,6 @@ import { existsSync } from 'node:fs' -import { readFileUtf8 } from '@socketsecurity/registry/lib/fs' +import { readFileUtf8 } from './fs.mts' export async function readLockfile( lockfilePath: string, diff --git a/src/utils/logger.mts b/src/utils/logger.mts new file mode 100644 index 0000000000..72631b097b --- /dev/null +++ b/src/utils/logger.mts @@ -0,0 +1,238 @@ +/* + * Adapted narrowly from @socketsecurity/registry 1.1.17, lib/logger.js. + * Copyright (c) 2024 Socket Inc. Licensed under the MIT License. + */ + +import { Console } from 'node:console' +import { Writable } from 'node:stream' + +import colors from 'yoctocolors-cjs' + +const isBlankString = (value: unknown): boolean => + typeof value === 'string' && (!value.length || /^\s+$/.test(value)) + +const applyLinePrefix = (value: string, prefix = ''): string => + prefix.length + ? `${prefix}${value.includes('\n') ? value.replace(/\n/g, `\n${prefix}`) : value}` + : value + +function isUnicodeSupported(): boolean { + if (process.platform !== 'win32') { + return process.env['TERM'] !== 'linux' + } + const { env } = process + if ( + env['WT_SESSION'] || + env['TERMINUS_SUBLIME'] || + env['ConEmuTask'] === '{cmd::Cmder}' + ) { + return true + } + const term = env['TERM'] + const termProgram = env['TERM_PROGRAM'] + return ( + termProgram === 'Terminus-Sublime' || + termProgram === 'vscode' || + term === 'xterm-256color' || + term === 'alacritty' || + term === 'rxvt-unicode' || + term === 'rxvt-unicode-256color' || + env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm' + ) +} + +const unicode = isUnicodeSupported() +// These getters defer reading the shared vendor chunk until a symbol is +// actually requested. This avoids observing its partial exports during the +// vendor↔utils initialization cycle in the Node SEA VFS loader. +export const LOG_SYMBOLS = Object.freeze({ + get fail(): string { + return colors.red(unicode ? '✖' : '×') + }, + get info(): string { + return colors.blue(unicode ? 'ℹ' : 'i') + }, + get success(): string { + return colors.green(unicode ? '✔' : '√') + }, + get warn(): string { + return colors.yellow(unicode ? '⚠' : '‼') + }, +}) + +export class Logger extends Console { + static LOG_SYMBOLS = LOG_SYMBOLS + + #indention = '' + #lastWasBlank = false + #logCallCount = 0 + + constructor(stdout?: Writable, stderr?: Writable, ignoreErrors?: boolean) { + super({ + stdout: stdout ?? process.stdout, + stderr: stderr ?? process.stderr, + ...(ignoreErrors === undefined ? {} : { ignoreErrors }), + }) + } + + get logCallCount(): number { + return this.#logCallCount + } + + #apply(method: 'log' | 'error', args: unknown[]): this { + const [text, ...rest] = args + const logArgs = + typeof text === 'string' + ? [applyLinePrefix(text, this.#indention), ...rest] + : args + Reflect.apply(Console.prototype[method], this, logArgs) + this.#lastWasBlank = isBlankString(logArgs[0]) + this.#logCallCount += 1 + return this + } + + #symbolApply(symbol: keyof typeof LOG_SYMBOLS, args: unknown[]): this { + const [first, ...rest] = args + const text = typeof first === 'string' ? first : '' + const extras = typeof first === 'string' ? rest : args + Console.prototype.error.call( + this, + applyLinePrefix(`${LOG_SYMBOLS[symbol]} ${text}`, this.#indention), + ...extras, + ) + this.#lastWasBlank = false + this.#logCallCount += 1 + return this + } + + override assert(value: unknown, ...message: unknown[]): this { + Console.prototype.assert.call(this, value, ...(message as any[])) + this.#lastWasBlank = false + if (!value) { + this.#logCallCount += 1 + } + return this + } + + override clear(): this { + Console.prototype.clear.call(this) + return this + } + override count(label?: string): this { + Console.prototype.count.call(this, label) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override countReset(label?: string): this { + Console.prototype.countReset.call(this, label) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override debug(...args: unknown[]): this { + return this.#apply('log', args) + } + override dir(item?: unknown, options?: object): this { + Console.prototype.dir.call(this, item, options) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override dirxml(...data: unknown[]): this { + Console.prototype.dirxml.call(this, ...data) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override error(...args: unknown[]): this { + return this.#apply('error', args) + } + override info(...args: unknown[]): this { + return this.#symbolApply('info', args) + } + override log(...args: unknown[]): this { + return this.#apply('log', args) + } + override table(tabularData?: unknown, properties?: string[]): this { + Console.prototype.table.call(this, tabularData, properties) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override time(label?: string): this { + Console.prototype.time.call(this, label) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override timeEnd(label?: string): this { + Console.prototype.timeEnd.call(this, label) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override timeLog(label?: string, ...data: unknown[]): this { + Console.prototype.timeLog.call(this, label, ...data) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override trace(...args: unknown[]): this { + Console.prototype.trace.call(this, ...args) + this.#logCallCount += 1 + this.#lastWasBlank = false + return this + } + override warn(...args: unknown[]): this { + return this.#symbolApply('warn', args) + } + + fail(...args: unknown[]): this { + return this.#symbolApply('fail', args) + } + success(...args: unknown[]): this { + return this.#symbolApply('success', args) + } + indent(spaces = 2): this { + this.#indention += ' '.repeat(Math.min(spaces, 1000)) + return this + } + dedent(spaces = 2): this { + this.#indention = this.#indention.slice(0, -spaces) + return this + } + resetIndent(): this { + this.#indention = '' + return this + } + logNewline(): this { + return this.#lastWasBlank ? this : this.log('') + } + errorNewline(): this { + return this.#lastWasBlank ? this : this.error('') + } + override group(...label: unknown[]): this { + if (label.length) { + this.log(...label) + } + this.indent() + return this + } + override groupCollapsed(...label: unknown[]): this { + return this.group(...label) + } + override groupEnd(): this { + return this.dedent() + } + createTask(name: string): { run(fn: () => T): T } { + return { + run: fn => { + this.log(name) + return fn() + }, + } + } +} + +export const logger = new Logger() diff --git a/src/utils/meow-with-subcommands.mts b/src/utils/meow-with-subcommands.mts index 930ff6258d..14d3401aea 100644 --- a/src/utils/meow-with-subcommands.mts +++ b/src/utils/meow-with-subcommands.mts @@ -2,21 +2,7 @@ import meow from 'meow' import terminalLink from 'terminal-link' import colors from 'yoctocolors-cjs' -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { logger } from '@socketsecurity/registry/lib/logger' -import { - getOwn, - hasOwn, - toSortedObject, -} from '@socketsecurity/registry/lib/objects' -import { normalizePath } from '@socketsecurity/registry/lib/path' -import { naturalCompare } from '@socketsecurity/registry/lib/sorts' -import { getCliSpinners } from '@socketsecurity/registry/lib/spinner' -import { - indentString, - trimNewlines, -} from '@socketsecurity/registry/lib/strings' - +import { joinAnd } from './arrays.mts' import { getConfigValueOrUndef, isConfigFromFlag, @@ -24,7 +10,13 @@ import { overrideConfigApiToken, } from './config.mts' import { isDebug } from './debug.mts' +import { logger } from './logger.mts' +import { getOwn, hasOwn, toSortedObject } from './objects.mts' import { getFlagListOutput, getHelpListOutput } from './output-formatting.mts' +import { normalizePath } from './path.mts' +import { naturalCompare } from './sorts.mts' +import { getCliSpinners } from './spinner.mts' +import { indentString, trimNewlines } from './strings.mts' import { socketPackageLink } from './terminal-link.mts' import constants, { API_V0_URL, @@ -701,6 +693,7 @@ export async function meowWithSubcommands( ` ${colors.italic('Aliases:')} GITHUB_TOKEN`, ' SOCKET_CLI_NO_API_TOKEN Make the default API token `undefined`', ' SOCKET_CLI_NPM_PATH The absolute location of the npm directory', + ' SOCKET_CLI_SOCKET_PATCH_PATH Executable socket-patch path for standalone builds', ' SOCKET_CLI_ORG_SLUG Specify the Socket organization slug', '', ' SOCKET_CLI_ACCEPT_RISKS Accept risks of a Socket wrapped npm/npx run', diff --git a/src/utils/npm-paths.mts b/src/utils/npm-paths.mts index ab0bcfb970..935836dd7f 100755 --- a/src/utils/npm-paths.mts +++ b/src/utils/npm-paths.mts @@ -2,9 +2,8 @@ import { existsSync } from 'node:fs' import Module from 'node:module' import path from 'node:path' -import { resolveBinPathSync } from '@socketsecurity/registry/lib/bin' -import { logger } from '@socketsecurity/registry/lib/logger' - +import { resolveBinPathSync } from './bin.mts' +import { logger } from './logger.mts' import constants, { NODE_MODULES, NPM } from '../constants.mts' import { findBinPathDetailsSync, findNpmDirPathSync } from './path-resolve.mts' diff --git a/src/utils/npm-paths.test.mts b/src/utils/npm-paths.test.mts index 6cb2c423c6..579ffea5cc 100644 --- a/src/utils/npm-paths.test.mts +++ b/src/utils/npm-paths.test.mts @@ -1,10 +1,19 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { + mkdirSync, + mkdtempSync, + realpathSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs' import os from 'node:os' import path from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { resolveBinPathSync } from './bin.mts' import { resolveBinPathToJsEntry } from './npm-paths.mts' +import { findNpmDirPathSync } from './path-resolve.mts' // A bash wrapper of the shape mise writes so it can run `mise reshim` after a // global install. It carries none of the `NPM_CLI_JS=` markers npm's own shim @@ -91,4 +100,50 @@ describe('resolveBinPathToJsEntry', () => { expect(resolveBinPathToJsEntry(binPath, 'npm-cli.js')).toBe(binPath) }) + + it.skipIf(process.platform === 'win32')( + 'resolves a shell npm symlink into its installed package before finding npm', + () => { + const install = path.join(tmpDir, 'share', 'nodejs', 'npm') + const binDir = path.join(install, 'bin') + mkdirSync(binDir, { recursive: true }) + mkdirSync(path.join(install, 'node_modules'), { recursive: true }) + writeFileSync(path.join(binDir, 'npm-cli.js'), '') + const linkDir = path.join(tmpDir, 'usr-bin') + mkdirSync(linkDir) + const link = path.join(linkDir, 'npm') + symlinkSync(path.join(binDir, 'npm-cli.js'), link) + + const resolved = resolveBinPathSync(link) + expect(resolved).toBe( + realpathSync.native(path.join(binDir, 'npm-cli.js')), + ) + expect(findNpmDirPathSync(resolved)).toBe(realpathSync.native(install)) + }, + ) + + it.skipIf(process.platform === 'win32')( + 'resolves a symlinked POSIX npm shell shim relative to the actual shim', + () => { + const install = path.join(tmpDir, 'share', 'nodejs', 'npm') + const binDir = path.join(install, 'bin') + mkdirSync(binDir, { recursive: true }) + mkdirSync(path.join(install, 'node_modules'), { recursive: true }) + writeFileSync( + path.join(binDir, 'npm'), + '#!/bin/sh\nNPM_CLI_JS="$CLI_BASEDIR/npm-cli.js"\n', + ) + writeFileSync(path.join(binDir, 'npm-cli.js'), '') + const linkDir = path.join(tmpDir, 'usr-bin') + mkdirSync(linkDir) + const link = path.join(linkDir, 'npm') + symlinkSync(path.join(binDir, 'npm'), link) + + const resolved = resolveBinPathSync(link) + expect(resolved).toBe( + realpathSync.native(path.join(binDir, 'npm-cli.js')), + ) + expect(findNpmDirPathSync(resolved)).toBe(realpathSync.native(install)) + }, + ) }) diff --git a/src/utils/objects.mts b/src/utils/objects.mts index 58e80600d1..706e1bcd44 100644 --- a/src/utils/objects.mts +++ b/src/utils/objects.mts @@ -1,3 +1,79 @@ +// Adapted from @socketsecurity/registry 1.1.17, lib/objects.js. +import { isObjectObject } from './is-object-object.mjs' +const { fromEntries: ObjectFromEntries, hasOwn: ObjectHasOwn } = Object +const { ownKeys: ReflectOwnKeys } = Reflect + +export type Remap = { [K in keyof T]: T[K] } extends infer O + ? { [K in keyof O]: O[K] } + : never + +let _localeCompare: ((a: string, b: string) => number) | undefined + +function entryKeyComparator( + a: [string | symbol, any], + b: [string | symbol, any], +): number { + if (_localeCompare === undefined) { + _localeCompare = new Intl.Collator().compare + } + const keyA = a[0] + const keyB = b[0] + const strKeyA = typeof keyA === 'string' ? keyA : String(keyA) + const strKeyB = typeof keyB === 'string' ? keyB : String(keyB) + return _localeCompare(strKeyA, strKeyB) +} + +export function getOwn(obj: any, propKey: string | symbol): any { + if (obj === null || obj === undefined) { + return undefined + } + return ObjectHasOwn(obj, propKey) ? obj[propKey] : undefined +} + +export function hasKeys(obj: any): boolean { + if (obj === null || obj === undefined) { + return false + } + for (const key in obj) { + if (ObjectHasOwn(obj, key)) { + return true + } + } + return false +} + +export function hasOwn(obj: any, propKey: string | symbol): boolean { + return obj !== null && obj !== undefined && ObjectHasOwn(obj, propKey) +} + +export function isObject(value: any): value is object { + return value !== null && typeof value === 'object' +} + +export { isObjectObject } + +export function toSortedObject>( + obj: T, +): Record { + const stringEntries: Array<[string | symbol, any]> = [] + const symbolEntries: Array<[string | symbol, any]> = [] + for (const key of ReflectOwnKeys(obj)) { + const entry = [key, obj[key as keyof T]] as [string | symbol, any] + if (typeof key === 'symbol') { + symbolEntries.push(entry) + } else { + stringEntries.push(entry) + } + } + if (!stringEntries.length && !symbolEntries.length) { + return [] as unknown as Record + } + return ObjectFromEntries([ + ...symbolEntries.sort(entryKeyComparator), + ...stringEntries.sort(entryKeyComparator), + ]) as Record +} + export function createEnum>( obj: T, ): Readonly { diff --git a/src/utils/output-formatting.mts b/src/utils/output-formatting.mts index 568162b723..c1eacfac41 100644 --- a/src/utils/output-formatting.mts +++ b/src/utils/output-formatting.mts @@ -19,14 +19,12 @@ * - Handles kebab-case conversion for flags */ -import { joinAnd } from '@socketsecurity/registry/lib/arrays' -import { isObject } from '@socketsecurity/registry/lib/objects' -import { naturalCompare } from '@socketsecurity/registry/lib/sorts' -import { indentString } from '@socketsecurity/registry/lib/strings' -import { pluralize } from '@socketsecurity/registry/lib/words' - +import { joinAnd } from './arrays.mts' +import { isObject } from './objects.mts' import { getRequirements, getRequirementsKey } from './requirements.mts' -import { camelToKebab } from './strings.mts' +import { naturalCompare } from './sorts.mts' +import { camelToKebab, indentString } from './strings.mts' +import { pluralize } from './words.mts' import type { MeowFlags } from '../flags.mts' diff --git a/src/utils/package-environment.mts b/src/utils/package-environment.mts index deb078053c..8665dfbabf 100644 --- a/src/utils/package-environment.mts +++ b/src/utils/package-environment.mts @@ -31,17 +31,16 @@ import browserslist from 'browserslist' import semver from 'semver' import { parse as parseBunLockb } from '@socketregistry/hyrious__bun.lockb/index.cjs' -import { resolveBinPathSync, whichBin } from '@socketsecurity/registry/lib/bin' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { readFileBinary, readFileUtf8 } from '@socketsecurity/registry/lib/fs' -import { Logger } from '@socketsecurity/registry/lib/logger' -import { readPackageJson } from '@socketsecurity/registry/lib/packages' -import { naturalCompare } from '@socketsecurity/registry/lib/sorts' -import { spawn } from '@socketsecurity/registry/lib/spawn' -import { isNonEmptyString } from '@socketsecurity/registry/lib/strings' +import { resolveBinPathSync, whichBin } from './bin.mts' import { cmdPrefixMessage } from './cmd.mts' -import { findUp } from './fs.mts' +import { debugDir, debugFn } from './debug.mts' +import { findUp, readFileBinary, readFileUtf8 } from './fs.mts' +import { Logger } from './logger.mts' +import { readPackageJson } from './packages.mts' +import { naturalCompare } from './sorts.mts' +import { spawn } from './spawn.mts' +import { isNonEmptyString } from './strings.mts' import constants, { FLAG_VERSION, PACKAGE_LOCK_JSON, @@ -50,8 +49,8 @@ import constants, { } from '../constants.mts' import type { CResult } from '../types.mts' -import type { Remap } from '@socketsecurity/registry/lib/objects' -import type { EditablePackageJson } from '@socketsecurity/registry/lib/packages' +import type { Remap } from './objects.mts' +import type { EditablePackageJson } from './packages.mts' import type { SemVer } from 'semver' const { @@ -390,8 +389,9 @@ export async function detectPackageEnvironment({ : undefined // Read Corepack `packageManager` field in package.json: // https://nodejs.org/api/packages.html#packagemanager - const pkgManager = isNonEmptyString(editablePkgJson?.content?.packageManager) - ? editablePkgJson.content.packageManager + const packageManager = editablePkgJson?.content['packageManager'] + const pkgManager = isNonEmptyString(packageManager) + ? packageManager : undefined let agent: Agent | undefined @@ -436,7 +436,9 @@ export async function detectPackageEnvironment({ let pkgMinAgentVersion = minSupportedAgentVersion let pkgMinNodeVersion = minSupportedNodeVersion if (editablePkgJson?.content) { - const { engines } = editablePkgJson.content + const engines = editablePkgJson.content['engines'] as + | Record + | undefined const engineAgentRange = engines?.[agent] const engineNodeRange = engines?.['node'] if (isNonEmptyString(engineAgentRange)) { diff --git a/src/utils/packages.mts b/src/utils/packages.mts new file mode 100644 index 0000000000..d238aa6fd0 --- /dev/null +++ b/src/utils/packages.mts @@ -0,0 +1,209 @@ +/** + * Narrow package helpers adapted from @socketsecurity/registry 1.1.17 + * lib/packages.js. Copyright (c) 2024 Socket Inc. Licensed under MIT. + */ + +import { promises as fs } from 'node:fs' +import path from 'node:path' + +import { readJson, readJsonSync } from './fs.mts' +import { PackageURL } from './purl.mts' +import manifest from './registry-manifest.json' with { type: 'json' } +import { stripBom } from './strings.mts' + +export type PackageJson = { + name?: string | undefined + version?: string | undefined + dependencies?: Record | undefined + devDependencies?: Record | undefined + optionalDependencies?: Record | undefined + peerDependencies?: Record | undefined + workspaces?: unknown + [key: string]: unknown +} + +/** Structural surface consumed from @npmcli/package-json's editable instance. */ +export type EditablePackageJson = { + content: PackageJson + update(data: Partial): EditablePackageJson + fromJSON(data: string | PackageJson): EditablePackageJson + save(options?: { + ignoreWhitespace?: boolean + sort?: boolean + }): Promise +} + +type ReadOptions = { + throws?: boolean | undefined + editable?: boolean | undefined +} + +function packageJsonPath(filepath: string): string { + return filepath.endsWith('package.json') + ? filepath + : path.join(filepath, 'package.json') +} + +export function readPackageJson( + filepath: string, + options: ReadOptions & { editable: true }, +): Promise +export function readPackageJson( + filepath: string, + options?: ReadOptions & { editable?: false | undefined }, +): Promise +export async function readPackageJson( + filepath: string, + options?: ReadOptions, +): Promise { + const filename = packageJsonPath(filepath) + if (!options?.editable) { + return (await readJson( + filename, + options?.throws === undefined ? undefined : { throws: options.throws }, + )) as PackageJson | null + } + let source: string + try { + source = await fs.readFile(filename, 'utf8') + } catch (error) { + if (options.throws !== false) { + throw error + } + return undefined + } + let parsed: PackageJson + try { + parsed = JSON.parse(stripBom(source)) as PackageJson + } catch (error) { + if (options.throws !== false) { + throw error + } + return undefined + } + return createEditablePackageJson(filename, parsed, source) +} + +export function readPackageJsonSync( + filepath: string, + options?: ReadOptions, +): PackageJson | null { + return readJsonSync( + packageJsonPath(filepath), + options?.throws === undefined ? undefined : { throws: options.throws }, + ) as PackageJson | null +} + +function createEditablePackageJson( + filename: string, + initial: PackageJson, + source: string, +): EditablePackageJson { + let content = { ...initial } + const bom = source.startsWith('\uFEFF') ? '\uFEFF' : '' + const indent = /\n([ \t]+)"/.exec(source)?.[1] ?? ' ' + const eol = source.includes('\r\n') ? '\r\n' : '\n' + const editable: EditablePackageJson = { + get content() { + return content + }, + update(data) { + for (const [key, value] of Object.entries(data)) { + if (value === undefined) { + delete content[key] + } else { + content[key] = value + } + } + return editable + }, + fromJSON(data) { + content = { + ...(typeof data === 'string' + ? (JSON.parse(data) as PackageJson) + : data), + } + return editable + }, + async save(saveOptions) { + const next = saveOptions?.sort + ? Object.fromEntries( + Object.entries(content).sort(([a], [b]) => a.localeCompare(b)), + ) + : content + const text = + bom + `${JSON.stringify(next, null, indent)}\n`.replaceAll('\n', eol) + if (saveOptions?.ignoreWhitespace && text.trim() === source.trim()) { + return false + } + if (text === source) { + return false + } + await fs.writeFile(filename, text) + source = text + content = { ...next } + return true + }, + } + return editable +} + +export function resolvePackageName( + purlObj: { name: string; namespace?: string | undefined }, + delimiter = '/', +): string { + const { name, namespace } = purlObj + return `${namespace ? `${namespace}${delimiter}` : ''}${name}` +} + +export type RegistryManifestData = { + name: string + package: string + version: string + [key: string]: unknown +} +export type RegistryManifestEntry = readonly [ + purl: string, + data: RegistryManifestData, +] + +/** + * Returns Socket registry metadata for an ecosystem or package name. + * + * Dataset copied verbatim from @socketsecurity/registry 1.1.17 manifest.json. + * Copyright (c) 2024 Socket Inc. MIT License (see registry LICENSE). + * Refresh by comparing the upstream manifest and reviewing the dataset diff. + */ +export function getManifestData( + ecosystem: string, +): readonly RegistryManifestEntry[] | undefined +export function getManifestData( + ecosystem: string, + sockRegPkgName: string, +): RegistryManifestData | undefined +export function getManifestData(): Record< + string, + readonly RegistryManifestEntry[] +> +export function getManifestData( + ecosystem?: string, + sockRegPkgName?: string, +): + | Record + | readonly RegistryManifestEntry[] + | RegistryManifestData + | undefined { + const entries = manifest as unknown as Record< + string, + readonly RegistryManifestEntry[] + > + if (ecosystem) { + const ecosystemEntries = entries[ecosystem] + return sockRegPkgName + ? ecosystemEntries?.find( + ([purl]) => PackageURL.fromString(purl).name === sockRegPkgName, + )?.[1] + : ecosystemEntries + } + return manifest as unknown as Record +} diff --git a/src/utils/packages.test.mts b/src/utils/packages.test.mts new file mode 100644 index 0000000000..0a8e49db97 --- /dev/null +++ b/src/utils/packages.test.mts @@ -0,0 +1,33 @@ +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { readPackageJson } from './packages.mts' + +describe('editable package.json', () => { + let dir: string | undefined + + afterEach(async () => { + if (dir) { + await fs.rm(dir, { recursive: true, force: true }) + } + dir = undefined + }) + + it('reads and saves UTF-8 BOM input while preserving indentation and line endings', async () => { + dir = await fs.mkdtemp(path.join(os.tmpdir(), 'socket-package-bom-')) + const filename = path.join(dir, 'package.json') + const source = '\uFEFF{\r\n "name": "before"\r\n}\r\n' + await fs.writeFile(filename, source) + + const editable = await readPackageJson(filename, { editable: true }) + expect(editable?.content.name).toBe('before') + editable?.update({ name: 'after' }) + expect(await editable?.save()).toBe(true) + expect(await fs.readFile(filename, 'utf8')).toBe( + '\uFEFF{\r\n "name": "after"\r\n}\r\n', + ) + }) +}) diff --git a/src/utils/path-resolve.mts b/src/utils/path-resolve.mts index 12dd96ad3f..79428bb1fe 100644 --- a/src/utils/path-resolve.mts +++ b/src/utils/path-resolve.mts @@ -1,12 +1,8 @@ import { existsSync } from 'node:fs' import path from 'node:path' -import { - resolveBinPathSync, - whichBinSync, -} from '@socketsecurity/registry/lib/bin' -import { isDirSync } from '@socketsecurity/registry/lib/fs' - +import { resolveBinPathSync, whichBinSync } from './bin.mts' +import { isDirSync } from './fs.mts' import constants, { NODE_MODULES, NPM } from '../constants.mts' import { createSupportedFilesFilter, diff --git a/src/utils/path-resolve.test.mts b/src/utils/path-resolve.test.mts index 2bbbb6d9a1..4bc4d399f9 100644 --- a/src/utils/path-resolve.test.mts +++ b/src/utils/path-resolve.test.mts @@ -1,14 +1,11 @@ -import { existsSync, readdirSync, rmSync } from 'node:fs' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import os from 'node:os' import path from 'node:path' -import { fileURLToPath } from 'node:url' -import mockFs from 'mock-fs' -import { afterEach, describe, expect, it } from 'vitest' - -import { normalizePath } from '@socketsecurity/registry/lib/path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { normalizePath } from './path.mts' import { - NODE_MODULES, PACKAGE_JSON, PACKAGE_LOCK_JSON, PNPM_LOCK_YAML, @@ -16,62 +13,23 @@ import { } from '../constants.mjs' import { getPackageFilesForScan } from './path-resolve.mts' -import type FileSystem from 'mock-fs/lib/filesystem' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) +let mockFixturePath = '' -const rootNmPath = path.join(__dirname, '../..', NODE_MODULES) -const mockFixturePath = normalizePath(path.join(__dirname, 'mock')) -const mockNmPath = normalizePath(rootNmPath) - -// Remove broken symlinks in node_modules before loading to prevent mock-fs errors. -function cleanupBrokenSymlinks(dirPath: string): void { - try { - if (!existsSync(dirPath)) { - return - } - const entries = readdirSync(dirPath, { withFileTypes: true }) - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name) - try { - if (entry.isSymbolicLink() && !existsSync(fullPath)) { - // Symlink exists but target does not, remove it. - rmSync(fullPath, { force: true }) - } else if (entry.isDirectory()) { - // Recursively check subdirectories. - cleanupBrokenSymlinks(fullPath) - } - } catch { - // Ignore errors for individual entries. +function mockTestFs(config: Record): void { + const writeEntry = (filename: string, value: unknown): void => { + if (value && typeof value === 'object' && !Buffer.isBuffer(value)) { + mkdirSync(filename, { recursive: true }) + for (const [name, child] of Object.entries(value)) { + writeEntry(path.join(filename, name), child) } + return } - } catch { - // If we cannot read the directory, skip cleanup. + mkdirSync(path.dirname(filename), { recursive: true }) + writeFileSync(filename, value as string | Uint8Array) } -} - -// Clean up broken symlinks before loading node_modules. -cleanupBrokenSymlinks(rootNmPath) - -// Load node_modules with error handling for any remaining issues. -const mockedNmCallback = (() => { - try { - return mockFs.load(rootNmPath) - } catch (e) { - // If loading fails due to broken symlinks or missing files, return empty mock. - console.warn( - `Warning: Failed to load node_modules for mock-fs: ${e instanceof Error ? e.message : String(e)}`, - ) - return {} + for (const [filename, value] of Object.entries(config)) { + writeEntry(filename, value) } -})() - -function mockTestFs(config: FileSystem.DirectoryItems) { - return mockFs({ - ...config, - [mockNmPath]: mockedNmCallback, - }) } const globPatterns = { @@ -135,7 +93,13 @@ const sortedGetPackageFilesFullScans = sortedPromise(getPackageFilesForScan) describe('Path Resolve', () => { afterEach(() => { - mockFs.restore() + rmSync(mockFixturePath, { recursive: true, force: true }) + }) + + beforeEach(() => { + mockFixturePath = normalizePath( + mkdtempSync(path.join(os.tmpdir(), 'path-resolve-test-')), + ) }) describe('getPackageFilesForScan()', () => { diff --git a/src/utils/path.mts b/src/utils/path.mts new file mode 100644 index 0000000000..2d97285ea7 --- /dev/null +++ b/src/utils/path.mts @@ -0,0 +1,100 @@ +/** + * Path helpers adapted from @socketsecurity/registry/lib/path.js. + * Copyright (c) 2024 Socket Inc. Licensed under the MIT License. + */ + +import { Buffer } from 'node:buffer' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const slashRegExp = /[/\\]/ + +function pathLikeToString(pathLike: string | Buffer | URL | unknown): string { + if (typeof pathLike === 'string') { + return pathLike + } + if (Buffer.isBuffer(pathLike)) { + return pathLike.toString('utf8') + } + if (pathLike instanceof URL) { + return fileURLToPath(pathLike) + } + return String(pathLike) +} + +export function isPath(pathLike: unknown): boolean { + return isRelative(pathLike) || path.isAbsolute(pathLike as string) +} + +function isRelative(pathLike: unknown): boolean { + const filepath = pathLikeToString(pathLike) + const { length } = filepath + if (length === 0) { + return false + } + if (filepath.charCodeAt(0) === 46 /* . */) { + if (length === 1) { + return true + } + let code = filepath.charCodeAt(1) + if (code === 46 /* . */) { + code = filepath.charCodeAt(2) + } + return code === 47 /* / */ || code === 92 /* \\ */ + } + return false +} + +export function normalizePath(pathLike: string | Buffer | URL): string { + const filepath = pathLikeToString(pathLike) + const { length } = filepath + if (length < 2) { + return length === 1 && filepath.charCodeAt(0) === 92 /* \\ */ + ? '/' + : filepath + } + + let code = 0 + let collapsed = '' + let start = 0 + let prefix = '' + if (length > 4 && filepath.charCodeAt(3) === 92 /* \\ */) { + const code2 = filepath.charCodeAt(2) + if ( + (code2 === 63 /* ? */ || code2 === 46) /* . */ && + filepath.charCodeAt(0) === 92 /* \\ */ && + filepath.charCodeAt(1) === 92 /* \\ */ + ) { + start = 2 + prefix = '//' + } + } + if (start === 0) { + while (((code = filepath.charCodeAt(start)), code === 47 || code === 92)) { + start += 1 + } + if (start) { + prefix = '/' + } + } + let nextIndex = filepath.slice(start).search(slashRegExp) + if (nextIndex === -1) { + return prefix + filepath.slice(start) + } + nextIndex += start + while (nextIndex !== -1) { + const segment = filepath.slice(start, nextIndex) + collapsed = collapsed + (collapsed.length === 0 ? '' : '/') + segment + start = nextIndex + 1 + while (((code = filepath.charCodeAt(start)), code === 47 || code === 92)) { + start += 1 + } + const found = filepath.slice(start).search(slashRegExp) + nextIndex = found === -1 ? -1 : start + found + } + const lastSegment = filepath.slice(start) + if (lastSegment.length !== 0) { + collapsed = collapsed + '/' + lastSegment + } + return prefix + collapsed +} diff --git a/src/utils/pnpm-paths.mts b/src/utils/pnpm-paths.mts index 12316120c9..41b4e10102 100644 --- a/src/utils/pnpm-paths.mts +++ b/src/utils/pnpm-paths.mts @@ -15,8 +15,7 @@ * - Prevents repeated PATH searches */ -import { logger } from '@socketsecurity/registry/lib/logger' - +import { logger } from './logger.mts' import { findBinPathDetailsSync } from './path-resolve.mts' function exitWithBinPathError(binName: string): never { diff --git a/src/utils/pnpm.mts b/src/utils/pnpm.mts index 965bbd641e..1cadee2d46 100644 --- a/src/utils/pnpm.mts +++ b/src/utils/pnpm.mts @@ -3,11 +3,10 @@ import { existsSync } from 'node:fs' import yaml from 'js-yaml' import semver from 'semver' -import { readFileUtf8 } from '@socketsecurity/registry/lib/fs' -import { isObjectObject } from '@socketsecurity/registry/lib/objects' -import { stripBom } from '@socketsecurity/registry/lib/strings' - +import { readFileUtf8 } from './fs.mts' +import { isObjectObject } from './objects.mts' import { idToNpmPurl } from './spec.mts' +import { stripBom } from './strings.mts' import type { LockfileObject, PackageSnapshot } from '@pnpm/lockfile.fs' import type { SemVer } from 'semver' diff --git a/src/utils/promises.mts b/src/utils/promises.mts new file mode 100644 index 0000000000..298a0f76c9 --- /dev/null +++ b/src/utils/promises.mts @@ -0,0 +1,108 @@ +/** Minimal bounded retry helper adapted from @socketsecurity/registry 1.1.17 lib/promises.js (MIT). */ + +import { setTimeout as delay } from 'node:timers/promises' + +/** Execute callbacks in bounded, sequential chunks, matching registry pEach's consumed surface. */ +/* eslint-disable no-await-in-loop -- Sequential promise helpers guarantee ordered settlement. */ +export async function pEach( + values: readonly T[], + callback: (value: T) => unknown | Promise, + options: { concurrency?: number | undefined } = {}, +): Promise { + const concurrency = options.concurrency ?? (values.length || 1) + if (!Number.isInteger(concurrency) || concurrency < 1) { + throw new RangeError( + `Expected concurrency to be a positive integer, got ${concurrency}`, + ) + } + for (let index = 0; index < values.length; index += concurrency) { + await Promise.all(values.slice(index, index + concurrency).map(callback)) + } +} + +export type RetryOptions = { + args?: readonly unknown[] + backoffFactor?: number + baseDelayMs?: number + jitter?: boolean + maxDelayMs?: number + onRetry?: (attempt: number, error: unknown, delayMs: number) => unknown + onRetryCancelOnFalse?: boolean + onRetryRethrow?: boolean + retries?: number + signal?: AbortSignal +} + +export function pRetry( + callback: (...args: any[]) => Promise | T, + options?: RetryOptions & { signal?: undefined }, +): Promise +export function pRetry( + callback: (...args: any[]) => Promise | T, + options: RetryOptions & { signal: AbortSignal }, +): Promise +export async function pRetry( + callback: (...args: any[]) => Promise | T, + options: RetryOptions = {}, +): Promise { + const { + args = [], + backoffFactor = 2, + baseDelayMs = 200, + jitter = true, + maxDelayMs = 10_000, + onRetry, + onRetryCancelOnFalse = false, + onRetryRethrow = false, + retries = 0, + signal, + } = options + if (signal?.aborted) { + return undefined + } + let backoffDelay = baseDelayMs + let firstError: unknown + let hasError = false + for (let attempt = 0; attempt <= retries && !signal?.aborted; attempt += 1) { + try { + return await callback(...args, { signal }) + } catch (error) { + if (!hasError) { + firstError = error + hasError = true + } + if (attempt === retries) { + break + } + const wait = Math.min( + backoffDelay + (jitter ? Math.floor(Math.random() * backoffDelay) : 0), + maxDelayMs, + ) + try { + if ( + onRetry?.(attempt + 1, error, wait) === false && + onRetryCancelOnFalse + ) { + break + } + } catch (retryError) { + if (onRetryRethrow) { + throw retryError + } + } + try { + await delay(wait, undefined, { signal }) + } catch (error) { + if (signal?.aborted) { + return undefined + } + throw error + } + backoffDelay = Math.min(backoffDelay * backoffFactor, maxDelayMs) + } + } + if (hasError) { + throw firstError + } + return undefined +} diff --git a/src/utils/prompts.mts b/src/utils/prompts.mts new file mode 100644 index 0000000000..3eee40107d --- /dev/null +++ b/src/utils/prompts.mts @@ -0,0 +1,84 @@ +/* + * Local prompt adapter copied narrowly from @socketsecurity/registry 1.1.17 + * (lib/prompts.js). Copyright (c) 2024 Socket Inc., MIT License. + * The prompt implementations remain the bundled @inquirer modules; only the + * Socket context, spinner lifecycle, abort behavior, and result normalization + * are kept here. + */ + +import confirmPrompt from '@inquirer/confirm' +import inputPrompt from '@inquirer/input' +import passwordPrompt from '@inquirer/password' +import selectPrompt, { Separator as InquirerSeparator } from '@inquirer/select' + +import spinner from './spinner.mts' + +type PromptContext = { signal?: AbortSignal; spinner?: typeof spinner } +type PromptSeparator = InstanceType +type PromptOptions = { + message?: string + choices?: ReadonlyArray | PromptSeparator> + [key: string]: unknown +} + +export type Choice = { + value: Value + disabled?: boolean | string + description?: string + name?: string + short?: string + type?: never +} + +const abortController = new AbortController() +for (const signal of ['SIGHUP', 'SIGINT', 'SIGTERM'] as const) { + process.once(signal, () => abortController.abort()) +} +process.once('exit', () => abortController.abort()) + +type PromptImplementation = ( + config: never, + context?: never, +) => Promise + +function wrapPrompt(prompt: PromptImplementation) { + return async ( + config: PromptOptions, + context?: PromptContext, + ): Promise => { + const { spinner: activeSpinner = spinner, ...contextWithoutSpinner } = + context ?? {} + const promptContext = { + signal: abortController.signal, + ...contextWithoutSpinner, + } + const wasSpinning = !!activeSpinner?.isSpinning + activeSpinner?.stop() + let result: Result | undefined + try { + result = await ( + prompt as unknown as ( + config: PromptOptions, + context?: PromptContext, + ) => Promise + )(config, promptContext) + } catch (error) { + if (error instanceof TypeError) { + throw error + } + } + if (wasSpinning) { + activeSpinner?.start() + } + return typeof result === 'string' ? (result.trim() as Result) : result + } +} + +export const confirm = wrapPrompt(confirmPrompt) +export const input = wrapPrompt(inputPrompt) +export const password = wrapPrompt(passwordPrompt) +export const select = wrapPrompt(selectPrompt) as ( + config: PromptOptions, + context?: PromptContext, +) => Promise +export { InquirerSeparator as Separator } diff --git a/src/utils/purl-legacy-npm-names.json b/src/utils/purl-legacy-npm-names.json new file mode 100644 index 0000000000..2ce01e2523 --- /dev/null +++ b/src/utils/purl-legacy-npm-names.json @@ -0,0 +1,2847 @@ +{ + "source": "npm/validate-npm-package-name v6 legacy names", + "copyright": "Copyright (c) 2015, npm, Inc", + "license": "ISC", + "licenseNotice": "ISC License\n\nCopyright (c) 2015, npm, Inc\n\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.", + "names": [ + "@antoinerey/comp-Fetch", + "@antoinerey/comp-VideoPlayer", + "@beisen/Accordion", + "@beisen/Approve", + "@beisen/AreaSelector", + "@beisen/AutoComplete", + "@beisen/AutoTree", + "@beisen/BaseButton", + "@beisen/Beaute", + "@beisen/BeisenCloudMobile", + "@beisen/BeisenCloudUI", + "@beisen/ButtonGroup", + "@beisen/ChaosUI", + "@beisen/ChaosUI-V1", + "@beisen/CheckboxList", + "@beisen/CommonMount", + "@beisen/CommonPop", + "@beisen/DataGrid", + "@beisen/DateTime", + "@beisen/DropDownButton", + "@beisen/DropDownList", + "@beisen/ExtendComponent", + "@beisen/FormUploader", + "@beisen/IconButton", + "@beisen/Loading", + "@beisen/MultiSelect", + "@beisen/NaDeStyle", + "@beisen/Paging", + "@beisen/PopLayer", + "@beisen/RadioList", + "@beisen/ReactTransformTenchmark", + "@beisen/Search", + "@beisen/selectedComponent", + "@beisen/Sidebar", + "@beisen/StaticFormLabel", + "@beisen/TabComponent", + "@beisen/Textarea", + "@beisen/Textbox", + "@beisen/TimePicker", + "@beisen/TitaFeed", + "@beisen/ToolTip", + "@beisen/Transfer", + "@beisen/Tree", + "@beisen/UserSelector", + "@chasidic/tsSchema", + "@chymz/DaStrap", + "@chymz/DaUsers", + "@claviska/jquery-ajaxSubmit", + "@cryptolize/FileSaver", + "@djforth/I18n_helper", + "@dostolu/baseController", + "@dostolu/exctractIntl", + "@dostolu/mongooseSlug", + "@dostolu/validationTransformer", + "@opam-alpha/ANSITerminal", + "@opam-alpha/BetterErrors", + "@opam-alpha/reactiveData", + "@pioug/MidiConvert", + "@smuuf/idleCat", + "@sycoraxya/validateJS", + "@tempest/endWhen", + "@tempest/fromPromise", + "@tempest/replaceError", + "@tempest/startWith", + "@tempest/throwError", + "@yuanhao/draft-js-mentionHashtag-plugin", + "3dBinPack", + "3DViewerComponent", + "4meFirst-github-example", + "9Wares-js", + "37FIS", + "A", + "ABAValidator", + "ABCEnd", + "AbokyBot", + "Accessor", + "Accessor_MongoDB", + "Accessor_MySQL", + "Accessor_Singleton", + "Account", + "accumulateArray", + "ACCUPLACERClient", + "AccuplacerClient", + "Acid", + "activaDocs", + "ActiveResource.js", + "ADBCordovaAnalytics", + "addTimeout", + "AdultJS", + "AesUtil", + "AgentX", + "AirBridgePlugin", + "airLogger", + "ajiThird", + "alaGDK", + "AlarmClock", + "alarmClock", + "Alchemyst", + "AlertLogic", + "alertsXYZ", + "ali-topSdk", + "AliceBot", + "alinkRNTest", + "aliOcrIdCard", + "AllCal.WebApp", + "alpacaDash", + "AmateurJS", + "AMD", + "AMGCryptLib", + "AmILate", + "AmILateAnand", + "amitTest", + "AmpCoreApi", + "amProductsearch", + "amqpWrapper", + "amrToMp3", + "angular-autoFields-bootstrap", + "angular-dateParser", + "angular-GAPI", + "angular-PubSub", + "Angular-test-child", + "Angular1", + "Angular2", + "angular2-Library", + "angular2-localStorage", + "angular2-Menu", + "angular2-quickstart-ngSemantic", + "angularApp", + "angularCubicColorPicker", + "angularjs-ES6-brunch-seed", + "angularjsSlider", + "AngularStompDK", + "Animated_GIF", + "animateJs", + "animateSCSS", + "AnimationFrame", + "AnimIt", + "Anirudhnodeapp", + "Anjali", + "annoteJS", + "ANSIdom", + "antFB", + "antFB-init", + "antFB-mobile", + "antFB-router-redux-ie8", + "AntMobileUI", + "AnToast", + "Antony", + "aoIoHw90B5sE1wG9", + "API-Documentation", + "APIConnect", + "APICreatorSDK", + "APlan", + "APM-mouse", + "APM.P2H", + "apMigStats", + "AporaPushNotification", + "App2App", + "applqpakTest", + "AppTracker", + "AQ", + "ArcusNode", + "AriesNode", + "array_handler_liz_Li", + "Array.prototype.forEachAsync", + "ArrayBuffer-shim", + "arrayFuncs", + "ArrowAulaExpress", + "Article-collider-packages", + "Arunkumar-Angular-Trial", + "asEvented", + "asJam", + "ASP.NET", + "assert", + "AssetPipeline", + "assignment2-BW", + "Assignment6", + "async_hooks", + "asyncBuilder", + "asyncEJS", + "AsyncHttpRequest-CordovaPlugin", + "AsyncProxy", + "AsyncStorage", + "asyncStorage", + "atom-C", + "atom-Fe", + "atom-Ge", + "atom-K", + "atom-Li", + "atom-Na", + "atom-Pb", + "atom-Rb", + "atom-Si", + "atom-Sn", + "AulaExpress", + "austin-vertebraeTest", + "authorStats", + "AutoFixture", + "autoLoader", + "AutoReact", + "AutoTasks", + "Autowebpcss", + "Avifors", + "AVNjs", + "AwesomeProject", + "AWSS3Drive", + "ax-rmdirRecursive", + "b_Tap", + "Babel", + "babel-preset-reactTeam", + "Bablic_Seo_SDK", + "BablicLogger", + "Backbone-Collection-Predefined-Filters", + "Backbone.Aggregator", + "backbone.browserStorage", + "Backbone.Chosen", + "Backbone.Marionette.Handlebars", + "Backbone.Mutators", + "Backbone.Overview", + "Backbone.Rpc", + "Backbone.Subset", + "baDataModel", + "Bag", + "BaiduMapManager", + "BandGravity", + "bangDM", + "banking-Josh-demo", + "BankWebservice", + "bannerFlip", + "BaremetricsCalendar", + "Barfer", + "BarneyRubble", + "Base", + "Base64", + "baseProject", + "Basic-Material-framework", + "BasicCredentials", + "basicFFmpeg", + "bbArray", + "Beegee", + "begineer_Practice", + "beijingDate", + "bem-countMaster", + "bem-countSlave", + "bem-getHistory", + "Bestpack", + "betterMatch", + "BetterRegExp", + "Bhellyer", + "BHP_MSD", + "BiDirectionalScrollingTable", + "BigAssFansAPI", + "BigInt", + "BIMserverWrapper", + "Binary-search-tree", + "binarySearch", + "bindAll", + "BinHeap", + "biojs-vis-RDFSchema", + "Biolac", + "Birbal", + "BitSetModule", + "BizzStream", + "Blackfeather", + "BlackMirror", + "Blacksmith", + "blacktea.jsonTemplates", + "Blaggie-System", + "BlankUp", + "Blink1Control2", + "blitzLib", + "Blob", + "BlobBuilder", + "BlobBuilder-browser", + "Blog", + "BlueOcean", + "BlueOps", + "Blueprint-Sugar", + "bluthLBC", + "blya!", + "BMFE_scaffold", + "Bmodule", + "Bo-colors-project", + "Boilerpipe-Scraper", + "Bondlib", + "bonTemplate", + "BootSideMenu", + "bornCordova", + "Botcord", + "Bottr-cli", + "Brackets", + "brain***_games***", + "Brave", + "BrewCore", + "BrianPingPong", + "BrianSuperComponents", + "BrickPlus", + "Brocket", + "Brosec", + "browserProxy", + "browserType", + "brush-Makefile", + "bTap", + "BtMacAddress", + "BubbleJS", + "Buffer", + "buffer", + "BufferList", + "Bugay", + "Build", + "BuildBox", + "Builder", + "Builders", + "BuildWithJavascript", + "BusinessObjects", + "Button", + "Buttons", + "Bynd", + "ByteBuffer", + "C9js", + "Cache-Service-Collector", + "Cacher", + "callbackQueue", + "CallbackRouter", + "callBlock-plugin", + "callBlock.plugin", + "camcardPlugin", + "CameraPreview", + "Canteen", + "canvas-toBlob", + "canvasColorPicker", + "Caoutchouc", + "Cap", + "Carbon", + "cardsJS", + "Cartogram-Utils", + "cascadeDrop", + "Cashew", + "Cat4D", + "catchTender", + "CategoryJS", + "catl-deploySSH", + "cbNetwork", + "CbolaInfra", + "CBQueue", + "CBuffer", + "ccNetViz", + "ccPagination", + "ccTpl", + "censoreMio", + "Censorify", + "censorify_Publish20160706", + "censorify_Vincent_Choe", + "censorifyAD", + "censorifyAshes", + "censorifyGuangyi", + "censorifyKatKat", + "censorifyRayL", + "censorifyTM", + "CETEIcean", + "cfUtilityService", + "CFViews", + "chadschwComponentTest0001", + "changelogFDV", + "Changling-dom", + "CharLS.js", + "Chart.Annotation.js", + "Chart.CallBack.js", + "Chart.Crosshairs.js", + "Chart.HorizontalBar.js", + "Chart.Smith.js", + "Chart.Zoom.drag.js", + "Chart.Zoom.js", + "ChartTime", + "chatSocketIo", + "ChattingRoom", + "checkForModuleDuplicates", + "cheferizeIt", + "chenouTestNode", + "child_process", + "chowYen", + "chrome-localIp", + "ChuckCSS", + "ChuckNorrisException", + "chunkArray", + "cjdsComponents", + "Class", + "Classy", + "clearInterval", + "ClearSilver", + "clearTimeout", + "CLI-todo", + "CLI-UI", + "cliappRafa", + "clientFrontEnd", + "ClientStorage", + "clipDouban", + "ClipJS", + "CloudMusicCover", + "CloudStore", + "Cls", + "cluster", + "CM-react-native-document-picker", + "CM1", + "coberturaJS", + "codeStr", + "Coeus", + "COFFEENODE", + "Coflux", + "colegislate-DynamoDbEventRepository", + "ColeTownsend", + "collabProvidesModules", + "CollectionMap", + "colWidth.js", + "com.emsaeng.cordova.plugin.AdMob", + "com.nickreed.cordova.plugin.brotherPrinter", + "com.none.alarmClock", + "com.zwchen.firstPlugin", + "com.zwchen.qqAdvice", + "combineJS", + "CometJS", + "Comfy", + "Comments", + "CommentsJS", + "comp-Fetch", + "Company", + "compareStrings", + "CompassSM", + "Complex", + "componentDoc", + "componentDoc-cli", + "CompoundSignal", + "Compress-CSS", + "Compression", + "concatAll", + "Concur", + "ConfluencePageAttacher", + "ConnectTheDotsDesktop", + "Console", + "console", + "constants", + "constelation-Animate_", + "constelation-BackgroundImage", + "constelation-Block", + "constelation-Button", + "constelation-Col", + "constelation-Event_", + "constelation-Flex", + "constelation-Inline", + "constelation-InlineBlock", + "constelation-InlineCol", + "constelation-InlineFlex", + "constelation-InlineRow", + "constelation-Painter", + "constelation-Row", + "constelation-Style_", + "constelation-Text", + "constelation-Video", + "constelation-View", + "ConstraintNetwork", + "ContactMe", + "ContentEdit", + "ContentSelect", + "ContentTools", + "convertPinyin", + "CoolBeans", + "Coolhelper", + "copyMe", + "cordova-plugin-adPlayCafebazaar", + "cordova-plugin-adPlayPushe", + "cordova-plugin-bluetoothClassic-serial", + "cordova-plugin-coolFunction", + "cordova-plugin-euroart93-smartConfig", + "cordova-plugin-ios-android-IAP", + "cordova-plugin-LineLogin", + "Cordova-Plugin-OpenTok-JBS", + "cordova-plugin-permissionScope", + "cordova-plugin-SchaffrathWebviewer", + "cordova-plugin-SDKAW", + "Cordova-Plugin-SystemBarDimmer", + "cordova-plugin-YtopPlugin", + "Cordova-react-redux-boilerplate", + "cordova-StarIO-plugin", + "CordovaSMS", + "CordovaWebSocketClientCert", + "coreApi", + "CornerCut", + "CornerJob", + "CorrespondenceAnalysis", + "cosBuffer", + "cosTask", + "Couch-cleaner", + "Couchbase-sync-gateway-REST", + "CouchCover", + "CouchDBChanges", + "CouchDBExternal", + "CountAdd_000001", + "cPlayer", + "cqjPack", + "Crawler", + "Create-React-App-SCSS-HMR", + "createClass", + "createDOC", + "createNpm", + "createServer", + "CRMWebAPI", + "crockpot-fromBinary", + "crockpot-fromEnglish", + "crockpot-fromRoman", + "crockpot-toEnglish", + "crockpot-toRoman", + "Cron", + "CropSr", + "crypto", + "CSDebug", + "CSDLParser", + "CSLogger", + "CSSMatrix", + "CSSselect", + "Csster", + "CSSwhat", + "CSV-JS", + "CTP_MARKET_DATA", + "cttv.bubblesView", + "cttv.diseaseGraph", + "cttv.expansionView", + "cttv.flowerView", + "cttv.speciesIcons", + "cttv.targetAssociationsBubbles", + "cttv.targetAssociationsTree", + "cttv.targetGeneTree", + "Cuber", + "cubicColorPicker", + "Cui-Dialog", + "CustomCamera", + "customComponent", + "customLibrary", + "CustomPlugin", + "CustomWebView", + "cuteLogger", + "cwebp-binLocal", + "CyberJS", + "D", + "d-fordeYoutube", + "D-Stats", + "D.Va", + "d3-bboxCollide", + "d3-pathLayout", + "d3.geoTile", + "D3.TimeSlider", + "Daja", + "Daniel_NPM_Library_Test", + "Dante2", + "DanTroy-utils", + "Dashboard", + "Dasher", + "dashr-widget-Weather", + "dashr-widget-World-Pool-Championships", + "Data-CSS", + "Data-Same-Height", + "dataAccess", + "Database-Jones", + "DataManager", + "dataStream", + "dateFormat-kwen", + "dateFormatW", + "DateHuatingzi", + "DateMaskr", + "dateModule", + "DatePicker", + "Datepicker.js", + "Dateselect", + "DateValidator", + "DateZ", + "Datum", + "Davis", + "dd-rc-mStock", + "DDEvents", + "deBijenkorf-protractor-tests", + "Debug-Tracker", + "Deci-mal", + "DeCurtis-Logger", + "deepEqualsWith", + "deepPick", + "defaultStr", + "Deferred", + "deferredEventEmitter", + "defineClass", + "defineJS", + "DelegateListener", + "deleteMoudles", + "Demo", + "Demo1", + "demoNeeeew", + "demoWei", + "demoYTC", + "Deneme", + "derivco-SoundJS", + "derpModule", + "DeskSet", + "Desktop-command", + "Devbridge-FrontEnd", + "Developer", + "deviousknightFirstNpm", + "devisPattern", + "devProxy", + "DFP", + "dgram", + "dgURI", + "diagnostics_channel", + "Dial", + "DiggernautAPI", + "Diogenes", + "DirScanner", + "dirStat", + "DirWatcher", + "Discord-Webhook", + "DiscordForge", + "diveSync", + "dkastner-JSONPath", + "DM.NodeJS", + "dns", + "Dock-command", + "docxtemplaterCopy", + "doLink", + "DOM", + "Domai.nr", + "domain", + "DOMArray", + "DOMBuilder", + "DOMino", + "DOMtastic", + "DOMtastic-npm", + "dotFormat", + "dotJS", + "DoubleCheck", + "Dove.js", + "downloadAPI", + "downLoadFile", + "DownloadManager", + "DownloadProxy", + "DPS", + "DQ", + "draftjsToHTML", + "dragOnZone", + "drakovNew", + "Draper", + "DrawPDF", + "Dribble", + "Drupal-Node.js", + "DT", + "Duckface", + "Dui", + "DVA", + "DvA", + "dVa", + "DXIV2Inst", + "DynamicBuffer", + "dynamoDB", + "DynamoDBStream", + "DynWorker", + "Easy-Peasy-Slide", + "easyCache", + "easyFe", + "easyRestWithABL", + "EasyUI", + "eavesTool", + "EBI-Icon-fonts", + "echartsEx", + "EclipseScroll", + "ECMASquasher", + "edfToHtmlConverter", + "edGoogleApi", + "edGraham", + "EfemerideList", + "efemerideList", + "efficientLoad", + "eFishCrawler", + "EhanAreesha", + "Elastic-Beanstalk-Sample-App", + "ElasticSlider-core", + "electron-isDev", + "ElectronAppUpdater", + "ElectronRouter", + "elementsJS", + "Elixirx", + "Elm-0.17-Gulp-Coffeescript-Stylus-Lodash-Browserify-Boilerplate", + "EmailClient", + "ember-cli-fullPagejs", + "ember-leaflet-geoJSON", + "emoJiS-interpreter", + "Empite", + "EmpiteApp", + "emptyObject", + "emptyString-loader", + "Encloud", + "encodeBase64", + "encodeID", + "energyCalculator-browser", + "EnglishTranslator", + "ensureDir", + "Enumjs", + "Environment.js", + "ep_disableChat", + "EPO_OPS_WRAPPER", + "equalViews-comparative-selection", + "eRx-build", + "ES-poc", + "es6-DOM-closest", + "eSlider", + "eslint-plugin-elemMods", + "EsmalteMx.ProductApi.Lambdas", + "Estro", + "ETag", + "eValue-bs", + "EVE", + "EventDispatcher", + "eventDrops", + "EventEmitter", + "EventField", + "EventFire", + "EventFire.js", + "EventHub", + "EventRelayEmitter", + "events", + "EventServer", + "eventstore.mongoDb", + "EventtownProject", + "EventUtil", + "EVEoj", + "EverCookie", + "ewdDOM", + "ewdGateway", + "ExBuffer", + "execSync", + "exFrame-configuration", + "exFrame-core", + "exFrame-generator", + "exFrame-logger", + "exFrame-mq", + "exFrame-rest", + "exFrame-rpc", + "exFrame-security", + "ExifEditor", + "Exitent", + "expectThat.jasmine-node", + "expectThat.mocha", + "Express", + "Express-web-app", + "expressApi", + "ExpressCart", + "ExpressCheckout", + "expressingFounder", + "ExpressMVC", + "ExpressNode", + "expressOne", + "expressSite", + "expressWeb", + "ExtraInfo", + "extraRedis", + "Eyas", + "EzetechT", + "EZVersion", + "F", + "F-chronus", + "f*", + "FabioPluginiUno", + "Facebook_Graph_API", + "facebookPhotos", + "FacebookYarn", + "factor-bundle-WA64", + "FAEN", + "Faker", + "Falcon", + "fast-artDialog", + "fastA_node", + "FastLegS", + "Fayer", + "fbRecursiveRequest", + "FeedbackModuleTest", + "feedBum", + "fenix-ui-DataEditor", + "fenix-ui-DSDEditor", + "Fermi-UI", + "FetchCallLog", + "fieldsValidator", + "fig-Componts", + "File", + "File_Reader_solly", + "FileBrowser", + "FileError", + "fileGlue", + "FileList", + "fileLog", + "FilePicker-Phonegap-iOS-Plugin", + "FileReader", + "FileSaver", + "FileSync", + "FileWriter", + "FileWriterSync", + "Finder-command", + "FirstApp", + "FirstCustomPlugin", + "firstModule", + "firstNodejsModule", + "firstYarn", + "fis-parse-requireAsyncRes", + "fis-postpackager-inCSSToWebP", + "fis3SmartyTool", + "FitText-UMD", + "Flamingo", + "flatToTrees", + "fleschDe", + "Flex-With-Benefits", + "FlickrJS", + "flipPage", + "Florence", + "FlowerPassword", + "flowMap", + "FLTEST", + "fnProxy", + "FontAwesome-webpack", + "fontEnd", + "FontLoader", + "foo!", + "foo~", + "forAsync", + "ForceCode", + "forceLock", + "forChangeFilesName", + "forEachAsync", + "formAnimation", + "formatDate", + "formBuilder", + "FormData", + "Formless", + "formValidate", + "FrameGenerator", + "freightCrane", + "French-stemmer", + "Frenchpress", + "FreshDocs", + "friendsOfTrowel-buttons-component", + "friendsOfTrowel-dropdowns-component", + "friendsOfTrowel-Forms-component", + "friendsOfTrowel-Layouts-component", + "Friggeri.net", + "Frog", + "frontBuild", + "Frontend-starter", + "FrontEndCentral-documentation", + "FrontJSON", + "FrontPress", + "Frozor-Logger", + "Fruma", + "fs", + "fs-uTool", + "FSM", + "FT232H", + "fuck!", + "Fuell", + "FuellDocTest", + "FuellSys", + "FuellTest", + "FullStack", + "FunDemo2", + "FURI", + "Fury", + "futSearch", + "futureDocBuilder", + "FyreWorks-Node", + "fzmFE", + "Gaiam", + "Ganescha-Bot-Jokes", + "gaoboHello", + "Garrett-pokemon", + "gatesJs", + "Gauge", + "gaugeJS", + "gaussianMixture", + "gbL-jsMop", + "GC-Sequence-Viewer", + "gdBuildLogs", + "gdBuilds", + "Gems.PairedDeviceClient", + "genData", + "generateIndex", + "generator-entityV2-widgets", + "generator-kittJS", + "generator-qccr-startKit", + "generator-reactpackSample", + "generator-zillionAngular", + "Gengar", + "GeoMatrix", + "GeosysDroid", + "GeosysTest", + "Gerardo", + "getDateformat", + "getExtPath", + "getSignature", + "GettyEmbeddy", + "ghostTools", + "GhostTube", + "GiftEditor", + "GirlJS", + "GitAzure", + "gitbook-plugin-prism-ASH", + "gitbook-plugin-specialText", + "gitbook-start-heroku-P8-josue-nayra", + "gitbook-start-heroku-P9-josue-nayra", + "gitForge", + "gitHub", + "GitHub-Network-Graph", + "GitHubTrending", + "gitProvider", + "gl-flyCamera", + "gl-simpleTextureGenerator", + "glMath", + "GLORB", + "glslCanvas", + "glslEditor", + "glslGallery", + "GLSlideshow", + "Glue", + "GMP", + "golbalModule", + "Goldfish", + "Gon", + "Google_Plus_API", + "Google_Plus_Server_Library", + "Google-Chrome-command", + "GoogleDrive", + "googleOAuthServer", + "googlePlaceAutocomplete", + "GoogleService-NodeJs", + "Gord", + "gPagesJS", + "Gps2zip", + "GRAD_leaveNotes", + "GRAD_makeFire", + "grad-customGear", + "grad-factions-VR", + "grad-leaveNotes", + "grad-makeFire", + "Grafar", + "Graph", + "graphLock.custom.plugin", + "graphQl-Mysql-Server", + "GridFS", + "GridManager", + "gridminCss", + "Gridtacular", + "GroupePSAConnectedCar", + "Grow.js", + "Grunt-build", + "grunt-checkFileSize", + "grunt-cmd-handlebarsWrap", + "grunt-ftp-getComponent", + "grunt-httpTohttps", + "grunt-latexTOpdf-conversion", + "grunt-Npm-grunts", + "grunt-po2mo-multiFiles", + "grunt-Replacebyrefs", + "grunt-syncFolder", + "grunt-urlCacheBuster", + "guideJs", + "gulp-addSuffix", + "gulp-combineHtml", + "gulp-imgToBase64", + "gulp-lowerCase", + "gulp-phpWebserver", + "gulp-spacingWord", + "Gulp-Tasks", + "GumbaJS", + "Gusto", + "gz2qiCalcModule", + "h2oUIKit", + "H5UI", + "H666", + "habibtestPublish", + "HackBuffer", + "handleStr", + "HansontableComponent", + "Haraka", + "HariVignesh", + "harmonyHubCLI", + "HarryPotterParty", + "harsh-Test-Module", + "Harshil", + "hash!", + "hashPage", + "hashTranslate", + "HASWallpaperManager", + "hasWord", + "HeartBeatWoT_pi", + "Hello", + "hello_test_spade69XXX", + "Hello_World", + "HelloBot", + "helloBySoo", + "helloDevelopersnodejs", + "HelloExpress", + "helloModule", + "HelloWorld", + "helloWorld", + "HelloWorld_hlhl_040", + "HelloWorldComponent", + "HelloWorldNodeJS", + "helloYJ", + "helpBy", + "helpCenter", + "herokuRun", + "Hesiir-components", + "HHello", + "Hidash", + "HiddenMarkovModel", + "hideShowPassword", + "highcharts-*", + "HighlightP", + "Highway", + "Hinclude", + "Hipmob", + "Hiraku", + "hm_firstPackage", + "HMTraining", + "homebridge-anelPowerControl", + "homebridge-bigAssFans", + "homebridge-CurrentAmbientLightLevel", + "homebridge-Homeseer", + "homebridge-LEDStrip", + "homebridge-MotionSensor", + "homebridge-RFbulb", + "Homematic-Hue-Interface", + "hoshiCustomContent", + "hoshiImageLoader", + "HotJS", + "Hotshot", + "hoverifyBootnav", + "howToNPM", + "Hppy", + "Hpy", + "htmlCutter", + "htmlKompressor", + "HTMLString", + "htmlToTree", + "http", + "http2", + "HTTPRequest", + "https", + "httpShell", + "httpTohttps", + "Hubik", + "Hubik-Demo", + "Hubik-Platform", + "Hubik-Platform-Chrome", + "Hubik-Plugin", + "Hubik-Plugin-Memory", + "Hubik-Plugin-Network", + "Hubik-Plugin-Rendering", + "Hubik-Util", + "hubot-yigeAi", + "HuK", + "hybridCrypto", + "i18next.mongoDb", + "Ian_Chu", + "IArray", + "Ibis.js", + "iCompute", + "iEnhance", + "IENotification", + "iFrameAPI", + "IFY-gulp-kit", + "II", + "IIF", + "iIndexed", + "iKeyed", + "iM880-serial-comm", + "imageCDN-webpack-loader", + "imageMagick", + "Imager", + "Imageresizer", + "imageTool", + "ImageViewer", + "iMagPay", + "iMemoized", + "iMessageModule", + "Imovie", + "Imp", + "Incheon", + "Index", + "indexedStore", + "inferModule-jsdoc-plugin", + "infieldLabel", + "Influxer", + "inputcheckMemo", + "inspector", + "INSPINIA", + "Insplash", + "inStyle", + "interactiveConsole", + "Interval", + "IO", + "IObject", + "ionic-gulp-browserify-typescript-postTransform", + "IonicSocket", + "iOS-HelloWorld", + "IOTSDK", + "iotsol-app-FAN", + "iotsol-app-test-Node-RED", + "iotsol-service-string-upperCase", + "IQVIS", + "Iris", + "iRobo-react-modal", + "iSecured", + "isElementInViewport", + "isEqual", + "iSeries", + "isFirefoxOrIE", + "isHolidayInChina", + "iSocketService", + "isPureFunction", + "iStorable", + "iTransactable", + "iTunes-command", + "iValidated", + "iWeYou", + "iZettle", + "iziModal", + "JabroniJS", + "jaCodeMap", + "Jade-Sass-Gulp-Starter", + "jadeBundler", + "jadiTest", + "jAlert", + "JamSwitch", + "JASON", + "JavaScript-101", + "JazzScript", + "jcarouselSwipe", + "jDataView", + "jDate", + "jetsExt", + "Jimmy-Johns", + "jingwenTest", + "JMSList", + "JMSlist.js", + "Jody", + "joi-dataURI", + "joi-string-dataURI", + "jordenAngular", + "jordenAngular2", + "JorupeCore", + "JorupeInstance", + "JOSS", + "JotihuntReact", + "Journaling-Hash", + "jpaCreate", + "jParser", + "JPath", + "jPlotter", + "jPlugins", + "JQ", + "jQ-validation-laravel-extras", + "JQDeferred", + "jQGA", + "jqGrid", + "jqNode", + "jqPaginator", + "jqplot.donutRenderer", + "jqPromise4node", + "jqTreeGridWithPagination", + "jQuery", + "jquery-adaptText", + "jquery-asAccordion", + "jquery-asBgPicker", + "jquery-asBreadcrumbs", + "jquery-asCheck", + "jquery-asChoice", + "jquery-asColor", + "jquery-asColorPicker", + "jquery-asDropdown", + "jquery-asFontEditor", + "jquery-asGalleryPicker", + "jquery-asGmap", + "jquery-asGradient", + "jquery-asHoverScroll", + "jquery-asIconPicker", + "jquery-asImagePicker", + "jquery-asItemList", + "jquery-asModal", + "jquery-asOffset", + "jquery-asPaginator", + "jquery-asPieProgress", + "jquery-asProgress", + "jquery-asRange", + "jquery-asScroll", + "jquery-asScrollable", + "jquery-asScrollbar", + "jquery-asSelect", + "jquery-asSpinner", + "jquery-asSwitch", + "jquery-asTooltip", + "jquery-asTree", + "jQuery-by-selector", + "jquery-dynamicNumber", + "jquery-idleTimeout-plus", + "jquery-loadingModal", + "jquery-navToSelect", + "jQuery-QueryBuilder", + "jquery-rsLiteGrid", + "jquery-rsRefPointer", + "jquery-rsSlideIt", + "jQuery-Scanner-Detection", + "jquery-scrollTo", + "jquery-scrollToTop", + "jquery-slidePanel", + "jQuery.component", + "jquery.customSelect", + "jquery.dataTables.min.js", + "jquery.Jcrop.js", + "jQuery.keyboard", + "jQuery.mmenu-less", + "jQuery.print", + "jquery.rsLiteGrid", + "jquery.rsOverview", + "jquery.rsRefPointer", + "jquery.rsSlideIt", + "jquery.rsSliderLens", + "jQuery.toggleModifier", + "jquery.waitforChild", + "jqueryPro", + "js-build-RomainTrouillard", + "JS-Entities", + "JS-string-minimization", + "JS.Responsive", + "jSaBOT", + "jsCicada", + "jsConcat", + "JSCPP", + "jsDAV", + "JSDev", + "jsdoc-TENSOR", + "jsDocGenFromJson", + "jsDump", + "jSelect", + "JSErrorMonitor", + "JSErrorMonitor-server", + "jsFeed", + "jsFiddleDownloader", + "JSFramework", + "JSLint-commonJS", + "JSLintCli", + "JSLogger", + "JSON", + "JSON-Splora", + "JSON.sh", + "JSON2", + "json8-isArray", + "json8-isBoolean", + "json8-isJSON", + "json8-isNull", + "json8-isNumber", + "json8-isObject", + "json8-isPrimitive", + "json8-isString", + "json8-isStructure", + "JSON2016", + "JSONloops", + "JSONPath", + "JSONPathCLI", + "JSONRpc", + "JSONSelect", + "JSONStream", + "JsonUri", + "JSONUtil", + "jsonX", + "JSplay", + "jspolyfill-array.prototype.findIndex", + "JSPP", + "JSpring", + "jsQueue", + "jsSourceCodeParser", + "jStat", + "JSUS", + "JSV", + "JSX", + "jsz-isType", + "JTemplate", + "JTmpl", + "jTool", + "JuliaStyles", + "JumanjiJS", + "Jupyter-Git-Extension", + "justifiedGallery", + "justJenker", + "JustMy.scss", + "JWBootstrapSwitchDirective", + "jWorkflow", + "jxLoader", + "JYF_restrict", + "K_Tasks", + "K--Ajax", + "K-Report", + "KAB.Client", + "Kahana", + "Kapsel-project", + "Katy", + "Kayzen-GS", + "KB", + "KB_Model", + "kelTool", + "kelTool2", + "KenjutsuUI", + "KevinLobo3377-node", + "KFui", + "kickoff-fluidVideo.css", + "Kid", + "kingBuilder", + "kiranApp", + "Kirk", + "Kissui", + "kittJS", + "Kiwoom-Helper", + "KLC3377-node", + "knockout.ajaxTemplateEngine", + "koa-artTemplate", + "koa-Router", + "koaPlus", + "koaVue", + "KonggeIm", + "kpPublicPerson", + "kpPublicVideo", + "krawlerWash", + "ktPlayer", + "kylpo-BackgroundImage", + "kylpo-Block", + "kylpo-Button", + "kylpo-Col", + "kylpo-Flex", + "kylpo-Inline", + "kylpo-InlineBlock", + "kylpo-InlineCol", + "kylpo-InlineFlex", + "kylpo-InlineRow", + "kylpo-Paint", + "kylpo-Painter", + "kylpo-Row", + "kylpo-Text", + "kylpo-View", + "kzFormDaimyo", + "L.TileLayer.Kartverket", + "L7", + "labBuilder", + "Lactate", + "Lade", + "laravel-jQvalidation", + "Large", + "lark-PM", + "LasStreamReader", + "latte_web_ladeView", + "latte_webServer4", + "lavaK", + "layaIdecode", + "Layar", + "Layout", + "LazyBoy", + "lazyBum", + "lazyConnections", + "lazyLoadingGrid", + "lcAudioPlayer", + "LCM", + "LDAP", + "Leaf.js", + "Leaflet-MovingMaker", + "Leaflet.AutoLayers", + "Leaflet.Deflate", + "Leaflet.GeoJSON.Encoded", + "Leaflet.GreatCircle", + "Leaflet.MultiOptionsPolyline", + "Leaflet.TileLayer.MBTiles", + "Leaflet.vector-markers", + "leapShell", + "LearningNPM", + "learnnode_by_HHM", + "leFunc", + "Legos", + "Libby-Client", + "LightCinematic", + "lihuanxiangNpm1", + "limitedQueue", + "linearJs", + "lineReader", + "Lingo", + "LinkedList", + "linkIt", + "LISP.js", + "liteParse", + "liuchengjunOrder0414", + "LiveController", + "LiveDocument", + "LiveScript", + "LiveScript-brunch", + "LiveView", + "liweiUitl", + "lizaorenqingTool", + "lmONE", + "LMUI", + "LMX-Data", + "LNS_weixin_h5", + "localeMaker_v1", + "localforage-memoryStorageDriver", + "LocalRecord", + "localStorage", + "localStorage-info", + "localStorage-mock", + "LoDashfromScratch", + "lofterG", + "Loganalyzer", + "LogbookMessageCreator", + "Logger", + "Logging", + "Loggy", + "logic2UI", + "LogosDistort", + "LogStorage.js", + "logStream", + "LOL", + "lolAJ", + "LongestCommonSubstring", + "loop-setTimeout", + "loopback-connector-rest-addCookie", + "lopataJs", + "Lorem", + "Losas", + "LP_test_task", + "Lucy", + "LUIS", + "LUIS_FB", + "Lumenize", + "Lush.js", + "LykkeFramework", + "M66_math_example", + "mac-cropSr", + "MacGyver", + "Mad.js", + "magentoExt", + "Maggi.js", + "Maggi.js-0.1", + "MagpieUI", + "MALjs", + "Mambo-UI", + "mangoSlugfy", + "mapleTree", + "mappumBot", + "Marionette-Require-Boilerplate", + "markupDiff", + "marryB", + "MasterDetailApplication", + "MaterialAngularWithNodeJS", + "Math", + "math_example_20160505163300BR", + "math_example_Hala", + "math_example_myown_ve-01119310520_V2", + "math_exampleCJG", + "math_exampleII", + "math_exampleX", + "math_ThisIsMe", + "math-Murasame", + "Math1105", + "mathAdd", + "mathExample", + "MathJax-node", + "MathJS", + "mathMagic", + "MathTest1", + "MatPack", + "Mavigator", + "MAX-AVT-homebridge-led", + "MAXAVTDemo", + "MAXIMjs", + "MaxUPS", + "MCom", + "MD5", + "MDLCOMPONENT", + "mdlReact", + "mdPickers", + "mdRangeSlider", + "mdToPdf", + "MEAN", + "MeanApp1", + "MeCab", + "mediaCheck", + "Mediany", + "medicalHistory", + "Mercury", + "Meridix-WebAPI-JS", + "Mers", + "MessageBus", + "MetaEditor", + "Meteor-Test-Installer", + "MetroTenerife", + "MFL-ng", + "MFRC522-node", + "mglib-GAMS.WEBCLIENT2", + "MIA", + "MicroServices", + "Midgard", + "midhunthomas_Test", + "mihoo_fileUpload", + "mini-fileSystem-WebServer", + "Mini-test", + "MiniAppOne", + "MiniAppTwo", + "minibuyCommonality", + "miniJsonp", + "MiniManager", + "MiniMVC", + "MinionCI", + "Minju003", + "Mirador", + "Misho_math_example", + "MJackpots", + "mjb44-playground-module-exporting-interface-and-type-method-B", + "mjb44-playground-module-exporting-interface-and-type-method-C", + "Mkoa", + "Mkoa-pg-session", + "MKOUpload", + "mlm603Test", + "mmAnimate", + "mmDux", + "MMM-alexa", + "mmRequest", + "mmRouter", + "mNotes", + "Mockery", + "modalDemo", + "modalDemo1", + "modalWin.js", + "module", + "ModuleBinder", + "modulebyAKB", + "ModuleC", + "moduleLoader", + "moduleTest", + "MoEventEmitter", + "Mokr", + "Mole", + "mon-appNon0", + "MonApp", + "MongoDAL", + "mongoose-schema-to-graphQL", + "mongooseSchema-to-graphQL", + "Monik", + "MonikCommon", + "MoniqueWeb", + "Monorail.js", + "Mopidy-Spotmop", + "mosesCheckIn", + "MovieJS", + "mOxie", + "MoxtraPlugin_1.1", + "MoxtraPlugin_1.2.1", + "mPortalUI", + "MQTTClient", + "Mr.Array", + "Mr.Async", + "Mr.Coverage", + "mraaStub", + "MrsYu", + "MrsYu1", + "msGetStarted", + "mSite", + "msJackson", + "mSnackbar", + "Mu", + "Muffin", + "MultiSlider", + "musuAppsas", + "MWS_Automation", + "my-awesome-nodejs-moduleHL", + "my-componentAnimesh", + "My-First-Module", + "My-first-Package", + "My-Fist-Project", + "my-HLabib", + "My1ink", + "MyAngularGruntt", + "MyAnimalModule", + "myappSriniAppala", + "myappUSBankExample", + "myAries", + "MyBlog", + "myCalclator", + "myDate", + "myDialog", + "myDu2", + "myDVA", + "myFirst-Nodejs-Module", + "MyFirstContribution", + "myfirstDemo", + "myFirstModule", + "myFirstNodeModule", + "myFirstNpm", + "myFirstPluginAji", + "myFirstProject", + "myFirstPub", + "myLib", + "myMath", + "MYMODAL", + "MyModule", + "myModule", + "myNodeJs", + "myNodeJsApp", + "myNodejsApp", + "myNpm", + "MYnpm1", + "myNpm0001", + "myNpm2", + "myNpm5", + "myNpm10", + "myNpm11", + "myNpm111", + "myNpm999", + "myNpmfei", + "myNpmfei1", + "myNpml", + "myNpmModule", + "myNpmrz1", + "MyPlugin", + "MyProject", + "MyProjNode", + "myPromise", + "myrikGoodModule", + "Mysql-Assistant", + "mysupermoduleXXX", + "myTest", + "Mytest_module", + "mytPieChart", + "N", + "N3-components", + "NA1", + "NageshTestapplication", + "NAME", + "Nameless13", + "NaNNaNBatman.js", + "nanoTest", + "NasimBotPlatform", + "NativeAds", + "NativeCall", + "NativeProject", + "nativescript-CallLog", + "nativescript-GMImagePicker", + "nativescript-logEntries", + "NavExercise", + "nCinoRabbit", + "ncURL", + "NDDB", + "neouiReact-button", + "Neptune", + "NERDERY.JS.NAT", + "nestedSortable", + "net", + "NeteaseCloudMusicApi", + "neteaseMusicApi", + "Netflow", + "Netlifer", + "NetMatch", + "NetOS", + "netOS", + "Netpath-Test", + "Neuro", + "Neuro-Company", + "NewModule1", + "newmsPong", + "newPackage", + "newPioneer", + "newStart", + "newtouchCloud", + "NewWebview", + "NexManager", + "NexmoJS", + "NFO-Generator", + "ng2-clockTST", + "ng2-dodo-materialTypeTransfer", + "ng2-QppWs", + "ng2GifPreview", + "NG2TableView", + "ngBrowserNotification", + "ngCart", + "ngChatScroller", + "ngComponentRouter-patched", + "ngCurrentGeolocation", + "ngDfp", + "ngDrag", + "ngFileReader", + "ngGen", + "ngGeolocation", + "ngHyperVideo", + "ngIceberg", + "ngImgHandler", + "ngIntercom", + "ngKit", + "ngPicker", + "ngPluralizeFilter", + "ngPluralizeFilter2", + "ngProgress-browserify", + "ngScroll", + "ngSinaEmoji", + "ngSmoothScroll", + "ngSqlite", + "ngTile", + "ngTimeInput", + "ngTreeView", + "ngUpload", + "ngUpload-forked", + "Nguyen_test", + "ngVue", + "ngYamlConfig", + "nHttpInterceptor", + "Nick_calc", + "NickSam_CGD", + "NightPro-Web", + "nightwatchGui", + "Nikmo", + "nImage", + "Nitish", + "nitish.kumar.IDS-LOGIC", + "NlpTextArea", + "nltco-lgpt-clean-A", + "nltco-lgpt-clean-B", + "nltco-lgpt-dedupe-simple-A", + "nltco-lgpt-dedupe-simple-B", + "nMingle", + "nmPhone", + "nMysql", + "NoCR", + "NODE", + "Node_POC", + "node-CORSproxy", + "Node-FacebookMessenger", + "Node-HelloWorld-Demo", + "node-iDR", + "node-iOS", + "Node-JavaScript-Preprocessor", + "node-localStorage", + "Node-Log", + "Node-Module-Test", + "node-myPow", + "node-red-contrib-samsungTV", + "node-red-contrib-wwsNodes", + "node-red-StefanoTest", + "node-TBD", + "NodeApp", + "nodeApp", + "nodeAuth", + "nodeBase", + "NodeBonocarmiol", + "nodeCalcPax", + "nodeCombo", + "nodeDemo9.26", + "nodeDocs", + "nodeEventedCommand", + "NodeFileBrowser", + "NodeFQL", + "nodeHCC", + "nodeInterface", + "NodeInterval", + "nodeIRCbot", + "nodeJS", + "NodeJS_Tutorial", + "nodeJs-zip", + "NodejsAgent", + "NodeJsApplication", + "nodejsFramework", + "nodejsLessons", + "NodeJsNote", + "NodeJsPractice", + "nodeJsPrograms", + "Nodejsricardo", + "NodeJSTraining-demo-9823742", + "nodejsTutorial", + "NodejsWebApp1", + "nodejsWorkSpace", + "NodeKeynote", + "nodeLearning", + "nodeMarvin", + "nodeMarvin2", + "NodeMini", + "nodeMysqlWrapper", + "nodeNES", + "nodeos-boot-multiUser", + "nodeos-boot-singleUser", + "nodeos-boot-singleUserMount", + "nodepackageBoopathi", + "nodePhpSessions", + "NodePlugwise", + "NodePlugwiseAPI", + "nodeQuery", + "nodes_Samples", + "NodeSDK-Base", + "NodeServerExtJS", + "NodeSSH", + "nodeSSO", + "NodeSTEP", + "nodeTest", + "NodeTestDee", + "nodeTTT", + "nodeTut", + "NoDevent", + "NodeView", + "nodeWebsite", + "NodObjC", + "Nonsense", + "NoobConfig", + "NoobHTTP", + "normalizeName", + "NORRIS", + "nOSCSender", + "Note.js", + "NotificationPushsafer", + "Notifly", + "Npm", + "npm-Demo", + "Npm-Doc-Study", + "npm-mydemo-pkgTest", + "npm-setArray", + "npm-wwmTest", + "npmCalc", + "npmFile", + "npmModel", + "npmModel1", + "npmModel2", + "npmTest", + "npmToying", + "npmTutorial", + "NPR_Test", + "nrRenamer", + "nStoreSession", + "nTPL", + "nTunes", + "NudeJS", + "nunjucks-includeData", + "O", + "O_o", + "o_O", + "O2-countdown", + "O2-tap", + "objectFitPolyfill", + "ObjectSnapshot", + "ObjJ-Node", + "ObservableQueue", + "OCA-api", + "ocamlAlpha", + "ocamlBetterErrors", + "OcamlBytes", + "ocamlBytes", + "ocamlRe", + "OhMyCache", + "OK-GOOGLE", + "Olive", + "onBoarding", + "OnCollect", + "OneDollar.js", + "oneTest", + "OpenBazaar-cli", + "OpenDolphin", + "OpenJPEG.js", + "openWeather", + "OperatorUI", + "OPFCORS", + "OPFSalesforce", + "OptionParser", + "OrangeTree", + "Orchestrator", + "Order", + "ORIENTALASIAN", + "os", + "Osifo-package", + "osu-ModPropertiesCalculator", + "OTPAutoVerification", + "overloadedFunction", + "OwnMicroService", + "OwnNormalizer", + "OwnPubSub", + "OwnPubSubClient", + "OwnPubSubServer", + "p2Pixi", + "PaasyMcPaasFace", + "pacemakerJS", + "packAdmin", + "Package", + "packageNodeCR-Jeff.json", + "packagePublished", + "packageTesting", + "Packery-rows", + "packing-template-artTemplate", + "Paddinator", + "Paginate", + "palindromeCalcPax", + "palindromePax", + "PanPG", + "Panzer", + "parameterBag", + "paramsValidator", + "Parse-Server-phone-number-auth", + "parseArgs", + "Parser", + "parseUri", + "Particle", + "Particleground.js", + "PassiveRedis", + "path", + "PatternLabStarter", + "patternReplacer", + "paytmGratify", + "PayzenJS", + "pDebug", + "pdf-to-dataURL", + "pdfTOthumbnail_convert", + "PeA_nut", + "Peek", + "PeepJS", + "Pega.IO", + "Peggy.js", + "Percolator", + "perf_hooks", + "performJs", + "pgnToJSON", + "PHibernate", + "phoeNix-cli", + "phoenixCLI", + "PhonegapAnalytics", + "PhonegapBeacon", + "PhonegapFeeds", + "PhonegapGeofence", + "PhonegapGrowth", + "PhonegapLocations", + "PhonegapPush", + "picardForTynt", + "PicoMachine", + "Pictionary", + "Pintu", + "pjEmojiTest", + "PJsonCouch", + "PK", + "PL8", + "placeHolder.js", + "PLATO", + "PlayStream", + "pluginCreater", + "pluginHelloWorld", + "pluginHelloworld", + "pluginTest", + "PlugMan", + "pluuuuHeader", + "PoistueJS", + "Pokeball-Scanner", + "PokeChat", + "PokedexJS", + "PokemonGoBot", + "PokemonGoNodeDashboard", + "polar-cookieParser", + "pollUntil", + "Polymer", + "POM", + "pomeloGlobalChannel", + "pomeloScale", + "portal-fe-devServer", + "PostgresClient", + "Postlog", + "PowerPlanDisplay", + "powerPlug", + "PP", + "ppublishDemo", + "Pre", + "Preprocessor", + "PrettyCSS", + "prettyJson", + "PrimaryJS", + "primerNodo", + "primo-explore-LinkedData", + "primo-explore-prmFacetsToLeft", + "primo-explore-prmFullViewAfter", + "primo-explore-prmLogoAfter", + "primo-explore-prmSearchBarAfter", + "PrimoEsempio", + "Printer", + "Prism", + "prjTemplate", + "Probes.js", + "process", + "proInterface", + "Project-A-VK", + "Prometheus", + "Promise", + "Promise.js", + "PromiseContext", + "promisify-syncStore", + "PropagAPISpecification", + "propCheckers", + "Propeller", + "properJSONify", + "Proto", + "proton-quark-rabbitMQ", + "ProtVista", + "ProUI-Utils", + "ProvaSimone", + "provinceCity.js", + "PSNjs", + "PTC-Creator", + "ptyzhuTest_20160813", + "PublishDemo", + "publishDigitalCrafts2016", + "PubSub", + "pubsubJS", + "pulsarDivya", + "punycode", + "PupaFM", + "Puppet.svg", + "PureBox", + "PureBox-Gallery-PlayEngine", + "purePlayer", + "PushMessage", + "PushPanel", + "PushPlugin_V2", + "pybee!batavia", + "Q", + "q-mod-cliElements", + "q-mod-cliPrinter", + "QAP-cli", + "QAP-SDK", + "Qarticles", + "QnA_Fore", + "QNtest", + "qqMap", + "qTip2", + "QuadMap", + "QuantumExperimentService", + "querystring", + "R", + "R.js", + "R2", + "RAD.js", + "Radical", + "raehoweNode", + "Rajas", + "random-fullName", + "randomCaddress", + "randomCname", + "randomCname.js", + "randomLib", + "randomNickname", + "RandomSelection", + "randomTestOne", + "randString", + "randString.js", + "Range.js", + "Rannalhi", + "rAppid.js", + "rAppid.js-server", + "rAppid.js-sprd", + "Rapydscriptify", + "RaspiKids", + "raZerdummy", + "RCTMessageUI", + "React_Components", + "React-Carousel", + "react-countTo", + "react-creditCard", + "React-ES5-To-ES6-Checklist", + "react-input-dateTime", + "react-InputText-component", + "react-komposer-watchQuery", + "react-materialUI-components", + "react-native-accountKit", + "react-native-cascadeGrid", + "react-native-checkBox", + "react-native-DebugServerHost", + "React-Native-Form-Field", + "react-native-isDeviceRooted", + "react-native-LoopAnimation", + "react-native-MultiSlider", + "react-native-portableView", + "react-native-swRefresh", + "react-PPT", + "React-Redux-Docker-Ngnix-Seed", + "react-refresh-infinite-tableView", + "React-Select-Country", + "React-Tabs", + "React-UI-Notification", + "react-uploadFile", + "reactClass", + "reactcordovaApp", + "ReactEslint", + "reactFormComponentTest1", + "reactGallery", + "reactHeaderComponentTest1", + "ReactHero", + "reactIntlJson-loader", + "ReactNaitveImagePreviewer", + "ReactNative-checkbox", + "reactNative-checkbox", + "reactNativeDatepicker", + "reactNativeLoading", + "ReactNativeNavbar", + "ReactNativeSlideyTabs", + "ReactNativeSocialLogin", + "ReactNativeStarterKit", + "ReactNativeToastAndroid", + "reactTwo", + "ReactUploader", + "readabilitySAX", + "ReadableFeeds", + "readline", + "ReadSettings", + "Reality3D", + "reallySimpleWeather", + "ReApp", + "ReasonDB", + "RecastAI-Library-JavaScript", + "recordType", + "recordWebsite", + "RedisCacheEngine", + "redisHelper", + "reDIx", + "RefreshMedia", + "registerSendMsg", + "reloadOnUpdate", + "remoteFileToS3", + "RemoteTestService", + "removeNPMAbsolutePaths", + "RentalAdvantage", + "repl", + "Replace", + "Replace2.0", + "Replen-FrontEnd", + "replNetServer", + "Require", + "requireAsync", + "Resin", + "resolveDependencies", + "responseHostInfo", + "ReST-API", + "RESTful-API", + "Restifytest", + "Restlastic", + "RESTLoader", + "Reston", + "RestTest", + "RetreveNumbers", + "rgbToHexa", + "RhinoStyle", + "Richard", + "richardUtils", + "rinuts-nodeunitDriver", + "Risks-Tables", + "RNBaiduMap", + "RNCommon", + "RNSVG", + "RNSwiftHealthkit", + "rNums", + "RobinGitHub", + "Robusta", + "RockSelect", + "Router", + "RP_Limpezas_Industriais", + "Rpm", + "RSK-Router", + "RT-react-toolbox", + "Rubytool", + "runQuery", + "runStormTest", + "runTestScenario", + "RunwayLogger", + "RWD-Table-Patterns", + "RWPromise", + "Safari-command", + "SafeObject.js", + "Safood-Parse", + "SaFood-Parse", + "sahibindenServer", + "salgueirimTeste", + "samepleMicroservice", + "samjs-mongo-isOwner", + "Sample", + "SamplePlugIn", + "SandboxTools", + "sandcastle_multiApp", + "Sanitizer.js", + "sanitizer.unescapeEntities", + "Sardines", + "Sass-Boost", + "Sass-JSON", + "Sass-layout", + "Saturday", + "SauceBreak", + "sayHelloByone", + "sbg-queueManager", + "sbUtils", + "SC-Expense-Plugin", + "Scaffolding", + "scalejs.metadataFactory", + "ScgiClient", + "Scheduler.js", + "schema-inspector-anyOf", + "scp-cleanRedis", + "Scrap", + "scriptTools", + "scrollAnimation", + "scrollPointerEvents", + "ScrollShow", + "Sdp-App", + "seaModel", + "searchBox.js", + "SecChat", + "SecureKeyStore", + "segnoJS", + "Seguranca", + "SegurancaBrasilcard", + "Select2", + "selfAsync", + "selfAutocomplete", + "SelfieJS", + "SenseJs", + "SenseOrm", + "Sentimental", + "SeptemTool", + "seqFlow", + "SerialDownloader", + "serveItQuick", + "Server", + "Service-Discovery-DLNA-SSDP", + "serviceDiscovery", + "SessionWebSocket", + "Set", + "setInterval", + "setRafTimeout", + "setTimeout", + "SexyJS", + "sfaClient", + "sgBase", + "sgCore", + "sgFramework", + "sgLayers", + "sgSay", + "Sharder", + "ShareSDK", + "SharingCMS", + "Shave", + "Sheet", + "SHI-Shire", + "sHistory", + "ShowNativeContact", + "SHPS4Node-auth", + "SHPS4Node-cache", + "SHPS4Node-commandline", + "SHPS4Node-Config", + "SHPS4Node-config", + "SHPS4Node-cookie", + "SHPS4Node-CSS", + "SHPS4Node-dependency", + "SHPS4Node-error", + "SHPS4Node-file", + "SHPS4Node-frontend", + "SHPS4Node-init", + "SHPS4Node-language", + "SHPS4Node-log", + "SHPS4Node-make", + "SHPS4Node-optimize", + "SHPS4Node-parallel", + "SHPS4Node-plugin", + "SHPS4Node-sandbox", + "SHPS4Node-schedule", + "SHPS4Node-session", + "SHPS4Node-SQL", + "shwang1aPackage1", + "shy-Do", + "shy-static-imgJoin", + "SignaturePrinter", + "Silvera", + "simoneDays", + "Simple", + "Simple-Cache", + "simple-hello-world-apiClientsideTest", + "simple-jQuery-slider", + "simpleArgsParser", + "simpleCsvToJson", + "SimpleHtdigest", + "SimpleQueue", + "SimpleRPC", + "Simplog", + "SingularityUI", + "sip.js-mnQf2Q2R", + "Sisense-node-schedule", + "SITA-JS-Wrapper", + "siteBuild", + "Skadi", + "SkelEktron", + "SKRCensorText", + "SkyLabels.js", + "Skype-command", + "slgComponents", + "Slidebars", + "Slidebars-legacy", + "slidePage", + "Slither-Server", + "sLog", + "slush-initPro", + "Smaller4You", + "Smart-Web-Proxy", + "SmartConfig", + "SmartyGrid", + "SMValidator", + "smyNpm1", + "Snake.js", + "SnipIt", + "SnsShare", + "SocialDig.js", + "Socialight", + "socketGW", + "SocketIPC", + "sortBy.js", + "Soumen", + "SoundCloud_Node_API", + "SpaceMagic", + "SpeechJS", + "Speedco", + "Speedonetzer", + "Sphero-Node-SDK", + "Spores", + "Spot", + "spotifyCurrentlyPlaying.js", + "SpotlightJS", + "Spring", + "SPUtility.js", + "SQLClient", + "SQProject", + "SquareOfNumber", + "Squirrel", + "squishMenu", + "Sslac", + "SSO", + "SSSDemoNPM7oct", + "SSuperSchool", + "StaceFlow", + "StanLee-WPTheme-Generator", + "star-initReact", + "Starr", + "startInt", + "starW-names", + "StaticServer", + "staticServer", + "staticSync", + "StatusBar", + "StdJSBuilder", + "steamAPI", + "STEPNode", + "Stewed", + "stickUp", + "stickyNavbar.js", + "stickyStack", + "StimShopPlugin", + "storeJSON", + "storkSQL", + "stormClient", + "Str.js", + "Stratagem", + "stream", + "string_decoder", + "String_module", + "string-DLL", + "string.prototype.htmlDecode", + "string.prototype.htmlEntityDecode", + "StringDistanceTS", + "StringMultiplier", + "StringScanner", + "STRUCT", + "Suckle", + "sudokuMaker", + "sudoTracker", + "SUI-Angular2-Modal", + "superClipBoard", + "SuperDank", + "superJoy", + "Supermodule", + "supermoduleBugay", + "supermoduleLyu", + "supermoduleNik", + "supermoduleShulumba", + "Supersonic", + "superUsingMod", + "svgSprite", + "swimCoachStopwatch", + "SwitchBoard", + "synchro_ByJoker", + "SyncRun", + "Syndication", + "Synergy", + "sys", + "Sysdate", + "sytemMonitor-client", + "szxPack", + "T_T", + "T-Box", + "table-Q", + "tableComponent", + "Tachyon", + "TagCloud", + "tagOf", + "TagSelect.js", + "TalkerNode", + "TALQS", + "talquingApp", + "TangramDocs", + "tap-linux-2BA", + "tap-win-2BA", + "tap-win-C94", + "Targis", + "Tattletale", + "Tayr", + "tbCLI", + "TDTwitterStream", + "Tea", + "TeamBuilder", + "TechNode", + "TechnoLib", + "TeeChart", + "Templ8", + "Template", + "Tempus", + "Ter", + "Tereshkovmodule", + "Terminal-command", + "test_helloWorld", + "Test-7", + "test-A", + "test-naamat-Al-Aswad", + "Test-Project", + "TestAmILate", + "testApi", + "testApp", + "Testchai2", + "Testchai21", + "testContrast", + "TESTdelete123", + "testDEMOABCD", + "testDirJackAtherton", + "Teste2", + "testeRealTime", + "testForThis", + "testMe", + "testModule", + "testModule-hui", + "testNode", + "TestNodeJsApplication", + "testPackage", + "testPackage2", + "TestPlugin", + "testPlugin", + "TestProject", + "testProject", + "testPublish", + "testPublisha", + "testPublishNpmModule", + "TFWhatIs", + "Thairon-node", + "Thanatos_pack", + "ThanhNV", + "Theater", + "TheGiver", + "Thimble", + "Thing.js", + "thingHolder", + "think-paymentService", + "think-qiniuService", + "think-quotationService", + "think-wechatService", + "ThinkHub", + "ThinkInsteon", + "ThirtyDaysOfReactNative", + "threadHandler", + "threejs-htmlRenderer", + "ThrustFS", + "ThumborJS", + "TigraphBot", + "tilejsonHttpShim", + "Time-Tracker-Cli", + "Timelined", + "Timeliner.Core", + "Timeliner.Index", + "Timepass", + "timers", + "timeTraveller", + "timeUtils", + "tiNanta", + "TinyAnimate", + "tinyChat", + "tinyEmiter", + "tinyFrame", + "tinyImages", + "tinyLoger", + "Titan", + "TJAngular", + "tls", + "tm-apps-poolApi", + "tmSensor", + "toBin", + "toDataURL", + "toDoList", + "toDots", + "Toji", + "tokenAndAuthorizationManager", + "tokenAndAuthorizationManger", + "Tom", + "tomloprodModal", + "Tool-bluej-gulp", + "Toolshed-Client", + "topSdk", + "TopuNet-AMD-modules", + "TopuNet-BaiduMap", + "TopuNet-CalendarScroller", + "TopuNet-dropDownLoad", + "TopuNet-GrayScale", + "TopuNet-ImageCropCompressorH5", + "TopuNet-JRoll", + "TopuNet-js-functions", + "TopuNet-JsHint4Sublime", + "TopuNet-JsHintify", + "TopuNet-Landscape_mask", + "TopuNet-Landscape-Mask", + "TopuNet-LayerShow", + "TopuNet-mobile-stop-moved", + "TopuNet-node-functions", + "TopuNet-Pic-code", + "TopuNet-PromptLayer-JS", + "TopuNet-QueueLazyLoad", + "TopuNet-RequireJS", + "TopuNet-RotatingBanner", + "TopuNet-WaterFall", + "TopuNet-weixin-node", + "TorrentBeam", + "TorrentCollection", + "toSrc", + "toString", + "touchController", + "toYaml", + "TPA", + "tr-O64", + "trace_events", + "TradeJS", + "Trains", + "TrainsController", + "TrainsModel", + "TramiteDocumentarioFront", + "TransactionRelay", + "transformConfigJson", + "transitionEnd", + "translateFzn", + "Travis", + "TrixCSS", + "truncateFilename", + "tslint-jasmine-noSkipOrFocus", + "TSN", + "ttm-Testing", + "tty", + "Tuio.js", + "Turntable", + "tuTrabajo-client", + "TweenTime", + "TwigJS", + "twitterApiWrapper", + "txtObj", + "Tyche", + "TypeCast", + "typedCj.js", + "TypedFunc", + "typescript-demo-MATC-Andrew", + "typography-theme-Wikipedia", + "typopro-web-TypoPRO-AmaticSC", + "typopro-web-TypoPRO-AnonymousPro", + "typopro-web-TypoPRO-Asap", + "typopro-web-TypoPRO-Astloch", + "typopro-web-TypoPRO-BebasNeue", + "typopro-web-TypoPRO-Bitter", + "typopro-web-TypoPRO-Chawp", + "typopro-web-TypoPRO-ComingSoon", + "typopro-web-TypoPRO-Cousine", + "typopro-web-TypoPRO-Coustard", + "typopro-web-TypoPRO-CraftyGirls", + "typopro-web-TypoPRO-Cuprum", + "typopro-web-TypoPRO-Damion", + "typopro-web-TypoPRO-DancingScript", + "typopro-web-TypoPRO-Delius", + "typopro-web-TypoPRO-Gidole", + "typopro-web-TypoPRO-GiveYouGlory", + "typopro-web-TypoPRO-GrandHotel", + "typopro-web-TypoPRO-GreatVibes", + "typopro-web-TypoPRO-Handlee", + "typopro-web-TypoPRO-HHSamuel", + "typopro-web-TypoPRO-Inconsolata", + "typopro-web-TypoPRO-IndieFlower", + "typopro-web-TypoPRO-Junction", + "typopro-web-TypoPRO-Kalam", + "typopro-web-TypoPRO-KingthingsPetrock", + "typopro-web-TypoPRO-Kreon", + "typopro-web-TypoPRO-LeagueGothic", + "typopro-web-TypoPRO-Lekton", + "typopro-web-TypoPRO-LibreBaskerville", + "typopro-web-TypoPRO-Milonga", + "typopro-web-TypoPRO-Montserrat", + "typopro-web-TypoPRO-Nickainley", + "typopro-web-TypoPRO-Oxygen", + "typopro-web-TypoPRO-Pacifico", + "typopro-web-TypoPRO-PatuaOne", + "typopro-web-TypoPRO-Poetsen", + "typopro-web-TypoPRO-Pompiere", + "typopro-web-TypoPRO-PTMono", + "typopro-web-TypoPRO-Rosario", + "typopro-web-TypoPRO-SansitaOne", + "typopro-web-TypoPRO-Satisfy", + "typopro-web-TypoPRO-Signika", + "typopro-web-TypoPRO-Slabo", + "typopro-web-TypoPRO-TopSecret", + "typopro-web-TypoPRO-Unifraktur", + "typopro-web-TypoPRO-Vegur", + "typopro-web-TypoPRO-VeteranTypewriter", + "typopro-web-TypoPRO-WeblySleek", + "typopro-web-TypoPRO-Yellowtail", + "Ubertesters", + "Ubi", + "UbibotSensor", + "UbidotsMoscaServer", + "UbiName", + "uDom", + "ueberDB", + "ueberDB-couch", + "ueberRemoteStorage", + "ugcFore", + "UIjson", + "UkGeoTool", + "UltraServerIO", + "UM007", + "uMech", + "uMicro", + "uMicro-invoke", + "UMiracleButton", + "uncaughtException", + "Underscore-1", + "UnderscoreKit", + "UnderscoreMatchersForJasmine", + "underscorePlus", + "underscoreWithTypings", + "Uniform", + "Unit-Bezier", + "unity-kjXmol-1", + "UniversalRoute", + "Up2Bucket", + "UParams", + "UploadCore", + "Uploader", + "URIjs", + "url", + "URLON", + "urlParser", + "urlWatch", + "USAJOBS", + "USAJOBS_Help_Center", + "UserID", + "userModule1123455", + "util", + "utilityFileSystem", + "utilityTool", + "Utils", + "uTool", + "uTool2", + "uvCharts", + "v8", + "Validate", + "Validator", + "VardeminChat", + "vc-buttonGroup", + "vcPagination", + "vdGlslCanvas", + "VDU-web", + "Vector", + "Velvet", + "vericredClient", + "VerifyInput.js", + "Videobox-MODX", + "videoBoxer", + "VideoStream", + "Vidzy", + "ViewAbility", + "ViewPort", + "ViewTest", + "vintageJS", + "Virsical", + "VK-Promise", + "VLC-command", + "vm", + "VmosoApiClient", + "vmSFTP", + "VoiceIt", + "voiceLive", + "Votesy", + "VoxFeed", + "Voyager-search", + "vPromise", + "vQ", + "vQMgArq1o4U1", + "vsGoogleAutocomplete", + "vue-dS", + "vue-scrollTo", + "vueLoadingBar", + "VueProject", + "VueProjectES5", + "VueTree", + "Vuk", + "W2G2", + "w5cValidator", + "w11k-dropdownToggle", + "Wamble", + "wamTool", + "Wanderer", + "wangeditorForReact", + "wantu-nodejsSDK", + "wasabiD", + "wasabiH", + "wasi", + "WasteOfTime", + "WatchWorker", + "watsonWebSocketSTTwrapper", + "wb-Wisteria", + "wBitmask", + "wColor", + "wColor256", + "wConsequence", + "wCopyable", + "WCordova", + "wDeployer", + "Web_GUI_Core", + "web3.onChange", + "Web4.0", + "webarrancoStarter", + "WebConsoleUI", + "Webcord", + "webdriverNode", + "webext-getBytesInUse-polyfill", + "WebHook", + "WebODF", + "webpack-dev-server-getApp", + "webpack-dynamicHash", + "webpack-Minimount-starter", + "WebParrot", + "webpay-webserviceAPI", + "webStart", + "WebStencil", + "webStorage", + "wechat-enterprise-for-kfService", + "wEventHandler", + "wFiles", + "wGluCal", + "WhereThingsHappened", + "WhiteRabbit", + "WigGLe", + "Wilson_U", + "Wilson_Util", + "WiredPanels", + "wkhtmltopdfWrapper", + "wLogger", + "Wmhao", + "WNdb", + "WoD-Dice", + "WolfyEventEmitter", + "woodwoodnine_FirstTest", + "wordCounting", + "WordDuelConstants", + "wPath", + "wProto", + "wqProj-cli", + "wRegexpObject", + "WSBroker", + "wscn-tilesetQuote-component", + "wsxRest", + "wTemplate", + "wTesting", + "WTGeo", + "wTools", + "wy-checkBrowser", + "X-date", + "X-editable", + "xBEM", + "xlsTjson", + "xlsxParser", + "xmlToJsonTs", + "Xnpmtools", + "xSpinner", + "xStore", + "xui-vue-WorkflowArrow", + "Xunfei", + "xuNpm", + "XWindow", + "xwjApp", + "xxxDemo", + "yaDeferred", + "YAEventEmitter", + "yaMap", + "yamQuery-excel", + "yamQuery-excelAnalizer", + "YamYam", + "yang-testingNPM", + "YaoXiaoMi", + "Yeezy-Case", + "Yggdrasil", + "YJS", + "YmpleCommerce", + "YouAreDaChef", + "YouSlackBot", + "yrdLmz", + "yuanMath", + "YuicompressorValidator", + "Yummy", + "Yummy-Yummy", + "YunUI", + "Yworkcli", + "Yworkshell", + "z-lib-structure-dqIndex", + "zhb_helloTest", + "Zhengzx", + "zigZag", + "Ziz", + "ZJJPackage", + "zkModules", + "zlib", + "zmqConnector", + "ZooKeeper", + "zzcBridge", + "zzcCopy", + "zzcDownloadApp" + ] +} diff --git a/src/utils/purl.mts b/src/utils/purl.mts index 4ea6a69dbd..561d80595a 100644 --- a/src/utils/purl.mts +++ b/src/utils/purl.mts @@ -23,14 +23,486 @@ * See: https://github.com/package-url/purl-spec */ -import { PackageURL, type PurlQualifiers } from '@socketregistry/packageurl-js' -import { isObjectObject } from '@socketsecurity/registry/lib/objects' +import { builtinModules } from 'node:module' + +import { isObjectObject } from './objects.mts' +import legacyNpmData from './purl-legacy-npm-names.json' with { type: 'json' } import type { SocketArtifact } from './alert/artifact.mts' import type { PURL_Type } from './ecosystem.mts' export type PurlObject = T & { type: PURL_Type } +export type PurlQualifiers = Record + +/** + * Minimal PackageURL implementation used by the CLI. + * + * Adapted from @socketregistry/packageurl-js 1.0.9, src/package-url.js, + * src/normalize.js, src/encode.js, src/decode.js, and the corresponding + * component validation helpers. Copyright (c) the purl authors. + * The upstream MIT license notice and terms are reproduced below. + */ +export class PackageURL { + type: string + name: string + namespace: string | undefined + version: string | undefined + qualifiers: PurlQualifiers | undefined + subpath: string | undefined + + constructor( + rawType: string, + rawNamespace?: string, + rawName?: string, + rawVersion?: string, + rawQualifiers?: + | PurlQualifiers + | URLSearchParams + | Map + | string, + rawSubpath?: string, + ) { + this.type = ( + typeof rawType === 'string' ? rawType.trim().toLowerCase() : rawType + ) as string + this.name = ( + typeof rawName === 'string' ? rawName.trim() : rawName + ) as string + this.namespace = ( + typeof rawNamespace === 'string' + ? normalizePath(rawNamespace) + : (rawNamespace ?? undefined) + ) as string | undefined + this.version = ( + typeof rawVersion === 'string' + ? rawVersion.trim() || undefined + : (rawVersion ?? undefined) + ) as string | undefined + this.qualifiers = ( + rawQualifiers == null + ? undefined + : typeof rawQualifiers === 'string' || typeof rawQualifiers === 'object' + ? normalizeQualifiers(rawQualifiers) + : rawQualifiers + ) as PurlQualifiers | undefined + this.subpath = ( + typeof rawSubpath === 'string' + ? normalizeSubpath(rawSubpath) + : (rawSubpath ?? undefined) + ) as string | undefined + validatePurlComponents(this) + normalizePurlType(this) + validatePurl(this) + } + + toString(): string { + let result = `pkg:${encodeURIComponent(this.type)}/` + if (this.namespace) { + result += `${encodeNamespace(this.namespace)}/` + } + result += encodeURIComponent(this.name).replaceAll('%3A', ':') + if (this.version) { + result += `@${encodeURIComponent(this.version).replaceAll('%3A', ':')}` + } + if (this.qualifiers) { + result += `?${encodeQualifiers(this.qualifiers)}` + } + if (this.subpath) { + result += `#${encodeURIComponent(this.subpath).replaceAll('%2F', '/')}` + } + return result + } + + static fromString(input: string): PackageURL { + if (typeof input !== 'string') { + throw new Error('A purl string argument is required.') + } + if (!input.trim()) { + throw new Error('Invalid purl: "type" is a required component') + } + let url: URL + let authorityUrl: URL + try { + const colon = input.indexOf(':') + const afterColon = input.slice(colon + 1) + const stripped = afterColon.replace(/^\/+/, '') + url = new URL(`${input.slice(0, colon)}:${stripped}`) + authorityUrl = + stripped.length === afterColon.length ? url : new URL(input) + } catch (cause) { + throw new Error('Invalid purl: failed to parse as URL', { cause }) + } + if (url.protocol !== 'pkg:') { + throw new Error('Invalid purl: missing required "pkg" scheme component') + } + if (authorityUrl.username || authorityUrl.password) { + throw new Error('Invalid purl: cannot contain a "user:pass@host:port"') + } + const path = url.pathname + const slash = path.indexOf('/') + const type = decodePurl('type', slash === -1 ? path : path.slice(0, slash)) + if (slash < 1) { + return new PackageURL(type) + } + let at = + type === 'npm' ? path.indexOf('@', slash + 2) : path.lastIndexOf('@') + if (at !== -1 && path.charCodeAt(at - 1) === 47) { + at = -1 + } + const beforeVersion = path.slice( + type.length + 1, + at === -1 ? path.length : at, + ) + const version = + at === -1 ? undefined : decodePurl('version', path.slice(at + 1)) + const lastSlash = beforeVersion.lastIndexOf('/') + const namespace = + lastSlash === -1 + ? undefined + : decodePurl('namespace', beforeVersion.slice(0, lastSlash)) + const name = decodePurl( + 'name', + lastSlash === -1 ? beforeVersion : beforeVersion.slice(lastSlash + 1), + ) + let qualifiers: PurlQualifiers | undefined + if (url.searchParams.size !== 0) { + const searchParams = new URLSearchParams() + const entries = url.search.slice(1).split('&') + for (const entry of entries) { + const pairs = entry.split('=') + const value = decodePurl('qualifiers', pairs[1] ?? '') + // Append keeps literal plus signs in decoded values; URLSearchParams + // would otherwise interpret them as spaces when parsing the query. + searchParams.append(pairs[0] ?? '', value) + } + qualifiers = searchParams as unknown as PurlQualifiers + } + const subpath = url.hash + ? decodePurl('subpath', url.hash.slice(1)) + : undefined + return new PackageURL(type, namespace, name, version, qualifiers, subpath) + } +} + +function decodePurl(component: string, value: string): string { + try { + return decodeURIComponent(value) + } catch { + throw new Error(`Invalid purl: unable to decode "${component}" component`) + } +} + +function normalizePath( + value?: string, + filter?: (part: string) => boolean, +): string | undefined { + if (typeof value !== 'string') { + return undefined + } + const parts = value + .split('/') + .filter(part => part && (!filter || filter(part))) + return parts.join('/') +} + +function normalizeSubpath(value?: string): string | undefined { + return normalizePath( + value, + part => part !== '.' && part !== '..' && !!part.trim(), + ) +} + +function normalizeQualifiers( + value?: PurlQualifiers | URLSearchParams | Map | string, +): PurlQualifiers | undefined { + let entries: Iterable<[string, unknown]> + if (typeof value === 'string') { + entries = new URLSearchParams(value).entries() + } else if (value && typeof value === 'object') { + const entriesMethod = ( + value as { entries?: () => Iterable<[string, unknown]> } + ).entries + entries = + typeof entriesMethod === 'function' + ? entriesMethod.call(value) + : Object.entries(value) + } else { + entries = [] + } + const result: PurlQualifiers = Object.create(null) + for (const [key, raw] of entries) { + const normalized = String(raw).trim() + if (normalized) { + result[key.toLowerCase()] = normalized + } + } + return Object.keys(result).length ? result : undefined +} + +function encodeNamespace(value: string): string { + return encodeURIComponent(value).replaceAll('%3A', ':').replaceAll('%2F', '/') +} + +function encodeQualifiers(value: PurlQualifiers): string { + const params = new URLSearchParams() + for (const key of Object.keys(value).sort()) { + params.set(key, value[key]!.replaceAll(' ', '%20')) + } + return params.toString().replaceAll('%2520', '%20').replaceAll('+', '%2B') +} + +function normalizePurlType(purl: PackageURL): void { + const lowerNameAndNamespace = new Set([ + 'alpm', + 'apk', + 'bitbucket', + 'composer', + 'deb', + 'github', + 'gitlab', + 'hex', + ]) + if (lowerNameAndNamespace.has(purl.type)) { + purl.name = purl.name.toLowerCase() + if (purl.namespace) { + purl.namespace = purl.namespace.toLowerCase() + } + } else if (purl.type === 'npm') { + if (purl.namespace) { + purl.namespace = purl.namespace.toLowerCase() + } + // npm has a large historical allowlist of mixed-case/otherwise-invalid + // names. Preserve those names; modern package names are lowercase. + if (!isLegacyNpmName(purl.namespace, purl.name)) { + purl.name = purl.name.toLowerCase() + } + } else if (purl.type === 'pypi') { + if (purl.namespace) { + purl.namespace = purl.namespace.toLowerCase() + } + purl.name = purl.name.toLowerCase().replaceAll('_', '-') + } else if ( + purl.type === 'bitnami' || + purl.type === 'oci' || + purl.type === 'pub' + ) { + purl.name = purl.name.toLowerCase() + if (purl.type === 'pub') { + purl.name = purl.name.replaceAll('-', '_') + } + } else if (purl.type === 'luarocks' || purl.type === 'huggingface') { + if (purl.version) { + purl.version = purl.version.toLowerCase() + } + } else if ( + purl.type === 'mlflow' && + purl.qualifiers?.['repository_url']?.includes('databricks') + ) { + purl.name = purl.name.toLowerCase() + } else if (purl.type === 'qpkg' || purl.type === 'rpm') { + if (purl.namespace) { + purl.namespace = purl.namespace.toLowerCase() + } + } +} + +function validatePurl(purl: PackageURL): void { + validatePurlComponents(purl) + switch (purl.type) { + case 'conan': + if (purl.qualifiers?.['channel'] && !purl.namespace) { + throw new Error( + 'Invalid purl: conan requires a "namespace" component when a "channel" qualifier is present', + ) + } + if (purl.namespace && !purl.qualifiers) { + throw new Error( + 'Invalid purl: conan requires a "qualifiers" component when a namespace is present', + ) + } + break + case 'cran': + if (!purl.version) { + throw new Error('Invalid purl: cran requires a "version" component') + } + break + case 'golang': + if (purl.version?.startsWith('v') && !isSemver(purl.version.slice(1))) { + throw new Error( + 'Invalid purl: golang "version" component starting with a "v" must be followed by a valid semver version', + ) + } + break + case 'maven': + if (!purl.namespace) { + throw new Error('Invalid purl: maven requires a "namespace" component') + } + break + case 'swift': + if (!purl.namespace) { + throw new Error('Invalid purl: swift requires a "namespace" component') + } + if (!purl.version) { + throw new Error('Invalid purl: swift requires a "version" component') + } + break + case 'mlflow': + case 'oci': + if (purl.namespace) { + throw new Error( + `Invalid purl: ${purl.type} "namespace" component must be empty`, + ) + } + break + case 'pub': + if (!/^[a-z0-9_]+$/.test(purl.name)) { + throw new Error( + 'Invalid purl: pub "name" component may only contain [a-z0-9_] characters', + ) + } + break + case 'npm': + validateNpm(purl) + break + } +} + +function validatePurlComponents(purl: PackageURL): void { + if ( + typeof purl.type !== 'string' || + !purl.type || + !/^[A-Za-z][A-Za-z0-9.-]*$/.test(purl.type) + ) { + throw new Error( + 'Invalid purl: type is required and must contain only letters, numbers, "." and "-"', + ) + } + if (typeof purl.name !== 'string' || !purl.name) { + throw new Error('Invalid purl: "name" is a required component') + } + for (const [component, value] of [ + ['namespace', purl.namespace], + ['name', purl.name], + ['version', purl.version], + ['subpath', purl.subpath], + ] as const) { + if (value !== undefined && typeof value !== 'string') { + throw new Error(`Invalid purl: "${component}" must be a string`) + } + } + if (purl.qualifiers !== undefined && typeof purl.qualifiers !== 'object') { + throw new Error('Invalid purl: "qualifiers" must be an object') + } + for (const key of Object.keys(purl.qualifiers ?? {})) { + if (!/^[A-Za-z_.-][A-Za-z0-9._-]*$/.test(key)) { + throw new Error( + `Invalid purl: qualifier "${key}" contains an illegal character`, + ) + } + } +} + +function isSemver(value: string): boolean { + return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test( + value, + ) +} + +function isLegacyNpmName(namespace: string | undefined, name: string): boolean { + const id = `${namespace ? `${namespace}/` : ''}${name}` + return legacyNpmData.names.includes(id) +} + +function validateNpm(purl: PackageURL): void { + const { name, namespace } = purl + const id = `${namespace ? `${namespace}/` : ''}${name}` + const leading = namespace || name + if (leading.startsWith('.') || leading.startsWith('_')) { + throw new Error( + `Invalid purl: npm "${namespace ? 'namespace' : 'name'}" component cannot start with ${leading.startsWith('.') ? 'a period' : 'an underscore'}`, + ) + } + if (name.trim() !== name || namespace?.trim() !== namespace) { + throw new Error( + 'Invalid purl: npm components cannot contain leading or trailing spaces', + ) + } + if (namespace && !namespace.startsWith('@')) { + throw new Error( + 'Invalid purl: npm "namespace" component must start with an "@" character', + ) + } + if (encodeURIComponent(name) !== name) { + throw new Error( + 'Invalid purl: npm "name" component can only contain URL-friendly characters', + ) + } + if ( + namespace && + encodeURIComponent(namespace.slice(1)) !== namespace.slice(1) + ) { + throw new Error( + 'Invalid purl: npm "namespace" component can only contain URL-friendly characters', + ) + } + const lowered = id.toLowerCase() + if (lowered === 'node_modules' || lowered === 'favicon.ico') { + throw new Error( + `Invalid purl: npm component of "${lowered}" is not allowed`, + ) + } + if (isLegacyNpmName(namespace, name)) { + return + } + if (id.length > 214) { + throw new Error( + 'Invalid purl: npm "namespace" and "name" components can not collectively be more than 214 characters', + ) + } + if (lowered !== id) { + throw new Error( + 'Invalid purl: npm "name" component can not contain capital letters', + ) + } + if (/[~'!()*]/.test(name)) { + throw new Error( + 'Invalid purl: npm "name" component can not contain special characters ("~\'!()*")', + ) + } + if ( + new Set( + builtinModules.map(moduleName => moduleName.replace(/^node:/, '')), + ).has(lowered) + ) { + throw new Error( + 'Invalid purl: npm "name" component can not be a core module name', + ) + } +} + +/* +Copyright (c) the purl authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + export type PurlLike = string | PackageURL | SocketArtifact export type CreatePurlObjectOptions = { diff --git a/src/utils/registry-manifest.json b/src/utils/registry-manifest.json new file mode 100644 index 0000000000..a686bae493 --- /dev/null +++ b/src/utils/registry-manifest.json @@ -0,0 +1,1875 @@ +{ + "npm": [ + [ + "pkg:npm/%40socketregistry/abab@1.0.8", + { + "categories": ["cleanup"], + "deprecated": true, + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "BSD-3-Clause", + "name": "@socketregistry/abab", + "package": "abab", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/aggregate-error@1.0.14", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs", "esm"], + "license": "MIT", + "name": "@socketregistry/aggregate-error", + "package": "aggregate-error", + "version": "1.0.14" + } + ], + [ + "pkg:npm/%40socketregistry/array-buffer-byte-length@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array-buffer-byte-length", + "package": "array-buffer-byte-length", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/array-flatten@1.0.10", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array-flatten", + "package": "array-flatten", + "skipTests": true, + "version": "1.0.10" + } + ], + [ + "pkg:npm/%40socketregistry/array-includes@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array-includes", + "package": "array-includes", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.from@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.from", + "package": "array.from", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/array.of@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.of", + "package": "array.of", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.at@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.at", + "package": "array.prototype.at", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.every@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.every", + "package": "array.prototype.every", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.filter@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.filter", + "package": "array.prototype.filter", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.find@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.find", + "package": "array.prototype.find", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.findlast@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.findlast", + "package": "array.prototype.findlast", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.findlastindex@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.findlastindex", + "package": "array.prototype.findlastindex", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.flat@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.flat", + "package": "array.prototype.flat", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.flatmap@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.flatmap", + "package": "array.prototype.flatmap", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.foreach@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.foreach", + "package": "array.prototype.foreach", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.map@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.map", + "package": "array.prototype.map", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.reduce@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.reduce", + "package": "array.prototype.reduce", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.toreversed@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.toreversed", + "package": "array.prototype.toreversed", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/array.prototype.tosorted@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/array.prototype.tosorted", + "package": "array.prototype.tosorted", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/arraybuffer.prototype.slice@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/arraybuffer.prototype.slice", + "package": "arraybuffer.prototype.slice", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/assert@1.0.21", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/assert", + "package": "assert", + "version": "1.0.21" + } + ], + [ + "pkg:npm/%40socketregistry/asynciterator.prototype@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/asynciterator.prototype", + "package": "asynciterator.prototype", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/available-typed-arrays@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/available-typed-arrays", + "package": "available-typed-arrays", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/date@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/date", + "package": "date", + "skipTests": true, + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/deep-equal@1.0.20", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/deep-equal", + "package": "deep-equal", + "version": "1.0.20" + } + ], + [ + "pkg:npm/%40socketregistry/define-properties@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/define-properties", + "package": "define-properties", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/es-aggregate-error@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/es-aggregate-error", + "package": "es-aggregate-error", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/es-define-property@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/es-define-property", + "package": "es-define-property", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/es-get-iterator@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/es-get-iterator", + "package": "es-get-iterator", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/es-iterator-helpers@1.0.13", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/es-iterator-helpers", + "package": "es-iterator-helpers", + "version": "1.0.13" + } + ], + [ + "pkg:npm/%40socketregistry/es-set-tostringtag@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/es-set-tostringtag", + "package": "es-set-tostringtag", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/es6-object-assign@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/es6-object-assign", + "package": "es6-object-assign", + "skipTests": true, + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/es6-symbol@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "ISC", + "name": "@socketregistry/es6-symbol", + "package": "es6-symbol", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/for-each@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/for-each", + "package": "for-each", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/function-bind@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/function-bind", + "package": "function-bind", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/function.prototype.name@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/function.prototype.name", + "package": "function.prototype.name", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/functions-have-names@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/functions-have-names", + "package": "functions-have-names", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/get-symbol-description@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/get-symbol-description", + "package": "get-symbol-description", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/globalthis@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/globalthis", + "package": "globalthis", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/gopd@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/gopd", + "package": "gopd", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/harmony-reflect@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "(Apache-2.0 OR MPL-1.1)", + "name": "@socketregistry/harmony-reflect", + "package": "harmony-reflect", + "skipTests": true, + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/has-property-descriptors@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/has-property-descriptors", + "package": "has-property-descriptors", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/has-proto@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/has-proto", + "package": "has-proto", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/has-symbols@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/has-symbols", + "package": "has-symbols", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/has-tostringtag@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/has-tostringtag", + "package": "has-tostringtag", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/has@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/has", + "package": "has", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/hasown@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/hasown", + "package": "hasown", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/hyrious__bun.lockb@1.0.18", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs", "esm"], + "license": "MIT", + "name": "@socketregistry/hyrious__bun.lockb", + "package": "@hyrious/bun.lockb", + "skipTests": true, + "version": "1.0.18" + } + ], + [ + "pkg:npm/%40socketregistry/indent-string@1.0.13", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs", "esm"], + "license": "MIT", + "name": "@socketregistry/indent-string", + "package": "indent-string", + "version": "1.0.13" + } + ], + [ + "pkg:npm/%40socketregistry/internal-slot@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/internal-slot", + "package": "internal-slot", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/is-arguments@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-arguments", + "package": "is-arguments", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-array-buffer@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-array-buffer", + "package": "is-array-buffer", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-bigint@1.0.11", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-bigint", + "package": "is-bigint", + "version": "1.0.11" + } + ], + [ + "pkg:npm/%40socketregistry/is-boolean-object@1.0.11", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-boolean-object", + "package": "is-boolean-object", + "version": "1.0.11" + } + ], + [ + "pkg:npm/%40socketregistry/is-core-module@1.0.10", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-core-module", + "package": "is-core-module", + "version": "1.0.10" + } + ], + [ + "pkg:npm/%40socketregistry/is-date-object@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-date-object", + "package": "is-date-object", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-generator-function@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-generator-function", + "package": "is-generator-function", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-interactive@1.0.6", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs", "esm"], + "license": "MIT", + "name": "@socketregistry/is-interactive", + "package": "is-interactive", + "version": "1.0.6" + } + ], + [ + "pkg:npm/%40socketregistry/is-map@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-map", + "package": "is-map", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-nan@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/is-nan", + "package": "is-nan", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/is-negative-zero@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/is-negative-zero", + "package": "is-negative-zero", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/is-number-object@1.0.10", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-number-object", + "package": "is-number-object", + "version": "1.0.10" + } + ], + [ + "pkg:npm/%40socketregistry/is-regex@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-regex", + "package": "is-regex", + "skipTests": true, + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-set@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-set", + "package": "is-set", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-shared-array-buffer@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-shared-array-buffer", + "package": "is-shared-array-buffer", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-string@1.0.11", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-string", + "package": "is-string", + "version": "1.0.11" + } + ], + [ + "pkg:npm/%40socketregistry/is-symbol@1.0.11", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-symbol", + "package": "is-symbol", + "version": "1.0.11" + } + ], + [ + "pkg:npm/%40socketregistry/is-typed-array@1.0.11", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-typed-array", + "package": "is-typed-array", + "version": "1.0.11" + } + ], + [ + "pkg:npm/%40socketregistry/is-unicode-supported@1.0.5", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs", "esm"], + "license": "MIT", + "name": "@socketregistry/is-unicode-supported", + "package": "is-unicode-supported", + "version": "1.0.5" + } + ], + [ + "pkg:npm/%40socketregistry/is-weakmap@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-weakmap", + "package": "is-weakmap", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/is-weakref@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/is-weakref", + "package": "is-weakref", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/is-weakset@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["browserify", "cjs"], + "license": "MIT", + "name": "@socketregistry/is-weakset", + "package": "is-weakset", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/isarray@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/isarray", + "package": "isarray", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/iterator.prototype@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/iterator.prototype", + "package": "iterator.prototype", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/json-stable-stringify@1.0.12", + { + "categories": ["cleanup", "levelup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/json-stable-stringify", + "package": "json-stable-stringify", + "version": "1.0.12" + } + ], + [ + "pkg:npm/%40socketregistry/jsonify@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "Public Domain", + "name": "@socketregistry/jsonify", + "package": "jsonify", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/number-is-nan@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/number-is-nan", + "package": "number-is-nan", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/object-assign@1.0.5", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object-assign", + "package": "object-assign", + "version": "1.0.5" + } + ], + [ + "pkg:npm/%40socketregistry/object-is@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object-is", + "package": "object-is", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/object-keys@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object-keys", + "package": "object-keys", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/object.assign@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object.assign", + "package": "object.assign", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/object.entries@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object.entries", + "package": "object.entries", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/object.fromentries@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object.fromentries", + "package": "object.fromentries", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/object.getownpropertydescriptors@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object.getownpropertydescriptors", + "package": "object.getownpropertydescriptors", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/object.getprototypeof@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object.getprototypeof", + "package": "object.getprototypeof", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/object.groupby@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object.groupby", + "package": "object.groupby", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/object.hasown@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object.hasown", + "package": "object.hasown", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/object.values@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/object.values", + "package": "object.values", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/packageurl-js@1.0.9", + { + "categories": ["levelup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/packageurl-js", + "package": "packageurl-js", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/path-parse@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/path-parse", + "package": "path-parse", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/promise.allsettled@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/promise.allsettled", + "package": "promise.allsettled", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/promise.any@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/promise.any", + "package": "promise.any", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/querystringify@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/querystringify", + "package": "querystringify", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/reflect.getprototypeof@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/reflect.getprototypeof", + "package": "reflect.getprototypeof", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/reflect.ownkeys@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/reflect.ownkeys", + "package": "reflect.ownkeys", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/regexp.prototype.flags@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/regexp.prototype.flags", + "package": "regexp.prototype.flags", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/safe-array-concat@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/safe-array-concat", + "package": "safe-array-concat", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/safe-buffer@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/safe-buffer", + "package": "safe-buffer", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/safe-regex-test@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/safe-regex-test", + "package": "safe-regex-test", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/safer-buffer@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/safer-buffer", + "package": "safer-buffer", + "skipTests": true, + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/set-function-length@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/set-function-length", + "package": "set-function-length", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/side-channel@1.0.9", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/side-channel", + "package": "side-channel", + "version": "1.0.9" + } + ], + [ + "pkg:npm/%40socketregistry/string.fromcodepoint@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.fromcodepoint", + "package": "string.fromcodepoint", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.at@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.at", + "package": "string.prototype.at", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.codepointat@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.codepointat", + "package": "string.prototype.codepointat", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.endswith@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.endswith", + "package": "string.prototype.endswith", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.includes@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.includes", + "package": "string.prototype.includes", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.matchall@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.matchall", + "package": "string.prototype.matchall", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.padend@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.padend", + "package": "string.prototype.padend", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.padstart@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.padstart", + "package": "string.prototype.padstart", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.repeat@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.repeat", + "package": "string.prototype.repeat", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.replaceall@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.replaceall", + "package": "string.prototype.replaceall", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.split@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.split", + "package": "string.prototype.split", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.startswith@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.startswith", + "package": "string.prototype.startswith", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.trim@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.trim", + "package": "string.prototype.trim", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.trimend@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.trimend", + "package": "string.prototype.trimend", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.trimleft@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.trimleft", + "package": "string.prototype.trimleft", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.trimright@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.trimright", + "package": "string.prototype.trimright", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/string.prototype.trimstart@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/string.prototype.trimstart", + "package": "string.prototype.trimstart", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/typed-array-buffer@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/typed-array-buffer", + "package": "typed-array-buffer", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/typed-array-byte-length@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/typed-array-byte-length", + "package": "typed-array-byte-length", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/typed-array-byte-offset@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/typed-array-byte-offset", + "package": "typed-array-byte-offset", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/typed-array-length@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/typed-array-length", + "package": "typed-array-length", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/typedarray.prototype.slice@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/typedarray.prototype.slice", + "package": "typedarray.prototype.slice", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/typedarray@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/typedarray", + "package": "typedarray", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/unbox-primitive@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/unbox-primitive", + "package": "unbox-primitive", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/util.promisify@1.0.10", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/util.promisify", + "package": "util.promisify", + "version": "1.0.10" + } + ], + [ + "pkg:npm/%40socketregistry/which-boxed-primitive@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/which-boxed-primitive", + "package": "which-boxed-primitive", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/which-collection@1.0.7", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/which-collection", + "package": "which-collection", + "version": "1.0.7" + } + ], + [ + "pkg:npm/%40socketregistry/which-typed-array@1.0.8", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/which-typed-array", + "package": "which-typed-array", + "version": "1.0.8" + } + ], + [ + "pkg:npm/%40socketregistry/yocto-spinner@1.0.23", + { + "categories": ["cleanup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs", "esm"], + "license": "MIT", + "name": "@socketregistry/yocto-spinner", + "package": "yocto-spinner", + "skipTests": true, + "version": "1.0.23" + } + ], + [ + "pkg:npm/shell-quote@1.8.3", + { + "categories": ["tuneup"], + "engines": { + "node": ">=18" + }, + "interop": ["cjs"], + "license": "MIT", + "name": "shell-quote", + "package": "shell-quote", + "version": "1.8.3" + } + ] + ] +} diff --git a/src/utils/sdk.mts b/src/utils/sdk.mts index 06614697c3..5beb0bd5bc 100644 --- a/src/utils/sdk.mts +++ b/src/utils/sdk.mts @@ -32,15 +32,14 @@ import { rootCertificates } from 'node:tls' import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent' import isInteractive from '@socketregistry/is-interactive/index.cjs' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { password } from '@socketsecurity/registry/lib/prompts' -import { isNonEmptyString } from '@socketsecurity/registry/lib/strings' -import { isUrl } from '@socketsecurity/registry/lib/url' import { SocketSdk, createUserAgentFromPkgJson } from '@socketsecurity/sdk' import { getConfigValueOrUndef } from './config.mts' -import { debugApiRequest, debugApiResponse } from './debug.mts' +import { debugApiRequest, debugApiResponse, debugFn } from './debug.mts' +import { logger } from './logger.mts' +import { password } from './prompts.mts' +import { isNonEmptyString } from './strings.mts' +import { isUrl } from './url.mts' import constants, { CONFIG_KEY_API_BASE_URL, CONFIG_KEY_API_PROXY, diff --git a/src/utils/sdk.test.mts b/src/utils/sdk.test.mts index 69a4ae4579..3589865918 100644 --- a/src/utils/sdk.test.mts +++ b/src/utils/sdk.test.mts @@ -80,10 +80,6 @@ const mockDebugApiResponse = vi.hoisted(() => vi.fn()) vi.mock('./debug.mts', () => ({ debugApiRequest: mockDebugApiRequest, debugApiResponse: mockDebugApiResponse, -})) - -// Mock registry debug functions used by getExtraCaCerts. -vi.mock('@socketsecurity/registry/lib/debug', () => ({ debugDir: vi.fn(), debugFn: vi.fn(), })) diff --git a/src/utils/self-update.mts b/src/utils/self-update.mts index e3bf91c7b1..e7b95f28a3 100644 --- a/src/utils/self-update.mts +++ b/src/utils/self-update.mts @@ -1,13 +1,20 @@ import updateNotifier from 'tiny-updater' import colors from 'yoctocolors-cjs' -import { logger } from '@socketsecurity/registry/lib/logger' - +import { logger } from './logger.mts' import constants from '../constants.mts' import { socketPackageLink } from './terminal-link.mts' +import { isStandaloneSea } from '../sea/runtime.mts' // Check the public npm registry for a newer socket release. export async function runSelfUpdateCheck(): Promise { + // A standalone executable cannot be updated through the npm package path. + // Keep its startup independent from registry metadata and avoid suggesting + // that an npm CLI update can replace this artifact. + if (isStandaloneSea()) { + return + } + await updateNotifier({ name: constants.SOCKET_CLI_BIN_NAME, registryUrl: 'https://registry.npmjs.org/', diff --git a/src/utils/serialize-result-json.mts b/src/utils/serialize-result-json.mts index 66c61badc4..e99a82200a 100644 --- a/src/utils/serialize-result-json.mts +++ b/src/utils/serialize-result-json.mts @@ -1,6 +1,6 @@ -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' -import { isObject } from '@socketsecurity/registry/lib/objects' +import { debugDir, debugFn } from './debug.mts' +import { logger } from './logger.mts' +import { isObject } from './objects.mts' import type { CResult } from '../types.mts' diff --git a/src/utils/socket-json.mts b/src/utils/socket-json.mts index 3ed36ad06d..be7c9f2cd2 100644 --- a/src/utils/socket-json.mts +++ b/src/utils/socket-json.mts @@ -21,11 +21,10 @@ import { existsSync, promises as fs, readFileSync } from 'node:fs' import path from 'node:path' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { logger } from '@socketsecurity/registry/lib/logger' - +import { debugDir, debugFn } from './debug.mts' import { formatErrorWithDetail } from './errors.mts' import { findUp } from './fs.mts' +import { logger } from './logger.mts' import { SOCKET_JSON, SOCKET_WEBSITE_URL } from '../constants.mts' import type { CResult } from '../types.mts' diff --git a/src/utils/socket-package-alert.mts b/src/utils/socket-package-alert.mts index 6f26f16ae0..015ee8c400 100644 --- a/src/utils/socket-package-alert.mts +++ b/src/utils/socket-package-alert.mts @@ -23,21 +23,18 @@ import semver from 'semver' import colors from 'yoctocolors-cjs' -import { getManifestData } from '@socketsecurity/registry' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { getOwn, hasOwn } from '@socketsecurity/registry/lib/objects' -import { resolvePackageName } from '@socketsecurity/registry/lib/packages' -import { naturalCompare } from '@socketsecurity/registry/lib/sorts' - import { isArtifactAlertCve } from './alert/artifact.mts' import { ALERT_FIX_TYPE } from './alert/fix.mts' import { ALERT_SEVERITY } from './alert/severity.mts' import { ColorOrMarkdown } from './color-or-markdown.mts' +import { debugDir, debugFn } from './debug.mts' import { toFilterConfig } from './filter-config.mts' -import { createEnum } from './objects.mts' +import { createEnum, getOwn, hasOwn } from './objects.mts' +import { getManifestData, resolvePackageName } from './packages.mts' import { createPurlObject, getPurlObject } from './purl.mts' import { getMajor } from './semver.mts' import { getSocketDevPackageOverviewUrl } from './socket-url.mts' +import { naturalCompare } from './sorts.mts' import { getTranslations } from './translations.mts' import type { @@ -47,8 +44,8 @@ import type { CveProps, } from './alert/artifact.mts' import type { PURL_Type } from './ecosystem.mts' +import type { Spinner } from './spinner.mts' import type { SocketYml } from '@socketsecurity/config' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' export const ALERT_SEVERITY_COLOR = createEnum({ critical: 'magenta', diff --git a/src/utils/socket-url.mts b/src/utils/socket-url.mts index aa6fb003ec..f6a6f2ee22 100644 --- a/src/utils/socket-url.mts +++ b/src/utils/socket-url.mts @@ -23,7 +23,7 @@ import { getPurlObject } from './purl.mts' import type { SocketArtifact } from './alert/artifact.mts' import type { PURL_Type } from './ecosystem.mts' -import type { PackageURL } from '@socketregistry/packageurl-js' +import type { PackageURL } from './purl.mts' export function getPkgFullNameFromPurl( purl: string | PackageURL | SocketArtifact, diff --git a/src/utils/sorts.mts b/src/utils/sorts.mts new file mode 100644 index 0000000000..c6b125017f --- /dev/null +++ b/src/utils/sorts.mts @@ -0,0 +1,14 @@ +/** + * Narrow copy of @socketsecurity/registry 1.1.17 lib/sorts.js naturalCompare. + * Copyright (c) Socket Inc. Licensed under the MIT License. + */ + +let naturalCompareFn: ((a: string, b: string) => number) | undefined + +export function naturalCompare(a: string, b: string): number { + naturalCompareFn ??= new Intl.Collator(undefined, { + numeric: true, + sensitivity: 'base', + }).compare + return naturalCompareFn(a, b) +} diff --git a/src/utils/spawn-sync.mts b/src/utils/spawn-sync.mts new file mode 100644 index 0000000000..b26ddfefad --- /dev/null +++ b/src/utils/spawn-sync.mts @@ -0,0 +1,49 @@ +/** Dependency-free synchronous spawn helper adapted from registry spawn (MIT). */ +import childProcess from 'node:child_process' +import path from 'node:path' + +import type { SpawnSyncOptions } from './spawn.mts' + +const windowsScriptExtRegExp = /\.(?:cmd|bat|ps1)$/i + +const ansiRegExp = + // eslint-disable-next-line no-control-regex -- The expression removes ANSI control codes. + /(?:\u001B\][\s\S]*?(?:\u0007|\u001B\\|\u009C)|[\u001B\u009B][[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])/g + +export function spawnSync( + command: string, + args?: string[] | readonly string[], + options?: SpawnSyncOptions, +): childProcess.SpawnSyncReturns { + const shell = options?.shell + if ( + process.platform === 'win32' && + shell && + windowsScriptExtRegExp.test(command) + ) { + command = path.basename(command, path.extname(command)) + } + const { stripAnsi = true, ...rawOptions } = { ...options } + const stdioString = rawOptions.stdioString !== false + const result = childProcess.spawnSync(command, args, { + encoding: stdioString ? 'utf8' : 'buffer', + ...rawOptions, + }) + if (stdioString) { + if (result.stdout) { + result.stdout = result.stdout.toString().trim() + } + if (result.stderr) { + result.stderr = result.stderr.toString().trim() + } + if (stripAnsi) { + if (typeof result.stdout === 'string') { + result.stdout = result.stdout.replace(ansiRegExp, '') + } + if (typeof result.stderr === 'string') { + result.stderr = result.stderr.replace(ansiRegExp, '') + } + } + } + return result as childProcess.SpawnSyncReturns +} diff --git a/src/utils/spawn.mts b/src/utils/spawn.mts new file mode 100644 index 0000000000..607d8cfaef --- /dev/null +++ b/src/utils/spawn.mts @@ -0,0 +1,266 @@ +/** + * Local copy of @socketsecurity/registry/lib/spawn from @socketsecurity/registry 1.1.17. + * Copyright (c) Socket Inc. and contributors. Licensed under the MIT license. + * Source: node_modules/@socketsecurity/registry/lib/spawn.js + * + * Kept local so the CLI can bundle its spawn runtime without reaching into the + * registry package's private source helpers. + */ +import Stream from 'node:stream' + +import crossSpawn from 'cross-spawn' + +import { spawnSync } from './spawn-sync.mts' +import constants from '../constants.mts' + +import type { Remap } from './objects.mts' +import type { Spinner } from './spinner.mts' +import type { + SpawnOptions as BaseSpawnOptions, + ChildProcess, + IOType, + StdioOptions, +} from 'node:child_process' + +export type SpawnError = { + args: string[] | readonly string[] + cmd: string + code: number + name: 'Error' + message: 'command failed' + signal: AbortSignal | null + stack: string + stderr: string | Buffer + stdout: string | Buffer +} +export type SpawnErrorWithOutputString = Omit< + SpawnError, + 'stderr' | 'stdout' +> & { + stdout: string + stderr: string +} +export type SpawnErrorWithOutputBuffer = Omit< + SpawnError, + 'stderr' | 'stdout' +> & { + stdout: Buffer + stderr: Buffer +} +export type SpawnExtra = Record +export type SpawnStdioResult = { + cmd: string + args: string[] | readonly string[] + code: number + signal: AbortSignal | null + stdout: Output + stderr: Output +} & Extra +export type SpawnResult = Promise< + SpawnStdioResult +> & { process: ChildProcess; stdin: Stream.Writable | null } +export type SpawnOptions = Remap< + BaseSpawnOptions & { + spinner?: Spinner | undefined + stdioString?: boolean | undefined + stripAnsi?: boolean | undefined + } +> +export type SpawnSyncOptions = Omit & { + encoding?: BufferEncoding | 'buffer' | null | undefined +} +export interface SpawnSyncOptionsWithStringEncoding extends SpawnSyncOptions { + encoding: BufferEncoding +} +export interface SpawnSyncOptionsWithBufferEncoding extends SpawnSyncOptions { + encoding?: 'buffer' | null | undefined +} +export interface SpawnSyncReturns { + pid: number + output: Array + stdout: T + stderr: T + status: number | null + signal: NodeJS.Signals | null + error?: Error | undefined +} +export type StdioType = IOType | 'ipc' | Array +const ansiRegExp = + // eslint-disable-next-line no-control-regex -- The expression removes ANSI control codes. + /(?:\u001B\][\s\S]*?(?:\u0007|\u001B\\|\u009C)|[\u001B\u009B][[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])/g + +function isSpawnError(value: unknown): value is SpawnError { + return ( + value !== null && + typeof value === 'object' && + Object.prototype.hasOwnProperty.call(value, 'code') && + typeof (value as SpawnError).code === 'number' && + Object.prototype.hasOwnProperty.call(value, 'cmd') && + typeof (value as SpawnError).cmd === 'string' && + Object.prototype.hasOwnProperty.call(value, 'args') && + Array.isArray((value as SpawnError).args) + ) +} + +function isStdioType( + stdio: StdioOptions | undefined, + type: StdioType, +): boolean { + return ( + stdio === type || + ((stdio === null || stdio === undefined) && type === 'pipe') || + (Array.isArray(stdio) && + stdio.length > 2 && + stdio[0] === type && + stdio[1] === type && + stdio[2] === type) + ) +} + +function stripAnsiFromSpawnResult< + T extends { stdout?: string | Buffer; stderr?: string | Buffer }, +>(result: T): T { + const { stderr, stdout } = result + if (typeof stdout === 'string') { + result.stdout = stdout.replace(ansiRegExp, '') + } + if (typeof stderr === 'string') { + result.stderr = stderr.replace(ansiRegExp, '') + } + return result +} + +function spawn( + cmd: string, + args: string[] | readonly string[], + options?: O, + extra?: SpawnExtra, +): SpawnResult< + O extends { stdioString: false } ? Buffer : string, + typeof extra +> { + const { + spinner: activeSpinner = constants.spinner, + stripAnsi: shouldStripAnsi = true, + ...spawnOptions + } = { __proto__: null, ...options } + const { env, stdio, stdioString = true } = spawnOptions + const wasSpinning = !!activeSpinner?.isSpinning + const shouldStopSpinner = + wasSpinning && !isStdioType(stdio, 'ignore') && !isStdioType(stdio, 'pipe') + if (shouldStopSpinner) { + activeSpinner.stop() + } + const childEnv = Object.assign( + Object.create(null) as NodeJS.ProcessEnv, + process.env, + env, + ) + const normalizedOptions = { + signal: abortSignal, + ...spawnOptions, + // cross-spawn launches Windows command shims without a shell. Do not pass + // through shell=true because command and arguments can come from PATH or + // package-manager environment values. + shell: false, + env: childEnv, + } as BaseSpawnOptions + const child = crossSpawn(cmd, [...args], normalizedOptions) + const stdout: Buffer[] = [] + const stderr: Buffer[] = [] + const collect = (stream: NodeJS.ReadableStream | null, chunks: Buffer[]) => { + stream?.on('data', (chunk: Buffer | string) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + }) + } + collect(child.stdout, stdout) + collect(child.stderr, stderr) + let resolvePromise!: (value: any) => void + let rejectPromise!: (reason: unknown) => void + let spawnPromise: any = new Promise((resolve, reject) => { + resolvePromise = resolve + rejectPromise = reject + }) + spawnPromise.process = child + spawnPromise.stdin = child.stdin + const getOutput = (chunks: Buffer[], fd: 1 | 2) => { + const stdioValue = Array.isArray(spawnOptions.stdio) + ? spawnOptions.stdio[fd] + : spawnOptions.stdio + if ( + stdioValue !== undefined && + stdioValue !== 'pipe' && + stdioValue !== null + ) { + return null + } + const output = Buffer.concat(chunks) + return stdioString ? output.toString().trim() : output + } + const getResult = (code: number | null, signal: NodeJS.Signals | null) => ({ + cmd, + args, + code, + signal, + stdout: getOutput(stdout, 1), + stderr: getOutput(stderr, 2), + ...extra, + }) + const closeError = new Error('command failed') + child.on('error', error => { + Object.assign(error, { + cmd, + args, + signal: normalizedOptions.signal ?? null, + stdout: getOutput(stdout, 1), + stderr: getOutput(stderr, 2), + ...extra, + }) + rejectPromise(error) + }) + child.on('close', (code, signal) => { + if (code || signal) { + rejectPromise(Object.assign(closeError, getResult(code, signal))) + } else { + resolvePromise(getResult(code, signal)) + } + }) + const oldSpawnPromise = spawnPromise + if (shouldStripAnsi && stdioString) { + spawnPromise = spawnPromise + .then(stripAnsiFromSpawnResult) + .catch((error: unknown) => { + throw stripAnsiFromSpawnResult( + error as { stdout?: string | Buffer; stderr?: string | Buffer }, + ) + }) + } + if (shouldStopSpinner) { + spawnPromise = spawnPromise.finally(() => activeSpinner.start()) + } + for (const key of Object.keys(oldSpawnPromise)) { + ;(spawnPromise as any)[key] = (oldSpawnPromise as any)[key] + } + return spawnPromise as SpawnResult< + O extends { stdioString: false } ? Buffer : string, + typeof extra + > +} + +const abortController = new AbortController() +const abortSignal = abortController.signal +for (const signal of ['SIGHUP', 'SIGINT', 'SIGTERM'] as const) { + const handler = () => { + abortController.abort() + process.removeListener(signal, handler) + process.kill(process.pid, signal) + } + try { + process.once(signal, handler) + } catch { + // Some signals are unavailable on Windows. + } +} +process.once('exit', () => abortController.abort()) + +export { isSpawnError, isStdioType, spawn, spawnSync } diff --git a/src/utils/spec.mts b/src/utils/spec.mts index b9fff74bb4..73b32d4614 100644 --- a/src/utils/spec.mts +++ b/src/utils/spec.mts @@ -3,7 +3,7 @@ import semver from 'semver' import { NPM } from '../constants.mts' import { stripPnpmPeerSuffix } from './pnpm.mts' -import type { PackageURL } from '@socketregistry/packageurl-js' +import type { PackageURL } from './purl.mts' export function idToNpmPurl(id: string): string { return `pkg:${NPM}/${id}` diff --git a/src/utils/spinner.mts b/src/utils/spinner.mts new file mode 100644 index 0000000000..127bb25ec3 --- /dev/null +++ b/src/utils/spinner.mts @@ -0,0 +1,217 @@ +/* + * Spinner compatibility helpers adapted from @socketsecurity/registry 1.1.17 + * (lib/spinner.js) and @socketregistry/yocto-spinner 1.0.23. + * Copyright (c) 2024 Socket Inc. Both sources are licensed under MIT. + */ + +import process from 'node:process' + +export type SpinnerStyle = { frames: string[]; interval?: number | undefined } +export type SpinnerOptions = { + color?: string | undefined + spinner?: SpinnerStyle | undefined + signal?: AbortSignal | undefined + stream?: NodeJS.WriteStream | undefined + text?: string | undefined +} + +const ciSpinner: SpinnerStyle = { frames: [''], interval: 2_147_483_647 } +const defaultStyle: SpinnerStyle = { + frames: + process.env['TERM'] === 'linux' + ? ['-', '\\', '|', '/'] + : ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], + interval: 80, +} +const cliSpinners: Record = { + default: defaultStyle, + ci: ciSpinner, +} + +export function getCliSpinners(styleName?: string): SpinnerStyle | undefined +export function getCliSpinners( + styleName?: string, +): SpinnerStyle | Record | undefined { + return styleName === undefined + ? cliSpinners + : Object.hasOwn(cliSpinners, styleName) + ? cliSpinners[styleName] + : undefined +} + +export class Spinner { + color = 'cyan' + text: string + spinner: SpinnerStyle + #stream: NodeJS.WriteStream + #timer: NodeJS.Timeout | undefined + #frame = 0 + #indent = 0 + #spinning = false + #interactive: boolean + + constructor(options: SpinnerOptions = {}) { + this.text = options.text?.trimStart() ?? '' + this.spinner = + options.spinner ?? (process.env['CI'] ? ciSpinner : defaultStyle) + this.#stream = options.stream ?? process.stderr + this.color = options.color ?? 'cyan' + this.#interactive = + !!this.#stream.isTTY && + process.env['TERM'] !== 'dumb' && + !('CI' in process.env) + if (options.signal?.aborted) { + return + } + options.signal?.addEventListener('abort', () => this.stop(), { once: true }) + } + + get isSpinning(): boolean { + return this.#spinning + } + getText(): string { + return this.text + } + setText(text = ''): this { + this.text = text.trimStart() + return this + } + clear(): this { + if (this.isSpinning) { + this.#stream.write('\r\u001b[K') + } + return this + } + indent(spaces = 2): this { + this.#indent += spaces + return this + } + dedent(spaces = 2): this { + this.#indent = Math.max(0, this.#indent - spaces) + return this + } + resetIndent(): this { + this.#indent = 0 + return this + } + + start(text?: string): this { + if (text !== undefined) { + this.setText(text) + } + if (this.isSpinning) { + return this + } + this.#spinning = true + this.#render() + if (this.#interactive) { + this.#timer = setInterval( + () => this.#render(), + this.spinner.interval ?? 80, + ) + } + return this + } + + stop(finalText?: string): this { + if (!this.isSpinning) { + return this + } + if (this.#timer) { + clearInterval(this.#timer) + } + this.#timer = undefined + this.#spinning = false + if (this.#interactive) { + this.#stream.write('\r\u001b[K\u001b[?25h') + } + if (finalText) { + this.#stream.write( + `${' '.repeat(this.#indent)}${finalText.trimStart()}\n`, + ) + } + this.text = '' + return this + } + + info(text = ''): this { + const spinning = this.isSpinning + this.stop(text) + if (spinning) { + this.start() + } + return this + } + infoAndStop(text = ''): this { + return this.stop(text) + } + success(text = ''): this { + return this.#symbol('✔', text, true) + } + successAndStop(text = ''): this { + return this.#symbol('✔', text, false) + } + fail(text = ''): this { + return this.#symbol('✖', text, true) + } + failAndStop(text = ''): this { + return this.#symbol('✖', text, false) + } + error(text = ''): this { + return this.fail(text) + } + errorAndStop(text = ''): this { + return this.failAndStop(text) + } + warn(text = ''): this { + return this.#symbol('⚠', text, true) + } + warnAndStop(text = ''): this { + return this.#symbol('⚠', text, false) + } + warning(text = ''): this { + return this.warn(text) + } + warningAndStop(text = ''): this { + return this.warnAndStop(text) + } + log(text = ''): this { + const spinning = this.isSpinning + this.stop(text) + if (spinning) { + this.start() + } + return this + } + logAndStop(text = ''): this { + return this.stop(text) + } + debug(text = ''): this { + return this.info(text) + } + debugAndStop(text = ''): this { + return this.infoAndStop(text) + } + + #symbol(symbol: string, text: string, keepSpinning: boolean): this { + const spinning = this.isSpinning + this.stop(`${symbol} ${text}`) + if (keepSpinning && spinning) { + this.start() + } + return this + } + + #render(): void { + const frames = this.spinner.frames + const frame = frames[this.#frame++ % frames.length] ?? '' + const value = `${' '.repeat(this.#indent)}${frame ? `${frame} ` : ''}${this.text}` + if (!value) { + return + } + this.#stream.write(this.#interactive ? `\r\u001b[K${value}` : `${value}\n`) + } +} + +const defaultSpinner = new Spinner() +export default defaultSpinner diff --git a/src/utils/streams.mts b/src/utils/streams.mts new file mode 100644 index 0000000000..669312f1fd --- /dev/null +++ b/src/utils/streams.mts @@ -0,0 +1,24 @@ +/** Minimal ordered async-iterable transform adapted from registry 1.1.17 (MIT). */ + +export async function* transform( + iterable: AsyncIterable | NodeJS.ReadableStream, + mapper: (value: T) => Promise | R, + options: { concurrency?: number } = {}, +): AsyncGenerator { + const concurrency = Math.max(1, Math.floor(options.concurrency ?? 1)) + let batch: T[] = [] + const flush = async function* (values: T[]): AsyncGenerator { + const mapped = await Promise.all(values.map(value => mapper(value))) + yield* mapped + } + for await (const value of iterable as AsyncIterable) { + batch.push(value) + if (batch.length >= concurrency) { + yield* flush(batch) + batch = [] + } + } + if (batch.length) { + yield* flush(batch) + } +} diff --git a/src/utils/strings.mts b/src/utils/strings.mts index 8f5ee105ba..bb28e9e368 100644 --- a/src/utils/strings.mts +++ b/src/utils/strings.mts @@ -4,6 +4,7 @@ * * Key Functions: * - camelToKebab: Convert camelCase to kebab-case + * - indentString, isNonEmptyString, stripAnsi, stripBom, trimNewlines: string helpers * * Usage: * - Command name transformations @@ -14,3 +15,68 @@ export function camelToKebab(str: string): string { return str === '' ? '' : str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() } + +export function indentString(str: string, count = 1): string { + return str.replace(/^(?!\s*$)/gm, ' '.repeat(count)) +} + +export function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.length > 0 +} + +// Inlined ansi-regex from ansi-regex 6.2.2. +// https://socket.dev/npm/package/ansi-regexp/overview/6.2.2 +// MIT License +// Copyright (c) Sindre Sorhus (https://sindresorhus.com) +function ansiRegex(): RegExp { + // Valid string terminator sequences are BEL, ESC\\, and 0x9c. + const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)' + // OSC sequences only: ESC ] ... ST (non-greedy until the first ST). + const osc = `(?:\\u001B\\][\\s\\S]*?${ST})` + // CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte. + const csi = + '[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]' + return new RegExp(`${osc}|${csi}`, 'g') +} + +export function stripAnsi(str: string): string { + return str.replace(ansiRegex(), '') +} + +export function stripBom(str: string): string { + return str.length > 0 && str.charCodeAt(0) === 0xfeff ? str.slice(1) : str +} + +export function trimNewlines(str: string): string { + const { length } = str + if (length === 0) { + return str + } + const first = str.charCodeAt(0) + const noFirstNewline = first !== 13 && first !== 10 + if (length === 1) { + return noFirstNewline ? str : '' + } + const last = str.charCodeAt(length - 1) + const noLastNewline = last !== 13 && last !== 10 + if (noFirstNewline && noLastNewline) { + return str + } + let start = 0 + let end = length + while (start < end) { + const code = str.charCodeAt(start) + if (code !== 13 && code !== 10) { + break + } + start += 1 + } + while (end > start) { + const code = str.charCodeAt(end - 1) + if (code !== 13 && code !== 10) { + break + } + end -= 1 + } + return start === 0 && end === length ? str : str.slice(start, end) +} diff --git a/src/utils/telemetry/integration.mts b/src/utils/telemetry/integration.mts index bb0cc8bbc6..ce3ee8515e 100644 --- a/src/utils/telemetry/integration.mts +++ b/src/utils/telemetry/integration.mts @@ -42,12 +42,11 @@ import { homedir } from 'node:os' import process from 'node:process' -import { debugFn } from '@socketsecurity/registry/lib/debug' -import { escapeRegExp } from '@socketsecurity/registry/lib/regexps' - -import { TelemetryService } from './service.mts' import constants, { CONFIG_KEY_DEFAULT_ORG } from '../../constants.mts' import { getConfigValueOrUndef } from '../config.mts' +import { debugFn } from '../debug.mts' +import { escapeRegExp } from '../escape-regexp.mts' +import { TelemetryService } from './service.mts' import type { TelemetryContext } from './types.mts' diff --git a/src/utils/telemetry/integration.test.mts b/src/utils/telemetry/integration.test.mts index 2a021855a2..bb20f4840a 100644 --- a/src/utils/telemetry/integration.test.mts +++ b/src/utils/telemetry/integration.test.mts @@ -53,7 +53,7 @@ vi.mock('./service.mts', () => ({ // Mock debug functions. const mockDebugFn = vi.hoisted(() => vi.fn()) -vi.mock('@socketsecurity/registry/lib/debug', () => ({ +vi.mock('../debug.mts', () => ({ debugFn: mockDebugFn, })) diff --git a/src/utils/telemetry/service.mts b/src/utils/telemetry/service.mts index 7c8bdfd673..022fca9b74 100644 --- a/src/utils/telemetry/service.mts +++ b/src/utils/telemetry/service.mts @@ -43,8 +43,7 @@ import { randomUUID } from 'node:crypto' -import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' - +import { debugDir, debugFn } from '../debug.mts' import { setupSdk } from '../sdk.mts' import type { TelemetryEvent } from './types.mts' diff --git a/src/utils/telemetry/service.test.mts b/src/utils/telemetry/service.test.mts index d12b694200..8ae2b9e7be 100644 --- a/src/utils/telemetry/service.test.mts +++ b/src/utils/telemetry/service.test.mts @@ -57,7 +57,7 @@ vi.mock('../sdk.mts', () => ({ const mockDebugFn = vi.hoisted(() => vi.fn()) const mockDebugDir = vi.hoisted(() => vi.fn()) -vi.mock('@socketsecurity/registry/lib/debug', () => ({ +vi.mock('../debug.mts', () => ({ debugDir: mockDebugDir, debugFn: mockDebugFn, })) diff --git a/src/utils/tildify.mts b/src/utils/tildify.mts index d5bd396e20..82049e1498 100644 --- a/src/utils/tildify.mts +++ b/src/utils/tildify.mts @@ -13,9 +13,8 @@ import path from 'node:path' -import { escapeRegExp } from '@socketsecurity/registry/lib/regexps' - import constants from '../constants.mts' +import { escapeRegExp } from './escape-regexp.mts' export function tildify(cwd: string) { return cwd.replace( diff --git a/src/utils/url.mts b/src/utils/url.mts new file mode 100644 index 0000000000..922efd6005 --- /dev/null +++ b/src/utils/url.mts @@ -0,0 +1,17 @@ +/** Minimal URL helpers adapted from @socketsecurity/registry 1.1.17 lib/url.js (MIT). */ + +export function parseUrl(value: string | URL): URL | null { + try { + return new URL(value) + } catch { + return null + } +} + +export function isUrl(value: unknown): value is string | URL { + return ( + ((typeof value === 'string' && value !== '') || + (value !== null && typeof value === 'object')) && + parseUrl(value as string | URL) !== null + ) +} diff --git a/src/utils/words.mts b/src/utils/words.mts new file mode 100644 index 0000000000..d10977bf07 --- /dev/null +++ b/src/utils/words.mts @@ -0,0 +1,8 @@ +/** + * Narrow copy of @socketsecurity/registry 1.1.17 lib/words.js pluralize. + * Copyright (c) Socket Inc. Licensed under the MIT License. + */ + +export function pluralize(word: string, count = 1): string { + return count === 0 || count > 1 ? `${word}s` : word +} diff --git a/src/utils/yarn-paths.mts b/src/utils/yarn-paths.mts index c3f23101e7..ba47b96151 100644 --- a/src/utils/yarn-paths.mts +++ b/src/utils/yarn-paths.mts @@ -1,5 +1,4 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - +import { logger } from './logger.mts' import { YARN } from '../constants.mts' import { findBinPathDetailsSync } from './path-resolve.mts' diff --git a/src/utils/yarn-version.mts b/src/utils/yarn-version.mts index 089f501ffc..24e9c93ea2 100644 --- a/src/utils/yarn-version.mts +++ b/src/utils/yarn-version.mts @@ -1,5 +1,4 @@ -import { spawnSync } from '@socketsecurity/registry/lib/spawn' - +import { spawnSync } from './spawn.mts' import { getYarnBinPath } from './yarn-paths.mts' import constants, { FLAG_VERSION, UTF8 } from '../constants.mts' @@ -17,7 +16,7 @@ export function isYarnBerry(): boolean { }) if (result.status === 0 && result.stdout) { - const version = result.stdout + const version = result.stdout.toString() // Yarn Berry starts from version 2.x const majorVersion = parseInt(version.split('.')[0]!, 10) _isYarnBerry = majorVersion >= 2 diff --git a/taze.config.mts b/taze.config.mts index 5c8b1b8a43..d1de62ff0b 100644 --- a/taze.config.mts +++ b/taze.config.mts @@ -1,8 +1,15 @@ import { defineConfig } from 'taze' export default defineConfig({ - // Exclude these packages (migrated from .ncurc.json reject list). - exclude: ['eslint-plugin-unicorn', 'terminal-link', 'yargs-parser'], + // Keep prior rejects and the pre-release PTY dependency on reviewed pins. + exclude: [ + '@termless/core', + '@termless/xtermjs', + 'eslint-plugin-unicorn', + 'node-pty', + 'terminal-link', + 'yargs-parser', + ], // Interactive mode disabled for automation. interactive: false, // Use minimal logging similar to ncu loglevel. diff --git a/test/dist-runtime-requires.test.mts b/test/dist-runtime-requires.test.mts index 2afeffe87f..94695cad17 100644 --- a/test/dist-runtime-requires.test.mts +++ b/test/dist-runtime-requires.test.mts @@ -18,6 +18,8 @@ const builtinNames = new Set( name.startsWith('node:') ? name.slice('node:'.length) : name, ), ) +// Node 27 adds the SEA-only API, which Node 22 cannot report in builtinModules. +builtinNames.add('sea') function isAstNode(value: unknown): value is AstNode { return ( diff --git a/test/external-bare-requires.test.mts b/test/external-bare-requires.test.mts index 40fc951ebc..2203fe637e 100644 --- a/test/external-bare-requires.test.mts +++ b/test/external-bare-requires.test.mts @@ -14,14 +14,8 @@ const rootPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..') const externalPath = path.join(rootPath, 'external') // Mirrors EXTERNAL_PACKAGES in .config/rollup.base.config.mjs. Scoped to what -// the build vendors into external/, because unbundled optional peers reached by -// bare name there (blessed's pty.js/term.js terminal widget, node-gyp under -// @socketsecurity/registry) are a separate, longstanding question. -const bundledNames = new Set([ - '@socketsecurity/registry', - 'blessed', - 'blessed-contrib', -]) +// the build vendors into external/. +const bundledNames = new Set(['blessed', 'blessed-contrib']) const bareRequireRegExp = /require[$\w]*(?:\.resolve)?\(\s*['"]([^'"]+)['"]\s*\)/g diff --git a/test/native-signing.test.mts b/test/native-signing.test.mts new file mode 100644 index 0000000000..a22f3cfe61 --- /dev/null +++ b/test/native-signing.test.mts @@ -0,0 +1,429 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + persistNativeSigningReceipt, + prepareNativeTestBinary, +} from '../scripts/test/native-signing.mts' + +const dirs: string[] = [] +afterEach(async () => { + await Promise.all( + dirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })), + ) +}) +async function fixture() { + const dir = await mkdtemp(path.join(os.tmpdir(), 'native-signing-')) + dirs.push(dir) + const binaryPath = path.join(dir, 'socket') + await writeFile(binaryPath, 'unsigned payload') + const { createHash } = await import('node:crypto') + const sourceSha256 = createHash('sha256') + .update('unsigned payload') + .digest('hex') + return { binaryPath, sourceSha256 } +} + +describe('native test signing policy', () => { + it('allows unsigned local mode', async () => { + const file = await fixture() + expect( + (await prepareNativeTestBinary({ ...file, env: {}, platform: 'darwin' })) + .mode, + ).toBe('unsigned-local') + }) + it('allows explicitly uncredentialed CI with an unsigned receipt', async () => { + const file = await fixture() + expect( + ( + await prepareNativeTestBinary({ + ...file, + env: { CI: 'true' }, + platform: 'darwin', + }) + ).mode, + ).toBe('unsigned-ci') + }) + it('signs and verifies configured credentials before issuing a Developer ID receipt', async () => { + const file = await fixture() + const calls: string[] = [] + const searchListUpdates: string[][] = [] + let captureOptions: + | { stdio?: unknown; timeout?: unknown; maxBuffer?: unknown } + | undefined + const receipt = await prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_DEVELOPER_ID_IDENTITY: + 'Developer ID Application: Example (ABCDEFGHIJ)', + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: + Buffer.from('p12').toString('base64'), + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner(command, args, options) { + calls.push(`${command} ${args.join(' ')}`) + if (command === 'security' && args[0] === 'list-keychains') { + if (args[3] === '-s') { + searchListUpdates.push(args.slice(4)) + } + return { + status: 0, + stdout: + args[3] === '-s' + ? '' + : ' "/Users/example/login.keychain-db"\n', + stderr: '', + } + } + if (command === 'security' && args[0] === 'find-identity') { + captureOptions = options + return { + status: 0, + stdout: + '1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Developer ID Application: Example (ABCDEFGHIJ)"\n', + stderr: '', + } + } + if (command === 'codesign' && args[0] === '--display') { + return { + status: 0, + stdout: '', + stderr: + 'Authority=Developer ID Application: Example (ABCDEFGHIJ)\nTeamIdentifier=ABCDEFGHIJ\nCodeDirectory v=20500 flags=0x10000(runtime)\ncom.apple.security.cs.allow-jit\ncom.apple.security.cs.allow-unsigned-executable-memory', + } + } + return { status: 0, stdout: '', stderr: '' } + }, + }) + expect(calls.some(call => call.includes('codesign --force'))).toBe(true) + expect( + calls.some(call => call.startsWith('security delete-keychain ')), + ).toBe(true) + expect(calls.some(call => call.includes('--entitlements'))).toBe(true) + expect(searchListUpdates).toHaveLength(2) + expect(searchListUpdates[0]).toHaveLength(2) + expect(searchListUpdates[0][1]).toBe('/Users/example/login.keychain-db') + expect(searchListUpdates[1]).toEqual(['/Users/example/login.keychain-db']) + expect(captureOptions?.stdio).toEqual(['ignore', 'pipe', 'pipe']) + expect(captureOptions?.timeout).toBe(120_000) + expect(captureOptions?.maxBuffer).toBe(65_536) + expect(receipt.mode).toBe('developer-id') + }) + it('rejects partial credentials and failed signing', async () => { + const file = await fixture() + await expect( + prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { APPLE_TEAM_ID: 'ABCDEFGHIJ' }, + }), + ).rejects.toThrow(/incomplete/) + await expect( + prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'password-only' }, + }), + ).rejects.toThrow(/incomplete/) + const calls: string[] = [] + await expect( + prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: 'cDEy', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner: (command, args) => { + calls.push(`${command} ${args.join(' ')}`) + return args[0] === 'delete-keychain' + ? { status: 0, stdout: '', stderr: '' } + : { status: 0, stdout: '', stderr: '' } + }, + }), + ).rejects.toThrow(/Developer ID identity/) + expect( + calls.some(call => call.startsWith('security delete-keychain ')), + ).toBe(true) + expect(calls.some(call => call.startsWith('codesign '))).toBe(false) + }) + it('rejects an ad hoc signature', async () => { + const file = await fixture() + await expect( + prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: 'cDEy', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner: (command, args) => ({ + status: 0, + stdout: + command === 'security' && args[0] === 'find-identity' + ? '1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Developer ID Application: Example (ABCDEFGHIJ)"' + : '', + stderr: + args[0] === '--display' + ? 'Signature=adhoc\nTeamIdentifier=ABCDEFGHIJ\n' + : '', + }), + }), + ).rejects.toThrow(/Developer ID Application/) + }) + it('fails closed when signature is required but credentials are absent', async () => { + const file = await fixture() + await expect( + prepareNativeTestBinary({ + ...file, + env: { + CI: 'true', + SOCKET_CLI_NATIVE_TEST_REQUIRE_APPLE_SIGNATURE: '1', + }, + platform: 'darwin', + }), + ).rejects.toThrow(/credentials are missing/) + }) + it('rejects signatures from another team', async () => { + const file = await fixture() + await expect( + prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: 'cDEy', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner: (command, args) => ({ + status: 0, + stdout: + command === 'security' && args[0] === 'find-identity' + ? '1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Developer ID Application: Example (ABCDEFGHIJ)"' + : '', + stderr: + args[0] === '--display' + ? 'Authority=Developer ID Application: Example (ABCDEFGHIJ)\nTeamIdentifier=ZZZZZZZZZZ\nCodeDirectory v=20500 flags=0x10000(runtime)\ncom.apple.security.cs.allow-jit\ncom.apple.security.cs.allow-unsigned-executable-memory' + : '', + }), + }), + ).rejects.toThrow(/hardened Developer ID/) + }) + it('reports signing failure distinctly and deletes the temporary keychain', async () => { + const file = await fixture() + const calls: string[] = [] + const searchListUpdates: string[][] = [] + let keychainPassword = '' + let failure: unknown + try { + await prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: 'cDEy', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner: (command, args) => { + calls.push(`${command} ${args.join(' ')}`) + if (command === 'security' && args[0] === 'create-keychain') { + keychainPassword = args[2]! + } + if (command === 'security' && args[0] === 'list-keychains') { + if (args[3] === '-s') { + searchListUpdates.push(args.slice(4)) + } + return { + status: 0, + stdout: + args[3] === '-s' ? '' : '"/Users/example/login.keychain-db"\n', + stderr: '', + } + } + if (command === 'security' && args[0] === 'find-identity') { + return { + status: 0, + stdout: + '1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Developer ID Application: Example (ABCDEFGHIJ)"', + stderr: '', + } + } + if (command === 'codesign' && args[0] === '--force') { + return { + status: 1, + stdout: '', + stderr: `resource unavailable ${keychainPassword} p12-password cDEy ABCDEFGHIJ`, + } + } + return { status: 0, stdout: '', stderr: '' } + }, + }) + } catch (error) { + failure = error + } + expect(failure).toBeInstanceOf(Error) + expect((failure as Error).message).toContain('resource unavailable') + expect((failure as Error).message).toContain('exit 1') + expect((failure as Error).message).not.toContain(keychainPassword) + expect((failure as Error).message).not.toContain('p12-password') + expect((failure as Error).message).not.toContain('cDEy') + expect((failure as Error).message).not.toContain('ABCDEFGHIJ') + expect( + calls.some(call => call.startsWith('security delete-keychain ')), + ).toBe(true) + expect(searchListUpdates).toHaveLength(2) + expect(searchListUpdates[1]).toEqual(['/Users/example/login.keychain-db']) + }) + it('restores the keychain search list when temporary keychain creation fails', async () => { + const file = await fixture() + const updates: string[][] = [] + await expect( + prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: 'cDEy', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner: (command, args) => { + if (command === 'security' && args[0] === 'list-keychains') { + if (args[3] === '-s') { + updates.push(args.slice(4)) + } + return { + status: 0, + stdout: + args[3] === '-s' ? '' : '"/Users/example/login.keychain-db"\n', + stderr: '', + } + } + if (command === 'security' && args[0] === 'create-keychain') { + return { status: 1, stdout: '', stderr: '' } + } + return { status: 0, stdout: '', stderr: '' } + }, + }), + ).rejects.toThrow(/security create-keychain failed/) + expect(updates).toEqual([['/Users/example/login.keychain-db']]) + }) + it('preserves signing and cleanup failures together', async () => { + const file = await fixture() + let caught: unknown + try { + await prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: 'cDEy', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner: (command, args) => { + if (command === 'security' && args[0] === 'find-identity') { + return { + status: 0, + stdout: + '1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Developer ID Application: Example (ABCDEFGHIJ)"', + stderr: '', + } + } + if (command === 'codesign' && args[0] === '--force') { + return { status: 1, stdout: '', stderr: '' } + } + if (command === 'security' && args[0] === 'delete-keychain') { + return { status: 1, stdout: '', stderr: '' } + } + return { status: 0, stdout: '', stderr: '' } + }, + }) + } catch (error) { + caught = error + } + expect(caught).toBeInstanceOf(AggregateError) + expect((caught as AggregateError).errors).toHaveLength(2) + expect((caught as AggregateError).errors[0].message).toMatch( + /codesign --force failed/, + ) + expect((caught as AggregateError).errors[1].message).toMatch( + /delete temporary Apple signing keychain/, + ) + }) + it('rejects an explicit identity that matches ambiguous keychain rows', async () => { + const file = await fixture() + await expect( + prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_DEVELOPER_ID_IDENTITY: + 'Developer ID Application: Example (ABCDEFGHIJ)', + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: 'cDEy', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner: (command, args) => ({ + status: 0, + stdout: + command === 'security' && args[0] === 'find-identity' + ? '1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Developer ID Application: Example (ABCDEFGHIJ)"\n2) BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB "Developer ID Application: Example (ABCDEFGHIJ)"' + : '', + stderr: '', + }), + }), + ).rejects.toThrow(/unavailable or ambiguous/) + }) + it('rejects entitlements that are present but not enabled', async () => { + const file = await fixture() + await expect( + prepareNativeTestBinary({ + ...file, + platform: 'darwin', + env: { + APPLE_TEAM_ID: 'ABCDEFGHIJ', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12: 'cDEy', + APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS: 'p12-password', + }, + runner: (command, args) => ({ + status: 0, + stdout: + command === 'security' && args[0] === 'find-identity' + ? '1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Developer ID Application: Example (ABCDEFGHIJ)"' + : '', + stderr: + command === 'codesign' && args[0] === '--display' + ? 'Authority=Developer ID Application: Example (ABCDEFGHIJ)\nTeamIdentifier=ABCDEFGHIJ\nCodeDirectory v=20500 flags=0x10000(runtime)\ncom.apple.security.cs.allow-jit\ncom.apple.security.cs.allow-unsigned-executable-memory' + : '', + }), + }), + ).rejects.toThrow(/hardened Developer ID/) + }) + it('persists only the nonsecret signing receipt fields', async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), 'native-receipt-')) + dirs.push(dir) + const receiptPath = path.join(dir, 'build', 'signing-receipt.json') + await persistNativeSigningReceipt( + { + mode: 'developer-id', + sourceSha256: 'source-digest', + stagedSha256: 'signed-digest', + teamId: 'ABCDEFGHIJ', + authority: 'Developer ID Application: private (ABCDEFGHIJ)', + }, + receiptPath, + ) + const { readFile } = await import('node:fs/promises') + expect(JSON.parse(await readFile(receiptPath, 'utf8'))).toEqual({ + mode: 'developer-id', + sourceSha256: 'source-digest', + stagedSha256: 'signed-digest', + }) + }) +}) diff --git a/test/native-snapshots/behavior-config-get.output.svg b/test/native-snapshots/behavior-config-get.output.svg new file mode 100644 index 0000000000..7dfc1be901 --- /dev/null +++ b/test/native-snapshots/behavior-config-get.output.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config get | org: native-fixture | token: (not set) + + + + + +defaultOrg: native-fixture + + + + + +Note: the config is in read-only mode, meaning at least one key was temporarily overridden from an e + + +nv var or command flag. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-config-get.sequence.json b/test/native-snapshots/behavior-config-get.sequence.json new file mode 100644 index 0000000000..c2228589bd --- /dev/null +++ b/test/native-snapshots/behavior-config-get.sequence.json @@ -0,0 +1 @@ +["output"] diff --git a/test/native-snapshots/behavior-config-invalid-key.output.svg b/test/native-snapshots/behavior-config-invalid-key.output.svg new file mode 100644 index 0000000000..6be1660e4b --- /dev/null +++ b/test/native-snapshots/behavior-config-invalid-key.output.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config get | org: (not set) | token: (not set) + + + + + +✖ + + Input error: + +Please review the input requirements and try again + + + + + + + +✖ + Config key should be the first arg ( +invalid config key +) + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-config-invalid-key.sequence.json b/test/native-snapshots/behavior-config-invalid-key.sequence.json new file mode 100644 index 0000000000..c2228589bd --- /dev/null +++ b/test/native-snapshots/behavior-config-invalid-key.sequence.json @@ -0,0 +1 @@ +["output"] diff --git a/test/native-snapshots/behavior-config-list.output.svg b/test/native-snapshots/behavior-config-list.output.svg new file mode 100644 index 0000000000..b942fa7217 --- /dev/null +++ b/test/native-snapshots/behavior-config-list.output.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config list | org: native-fixture | token: (not set) + + + + + +# Local CLI Config + + + + + +This is the local CLI config (full=false): + + + + + +- apiToken: ******** + + +- defaultOrg: native-fixture + + +- org: native-fixture + + + + + +Note: the config is in read-only mode, meaning at least one key was temporarily + + + overridden from an env var or command flag. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-config-list.sequence.json b/test/native-snapshots/behavior-config-list.sequence.json new file mode 100644 index 0000000000..c2228589bd --- /dev/null +++ b/test/native-snapshots/behavior-config-list.sequence.json @@ -0,0 +1 @@ +["output"] diff --git a/test/native-snapshots/behavior-json-missing.output.svg b/test/native-snapshots/behavior-json-missing.output.svg new file mode 100644 index 0000000000..8897d7c6e8 --- /dev/null +++ b/test/native-snapshots/behavior-json-missing.output.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket json | org: (not set) | token: (not set) + + + + + +ℹ + Target cwd: <NATIVE_FIXTURE> + + +✖ + Not found: <NATIVE_FIXTURE>/socket.json + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-json-missing.sequence.json b/test/native-snapshots/behavior-json-missing.sequence.json new file mode 100644 index 0000000000..c2228589bd --- /dev/null +++ b/test/native-snapshots/behavior-json-missing.sequence.json @@ -0,0 +1 @@ +["output"] diff --git a/test/native-snapshots/behavior-json-seeded.output.svg b/test/native-snapshots/behavior-json-seeded.output.svg new file mode 100644 index 0000000000..399bc3a908 --- /dev/null +++ b/test/native-snapshots/behavior-json-seeded.output.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket json | org: (not set) | token: (not set) + + + + + +ℹ + Target cwd: <NATIVE_FIXTURE> + + +✔ + This is the contents of <NATIVE_FIXTURE>/socket.json: + + + + + +{"version":1,"projectIgnorePaths":["fixture/**"]} + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-json-seeded.sequence.json b/test/native-snapshots/behavior-json-seeded.sequence.json new file mode 100644 index 0000000000..c2228589bd --- /dev/null +++ b/test/native-snapshots/behavior-json-seeded.sequence.json @@ -0,0 +1 @@ +["output"] diff --git a/test/native-snapshots/behavior-manifest-setup.canceled.svg b/test/native-snapshots/behavior-manifest-setup.canceled.svg new file mode 100644 index 0000000000..65231e7b7a --- /dev/null +++ b/test/native-snapshots/behavior-manifest-setup.canceled.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest setup | org: (not set) | token: (not set) + + + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + + + +Note: This tool will set up flag and argument defaults for certain + + + CLI commands. You can still override them by explicitly + + + setting the flag. It is meant to be a convenience tool. + + + + + +This command will generate a socket.json file in the target cwd. + + +You can choose to add this file to your repo (handy for collaboration) + + +or to add it to the ignored files, or neither. This file is only + + +used in CLI workflows. + + + + + +✔ + +Select ecosystem manifest generator to configure + +None, exit configurator + + + + + + +ℹ + User canceled + + + + + +✔ + Setup complete + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-manifest-setup.exit-selected.svg b/test/native-snapshots/behavior-manifest-setup.exit-selected.svg new file mode 100644 index 0000000000..7e2cf2264e --- /dev/null +++ b/test/native-snapshots/behavior-manifest-setup.exit-selected.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest setup | org: (not set) | token: (not set) + + + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + + + +Note: This tool will set up flag and argument defaults for certain + + + CLI commands. You can still override them by explicitly + + + setting the flag. It is meant to be a convenience tool. + + + + + +This command will generate a socket.json file in the target cwd. + + +You can choose to add this file to your repo (handy for collaboration) + + +or to add it to the ignored files, or neither. This file is only + + +used in CLI workflows. + + + + + +? + +Select ecosystem manifest generator to configure + + + +❯ None, exit configurator + + + + Conda + + + .NET (dotnet) + + + Gradle + + + Kotlin (gradle) + + + Scala (gradle) + + + Maven + + +Exit setup + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-manifest-setup.menu.svg b/test/native-snapshots/behavior-manifest-setup.menu.svg new file mode 100644 index 0000000000..649af43e7f --- /dev/null +++ b/test/native-snapshots/behavior-manifest-setup.menu.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest setup | org: (not set) | token: (not set) + + + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + + + +Note: This tool will set up flag and argument defaults for certain + + + CLI commands. You can still override them by explicitly + + + setting the flag. It is meant to be a convenience tool. + + + + + +This command will generate a socket.json file in the target cwd. + + +You can choose to add this file to your repo (handy for collaboration) + + +or to add it to the ignored files, or neither. This file is only + + +used in CLI workflows. + + + + + +? + +Select ecosystem manifest generator to configure + + + +❯ Conda + + + + .NET (dotnet) + + + Gradle + + + Kotlin (gradle) + + + Scala (gradle) + + + Maven + + + Scala (sbt) + + +(Use arrow keys to reveal more choices) + + + +Generate requirements.txt from a Conda environment.yml + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-manifest-setup.sequence.json b/test/native-snapshots/behavior-manifest-setup.sequence.json new file mode 100644 index 0000000000..d7dd801c06 --- /dev/null +++ b/test/native-snapshots/behavior-manifest-setup.sequence.json @@ -0,0 +1 @@ +["menu","exit-selected","canceled"] diff --git a/test/native-snapshots/behavior-wrapper-on-dry-run.output.svg b/test/native-snapshots/behavior-wrapper-on-dry-run.output.svg new file mode 100644 index 0000000000..ff643a3d72 --- /dev/null +++ b/test/native-snapshots/behavior-wrapper-on-dry-run.output.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket wrapper | org: (not set) | token: (not set) + + + + + +[DryRun]: Bailing now + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/behavior-wrapper-on-dry-run.sequence.json b/test/native-snapshots/behavior-wrapper-on-dry-run.sequence.json new file mode 100644 index 0000000000..c2228589bd --- /dev/null +++ b/test/native-snapshots/behavior-wrapper-on-dry-run.sequence.json @@ -0,0 +1 @@ +["output"] diff --git a/test/native-snapshots/socket-cdxgen-unsupported-type.sequence.json b/test/native-snapshots/socket-cdxgen-unsupported-type.sequence.json new file mode 100644 index 0000000000..e3340172cb --- /dev/null +++ b/test/native-snapshots/socket-cdxgen-unsupported-type.sequence.json @@ -0,0 +1 @@ +["unsupported-type"] diff --git a/test/native-snapshots/socket-cdxgen-unsupported-type.unsupported-type.svg b/test/native-snapshots/socket-cdxgen-unsupported-type.unsupported-type.svg new file mode 100644 index 0000000000..ae14b18cac --- /dev/null +++ b/test/native-snapshots/socket-cdxgen-unsupported-type.unsupported-type.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest cdxgen | org: (not set) | token: (not set) + + + + + +ℹ + Setting cdxgen --lifecycle to "pre-build" to avoid arbitrary code execution on this scan. + + + Pass "--lifecycle build" to generate a BOM consisting of information obtained during the build pro + + +cess. + + + See cdxgen BOM lifecycles documentation (​https://cyclonedx.github.io/cdxgen/#/ADVANCED?id=bom-life + + +cycles​) for more details. + + + + + + + + + + + +✖ + + Unexpected error: + +The standalone cdxgen build supports only npm projects. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-analytics.help.svg b/test/native-snapshots/socket-help-analytics.help.svg new file mode 100644 index 0000000000..7aa323a550 --- /dev/null +++ b/test/native-snapshots/socket-help-analytics.help.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket analytics | org: (not set) | token: (not set) + + + + + + + + + Look up analytics data + + + + + + Usage + + + $ socket analytics [options] [ "org" | "repo" <reponame>] [TIME] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: report:write + + + + + + The scope is either org or repo level, defaults to org. + + + + + + When scope is repo, a repo slug must be given as well. + + + + + + The TIME argument must be number 7, 30, or 90 and defaults to 30. + + + + + + Options + + + --file Path to store result, only valid with --json/--markdown + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + Examples + + + $ socket analytics org 7 + + + $ socket analytics repo test-repo 30 + + + $ socket analytics 90 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-analytics.sequence.json b/test/native-snapshots/socket-help-analytics.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-analytics.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-audit-log.help.svg b/test/native-snapshots/socket-help-audit-log.help.svg new file mode 100644 index 0000000000..3892f4f751 --- /dev/null +++ b/test/native-snapshots/socket-help-audit-log.help.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket audit-log | org: (not set) | token: (not set) + + + + + + + + + Look up the audit log for an organization + + + + + + Usage + + + $ socket audit-log [options] [FILTER] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: audit-log:list + + + + + + This feature requires an Enterprise Plan. To learn more about getting access + + + to this feature and many more, please visit the Socket pricing page (​https://socket.dev/pricing​). + + + + + + The type FILTER arg is an enum. Defaults to any. It should be one of these: + + + associateLabel, cancelInvitation, changeMemberRole, changePlanSubscriptionSeats, + + + createApiToken, createLabel, deleteLabel, deleteLabelSetting, deleteReport, + + + deleteRepository, disassociateLabel, joinOrganization, removeMember, + + + resetInvitationLink, resetOrganizationSettingToDefault, rotateApiToken, + + + sendInvitation, setLabelSettingToDefault, syncOrganization, transferOwnership, + + + updateAlertTriage, updateApiTokenCommitter, updateApiTokenMaxQuota, + + + updateApiTokenName', updateApiTokenScopes, updateApiTokenVisibility, + + + updateLabelSetting, updateOrganizationSetting, upgradeOrganizationPlan + + + + + + The page arg should be a positive integer, offset 1. Defaults to 1. + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. + + + Use --no-interactive to prevent any input questions, defaulting them to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --page Result page to fetch + + + --per-page Results per page - default is 30 + + + + + + Examples + + + $ socket audit-log + + + $ socket audit-log deleteReport --page 2 --per-page 10 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-audit-log.sequence.json b/test/native-snapshots/socket-help-audit-log.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-audit-log.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-cdxgen.help.svg b/test/native-snapshots/socket-help-cdxgen.help.svg new file mode 100644 index 0000000000..0000b9114d --- /dev/null +++ b/test/native-snapshots/socket-help-cdxgen.help.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket cdxgen | org: (not set) | token: (not set) + + + + + + + + +Usage + + + socket manifest cdxgen [options] [path] + + + + + +Options + + + -h, --help Show this help + + + -o, --output <file> Output file (default: socket-cdx.json) + + + -t, --type <type> npm, nodejs, javascript, js, typescript, ts, or tsx + + + --lifecycle pre-build Read dependency metadata without running project installs + + + + + +Generates a CycloneDX SBOM from a root npm package-lock.json v3. + + +Only one npm project is supported in this standalone build. Other cdxgen modes are unavailable. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-cdxgen.sequence.json b/test/native-snapshots/socket-help-cdxgen.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-cdxgen.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-ci.help.svg b/test/native-snapshots/socket-help-ci.help.svg new file mode 100644 index 0000000000..ba9e0235c7 --- /dev/null +++ b/test/native-snapshots/socket-help-ci.help.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket ci | org: (not set) | token: (not set) + + + + + + + + + Alias for `socket scan create --report` (creates report and exits with error if unhealthy) + + + + + + Usage + + + $ socket ci [options] + + + + + + Options + + + --auto-manifest Auto generate manifest files where detected? See autoManifest flag in `socket scan create` + + + + + + This command is intended to use in CI runs to allow automated systems to + + + accept or reject a current build. It will use the default org of the + + + Socket API token. The exit code will be non-zero when the scan does not pass + + + your security policy. + + + + + + The --auto-manifest flag does the same as the one from `socket scan create` + + + but is not enabled by default since the CI is less likely to be set up with + + + all the necessary dev tooling. Enable it if you want the scan to include + + + locally generated manifests like for gradle and sbt. + + + + + + Examples + + + $ socket ci + + + $ socket ci --auto-manifest + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-ci.sequence.json b/test/native-snapshots/socket-help-ci.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-ci.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-config-auto.help.svg b/test/native-snapshots/socket-help-config-auto.help.svg new file mode 100644 index 0000000000..f3bc6b15a5 --- /dev/null +++ b/test/native-snapshots/socket-help-config-auto.help.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config auto | org: (not set) | token: (not set) + + + + + + + + + Automatically discover and set the correct value config item + + + + + + Usage + + + $ socket config auto [options] KEY + + + + + + Options + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + Attempt to automatically discover the correct value for a given config KEY. + + + + + + Examples + + + $ socket config auto defaultOrg + + + + + + Keys: + + + - apiBaseUrl -- Base URL of the Socket API endpoint + + + - apiProxy -- A proxy through which to access the Socket API + + + - apiToken -- The Socket API token required to access most Socket API endpoints + + + - defaultOrg -- The default org slug to use; usually the org your Socket API token has access to. When set, all orgSlug arguments are imp + + +lied to be this value. + + + - enforcedOrgs -- Orgs in this list have their security policies enforced on this machine + + + - org -- Alias for defaultOrg + + + - skipAskToPersistDefaultOrg -- This flag prevents the Socket CLI from asking you to persist the org slug when you selected one interacti + + +vely + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-config-auto.sequence.json b/test/native-snapshots/socket-help-config-auto.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-config-auto.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-config-get.help.svg b/test/native-snapshots/socket-help-config-get.help.svg new file mode 100644 index 0000000000..da4cd18374 --- /dev/null +++ b/test/native-snapshots/socket-help-config-get.help.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config get | org: (not set) | token: (not set) + + + + + + + + + Get the value of a local CLI config item + + + + + + Usage + + + $ socket config get [options] KEY + + + + + + Retrieve the value for given KEY at this time. If you have overridden the + + + config then the value will come from that override. + + + + + + Options + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + KEY is an enum. Valid keys: + + + + + + - apiBaseUrl -- Base URL of the Socket API endpoint + + + - apiProxy -- A proxy through which to access the Socket API + + + - apiToken -- The Socket API token required to access most Socket API endpoints + + + - defaultOrg -- The default org slug to use; usually the org your Socket API token has access to. When set, all orgSlug arguments are imp + + +lied to be this value. + + + - enforcedOrgs -- Orgs in this list have their security policies enforced on this machine + + + - org -- Alias for defaultOrg + + + - skipAskToPersistDefaultOrg -- This flag prevents the Socket CLI from asking you to persist the org slug when you selected one interacti + + +vely + + + + + + Examples + + + $ socket config get defaultOrg + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-config-get.sequence.json b/test/native-snapshots/socket-help-config-get.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-config-get.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-config-list.help.svg b/test/native-snapshots/socket-help-config-list.help.svg new file mode 100644 index 0000000000..4212f5fc3e --- /dev/null +++ b/test/native-snapshots/socket-help-config-list.help.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config list | org: (not set) | token: (not set) + + + + + + + + + Show all local CLI config items and their values + + + + + + Usage + + + $ socket config list [options] + + + + + + Options + + + --full Show full tokens in plaintext (unsafe) + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + Examples + + + $ socket config list + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-config-list.sequence.json b/test/native-snapshots/socket-help-config-list.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-config-list.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-config-set.help.svg b/test/native-snapshots/socket-help-config-set.help.svg new file mode 100644 index 0000000000..c7144a818c --- /dev/null +++ b/test/native-snapshots/socket-help-config-set.help.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config set | org: (not set) | token: (not set) + + + + + + + + + Update the value of a local CLI config item + + + + + + Usage + + + $ socket config set [options] <KEY> <VALUE> + + + + + + Options + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + This is a crude way of updating the local configuration for this CLI tool. + + + + + + Note that updating a value here is nothing more than updating a key/value + + + store entry. No validation is happening. The server may reject your values + + + in some cases. Use at your own risk. + + + + + + Note: use `socket config unset` to restore to defaults. Setting a key + + + to `undefined` will not allow default values to be set on it. + + + + + + Keys: + + + + + + - apiBaseUrl -- Base URL of the Socket API endpoint + + + - apiProxy -- A proxy through which to access the Socket API + + + - apiToken -- The Socket API token required to access most Socket API endpoints + + + - defaultOrg -- The default org slug to use; usually the org your Socket API token has access to. When set, all orgSlug arguments are imp + + +lied to be this value. + + + - enforcedOrgs -- Orgs in this list have their security policies enforced on this machine + + + - org -- Alias for defaultOrg + + + - skipAskToPersistDefaultOrg -- This flag prevents the Socket CLI from asking you to persist the org slug when you selected one interacti + + +vely + + + + + + Examples + + + $ socket config set apiProxy https://example.com + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-config-set.sequence.json b/test/native-snapshots/socket-help-config-set.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-config-set.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-config-unset.help.svg b/test/native-snapshots/socket-help-config-unset.help.svg new file mode 100644 index 0000000000..a09af4a8ee --- /dev/null +++ b/test/native-snapshots/socket-help-config-unset.help.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config unset | org: (not set) | token: (not set) + + + + + + + + + Clear the value of a local CLI config item + + + + + + Usage + + + $ socket config unset [options] <KEY> <VALUE> + + + + + + Options + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + Removes a value from a config key, allowing the default value to be used + + + for it instead. + + + + + + Keys: + + + + + + - apiBaseUrl -- Base URL of the Socket API endpoint + + + - apiProxy -- A proxy through which to access the Socket API + + + - apiToken -- The Socket API token required to access most Socket API endpoints + + + - defaultOrg -- The default org slug to use; usually the org your Socket API token has access to. When set, all orgSlug arguments are imp + + +lied to be this value. + + + - enforcedOrgs -- Orgs in this list have their security policies enforced on this machine + + + - org -- Alias for defaultOrg + + + - skipAskToPersistDefaultOrg -- This flag prevents the Socket CLI from asking you to persist the org slug when you selected one interacti + + +vely + + + + + + Examples + + + $ socket config unset defaultOrg + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-config-unset.sequence.json b/test/native-snapshots/socket-help-config-unset.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-config-unset.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-config.help.svg b/test/native-snapshots/socket-help-config.help.svg new file mode 100644 index 0000000000..e3dcfee5c8 --- /dev/null +++ b/test/native-snapshots/socket-help-config.help.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket config | org: (not set) | token: (not set) + + + + + + Manage Socket CLI configuration + + + + + + Usage + + + $ socket config <command> + + + + + + Commands + + + auto Automatically discover and set the correct value config item + + + get Get the value of a local CLI config item + + + list Show all local CLI config items and their values + + + set Update the value of a local CLI config item + + + unset Clear the value of a local CLI config item + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-config.sequence.json b/test/native-snapshots/socket-help-config.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-config.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-dependencies.help.svg b/test/native-snapshots/socket-help-dependencies.help.svg new file mode 100644 index 0000000000..691acd389d --- /dev/null +++ b/test/native-snapshots/socket-help-dependencies.help.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket dependencies | org: (not set) | token: (not set) + + + + + + + + + Search for any dependency that is being used in your organization + + + + + + Usage + + + socket dependencies [options] + + + + + + API Token Requirements + + + (none) + + + + + + Options + + + --json Output as JSON + + + --limit Maximum number of dependencies returned + + + --markdown Output as Markdown + + + --offset Page number + + + + + + Examples + + + socket dependencies + + + socket dependencies --limit 20 --offset 10 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-dependencies.sequence.json b/test/native-snapshots/socket-help-dependencies.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-dependencies.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-fix.help.svg b/test/native-snapshots/socket-help-fix.help.svg new file mode 100644 index 0000000000..4353b8b3f4 --- /dev/null +++ b/test/native-snapshots/socket-help-fix.help.svg @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket fix | org: (not set) | token: (not set) + + + + + + + + + Fix CVEs in dependencies + + + + + + Usage + + + $ socket fix [options] [CWD=.] + + + + + + API Token Requirements + + + - Quota: 101 units + + + - Permissions: fixes:list, full-scans:create, and packages:list + + + + + + Options + + + --all Process all discovered vulnerabilities in local mode. Cannot be used with --id. + + + --allow-overrides When the only fix for a vulnerability is blocked by a parent package's declared version range, write an override or + + +resolution that forces the fixed version under that parent. This can install a version outside the range the parent declares, so test the pa + + +rent afterwards. Works for npm, pnpm, Yarn and Rush projects. + + + --autopilot Enable auto-merge for pull requests that Socket opens. + + + See GitHub documentation (​https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository + + +/configuring-pull-request-merges/managing-auto-merge-for-pull-requests-in-your-repository​) for managing auto-merge for pull requests in your + + + repository. + + + --debug Enable debug logging in the Coana-based Socket Fix CLI invocation. + + + --disable-external-tool-checks Disable external tool checks during fix analysis. + + + --dynamic-sbom-inference For Gradle, sbt, Maven and .NET: generate a Socket facts SBOM (produced directly by each package manager) per + + +independent build root, instead of one synthetic root. Fixes are then attributed to the projects/modules that actually resolve each vulnerab + + +le dependency. The generated files are removed afterwards. + + + --dynamic-sbom-inference-ecosystems With --dynamic-sbom-inference: generate Socket facts only for these ecosystems, as either a comma s + + +eparated value or as multiple flags; the others keep their manifest files as they are. Supported: maven (Gradle, Maven, and sbt) and nuget ( + + +.NET). Defaults to all of them. + + + --ecosystems Limit fix analysis to specific ecosystems. Accepts space- or comma-separated values and is case-insensitive. Default + + +s to all ecosystems. + + + --exclude-paths Skip matching paths from the scan entirely: manifests under these paths are not uploaded, and fixes are not applied + + +to workspaces under them. Patterns are anchored micromatch globs matched relative to the target directory (CWD); `data/postgres/pgdata` matc + + +hes that exact path, `**/pgdata` matches at any depth. Use this to skip directories the current user cannot read so they do not abort manife + + +st collection. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags. + + + --fix-version Override the version of @coana-tech/cli used for fix analysis. Default: 15.14.2. + + + --id Provide a list of vulnerability identifiers to compute fixes for: + + + - GHSA IDs (​https://docs.github.com/en/code-security/security-advisories/working-with-global-security-advisories + + +-from-the-github-advisory-database/about-the-github-advisory-database#about-ghsa-ids​) (e.g., GHSA-xxxx-xxxx-xxxx) + + + - CVE IDs (​https://cve.mitre.org/cve/identifiers/​) (e.g., CVE-2026-1234) - automatically converted to GHSA + + + - PURLs (​https://github.com/package-url/purl-spec​) (e.g., pkg:npm/package@1.0.0) - automatically converted to GH + + +SA + + + Can be provided as comma separated values or as multiple flags. Cannot be used with --all. + + + --include Include workspaces matching these glob patterns. Can be provided as comma separated values or as multiple flags + + + --json Output as JSON + + + --markdown Output as Markdown + + + --minimum-release-age Set a minimum age requirement for suggested upgrade versions (e.g., 1h, 2d, 3w). A higher age requirement reduces + + + the risk of upgrading to malicious versions. For example, setting the value to 1 week (1w) gives ecosystem maintainers one week to remove p + + +otentially malicious versions. + + + --no-apply-fixes Compute fixes only, do not apply them. Logs what upgrades would be applied. If combined with --output-file, the outp + + +ut file will contain the upgrades that would be applied. + + + --no-major-updates Do not suggest or apply fixes that require major version updates of direct or transitive dependencies + + + --output-file Path to store upgrades as a JSON file at this path. + + + --package-managers Limit fix analysis to specific package managers within an ecosystem (e.g. NPM, PNPM, YARN, MAVEN, POETRY). Accepts s + + +pace- or comma-separated values and is case-insensitive. When combined with --ecosystems, an artifact must satisfy both filters. + + + --pr-limit Maximum number of pull requests to create in CI mode (default 10). Has no effect in local mode. + + + --range-style Define how dependency version ranges are updated in package.json (default 'preserve'). + + + Available styles: + + + * pin - Use the exact version (e.g. 1.2.3) + + + * preserve - Retain the existing version range style as-is + + + --show-affected-direct-dependencies List the direct dependencies responsible for introducing transitive vulnerabilities and list the up + + +dates required to resolve the vulnerabilities + + + --silence Silence all output except the final result + + + + + + Environment Variables (for CI/PR mode) + + + CI Set to enable CI mode + + + SOCKET_CLI_GITHUB_TOKEN GitHub token for PR creation (or GITHUB_TOKEN) + + + SOCKET_CLI_GIT_USER_NAME Git username for commits + + + SOCKET_CLI_GIT_USER_EMAIL Git email for commits + + + + + + Examples + + + $ socket fix + + + $ socket fix --id CVE-2021-23337 + + + $ socket fix ./path/to/project --range-style pin + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-fix.sequence.json b/test/native-snapshots/socket-help-fix.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-fix.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-install-completion.help.svg b/test/native-snapshots/socket-help-install-completion.help.svg new file mode 100644 index 0000000000..1b753949ba --- /dev/null +++ b/test/native-snapshots/socket-help-install-completion.help.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket install completion | org: (not set) | token: (not set) + + + + + + + + + Install bash completion for Socket CLI + + + + + + Usage + + + $ socket install completion [options] [NAME=socket] + + + + + + Installs bash completion for the Socket CLI. This will: + + + 1. Source the completion script in your current shell + + + 2. Add the source command to your ~/.bashrc if it's not already there + + + + + + This command will only setup tab completion, nothing else. + + + + + + Afterwards you should be able to type `socket ` and then press tab to + + + have bash auto-complete/suggest the sub/command or flags. + + + + + + Currently only supports bash. + + + + + + The optional name argument allows you to enable tab completion on a command + + + name other than "socket". Mostly for debugging but also useful if you use a + + + different alias for socket on your system. + + + + + + Options + + + (none) + + + + + + Examples + + + + + + $ socket install completion + + + $ socket install completion sd + + + $ socket install completion ./sd + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-install-completion.sequence.json b/test/native-snapshots/socket-help-install-completion.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-install-completion.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-install.help.svg b/test/native-snapshots/socket-help-install.help.svg new file mode 100644 index 0000000000..4136a7db4a --- /dev/null +++ b/test/native-snapshots/socket-help-install.help.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket install | org: (not set) | token: (not set) + + + + + + Install Socket CLI tab completion + + + + + + Usage + + + $ socket install <command> + + + + + + Commands + + + completion Install bash completion for Socket CLI + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-install.sequence.json b/test/native-snapshots/socket-help-install.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-install.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-json.help.svg b/test/native-snapshots/socket-help-json.help.svg new file mode 100644 index 0000000000..336e5c6fac --- /dev/null +++ b/test/native-snapshots/socket-help-json.help.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket json | org: (not set) | token: (not set) + + + + + + + + + Display the `socket.json` that would be applied for target folder + + + + + + Usage + + + $ socket json [options] [CWD=.] + + + + + + Display the `socket.json` file that would apply when running relevant commands + + + in the target directory. + + + + + + Examples + + + $ socket json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-json.sequence.json b/test/native-snapshots/socket-help-json.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-json.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-license.help.svg b/test/native-snapshots/socket-help-license.help.svg new file mode 100644 index 0000000000..10b9230b5e --- /dev/null +++ b/test/native-snapshots/socket-help-license.help.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket license | org: (not set) | token: (not set) + + + + + + + + + Retrieve the license policy of an organization + + + + + + Usage + + + $ socket license [options] + + + + + + API Token Requirements + + + (none) + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Your API token will need the `license-policy:read` permission otherwise + + + the request will fail with an authentication error. + + + + + + Examples + + + $ socket license + + + $ socket license --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-license.sequence.json b/test/native-snapshots/socket-help-license.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-license.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-login.help.svg b/test/native-snapshots/socket-help-login.help.svg new file mode 100644 index 0000000000..353c44061c --- /dev/null +++ b/test/native-snapshots/socket-help-login.help.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket login | org: (not set) | token: (not set) + + + + + + + + + Setup Socket CLI with an API token and defaults + + + + + + Usage + + + $ socket login [options] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + + + + Logs into the Socket API by prompting for an API token + + + + + + Options + + + --api-base-url API server to connect to for login + + + --api-proxy Proxy to use when making connection to API server + + + + + + Examples + + + $ socket login + + + $ socket login --api-proxy=http://localhost:1234 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-login.sequence.json b/test/native-snapshots/socket-help-login.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-login.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-logout.help.svg b/test/native-snapshots/socket-help-logout.help.svg new file mode 100644 index 0000000000..28fed52c18 --- /dev/null +++ b/test/native-snapshots/socket-help-logout.help.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket logout | org: (not set) | token: (not set) + + + + + + + + + Socket API logout + + + + + + Usage + + + $ socket logout [options] + + + + + + Logs out of the Socket API and clears all Socket credentials from disk + + + + + + Examples + + + $ socket logout + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-logout.sequence.json b/test/native-snapshots/socket-help-logout.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-logout.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-auto.help.svg b/test/native-snapshots/socket-help-manifest-auto.help.svg new file mode 100644 index 0000000000..6d0454fc7c --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-auto.help.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest auto | org: (not set) | token: (not set) + + + + + + + + + Auto-detect build and attempt to generate manifest file + + + + + + Usage + + + $ socket manifest auto [options] [CWD=.] + + + + + + Options + + + --exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched rela + + +tive to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not + + +supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET project is not analysed as a project of i + + +ts own, but where a project that is not excluded depends on it, its code is analysed as part of that project. A build module or project outs + + +ide CWD cannot be excluded. + + + --verbose Enable debug output (only for auto itself; sub-steps need to have it pre-configured), may help when running into err + + +ors + + + + + + Tries to figure out what language your target repo uses. If it finds a + + + supported case then it will try to generate the manifest file for that + + + language with the default or detected settings. + + + + + + Note: you can exclude languages from being auto-generated if you don't want + + + them to. Run `socket manifest setup` in the same dir to disable it. + + + + + + Examples + + + + + + $ socket manifest auto + + + $ socket manifest auto ./project/foo + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-auto.sequence.json b/test/native-snapshots/socket-help-manifest-auto.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-auto.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-bazel.help.svg b/test/native-snapshots/socket-help-manifest-bazel.help.svg new file mode 100644 index 0000000000..e2eb3f4c66 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-bazel.help.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest bazel | org: (not set) | token: (not set) + + + + + + + + + [beta] Bazel SBOM support — generate manifest files for a Bazel project (Maven, PyPI) + + + + + + Usage + + + $ socket manifest bazel [options] [CWD=.] + + + + + + Options + + + --bazel Path to bazel/bazelisk binary; default: $(which bazelisk) || $(which bazel) + + + --bazel-flags Bazel command flags, with quoted values and literal backslashes + + + --bazel-output-base Bazel --output_base for read-only-cache CI environments + + + --bazel-rc Path to additional .bazelrc fragments forwarded to bazel + + + --ecosystem Ecosystem(s) to extract; repeatable. Supported: maven, pypi. Default: maven. + + + --out Output directory for generated manifests; default: ./.socket/bazel-manifests/ + + + --per-repo-timeout Per-hub bazel cquery timeout in milliseconds; default: 120000 + + + --verbose Emit bounded Bazel diagnostics with argv, duration, exit status, and output sizes + + + + + + [beta] Generates Bazel SBOM manifests for Maven (`maven_install.json`) + + + by running `bazel query` against discovered dependency repos. + + + PyPI requirements generation is available with `--ecosystem pypi`. + + + Output is consumed by + + + `socket scan create`'s server-side parser. + + + + + + --ecosystem may be repeated to select which ecosystems to extract. + + + When omitted, Maven is generated by default. PyPI is explicit opt-in. + + + + + + Note: this command generates dependency manifests for Bazel workspaces. + + + It does not run reachability analysis. + + + + + + Native Windows support currently covers Maven. Use PowerShell or + + + Command Prompt with a working Python 3 python.exe and a JDK on PATH. + + + PyPI extraction on Windows is not supported yet. + + + + + + Set SOCKET_BAZEL_OUTPUT_ROOT to a short, writable parent directory, + + + such as C:/sb, to shorten connector-owned Bazel output paths. Each + + + invocation gets a separate directory. An explicit --bazel-output-base + + + takes precedence for Bazel's cache and is kept after the scan. The + + + scan only stops Bazel servers it started. + + + + + + In --bazel-flags, single or double quotes at the start of a value or + + + after = group values containing spaces. Backslashes stay literal. Double a matching quote to include + + + it within a quoted value. Shell variables are not expanded. + + + + + + Maven hub discovery: under Bzlmod, hubs are enumerated from + + + `bazel mod show_extension` and filtered to the root module's own hubs. + + + Under legacy WORKSPACE mode (no `show_extension`), only conventionally + + + named hubs are probed (`maven`, `maven_install`, `maven_dev`, …). A hub + + + with a non-conventional name that `show_extension` does not enumerate is + + + not discovered yet; a flag to name extra hubs is planned. + + + + + + To generate AND upload in one step, use `socket scan create --auto-manifest` + + + instead — it detects Bazel workspaces, generates Maven manifests by + + + default, and uploads the result. This subcommand is for generation only. + + + + + + Examples + + + $ socket manifest bazel . + + + $ socket manifest bazel --ecosystem pypi . + + + $ socket manifest bazel --ecosystem maven --ecosystem pypi . + + + $ socket manifest bazel --bazel=/usr/local/bin/bazelisk . + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-bazel.sequence.json b/test/native-snapshots/socket-help-manifest-bazel.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-bazel.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-cdxgen.help.svg b/test/native-snapshots/socket-help-manifest-cdxgen.help.svg new file mode 100644 index 0000000000..17c428c842 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-cdxgen.help.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest cdxgen | org: (not set) | token: (not set) + + + + + + + + +Usage + + + socket manifest cdxgen [options] [path] + + + + + +Options + + + -h, --help Show this help + + + -o, --output <file> Output file (default: socket-cdx.json) + + + -t, --type <type> npm, nodejs, javascript, js, typescript, ts, or tsx + + + --lifecycle pre-build Read dependency metadata without running project installs + + + + + +Generates a CycloneDX SBOM from a root npm package-lock.json v3. + + +Only one npm project is supported in this standalone build. Other cdxgen modes are unavailable. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-cdxgen.sequence.json b/test/native-snapshots/socket-help-manifest-cdxgen.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-cdxgen.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-conda.help.svg b/test/native-snapshots/socket-help-manifest-conda.help.svg new file mode 100644 index 0000000000..06a7684e1b --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-conda.help.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest conda | org: (not set) | token: (not set) + + + + + + + + + [beta] Convert a Conda environment.yml file to a python requirements.txt + + + + + + Usage + + + $ socket manifest conda [options] [CWD=.] + + + + + + Warning: While we don't support Conda necessarily, this tool extracts the pip + + + block from an environment.yml and outputs it as a requirements.txt + + + which you can scan as if it were a PyPI package. + + + + + + USE AT YOUR OWN RISK + + + + + + Note: FILE can be a dash (-) to indicate stdin. This way you can pipe the + + + contents of a file to have it processed. + + + + + + Options + + + --file Input file name (by default for Conda this is "environment.yml"), relative to cwd + + + --json Output as JSON + + + --markdown Output as Markdown + + + --out Output path (relative to cwd) + + + --stdin Read the input from stdin (supersedes --file) + + + --stdout Print resulting requirements.txt to stdout (supersedes --out) + + + --verbose Print debug messages + + + + + + Examples + + + + + + $ socket manifest conda + + + $ socket manifest conda ./project/foo --file environment.yaml + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-conda.sequence.json b/test/native-snapshots/socket-help-manifest-conda.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-conda.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-dotnet.help.svg b/test/native-snapshots/socket-help-manifest-dotnet.help.svg new file mode 100644 index 0000000000..19f78b36cc --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-dotnet.help.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest dotnet | org: (not set) | token: (not set) + + + + + + + + + [beta] Generate Socket facts files from .NET solutions and projects + + + + + + Usage + + + $ socket manifest dotnet [options] [CWD=.|FILE] + + + + + + Options + + + --bin Location of the dotnet binary to use, default: dotnet on PATH + + + --dotnet-opts Additional options to pass on to `dotnet restore`, e.g. `-p:Configuration=Release --configfile nuget.config`. MSBuil + + +d properties among them also apply to project evaluation + + + --exclude-configs Comma-separated glob patterns; target frameworks matching any pattern are skipped (applied after --include-configs) + + + --exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched rela + + +tive to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not + + +supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET project is not analysed as a project of i + + +ts own, but where a project that is not excluded depends on it, its code is analysed as part of that project. A build module or project outs + + +ide CWD cannot be excluded. + + + --ignore-unresolved Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) + + + --include-configs Comma-separated glob patterns matched against target frameworks (case-sensitive; `*`, `?`, and `[...]` wildcards). O + + +nly target frameworks matching at least one pattern are read, e.g. `net8.0`. A runtime-specific target (`net8.0/win-x64`) also matches its f + + +ramework. Default: every target framework + + + --verbose Print debug messages + + + + + + Emits a `<file>.socket.facts.json` per solution or project describing its + + + resolved NuGet dependency graph. Given a directory, it builds every + + + solution (.sln, .slnx) and project (.csproj, .fsproj, .vbproj) at its top + + + level, solutions first, skipping projects a solution already covers; given + + + a file, just that one (a .slnf solution filter too). + + + + + + Dependencies come from your own `dotnet restore` (same SDK, global.json, + + + nuget.config and credential providers), so a build that restores for you + + + restores here. PackageReference projects (central package management and + + + lock files included) and packages.config projects are supported. An + + + unresolved dependency is a fatal error unless --ignore-unresolved. + + + + + + Support is beta. Please report issues or give us feedback on what's missing. + + + + + + Examples + + + + + + $ socket manifest dotnet . + + + $ socket manifest dotnet ./App.sln + + + $ socket manifest dotnet --dotnet-opts="-p:Configuration=Release" . + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-dotnet.sequence.json b/test/native-snapshots/socket-help-manifest-dotnet.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-dotnet.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-dynamic-sbom-inference.help.svg b/test/native-snapshots/socket-help-manifest-dynamic-sbom-inference.help.svg new file mode 100644 index 0000000000..3f7d360753 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-dynamic-sbom-inference.help.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest dynamic-sbom-inference | org: (not set) | token: (not set) + + + + + + + + + Recursively discover .NET, Gradle, Maven, and sbt build roots and generate a Socket facts SBOM for each + + + + + + Usage + + + $ socket manifest dynamic-sbom-inference [options] [CWD=.] + + + + + + Recursively walks CWD, discovers independent gradle, sbt, and maven build + + + roots, and generates a Socket facts SBOM for each + + + (pom.xml.socket.facts.json, gradle.socket.facts.json, or + + + sbt.socket.facts.json, so builds sharing a directory never overwrite each + + + other), + + + skipping build roots whose build file a parent build already reports for + + + one of its subprojects. Unlike `socket manifest auto`, this looks beyond + + + CWD itself. + + + + + + Options + + + --exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched rela + + +tive to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not + + +supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET project is not analysed as a project of i + + +ts own, but where a project that is not excluded depends on it, its code is analysed as part of that project. A build module or project outs + + +ide CWD cannot be excluded. + + + --ignore-unresolved Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts files); ov + + +errides each build's socket.json setting + + + --json Output as JSON + + + --markdown Output as Markdown + + + --verbose Print debug messages + + + + + + Examples + + + + + + $ socket manifest dynamic-sbom-inference + + + $ socket manifest dynamic-sbom-inference ./monorepo + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-dynamic-sbom-inference.sequence.json b/test/native-snapshots/socket-help-manifest-dynamic-sbom-inference.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-dynamic-sbom-inference.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-gradle.help.svg b/test/native-snapshots/socket-help-manifest-gradle.help.svg new file mode 100644 index 0000000000..55f53d780e --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-gradle.help.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest gradle | org: (not set) | token: (not set) + + + + + + + + + [beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Gradle/Java/Kotlin/etc project + + + + + + Usage + + + $ socket manifest gradle [options] [CWD=.] + + + + + + Options + + + --bin Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH + + + --exclude-configs When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applie + + +d after --include-configs) + + + --exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched rela + + +tive to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not + + +supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET project is not analysed as a project of i + + +ts own, but where a project that is not excluded depends on it, its code is analysed as part of that project. A build module or project outs + + +ide CWD cannot be excluded. + + + --facts Emit a Socket facts JSON file (`gradle.socket.facts.json`) describing the resolved dependency graph. This is the def + + +ault; pass `--pom` to generate `pom.xml` files instead + + + --gradle-opts Additional options to pass on to ./gradlew, see `./gradlew --help` + + + --ignore-unresolved When generating facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted + + +to the facts file) + + + --include-configs When generating facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive; `*` + + +, `?`, and `[...]` wildcards). Only configurations matching at least one pattern are resolved. e.g. `*CompileClasspath,*RuntimeClasspath`. D + + +efault: every resolvable configuration + + + --pom Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`gradle.socket.facts.json`) + + + --verbose Print debug messages + + + + + + By default, emits a single `gradle.socket.facts.json` describing the + + + resolved dependency graph of the whole build, using gradle (preferably your + + + local `gradlew`). An unresolved dependency is a fatal error. You can pass + + + --include-configs / --exclude-configs (comma-separated glob patterns) to + + + control which configurations are resolved (e.g. + + + --include-configs=`*CompileClasspath,*RuntimeClasspath`), and + + + --ignore-unresolved to warn on unresolved dependencies instead of failing. + + + + + + Pass --pom to instead generate `pom.xml` manifest files via gradle (one per + + + task). The `pom.xml` is a manifest file similar to `package.json` for npm + + + (or requirements.txt for PyPi), but specifically for Maven, which is + + + Java's dependency repository. Caveats of the `pom.xml` conversion: + + + + + + - each task generates its own xml file (one per task by default) + + + + + + - certain features may not translate well into the xml; reach out if + + + something you need is missing + + + + + + - it works with your `gradlew` from your repo and local settings and config + + + + + + Support is beta. Please report issues or give us feedback on what's missing. + + + + + + Examples + + + + + + $ socket manifest gradle . + + + $ socket manifest gradle --pom . + + + $ socket manifest gradle --bin=../gradlew . + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-gradle.sequence.json b/test/native-snapshots/socket-help-manifest-gradle.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-gradle.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-kotlin.help.svg b/test/native-snapshots/socket-help-manifest-kotlin.help.svg new file mode 100644 index 0000000000..bd656ace8e --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-kotlin.help.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest kotlin | org: (not set) | token: (not set) + + + + + + + + + [beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Kotlin project + + + + + + Usage + + + $ socket manifest kotlin [options] [CWD=.] + + + + + + Options + + + --bin Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH + + + --exclude-configs When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applie + + +d after --include-configs) + + + --exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched rela + + +tive to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not + + +supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET project is not analysed as a project of i + + +ts own, but where a project that is not excluded depends on it, its code is analysed as part of that project. A build module or project outs + + +ide CWD cannot be excluded. + + + --facts Emit a Socket facts JSON file (`gradle.socket.facts.json`) describing the resolved dependency graph. This is the def + + +ault; pass `--pom` to generate `pom.xml` files instead + + + --gradle-opts Additional options to pass on to ./gradlew, see `./gradlew --help` + + + --ignore-unresolved When generating facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted + + +to the facts file) + + + --include-configs When generating facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive; `*` + + +, `?`, and `[...]` wildcards). Only configurations matching at least one pattern are resolved. e.g. `*CompileClasspath,*RuntimeClasspath`. D + + +efault: every resolvable configuration + + + --pom Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`gradle.socket.facts.json`) + + + --verbose Print debug messages + + + + + + By default, emits a single `gradle.socket.facts.json` describing the + + + resolved dependency graph of the whole build, using gradle (preferably your + + + local `gradlew`). An unresolved dependency is a fatal error. You can pass + + + --include-configs / --exclude-configs (comma-separated glob patterns) to + + + control which configurations are resolved (e.g. + + + --include-configs=`*CompileClasspath,*RuntimeClasspath`), and + + + --ignore-unresolved to warn on unresolved dependencies instead of failing. + + + + + + Pass --pom to instead generate `pom.xml` manifest files via gradle (one per + + + task). The `pom.xml` is a manifest file similar to `package.json` for npm + + + (or requirements.txt for PyPi), but specifically for Maven, which is + + + Java's dependency repository. Caveats of the `pom.xml` conversion: + + + + + + - each task generates its own xml file (one per task by default) + + + + + + - certain features may not translate well into the xml; reach out if + + + something you need is missing + + + + + + - it works with your `gradlew` from your repo and local settings and config + + + + + + Support is beta. Please report issues or give us feedback on what's missing. + + + + + + Examples + + + + + + $ socket manifest kotlin . + + + $ socket manifest kotlin --pom . + + + $ socket manifest kotlin --bin=../gradlew . + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-kotlin.sequence.json b/test/native-snapshots/socket-help-manifest-kotlin.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-kotlin.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-maven.help.svg b/test/native-snapshots/socket-help-manifest-maven.help.svg new file mode 100644 index 0000000000..fd484f7788 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-maven.help.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest maven | org: (not set) | token: (not set) + + + + + + + + + [beta] Generate a Socket facts file from a Maven `pom.xml` project + + + + + + Usage + + + $ socket manifest maven [options] [CWD=.] + + + + + + Options + + + --bin Location of the maven binary to use, default: ./mvnw if present, else mvn on PATH + + + --exclude-configs Comma-separated glob patterns; Maven scopes matching any pattern are skipped (applied after --include-configs) + + + --exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched rela + + +tive to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not + + +supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET project is not analysed as a project of i + + +ts own, but where a project that is not excluded depends on it, its code is analysed as part of that project. A build module or project outs + + +ide CWD cannot be excluded. + + + --ignore-unresolved Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) + + + --include-configs Comma-separated glob patterns matched against Maven dependency scopes (case-sensitive; `*`, `?`, and `[...]` wildcar + + +ds). Only scopes matching at least one pattern are resolved. e.g. `compile,runtime`. Default: every scope + + + --maven-opts Additional options to pass on to maven, e.g. `-P <profile> -s <settings.xml>` + + + --verbose Print debug messages + + + + + + Emits a single `pom.xml.socket.facts.json` (named after the POM Maven + + + runs on) describing the resolved dependency graph of your Maven project, using maven (`mvn` on PATH by default). It + + + reads dependency metadata only and never downloads artifacts; an unresolved + + + dependency is a fatal error. You can pass --include-configs / + + + --exclude-configs (comma-separated glob patterns) to control which Maven + + + scopes are resolved (e.g. --include-configs=`compile,runtime`), and + + + --ignore-unresolved to warn on unresolved dependencies instead of failing. + + + + + + You can specify --bin to override the path to the `mvn` binary to invoke + + + (e.g. a project `./mvnw` wrapper), and --maven-opts to pass extra options + + + through to maven (e.g. `-P <profile> -s <settings.xml>`). + + + + + + Support is beta. Please report issues or give us feedback on what's missing. + + + + + + Examples + + + + + + $ socket manifest maven . + + + $ socket manifest maven --bin=./mvnw . + + + $ socket manifest maven --maven-opts="-P release" . + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-maven.sequence.json b/test/native-snapshots/socket-help-manifest-maven.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-maven.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-scala.help.svg b/test/native-snapshots/socket-help-manifest-scala.help.svg new file mode 100644 index 0000000000..b33efad792 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-scala.help.svg @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest scala | org: (not set) | token: (not set) + + + + + + + + + [beta] Generate a Socket facts file (or `pom.xml` with --pom) from a Scala `build.sbt` project + + + + + + Usage + + + $ socket manifest scala [options] [CWD=.] + + + + + + Options + + + --bin Location of sbt binary to use + + + --exclude-configs When generating facts: comma-separated glob patterns; sbt configurations matching any pattern are skipped (applied a + + +fter --include-configs) + + + --exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched rela + + +tive to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not + + +supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET project is not analysed as a project of i + + +ts own, but where a project that is not excluded depends on it, its code is analysed as part of that project. A build module or project outs + + +ide CWD cannot be excluded. + + + --facts Emit a Socket facts JSON file (`sbt.socket.facts.json`) describing the resolved dependency graph. This is the defaul + + +t; pass `--pom` to generate `pom.xml` files instead + + + --ignore-unresolved When generating facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted + + +to the facts file) + + + --include-configs When generating facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive; `*`, ` + + +?`, and `[...]` wildcards). Only configurations matching at least one pattern are resolved. e.g. `compile,test`. Default: compile,optional,p + + +rovided,runtime,test + + + --out Only with --pom: path of the output `pom.xml`, see also --stdout. Does not apply when generating Socket facts (alway + + +s written to the project root as `sbt.socket.facts.json`) + + + --pom Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`sbt.socket.facts.json`) + + + --sbt-opts Additional options to pass on to sbt, as per `sbt --help` + + + --stdout Only with --pom: print the resulting `pom.xml` to stdout (supersedes --out). Does not apply when generating Socket f + + +acts + + + --verbose Print debug messages + + + + + + By default, emits a single `sbt.socket.facts.json` describing the + + + resolved dependency graph of the whole build. It reads dependency metadata only and + + + never downloads artifacts; an unresolved dependency is a fatal error. You + + + can pass --include-configs / --exclude-configs (comma-separated glob + + + patterns) to control which sbt configurations are resolved (e.g. + + + --include-configs=`compile,test`), and --ignore-unresolved to warn on + + + unresolved dependencies instead of failing the run. + + + + + + Pass --pom to instead generate a `pom.xml` via `sbt makePom` from your + + + `build.sbt`. The xml is the dependency manifest (like a package.json for + + + Node.js or requirements.txt for PyPi), but specifically for Scala. + + + Caveats of the `build.sbt` to `pom.xml` conversion: + + + + + + - the xml is exported as pom.xml at the project root so Socket scan picks + + + it up; sbt itself first writes it inside your /target/sbt<version> folder + + + (as a different name). Use --out to override if you already have a + + + hand-authored pom.xml at the project root. + + + + + + - the pom.xml format (standard by Scala) does not support certain sbt features + + + - `excludeAll()`, `dependencyOverrides`, `force()`, `relativePath` + + + - For details: https://www.scala-sbt.org/1.x/docs/Library-Management.html + + + + + + - it uses your sbt settings and local configuration verbatim + + + + + + - it can only export one target per run, so if you have multiple targets like + + + development and production, you must run them separately. + + + + + + You can specify --bin to override the path to the `sbt` binary to invoke. + + + + + + Support is beta. Please report issues or give us feedback on what's missing. + + + + + + This is only for SBT. If your Scala setup uses gradle, please see the help + + + sections for `socket manifest gradle` or `socket cdxgen`. + + + + + + Examples + + + + + + $ socket manifest scala + + + $ socket manifest scala --pom . + + + $ socket manifest scala ./proj --bin=/usr/bin/sbt --file=boot.sbt + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-scala.sequence.json b/test/native-snapshots/socket-help-manifest-scala.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-scala.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest-setup.help.svg b/test/native-snapshots/socket-help-manifest-setup.help.svg new file mode 100644 index 0000000000..a7ba88d4d3 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-setup.help.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest setup | org: (not set) | token: (not set) + + + + + + + + + Start interactive configurator to customize default flag values for `socket manifest` in this dir + + + + + + Usage + + + $ socket manifest setup [CWD=.] + + + + + + Options + + + --default-on-read-error If reading the socket.json fails, just use a default config? Warning: This might override the existing json fil + + +e! + + + --dynamic-sbom-inference Uses .NET, Gradle, Maven, and sbt tools to generate dynamic SBOMs with more accurate results than static resol + + +ution. It scans build roots under CWD, asks only about detected ecosystems, then disables roots matched by --exclude-paths and configures th + + +e rest individually. + + + --exclude-paths Build roots matching these glob patterns (and everything beneath them) are marked disabled. Patterns are anchored mi + + +cromatch globs matched relative to CWD: `legacy` matches only `<cwd>/legacy`; use `**/legacy` to match at any depth. Negation patterns (`!pa + + +th`) are not supported. Accepts a comma-separated value or multiple flags. + + + + + + This command will try to detect all supported ecosystems in given CWD. Then + + + it starts a configurator where you can setup default values for certain flags + + + when creating manifest files in that dir. These configuration details are + + + then stored in a local `socket.json` file (which you may or may not commit + + + to the repo). Next time you run `socket manifest ...` it will load this + + + json file and any flags which are not explicitly set in the command but which + + + have been registered in the json file will get the default value set to that + + + value you stored rather than the hardcoded defaults. + + + + + + This helps with for example when your build binary is in a particular path + + + or when your build tool needs specific opts and you don't want to specify + + + them when running the command every time. + + + + + + You can also disable manifest generation for certain ecosystems. + + + + + + This generated configuration file will only be used locally by the CLI. You + + + can commit it to the repo (useful for collaboration) or choose to add it to + + + your .gitignore all the same. Only this CLI will use it. + + + + + + Examples + + + $ socket manifest setup + + + $ socket manifest setup ./proj + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest-setup.sequence.json b/test/native-snapshots/socket-help-manifest-setup.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest-setup.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-manifest.help.svg b/test/native-snapshots/socket-help-manifest.help.svg new file mode 100644 index 0000000000..ce48193dcb --- /dev/null +++ b/test/native-snapshots/socket-help-manifest.help.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest | org: (not set) | token: (not set) + + + + + + Generate a dependency manifest for certain ecosystems + + + + + + Usage + + + $ socket manifest <command> + + + + + + Commands + + + auto Auto-detect build and attempt to generate manifest file + + + bazel [beta] Bazel SBOM support — generate manifest files for a Bazel project (Maven, PyPI) + + + cdxgen Run cdxgen for SBOM generation + + + conda [beta] Convert a Conda environment.yml file to a python requirements.txt + + + dotnet [beta] Generate Socket facts files from .NET solutions and projects + + + dynamic-sbom-inference Recursively discover .NET, Gradle, Maven, and sbt build roots and generate a Socket facts SBOM for each + + + gradle [beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Gradle/Java/Kotlin/etc project + + + kotlin [beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Kotlin project + + + maven [beta] Generate a Socket facts file from a Maven `pom.xml` project + + + scala [beta] Generate a Socket facts file (or `pom.xml` with --pom) from a Scala `build.sbt` project + + + setup Start interactive configurator to customize default flag values for `socket manifest` in this dir + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-manifest.sequence.json b/test/native-snapshots/socket-help-manifest.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-manifest.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-npm.help.svg b/test/native-snapshots/socket-help-npm.help.svg new file mode 100644 index 0000000000..29c963c520 --- /dev/null +++ b/test/native-snapshots/socket-help-npm.help.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket npm | org: (not set) | token: (not set) + + + + + + + + + Wraps npm with Socket security scanning + + + + + + Usage + + + $ socket npm ... + + + + + + API Token Requirements + + + - Quota: 100 units + + + - Permissions: packages:list + + + + + + Note: Everything after "npm" is passed to the npm command. + + + Only the `--dry-run` and `--help` flags are caught here. + + + + + + Use `socket wrapper on` to alias this command as `npm`. + + + + + + Examples + + + $ socket npm + + + $ socket npm install -g cowsay + + + $ socket npm exec cowsay + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-npm.sequence.json b/test/native-snapshots/socket-help-npm.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-npm.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-npx.help.svg b/test/native-snapshots/socket-help-npx.help.svg new file mode 100644 index 0000000000..ecb0c98704 --- /dev/null +++ b/test/native-snapshots/socket-help-npx.help.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket npx | org: (not set) | token: (not set) + + + + + + + + + Wraps npx with Socket security scanning + + + + + + Usage + + + $ socket npx ... + + + + + + API Token Requirements + + + - Quota: 100 units + + + - Permissions: packages:list + + + + + + Note: Everything after "npx" is passed to the npx command. + + + Only the `--dry-run` and `--help` flags are caught here. + + + + + + Use `socket wrapper on` to alias this command as `npx`. + + + + + + Examples + + + $ socket npx cowsay + + + $ socket npx cowsay@1.6.0 hello + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-npx.sequence.json b/test/native-snapshots/socket-help-npx.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-npx.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-oops.help.svg b/test/native-snapshots/socket-help-oops.help.svg new file mode 100644 index 0000000000..1274b54d2a --- /dev/null +++ b/test/native-snapshots/socket-help-oops.help.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket oops | org: (not set) | token: (not set) + + + + + + + + + Trigger an intentional error (for development) + + + + + + Usage + + + $ socket oops oops + + + + + + Don't run me. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-oops.sequence.json b/test/native-snapshots/socket-help-oops.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-oops.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-optimize.help.svg b/test/native-snapshots/socket-help-optimize.help.svg new file mode 100644 index 0000000000..3829dd4eb3 --- /dev/null +++ b/test/native-snapshots/socket-help-optimize.help.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket optimize | org: (not set) | token: (not set) + + + + + + + + + Optimize dependencies with @socketregistry overrides + + + + + + Usage + + + $ socket optimize [options] [CWD=.] + + + + + + API Token Requirements + + + - Quota: 100 units + + + - Permissions: packages:list + + + + + + Options + + + --pin Pin overrides to latest version + + + --prod Add overrides for production dependencies only + + + + + + Examples + + + $ socket optimize + + + $ socket optimize ./path/to/project --pin + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-optimize.sequence.json b/test/native-snapshots/socket-help-optimize.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-optimize.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-organization-dependencies.help.svg b/test/native-snapshots/socket-help-organization-dependencies.help.svg new file mode 100644 index 0000000000..785f84f1f3 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-dependencies.help.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket organization dependencies | org: (not set) | token: (not set) + + + + + + + + + Search for any dependency that is being used in your organization + + + + + + Usage + + + socket organization dependencies [options] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + + + + Options + + + --json Output as JSON + + + --limit Maximum number of dependencies returned + + + --markdown Output as Markdown + + + --offset Page number + + + + + + Examples + + + socket organization dependencies + + + socket organization dependencies --limit 20 --offset 10 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-organization-dependencies.sequence.json b/test/native-snapshots/socket-help-organization-dependencies.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-dependencies.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-organization-list.help.svg b/test/native-snapshots/socket-help-organization-list.help.svg new file mode 100644 index 0000000000..0c77a600aa --- /dev/null +++ b/test/native-snapshots/socket-help-organization-list.help.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket organization list | org: (not set) | token: (not set) + + + + + + + + + List organizations associated with the Socket API token + + + + + + Usage + + + $ socket organization list [options] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + + + + Options + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + Examples + + + $ socket organization list + + + $ socket organization list --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-organization-list.sequence.json b/test/native-snapshots/socket-help-organization-list.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-list.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-organization-policy-license.help.svg b/test/native-snapshots/socket-help-organization-policy-license.help.svg new file mode 100644 index 0000000000..f69baf1ad3 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-policy-license.help.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket organization policy license | org: (not set) | token: (not set) + + + + + + + + + Retrieve the license policy of an organization + + + + + + Usage + + + $ socket organization policy license [options] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: license-policy:read + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Your API token will need the `license-policy:read` permission otherwise + + + the request will fail with an authentication error. + + + + + + Examples + + + $ socket organization policy license + + + $ socket organization policy license --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-organization-policy-license.sequence.json b/test/native-snapshots/socket-help-organization-policy-license.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-policy-license.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-organization-policy-security.help.svg b/test/native-snapshots/socket-help-organization-policy-security.help.svg new file mode 100644 index 0000000000..f108d9a1ad --- /dev/null +++ b/test/native-snapshots/socket-help-organization-policy-security.help.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket organization policy security | org: (not set) | token: (not set) + + + + + + + + + Retrieve the security policy of an organization + + + + + + Usage + + + $ socket organization policy security [options] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: security-policy:read + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Your API token will need the `security-policy:read` permission otherwise + + + the request will fail with an authentication error. + + + + + + Examples + + + $ socket organization policy security + + + $ socket organization policy security --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-organization-policy-security.sequence.json b/test/native-snapshots/socket-help-organization-policy-security.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-policy-security.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-organization-policy.help.svg b/test/native-snapshots/socket-help-organization-policy.help.svg new file mode 100644 index 0000000000..45bf27b187 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-policy.help.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket organization policy | org: (not set) | token: (not set) + + + + + + Organization policy details + + + + + + Usage + + + $ socket organization policy <command> + + + + + + Commands + + + license Retrieve the license policy of an organization + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-organization-policy.sequence.json b/test/native-snapshots/socket-help-organization-policy.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-policy.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-organization-quota.help.svg b/test/native-snapshots/socket-help-organization-quota.help.svg new file mode 100644 index 0000000000..b3ad3c1caa --- /dev/null +++ b/test/native-snapshots/socket-help-organization-quota.help.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket organization quota | org: (not set) | token: (not set) + + + + + + + + + List organizations associated with the Socket API token + + + + + + Usage + + + $ socket organization quota [options] + + + + + + Options + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + Examples + + + $ socket organization quota + + + $ socket organization quota --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-organization-quota.sequence.json b/test/native-snapshots/socket-help-organization-quota.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-organization-quota.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-organization.help.svg b/test/native-snapshots/socket-help-organization.help.svg new file mode 100644 index 0000000000..2630314063 --- /dev/null +++ b/test/native-snapshots/socket-help-organization.help.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket organization | org: (not set) | token: (not set) + + + + + + Manage Socket organization account details + + + + + + Usage + + + $ socket organization <command> + + + + + + Commands + + + dependencies Search for any dependency that is being used in your organization + + + list List organizations associated with the Socket API token + + + policy Organization policy details + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-organization.sequence.json b/test/native-snapshots/socket-help-organization.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-organization.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-package-score.help.svg b/test/native-snapshots/socket-help-package-score.help.svg new file mode 100644 index 0000000000..dfc8283023 --- /dev/null +++ b/test/native-snapshots/socket-help-package-score.help.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket package score | org: (not set) | token: (not set) + + + + + + + + + Look up score for one package which reflects all of its transitive dependencies as well + + + + + + Usage + + + $ socket package score [options] <<ECOSYSTEM> <NAME> | <PURL>> + + + + + + API Token Requirements + + + - Quota: 100 units + + + - Permissions: packages:list + + + + + + Options + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + Show deep scoring details for one package. The score will reflect the package + + + itself, any of its dependencies, and any of its transitive dependencies. + + + + + + When you want to know whether to trust a package, this is the command to run. + + + + + + See also the `socket package shallow` command, which returns the shallow + + + score for any number of packages. That will not reflect the dependency scores. + + + + + + Only a few ecosystems are supported like npm, pypi, nuget, gem, golang, and maven. + + + + + + A "purl" is a standard package name formatting: `pkg:eco/name@version` + + + This command will automatically prepend "pkg:" when not present. + + + + + + The version is optional but when given should be a direct match. The `pkg:` + + + prefix is optional. + + + + + + Note: if a package cannot be found it may be too old or perhaps was removed + + + before we had the opportunity to process it. + + + + + + Examples + + + $ socket package score npm babel-cli + + + $ socket package score npm eslint@1.0.0 --json + + + $ socket package score pkg:golang/github.com/steelpoor/tlsproxy@v0.0.0-20250304082521-29051ed19c60 + + + $ socket package score nuget/needpluscommonlibrary@1.0.0 --markdown + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-package-score.sequence.json b/test/native-snapshots/socket-help-package-score.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-package-score.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-package-shallow.help.svg b/test/native-snapshots/socket-help-package-shallow.help.svg new file mode 100644 index 0000000000..a0483092a5 --- /dev/null +++ b/test/native-snapshots/socket-help-package-shallow.help.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket package shallow | org: (not set) | token: (not set) + + + + + + + + + Look up info regarding one or more packages but not their transitives + + + + + + Usage + + + $ socket package shallow [options] <<ECOSYSTEM> <PKGNAME> [<PKGNAME> ...] | <PURL> [<PURL> ...]> + + + + + + API Token Requirements + + + - Quota: 100 units + + + - Permissions: packages:list + + + + + + Options + + + --json Output as JSON + + + --markdown Output as Markdown + + + + + + Show scoring details for one or more packages purely based on their own package. + + + This means that any dependency scores are not reflected by the score. You can + + + use the `socket package score <pkg>` command to get its full transitive score. + + + + + + Only a few ecosystems are supported like npm, pypi, nuget, gem, golang, and maven. + + + + + + A "purl" is a standard package name formatting: `pkg:eco/name@version` + + + This command will automatically prepend "pkg:" when not present. + + + + + + If the first arg is an ecosystem, remaining args that are not a purl are + + + assumed to be scoped to that ecosystem. The `pkg:` prefix is optional. + + + + + + Note: if a package cannot be found, it may be too old or perhaps was removed + + + before we had the opportunity to process it. + + + + + + Examples + + + $ socket package shallow npm webtorrent + + + $ socket package shallow npm webtorrent@1.9.1 + + + $ socket package shallow npm/webtorrent@1.9.1 + + + $ socket package shallow pkg:npm/webtorrent@1.9.1 + + + $ socket package shallow maven webtorrent babel + + + $ socket package shallow npm/webtorrent golang/babel + + + $ socket package shallow npm npm/webtorrent@1.0.1 babel + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-package-shallow.sequence.json b/test/native-snapshots/socket-help-package-shallow.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-package-shallow.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-package.help.svg b/test/native-snapshots/socket-help-package.help.svg new file mode 100644 index 0000000000..567b6adc3a --- /dev/null +++ b/test/native-snapshots/socket-help-package.help.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket package | org: (not set) | token: (not set) + + + + + + Look up published package details + + + + + + Usage + + + $ socket package <command> + + + + + + Commands + + + score Look up score for one package which reflects all of its transitive dependencies as well + + + shallow Look up info regarding one or more packages but not their transitives + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-package.sequence.json b/test/native-snapshots/socket-help-package.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-package.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-patch.help.svg b/test/native-snapshots/socket-help-patch.help.svg new file mode 100644 index 0000000000..eec74922df --- /dev/null +++ b/test/native-snapshots/socket-help-patch.help.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + +CLI tool for applying security patches to dependencies + + + + + +Usage: + +socket-patch + <COMMAND> + + + + + +Commands: + + + + +apply + Apply security patches to dependencies + + + +rollback + Rollback patches to restore original files + + + +get + Get security patches from Socket API and apply them [aliases: download] + + + +scan + Scan installed packages for available security patches + + + +list + List all patches in the local manifest + + + +remove + Remove a patch from the manifest by PURL or UUID (rolls back files first) + + + +setup + Configure package.json postinstall scripts to apply patches + + + +repair + Download missing blobs and clean up unused blobs [aliases: gc] + + + +help + Print this message or the help of the given subcommand(s) + + + + + +Options: + + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-patch.sequence.json b/test/native-snapshots/socket-help-patch.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-patch.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-pnpm.help.svg b/test/native-snapshots/socket-help-pnpm.help.svg new file mode 100644 index 0000000000..47a6b4197d --- /dev/null +++ b/test/native-snapshots/socket-help-pnpm.help.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket pnpm | org: (not set) | token: (not set) + + + + + + + + + Wraps pnpm with Socket security scanning + + + + + + Usage + + + $ socket pnpm ... + + + + + + API Token Requirements + + + (none) + + + + + + Note: Everything after "pnpm" is passed to the pnpm command. + + + Only the `--dry-run` and `--help` flags are caught here. + + + + + + Use `socket wrapper on` to alias this command as `pnpm`. + + + + + + Examples + + + $ socket pnpm + + + $ socket pnpm install + + + $ socket pnpm add package-name + + + $ socket pnpm dlx package-name + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-pnpm.sequence.json b/test/native-snapshots/socket-help-pnpm.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-pnpm.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-raw-npm.help.svg b/test/native-snapshots/socket-help-raw-npm.help.svg new file mode 100644 index 0000000000..aeabc28e37 --- /dev/null +++ b/test/native-snapshots/socket-help-raw-npm.help.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket raw-npm | org: (not set) | token: (not set) + + + + + + + + + Run npm without the Socket wrapper + + + + + + Usage + + + $ socket raw-npm ... + + + + + + Execute `npm` without gating installs through the Socket API. + + + Useful when `socket wrapper on` is enabled and you want to bypass + + + the Socket wrapper. Use at your own risk. + + + + + + Note: Everything after "raw-npm" is passed to the npm command. + + + Only the `--dry-run` and `--help` flags are caught here. + + + + + + Examples + + + $ socket raw-npm install -g cowsay + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-raw-npm.sequence.json b/test/native-snapshots/socket-help-raw-npm.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-raw-npm.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-raw-npx.help.svg b/test/native-snapshots/socket-help-raw-npx.help.svg new file mode 100644 index 0000000000..1e8d7d0c19 --- /dev/null +++ b/test/native-snapshots/socket-help-raw-npx.help.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket raw-npx | org: (not set) | token: (not set) + + + + + + + + + Run npx without the Socket wrapper + + + + + + Usage + + + $ socket raw-npx ... + + + + + + Execute `npx` without gating installs through the Socket API. + + + Useful when `socket wrapper on` is enabled and you want to bypass + + + the Socket wrapper. Use at your own risk. + + + + + + Note: Everything after "raw-npx" is passed to the npx command. + + + Only the `--dry-run` and `--help` flags are caught here. + + + + + + Examples + + + $ socket raw-npx cowsay + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-raw-npx.sequence.json b/test/native-snapshots/socket-help-raw-npx.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-raw-npx.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-repository-create.help.svg b/test/native-snapshots/socket-help-repository-create.help.svg new file mode 100644 index 0000000000..e3f0b4f1c6 --- /dev/null +++ b/test/native-snapshots/socket-help-repository-create.help.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket repository create | org: (not set) | token: (not set) + + + + + + + + + Create a repository in an organization + + + + + + Usage + + + $ socket repository create [options] <REPO> + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: repo:create + + + + + + The REPO name should be a "slug". Follows the same naming convention as GitHub. + + + + + + Options + + + --default-branch Repository default branch. Defaults to "main" + + + --homepage Repository url + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --repo-description Repository description + + + --visibility Repository visibility (Default Private) + + + + + + Examples + + + $ socket repository create test-repo + + + $ socket repository create our-repo --homepage=socket.dev --default-branch=trunk + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-repository-create.sequence.json b/test/native-snapshots/socket-help-repository-create.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-repository-create.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-repository-del.help.svg b/test/native-snapshots/socket-help-repository-del.help.svg new file mode 100644 index 0000000000..6c4f7185f1 --- /dev/null +++ b/test/native-snapshots/socket-help-repository-del.help.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket repository del | org: (not set) | token: (not set) + + + + + + + + + Delete a repository in an organization + + + + + + Usage + + + $ socket repository del [options] <REPO> + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: repo:delete + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Examples + + + $ socket repository del test-repo + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-repository-del.sequence.json b/test/native-snapshots/socket-help-repository-del.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-repository-del.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-repository-list.help.svg b/test/native-snapshots/socket-help-repository-list.help.svg new file mode 100644 index 0000000000..f6205bbbbe --- /dev/null +++ b/test/native-snapshots/socket-help-repository-list.help.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket repository list | org: (not set) | token: (not set) + + + + + + + + + List repositories in an organization + + + + + + Usage + + + $ socket repository list [options] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: repo:list + + + + + + Options + + + --all By default view shows the last n repos. This flag allows you to fetch the entire list. Will ignore --page and --per- + + +page. + + + --direction Direction option + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --page Page number + + + --per-page Number of results per page + + + --sort Sorting option + + + + + + Examples + + + $ socket repository list + + + $ socket repository list --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-repository-list.sequence.json b/test/native-snapshots/socket-help-repository-list.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-repository-list.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-repository-update.help.svg b/test/native-snapshots/socket-help-repository-update.help.svg new file mode 100644 index 0000000000..1e1d39a35a --- /dev/null +++ b/test/native-snapshots/socket-help-repository-update.help.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket repository update | org: (not set) | token: (not set) + + + + + + + + + Update a repository in an organization + + + + + + Usage + + + $ socket repository update [options] <REPO> + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: repo:update + + + + + + Options + + + --default-branch Repository default branch + + + --homepage Repository url + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --repo-description Repository description + + + --visibility Repository visibility (Default Private) + + + + + + Examples + + + $ socket repository update test-repo + + + $ socket repository update test-repo --homepage https://example.com + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-repository-update.sequence.json b/test/native-snapshots/socket-help-repository-update.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-repository-update.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-repository-view.help.svg b/test/native-snapshots/socket-help-repository-view.help.svg new file mode 100644 index 0000000000..640b9a3909 --- /dev/null +++ b/test/native-snapshots/socket-help-repository-view.help.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket repository view | org: (not set) | token: (not set) + + + + + + + + + View repositories in an organization + + + + + + Usage + + + $ socket repository view [options] <REPO> + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: repo:list + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Examples + + + $ socket repository view test-repo + + + $ socket repository view test-repo --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-repository-view.sequence.json b/test/native-snapshots/socket-help-repository-view.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-repository-view.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-repository.help.svg b/test/native-snapshots/socket-help-repository.help.svg new file mode 100644 index 0000000000..7a9238d5a2 --- /dev/null +++ b/test/native-snapshots/socket-help-repository.help.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket repository | org: (not set) | token: (not set) + + + + + + Manage registered repositories + + + + + + Usage + + + $ socket repository <command> + + + + + + Commands + + + create Create a repository in an organization + + + del Delete a repository in an organization + + + list List repositories in an organization + + + update Update a repository in an organization + + + view View repositories in an organization + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-repository.sequence.json b/test/native-snapshots/socket-help-repository.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-repository.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-create.help.svg b/test/native-snapshots/socket-help-scan-create.help.svg new file mode 100644 index 0000000000..c8f435c752 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-create.help.svg @@ -0,0 +1,414 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan create | org: (not set) | token: (not set) + + + + + + + + + Create a new Socket scan and report + + + + + + Usage + + + $ socket scan create [options] [TARGET...] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: full-scans:create + + + + + + Options + + + --auto-manifest Run `socket manifest auto` before collecting manifest files, which generates them for ecosystems that need their bui + + +ld tool to resolve dependencies, such as Scala, Gradle, and Kotlin. See `socket manifest auto --help`. For Gradle, sbt, Maven and .NET, --dy + + +namic-sbom-inference generates a Socket facts SBOM per build root instead. + + + --branch Branch name + + + --commit-hash Commit hash + + + --commit-message Commit message + + + --committers Committers + + + --cwd working directory, defaults to process.cwd() + + + --default-branch Set the default branch of the repository to the branch of this full-scan. Should only need to be done once, for exam + + +ple for the "main" or "master" branch. + + + --dynamic-sbom-inference For Gradle, sbt, Maven and .NET: generate a Socket facts SBOM (produced directly by each package manager) per + + +independent build root, instead of one synthetic root. Combine with --reach to split the reachability analysis per project/module. + + + --dynamic-sbom-inference-ecosystems With --dynamic-sbom-inference: generate Socket facts only for these ecosystems, as either a comma s + + +eparated value or as multiple flags; the others keep their manifest files as they are. Supported: maven (Gradle, Maven, and sbt) and nuget ( + + +.NET). Defaults to all of them. + + + --exclude-paths List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) + + +full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the comman + + +d working directory (`--cwd` if set), not the reachability target: `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. + + + Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET proj + + +ect is not analysed as a project of its own, but where a project that is not excluded depends on it, its code is analysed as part of that pr + + +oject. A build module or project outside the scan root cannot be excluded. + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --pull-request Pull request number + + + --reach Run full application reachability analysis + + + --read-only Similar to --dry-run except it can read from remote, stops before it would create an actual report + + + --repo Repository name + + + --report Wait for the scan creation to complete, then basically run `socket scan report` on it + + + --report-level Which policy level alerts should be reported (default 'error') + + + --set-as-alerts-page When true and if this is the "default branch" then this Scan will be the one reflected on your alerts page. See he + + +lp for details. Defaults to true. + + + --tmp Set the visibility (true/false) of the scan in your dashboard. + + + --uv-members Scan each TARGET directory as a uv project, using the versions pinned in its uv.lock or its workspace root uv.lock. + + +Uploads a CycloneDX dependency graph per TARGET in place of manifest discovery, including all extras and dependency groups. Requires uv on P + + +ATH; standalone builds use SOCKET_CLI_UV_PATH. + + + --workspace The workspace in the Socket Organization that the repository is in to associate with the full scan. + + + + + + Reachability Options (when --reach is used) + + + --reach-advisories List of advisories to run reachability analysis for, as GHSA or CVE IDs, either as a comma separated value or as mul + + +tiple flags. CVE IDs are converted to GHSA IDs through the GitHub API. Vulnerabilities from other advisories get unknown reachability. Defau + + +lts to all advisories. + + + --reach-analysis-memory-limit The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. + + +512MB, 8GB). The default is 8GB. + + + --reach-analysis-timeout Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10 + + +m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly. + + + --reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level th + + +at ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available. + + + --reach-continue-on-analysis-errors Continue reachability analysis when errors occur (timeouts, OOM, parse errors, etc.), falling back + + +to precomputed reachability results. By default, the CLI halts on analysis errors. + + + --reach-continue-on-install-errors Continue reachability analysis when package installation fails, falling back to precomputed reachabi + + +lity results. By default, the CLI halts on installation errors. + + + --reach-continue-on-missing-lock-files Continue reachability analysis when a Gradle or SBT project is missing its lock file (or version + + + catalog / pre-generated SBOM). By default, the CLI halts. + + + --reach-continue-on-no-source-files Continue reachability analysis when a workspace contains no source files for its ecosystem. By defa + + +ult, the CLI halts. + + + --reach-debug Enable debug mode for reachability analysis. Provides verbose logging from the reachability CLI. + + + --reach-detailed-analysis-log-file A log file with detailed analysis logs is written to root of each analyzed workspace. + + + --reach-disable-analytics Disable reachability analytics sharing with Socket. Also disables caching-based optimizations. + + + --reach-disable-dependency-downloads Only analyze dependencies that are already installed (e.g. in the project's .venv, vendor director + + +y or module cache). Missing dependencies count as installation failures. + + + --reach-disable-external-tool-checks Disable external tool checks during reachability analysis. + + + --reach-ecosystems List of ecosystems to conduct reachability analysis on, as either a comma separated value or as multiple flags. Supp + + +orted: cargo, composer, gem, golang, maven, npm, nuget, pypi. Defaults to all supported ecosystems. + + + --reach-enable-analysis-splitting Allow the reachability analysis to partition CVEs into buckets that are processed in separate analysi + + +s runs. May improve accuracy, but not recommended by default. + + + --reach-fallback-to-regular-scan If reachability analysis fails, continue with a regular SCA scan (without reachability results) instea + + +d of halting. By default, the CLI halts on reachability errors. + + + --reach-retain-facts-file Keep the `.socket.facts.json` reachability report that the analysis writes to the scan directory instead of d + + +eleting it after a successful scan. Delete it once inspected: later reachability scans ignore it, but a scan without --reach uploads it as a + + +n SBOM carrying stale reachability results. + + + --reach-skip-cache Skip caching-based optimizations. By default, the reachability analysis will use cached configurations from previous + + + runs to speed up the analysis. + + + --reach-use-only-pregenerated-sboms When using this option, the scan is created based only on pre-generated CDX and SPDX files in your + + +project. + + + --reach-version Override the version of @coana-tech/cli used for reachability analysis. Default: 15.14.2. + + + + + + Uploads the specified dependency manifest files for Go, Gradle, JavaScript, + + + Kotlin, Python, and Scala. Files like "package.json" and "requirements.txt". + + + If any folder is specified, the ones found in there recursively are uploaded. + + + + + + Details on TARGET: + + + + + + - Defaults to the current dir (cwd) if none given + + + - Multiple targets can be specified + + + - If a target is a file, only that file is checked + + + - If it is a dir, the dir is scanned for any supported manifest files + + + - Dirs MUST be within the current dir (cwd), you can use --cwd to change it + + + - Supports globbing such as "**/package.json", "**/requirements.txt", etc. + + + - Ignores any file specified in your project's ".gitignore" + + + - Also a sensible set of default ignores from the "ignore-by-default" module + + + + + + The --repo and --branch flags tell Socket to associate this Scan with that + + + repo/branch. The names will show up on your dashboard on the Socket website. + + + + + + Note: for a first run you probably want to set --default-branch to indicate + + + the default branch name, like "main" or "master". + + + + + + The "alerts page" (​https://socket.dev/dashboard/org/YOURORG/alerts​) will show + + + the results from the last scan designated as the "pending head" on the branch + + + configured on Socket to be the "default branch". When creating a scan the + + + --set-as-alerts-page flag will default to true to update this. You can prevent + + + this by using --no-set-as-alerts-page. This flag is ignored for any branch that + + + is not designated as the "default branch". It is disabled when using --tmp. + + + + + + You can use `socket scan setup` to configure certain repo flag defaults. + + + + + + Examples + + + $ socket scan create + + + $ socket scan create ./proj --json + + + $ socket scan create --repo=test-repo --branch=main ./package.json + + + $ socket scan create --uv-members ./packages/api ./packages/worker + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-create.sequence.json b/test/native-snapshots/socket-help-scan-create.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-create.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-del.help.svg b/test/native-snapshots/socket-help-scan-del.help.svg new file mode 100644 index 0000000000..ef516f17e2 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-del.help.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan del | org: (not set) | token: (not set) + + + + + + + + + Delete a scan + + + + + + Usage + + + $ socket scan del [options] <SCAN_ID> + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: full-scans:delete + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Examples + + + $ socket scan del 000aaaa1-0000-0a0a-00a0-00a0000000a0 + + + $ socket scan del 000aaaa1-0000-0a0a-00a0-00a0000000a0 --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-del.sequence.json b/test/native-snapshots/socket-help-scan-del.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-del.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-diff.help.svg b/test/native-snapshots/socket-help-scan-diff.help.svg new file mode 100644 index 0000000000..a33bccec27 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-diff.help.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan diff | org: (not set) | token: (not set) + + + + + + + + + See what changed between two Scans + + + + + + Usage + + + $ socket scan diff [options] <SCAN_ID1> <SCAN_ID2> + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: full-scans:list + + + + + + This command displays the package changes between two scans. The full output + + + can be pretty large depending on the size of your repo and time range. It is + + + best stored to disk (with --json) to be further analyzed by other tools. + + + + + + Note: While it will work in any order, the first Scan ID is assumed to be the + + + older ID, even if it is a newer Scan. This is only relevant for the + + + added/removed list (similar to diffing two files with git). + + + + + + Options + + + --depth Max depth of JSON to display before truncating, use zero for no limit (without --json/--file) + + + --file Path to a local file where the output should be saved. Use `-` to force stdout. + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Examples + + + $ socket scan diff aaa0aa0a-aaaa-0000-0a0a-0000000a00a0 aaa1aa1a-aaaa-1111-1a1a-1111111a11a1 + + + $ socket scan diff aaa0aa0a-aaaa-0000-0a0a-0000000a00a0 aaa1aa1a-aaaa-1111-1a1a-1111111a11a1 --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-diff.sequence.json b/test/native-snapshots/socket-help-scan-diff.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-diff.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-github.help.svg b/test/native-snapshots/socket-help-scan-github.help.svg new file mode 100644 index 0000000000..b72f0e5b48 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-github.help.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan github | org: (not set) | token: (not set) + + + + + + + + + Create a scan for given GitHub repo + + + + + + Usage + + + $ socket scan github [options] [CWD=.] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: full-scans:create + + + + + + This is similar to the `socket scan create` command except it pulls the files + + + from GitHub. See the help for that command for more details. + + + + + + A GitHub Personal Access Token (PAT) will at least need read access to the repo + + + ("contents", read-only) for this command to work. + + + + + + Note: This command cannot run the `socket manifest auto` things because that + + + requires local access to the repo while this command runs entirely through the + + + GitHub for file access. + + + + + + You can use `socket scan setup` to configure certain repo flag defaults. + + + + + + Options + + + --all Apply for all known repositories reported by the Socket API. Supersedes `repos`. + + + --github-api-url Base URL of the GitHub API (default: https://api.github.com) + + + --github-token Required GitHub token for authentication. + + + May set environment variable GITHUB_TOKEN or SOCKET_CLI_GITHUB_TOKEN instead. + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --org-github Alternate GitHub Org if the name is different than the Socket Org + + + --repos List of repos to target in a comma-separated format (e.g., repo1,repo2). If not specified, the script will pull the + + +list from Socket and ask you to pick one. Use --all to use them all. + + + + + + Examples + + + $ socket scan github + + + $ socket scan github ./proj + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-github.sequence.json b/test/native-snapshots/socket-help-scan-github.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-github.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-list.help.svg b/test/native-snapshots/socket-help-scan-list.help.svg new file mode 100644 index 0000000000..0d2b7bb0bd --- /dev/null +++ b/test/native-snapshots/socket-help-scan-list.help.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan list | org: (not set) | token: (not set) + + + + + + + + + List the scans for an organization + + + + + + Usage + + + $ socket scan list [options] [REPO [BRANCH]] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: full-scans:list + + + + + + Optionally filter by REPO. If you specify a repo, you can also specify a + + + branch to filter by. (Note: If you don't specify a repo then you must use + + + `--branch` to filter by branch across all repos). + + + + + + Options + + + --branch Filter to show only scans with this branch name + + + --direction Direction option (`desc` or `asc`) - Default is `desc` + + + --from-time From time - as a unix timestamp + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --page Page number - Default is 1 + + + --per-page Results per page - Default is 30 + + + --sort Sorting option (`name` or `created_at`) - default is `created_at` + + + --until-time Until time - as a unix timestamp + + + + + + Examples + + + $ socket scan list + + + $ socket scan list webtools badbranch --markdown + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-list.sequence.json b/test/native-snapshots/socket-help-scan-list.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-list.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-metadata.help.svg b/test/native-snapshots/socket-help-scan-metadata.help.svg new file mode 100644 index 0000000000..fb8b732e74 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-metadata.help.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan metadata | org: (not set) | token: (not set) + + + + + + + + + Get a scan's metadata + + + + + + Usage + + + $ socket scan metadata [options] <SCAN_ID> + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: full-scans:list + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Examples + + + $ socket scan metadata 000aaaa1-0000-0a0a-00a0-00a0000000a0 + + + $ socket scan metadata 000aaaa1-0000-0a0a-00a0-00a0000000a0 --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-metadata.sequence.json b/test/native-snapshots/socket-help-scan-metadata.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-metadata.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-reach.help.svg b/test/native-snapshots/socket-help-scan-reach.help.svg new file mode 100644 index 0000000000..9cec0736e8 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-reach.help.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan reach | org: (not set) | token: (not set) + + + + + + + + + Compute full application reachability + + + + + + Usage + + + $ socket scan reach [options] [CWD=.] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: full-scans:create + + + + + + Options + + + --cwd working directory, defaults to process.cwd() + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --output Path to write the reachability report to (must end with .json). Defaults to .socket.facts.json in the current workin + + +g directory. + + + + + + Reachability Options + + + --dynamic-sbom-inference For Gradle, sbt, Maven and .NET: generate a Socket facts SBOM (produced directly by each package manager) per + + +independent build root, instead of one synthetic root. The reachability analysis is split per project/module accordingly. + + + --dynamic-sbom-inference-ecosystems With --dynamic-sbom-inference: generate Socket facts only for these ecosystems, as either a comma s + + +eparated value or as multiple flags; the others keep their manifest files as they are. Supported: maven (Gradle, Maven, and sbt) and nuget ( + + +.NET). Defaults to all of them. + + + --exclude-paths List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) + + +full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the comman + + +d working directory (`--cwd` if set), not the reachability target: `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. + + + Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module or .NET proj + + +ect is not analysed as a project of its own, but where a project that is not excluded depends on it, its code is analysed as part of that pr + + +oject. A build module or project outside the scan root cannot be excluded. + + + --reach-advisories List of advisories to run reachability analysis for, as GHSA or CVE IDs, either as a comma separated value or as mul + + +tiple flags. CVE IDs are converted to GHSA IDs through the GitHub API. Vulnerabilities from other advisories get unknown reachability. Defau + + +lts to all advisories. + + + --reach-analysis-memory-limit The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. + + +512MB, 8GB). The default is 8GB. + + + --reach-analysis-timeout Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10 + + +m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly. + + + --reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level th + + +at ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available. + + + --reach-continue-on-analysis-errors Continue reachability analysis when errors occur (timeouts, OOM, parse errors, etc.), falling back + + +to precomputed reachability results. By default, the CLI halts on analysis errors. + + + --reach-continue-on-install-errors Continue reachability analysis when package installation fails, falling back to precomputed reachabi + + +lity results. By default, the CLI halts on installation errors. + + + --reach-continue-on-missing-lock-files Continue reachability analysis when a Gradle or SBT project is missing its lock file (or version + + + catalog / pre-generated SBOM). By default, the CLI halts. + + + --reach-continue-on-no-source-files Continue reachability analysis when a workspace contains no source files for its ecosystem. By defa + + +ult, the CLI halts. + + + --reach-debug Enable debug mode for reachability analysis. Provides verbose logging from the reachability CLI. + + + --reach-detailed-analysis-log-file A log file with detailed analysis logs is written to root of each analyzed workspace. + + + --reach-disable-analytics Disable reachability analytics sharing with Socket. Also disables caching-based optimizations. + + + --reach-disable-dependency-downloads Only analyze dependencies that are already installed (e.g. in the project's .venv, vendor director + + +y or module cache). Missing dependencies count as installation failures. + + + --reach-disable-external-tool-checks Disable external tool checks during reachability analysis. + + + --reach-ecosystems List of ecosystems to conduct reachability analysis on, as either a comma separated value or as multiple flags. Supp + + +orted: cargo, composer, gem, golang, maven, npm, nuget, pypi. Defaults to all supported ecosystems. + + + --reach-enable-analysis-splitting Allow the reachability analysis to partition CVEs into buckets that are processed in separate analysi + + +s runs. May improve accuracy, but not recommended by default. + + + --reach-retain-facts-file Keep the `.socket.facts.json` reachability report that the analysis writes to the scan directory instead of d + + +eleting it after a successful scan. Delete it once inspected: later reachability scans ignore it, but a scan without --reach uploads it as a + + +n SBOM carrying stale reachability results. + + + --reach-skip-cache Skip caching-based optimizations. By default, the reachability analysis will use cached configurations from previous + + + runs to speed up the analysis. + + + --reach-use-only-pregenerated-sboms When using this option, the scan is created based only on pre-generated CDX and SPDX files in your + + +project. + + + --reach-version Override the version of @coana-tech/cli used for reachability analysis. Default: 15.14.2. + + + + + + Runs the Socket reachability analysis without creating a scan in Socket. + + + The output is written to .socket.facts.json in the current working directory + + + unless the --output flag is specified. + + + + + + Note: Manifest files are uploaded to Socket's backend services because the + + + reachability analysis requires creating a Software Bill of Materials (SBOM) + + + from these files before the analysis can run. + + + + + + Examples + + + $ socket scan reach + + + $ socket scan reach ./proj + + + $ socket scan reach ./proj --reach-ecosystems npm,pypi + + + $ socket scan reach ./monorepo --dynamic-sbom-inference + + + $ socket scan reach --output custom-report.json + + + $ socket scan reach ./proj --output ./reports/analysis.json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-reach.sequence.json b/test/native-snapshots/socket-help-scan-reach.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-reach.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-report.help.svg b/test/native-snapshots/socket-help-scan-report.help.svg new file mode 100644 index 0000000000..1da9c69e02 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-report.help.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan report | org: (not set) | token: (not set) + + + + + + + + + Check whether a scan result passes the organizational policies (security, license) + + + + + + Usage + + + $ socket scan report [options] <SCAN_ID> [OUTPUT_PATH] + + + + + + API Token Requirements + + + - Quota: 2 units + + + - Permissions: full-scans:list and security-policy:read + + + + + + Options + + + --fold Fold reported alerts to some degree (default 'none') + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --license Also report the license policy status. Default: false + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --report-level Which policy level alerts should be reported (default 'warn') + + + --short Report only the healthy status + + + + + + When no output path is given the contents is sent to stdout. + + + + + + By default the result is a nested object that looks like this: + + + `{ + + + [ecosystem]: { + + + [pkgName]: { + + + [version]: { + + + [file]: { + + + [line:col]: alert + + + }}}}` + + + So one alert for each occurrence in every file, version, etc, a huge response. + + + + + + You can --fold these up to given level: 'pkg', 'version', 'file', and 'none'. + + + For example: `socket scan report --fold=version` will dedupe alerts to only + + + show one alert of a particular kind, no matter how often it was found in a + + + file or in how many files it was found. At most one per version that has it. + + + + + + By default only the warn and error policy level alerts are reported. You can + + + override this and request more ('defer' < 'ignore' < 'monitor' < 'warn' < 'error') + + + + + + Short responses look like this: + + + --json: `{healthy:bool}` + + + --markdown: `healthy = bool` + + + neither: `OK/ERR` + + + + + + Examples + + + $ socket scan report 000aaaa1-0000-0a0a-00a0-00a0000000a0 --json --fold=version + + + $ socket scan report 000aaaa1-0000-0a0a-00a0-00a0000000a0 --license --markdown --short + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-report.sequence.json b/test/native-snapshots/socket-help-scan-report.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-report.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-setup.help.svg b/test/native-snapshots/socket-help-scan-setup.help.svg new file mode 100644 index 0000000000..5a2e37f447 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-setup.help.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan setup | org: (not set) | token: (not set) + + + + + + + + + Start interactive configurator to customize default flag values for `socket scan` in this dir + + + + + + Usage + + + $ socket scan setup [options] [CWD=.] + + + + + + Options + + + --default-on-read-error If reading the socket.json fails, just use a default config? Warning: This might override the existing json fil + + +e! + + + + + + Interactive configurator to create a local json file in the target directory + + + that helps to set flag defaults for `socket scan create`. + + + + + + This helps to configure the (Socket reported) repo and branch names, as well + + + as which branch name is the "default branch" (main, master, etc). This way + + + you don't have to specify these flags when creating a scan in this dir. + + + + + + This generated configuration file will only be used locally by the CLI. You + + + can commit it to the repo (useful for collaboration) or choose to add it to + + + your .gitignore all the same. Only this CLI will use it. + + + + + + Examples + + + + + + $ socket scan setup + + + $ socket scan setup ./proj + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-setup.sequence.json b/test/native-snapshots/socket-help-scan-setup.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-setup.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan-view.help.svg b/test/native-snapshots/socket-help-scan-view.help.svg new file mode 100644 index 0000000000..494098c625 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-view.help.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan view | org: (not set) | token: (not set) + + + + + + + + + View the raw results of a scan + + + + + + Usage + + + $ socket scan view [options] <SCAN_ID> [OUTPUT_FILE] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: full-scans:list + + + + + + When no output path is given the contents is sent to stdout. + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --stream Only valid with --json. Streams the response as "ndjson" (chunks of valid json blobs). + + + + + + Examples + + + $ socket scan view 000aaaa1-0000-0a0a-00a0-00a0000000a0 + + + $ socket scan view 000aaaa1-0000-0a0a-00a0-00a0000000a0 ./stream.txt + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan-view.sequence.json b/test/native-snapshots/socket-help-scan-view.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan-view.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-scan.help.svg b/test/native-snapshots/socket-help-scan.help.svg new file mode 100644 index 0000000000..7cd8e640b8 --- /dev/null +++ b/test/native-snapshots/socket-help-scan.help.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan | org: (not set) | token: (not set) + + + + + + Manage Socket scans + + + + + + Usage + + + $ socket scan <command> + + + + + + Commands + + + create Create a new Socket scan and report + + + del Delete a scan + + + diff See what changed between two Scans + + + list List the scans for an organization + + + metadata Get a scan's metadata + + + reach Compute full application reachability + + + report Check whether a scan result passes the organizational policies (security, license) + + + setup Start interactive configurator to customize default flag values for `socket scan` in this dir + + + view View the raw results of a scan + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-scan.sequence.json b/test/native-snapshots/socket-help-scan.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-scan.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-security.help.svg b/test/native-snapshots/socket-help-security.help.svg new file mode 100644 index 0000000000..1879bf8da4 --- /dev/null +++ b/test/native-snapshots/socket-help-security.help.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket security | org: (not set) | token: (not set) + + + + + + + + + Retrieve the security policy of an organization + + + + + + Usage + + + $ socket security [options] + + + + + + API Token Requirements + + + (none) + + + + + + Options + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + + + + Your API token will need the `security-policy:read` permission otherwise + + + the request will fail with an authentication error. + + + + + + Examples + + + $ socket security + + + $ socket security --json + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-security.sequence.json b/test/native-snapshots/socket-help-security.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-security.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-threat-feed.help.svg b/test/native-snapshots/socket-help-threat-feed.help.svg new file mode 100644 index 0000000000..30dba603e8 --- /dev/null +++ b/test/native-snapshots/socket-help-threat-feed.help.svg @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket threat-feed | org: (not set) | token: (not set) + + + + + + + + + [Beta] View the threat-feed + + + + + + Usage + + + $ socket threat-feed [options] [ECOSYSTEM] [TYPE_FILTER] + + + + + + API Token Requirements + + + - Quota: 1 unit + + + - Permissions: threat-feed:list + + + - Special access + + + + + + This feature requires a Threat Feed license. Please contact + + + sales@socket.dev (​mailto:sales@socket.dev​) if you are interested in purchasing this access. + + + + + + Options + + + --direction Order asc or desc by the createdAt attribute + + + --eco Only show threats for a particular ecosystem + + + --filter Filter what type of threats to return + + + --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting th + + +em to cancel/no. + + + --json Output as JSON + + + --markdown Output as Markdown + + + --org Force override the organization slug, overrides the default org from config + + + --page Page token + + + --per-page Number of items per page + + + --pkg Filter by this package name + + + --version Filter by this package version + + + + + + Valid ecosystems: + + + + + + - gem + + + - golang + + + - maven + + + - npm + + + - nuget + + + - pypi + + + + + + Valid type filters: + + + + + + - anom Anomaly + + + - c Do not filter + + + - fp False Positives + + + - joke Joke / Fake + + + - mal Malware and Possible Malware [default] + + + - secret Secrets + + + - spy Telemetry + + + - tp False Positives and Unreviewed + + + - typo Typo-squat + + + - u Unreviewed + + + - vuln Vulnerability + + + + + + Note: if you filter by package name or version, it will do so for anything + + + unless you also filter by that ecosystem and/or package name. When in + + + doubt, look at the threat-feed and see the names in the name/version + + + column. That's what you want to search for. + + + + + + You can put filters as args instead, we'll try to match the strings with the + + + correct filter type but since this would not allow you to search for a package + + + called "mal", you can also specify the filters through flags. + + + + + + First arg that matches a typo, eco, or version enum is used as such. First arg + + + that matches none of them becomes the package name filter. Rest is ignored. + + + + + + Note: The version filter is a prefix search, pkg name is a substring search. + + + + + + Examples + + + $ socket threat-feed + + + $ socket threat-feed maven --json + + + $ socket threat-feed typo + + + $ socket threat-feed npm joke 1.0.0 --per-page=5 --page=2 --direction=asc + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-threat-feed.sequence.json b/test/native-snapshots/socket-help-threat-feed.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-threat-feed.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-uninstall-completion.help.svg b/test/native-snapshots/socket-help-uninstall-completion.help.svg new file mode 100644 index 0000000000..a271abe034 --- /dev/null +++ b/test/native-snapshots/socket-help-uninstall-completion.help.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket uninstall completion | org: (not set) | token: (not set) + + + + + + + + + Uninstall bash completion for Socket CLI + + + + + + Usage + + + $ socket uninstall completion [options] [COMMAND_NAME=socket] + + + + + + Uninstalls bash tab completion for the Socket CLI. This will: + + + 1. Remove tab completion from your current shell for given command + + + 2. Remove the setup for given command from your ~/.bashrc + + + + + + The optional name is required if you installed tab completion for an alias + + + other than the default "socket". This will NOT remove the command, only the + + + tab completion that is registered for it in bash. + + + + + + Options + + + (none) + + + + + + Examples + + + + + + $ socket uninstall completion + + + $ socket uninstall completion sd + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-uninstall-completion.sequence.json b/test/native-snapshots/socket-help-uninstall-completion.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-uninstall-completion.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-uninstall.help.svg b/test/native-snapshots/socket-help-uninstall.help.svg new file mode 100644 index 0000000000..9555207aaf --- /dev/null +++ b/test/native-snapshots/socket-help-uninstall.help.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket uninstall | org: (not set) | token: (not set) + + + + + + Uninstall Socket CLI tab completion + + + + + + Usage + + + $ socket uninstall <command> + + + + + + Commands + + + completion Uninstall bash completion for Socket CLI + + + + + + Options + + + + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-uninstall.sequence.json b/test/native-snapshots/socket-help-uninstall.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-uninstall.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-wrapper.help.svg b/test/native-snapshots/socket-help-wrapper.help.svg new file mode 100644 index 0000000000..87104386d6 --- /dev/null +++ b/test/native-snapshots/socket-help-wrapper.help.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket wrapper | org: (not set) | token: (not set) + + + + + + + + + Enable or disable the Socket npm/npx wrapper + + + + + + Usage + + + $ socket wrapper <"on" | "off"> + + + + + + Options + + + (none) + + + + + + While enabled, the wrapper makes it so that when you call npm/npx on your + + + machine, it will automatically actually run `socket npm` / `socket npx` + + + instead. + + + + + + Examples + + + $ socket wrapper on + + + $ socket wrapper off + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-wrapper.sequence.json b/test/native-snapshots/socket-help-wrapper.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-wrapper.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help-yarn.help.svg b/test/native-snapshots/socket-help-yarn.help.svg new file mode 100644 index 0000000000..e51db64bac --- /dev/null +++ b/test/native-snapshots/socket-help-yarn.help.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket yarn | org: (not set) | token: (not set) + + + + + + + + + Wraps yarn with Socket security scanning + + + + + + Usage + + + $ socket yarn ... + + + + + + API Token Requirements + + + (none) + + + + + + Note: Everything after "yarn" is passed to the yarn command. + + + Only the `--dry-run` and `--help` flags are caught here. + + + + + + Use `socket wrapper on` to alias this command as `yarn`. + + + + + + Examples + + + $ socket yarn + + + $ socket yarn install + + + $ socket yarn add package-name + + + $ socket yarn dlx package-name + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help-yarn.sequence.json b/test/native-snapshots/socket-help-yarn.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help-yarn.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-help.help.svg b/test/native-snapshots/socket-help.help.svg new file mode 100644 index 0000000000..e6fdc1a340 --- /dev/null +++ b/test/native-snapshots/socket-help.help.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket | org: (not set) | token: (not set) + + + + + + CLI for Socket.dev + + + + + + Usage + + + $ socket <command> + + + $ socket scan create --json + + + $ socket package score npm lodash --markdown + + + + + + Note: All commands have their own --help + + + + + + Main commands + + + socket login Setup Socket CLI with an API token and defaults + + + socket scan create Create a new Socket scan and report + + + socket npm/lodash@4.17.21 Request the Socket score of a package + + + socket fix Fix CVEs in dependencies + + + socket optimize Optimize dependencies with @socketregistry overrides + + + socket cdxgen Run cdxgen for SBOM generation + + + socket ci Alias for `socket scan create --report` (creates report and exits with error if unhealthy) + + + + + + Socket API + + + analytics Look up analytics data + + + audit-log Look up the audit log for an organization + + + organization Manage Socket organization account details + + + package Look up published package details + + + repository Manage registered repositories + + + scan Manage Socket scans + + + threat-feed [Beta] View the threat-feed + + + + + + Local tools + + + manifest Generate a dependency manifest for certain ecosystems + + + npm Wraps npm with Socket security scanning + + + npx Wraps npx with Socket security scanning + + + patch Apply, manage, and rollback Socket security patches for vulnerable dependencies + + + raw-npm Run npm without the Socket wrapper + + + raw-npx Run npx without the Socket wrapper + + + + + + CLI configuration + + + config Manage Socket CLI configuration + + + install Install Socket CLI tab completion + + + login Socket API login and CLI setup + + + logout Socket API logout + + + uninstall Uninstall Socket CLI tab completion + + + wrapper Enable or disable the Socket npm/npx wrapper + + + + + + Options + + + Note: All commands have these flags even when not displayed in their help + + + + + + --compact-header Use compact single-line header format (auto-enabled in CI) + + + --config Override the local config with this JSON + + + --dry-run Run without uploading + + + --help Show help + + + --help-full Show full help including environment variables + + + --no-banner Hide the Socket banner + + + --no-spinner Hide the console spinner + + + --version Print the app version + + + + + + Environment variables [more...] + + + Use +--help-full + to view all environment variables + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-help.sequence.json b/test/native-snapshots/socket-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-manifest-dry-run.dry-run.svg b/test/native-snapshots/socket-manifest-dry-run.dry-run.svg new file mode 100644 index 0000000000..42608340b2 --- /dev/null +++ b/test/native-snapshots/socket-manifest-dry-run.dry-run.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket manifest auto | org: (not set) | token: en*** (--config flag + + +) + + + + + +[DryRun]: Bailing now + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-manifest-dry-run.sequence.json b/test/native-snapshots/socket-manifest-dry-run.sequence.json new file mode 100644 index 0000000000..445488a1f2 --- /dev/null +++ b/test/native-snapshots/socket-manifest-dry-run.sequence.json @@ -0,0 +1 @@ +["dry-run"] diff --git a/test/native-snapshots/socket-patch-apply-help.help.svg b/test/native-snapshots/socket-patch-apply-help.help.svg new file mode 100644 index 0000000000..c133c00113 --- /dev/null +++ b/test/native-snapshots/socket-patch-apply-help.help.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + +Apply security patches to dependencies + + + + + +Usage: + +socket-patch apply + [OPTIONS] + + + + + +Options: + + + + +--cwd + <CWD> Working directory [default: .] + + + +-d +, +--dry-run + Verify patches can be applied without modifying files + + + +-s +, +--silent + Only output errors + + + +-m +, +--manifest-path + <MANIFEST_PATH> Path to patch manifest file [default: .socket/manifest.json] + + + +--offline + Do not download missing blobs, fail if any are missing + + + +-g +, +--global + Apply patches to globally installed npm packages + + + +--global-prefix + <GLOBAL_PREFIX> Custom path to global node_modules + + + +--ecosystems + <ECOSYSTEMS> Restrict patching to specific ecosystems + + + +-f +, +--force + Skip pre-application hash verification (apply even if package version differs) + + + +--json + Output results as JSON + + + +-v +, +--verbose + Show detailed per-file verification information + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-patch-apply-help.sequence.json b/test/native-snapshots/socket-patch-apply-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-patch-apply-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-patch-get-help.help.svg b/test/native-snapshots/socket-patch-get-help.help.svg new file mode 100644 index 0000000000..e98f101b12 --- /dev/null +++ b/test/native-snapshots/socket-patch-get-help.help.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + +Get security patches from Socket API and apply them + + + + + +Usage: + +socket-patch get + [OPTIONS] <IDENTIFIER> + + + + + +Arguments: + + + + <IDENTIFIER> Patch identifier (UUID, CVE ID, GHSA ID, PURL, or package name) + + + + + +Options: + + + + +--org + <ORG> Organization slug + + + +--cwd + <CWD> Working directory [default: .] + + + +--id + Force identifier to be treated as a patch UUID + + + +--cve + Force identifier to be treated as a CVE ID + + + +--ghsa + Force identifier to be treated as a GHSA ID + + + +-p +, +--package + Force identifier to be treated as a package name + + + +-y +, +--yes + Skip confirmation prompt for multiple patches + + + +--api-url + <API_URL> Socket API URL (overrides SOCKET_API_URL env var) + + + +--api-token + <API_TOKEN> Socket API token (overrides SOCKET_API_TOKEN env var) + + + +--save-only + Download patch without applying it + + + +-g +, +--global + Apply patch to globally installed npm packages + + + +--global-prefix + <GLOBAL_PREFIX> Custom path to global node_modules + + + +--one-off + Apply patch immediately without saving to .socket folder + + + +--json + Output results as JSON + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-patch-get-help.sequence.json b/test/native-snapshots/socket-patch-get-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-patch-get-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-patch-list-help.help.svg b/test/native-snapshots/socket-patch-list-help.help.svg new file mode 100644 index 0000000000..1acced7153 --- /dev/null +++ b/test/native-snapshots/socket-patch-list-help.help.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + +List all patches in the local manifest + + + + + +Usage: + +socket-patch list + [OPTIONS] + + + + + +Options: + + + + +--cwd + <CWD> Working directory [default: .] + + + +-m +, +--manifest-path + <MANIFEST_PATH> Path to patch manifest file [default: .socket/manifest.json] + + + +--json + Output as JSON + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-patch-list-help.sequence.json b/test/native-snapshots/socket-patch-list-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-patch-list-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-patch-remove-help.help.svg b/test/native-snapshots/socket-patch-remove-help.help.svg new file mode 100644 index 0000000000..36ad4c77a3 --- /dev/null +++ b/test/native-snapshots/socket-patch-remove-help.help.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + +Remove a patch from the manifest by PURL or UUID (rolls back files first) + + + + + +Usage: + +socket-patch remove + [OPTIONS] <IDENTIFIER> + + + + + +Arguments: + + + + <IDENTIFIER> Package PURL or patch UUID + + + + + +Options: + + + + +--cwd + <CWD> Working directory [default: .] + + + +-m +, +--manifest-path + <MANIFEST_PATH> Path to patch manifest file [default: .socket/manifest.json] + + + +--skip-rollback + Skip rolling back files before removing (only update manifest) + + + +-y +, +--yes + Skip confirmation prompts + + + +-g +, +--global + Remove patches from globally installed npm packages + + + +--global-prefix + <GLOBAL_PREFIX> Custom path to global node_modules + + + +--json + Output results as JSON + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-patch-remove-help.sequence.json b/test/native-snapshots/socket-patch-remove-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-patch-remove-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-patch-repair-help.help.svg b/test/native-snapshots/socket-patch-repair-help.help.svg new file mode 100644 index 0000000000..dc5ed1571e --- /dev/null +++ b/test/native-snapshots/socket-patch-repair-help.help.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + +Download missing blobs and clean up unused blobs + + + + + +Usage: + +socket-patch repair + [OPTIONS] + + + + + +Options: + + + + +--cwd + <CWD> Working directory [default: .] + + + +-m +, +--manifest-path + <MANIFEST_PATH> Path to patch manifest file [default: .socket/manifest.json] + + + +-d +, +--dry-run + Show what would be done without actually doing it + + + +--offline + Skip network operations (cleanup only) + + + +--download-only + Only download missing blobs, do not clean up + + + +--json + Output results as JSON + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-patch-repair-help.sequence.json b/test/native-snapshots/socket-patch-repair-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-patch-repair-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-patch-rollback-help.help.svg b/test/native-snapshots/socket-patch-rollback-help.help.svg new file mode 100644 index 0000000000..5e2d1b2520 --- /dev/null +++ b/test/native-snapshots/socket-patch-rollback-help.help.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + +Rollback patches to restore original files + + + + + +Usage: + +socket-patch rollback + [OPTIONS] [IDENTIFIER] + + + + + +Arguments: + + + + [IDENTIFIER] Package PURL or patch UUID to rollback. Omit to rollback all patches + + + + + +Options: + + + + +--cwd + <CWD> Working directory [default: .] + + + +-d +, +--dry-run + Verify rollback can be performed without modifying files + + + +-s +, +--silent + Only output errors + + + +-m +, +--manifest-path + <MANIFEST_PATH> Path to patch manifest file [default: .socket/manifest.json] + + + +--offline + Do not download missing blobs, fail if any are missing + + + +-g +, +--global + Rollback patches from globally installed npm packages + + + +--global-prefix + <GLOBAL_PREFIX> Custom path to global node_modules + + + +--one-off + Rollback a patch by fetching beforeHash blobs from API (no manifest required) + + + +--org + <ORG> Organization slug + + + +--api-url + <API_URL> Socket API URL (overrides SOCKET_API_URL env var) + + + +--api-token + <API_TOKEN> Socket API token (overrides SOCKET_API_TOKEN env var) + + + +--ecosystems + <ECOSYSTEMS> Restrict rollback to specific ecosystems + + + +--json + Output results as JSON + + + +-v +, +--verbose + Show detailed per-file verification information + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-patch-rollback-help.sequence.json b/test/native-snapshots/socket-patch-rollback-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-patch-rollback-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-patch-scan-help.help.svg b/test/native-snapshots/socket-patch-scan-help.help.svg new file mode 100644 index 0000000000..3c65da5f0b --- /dev/null +++ b/test/native-snapshots/socket-patch-scan-help.help.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + +Scan installed packages for available security patches + + + + + +Usage: + +socket-patch scan + [OPTIONS] + + + + + +Options: + + + + +--cwd + <CWD> Working directory [default: .] + + + +--org + <ORG> Organization slug + + + +--json + Output results as JSON + + + +-y +, +--yes + Skip confirmation prompts + + + +-g +, +--global + Scan globally installed npm packages + + + +--global-prefix + <GLOBAL_PREFIX> Custom path to global node_modules + + + +--batch-size + <BATCH_SIZE> Number of packages to query per API request [default: 100] + + + +--api-url + <API_URL> Socket API URL (overrides SOCKET_API_URL env var) + + + +--api-token + <API_TOKEN> Socket API token (overrides SOCKET_API_TOKEN env var) + + + +--ecosystems + <ECOSYSTEMS> Restrict scanning to specific ecosystems (comma-separated: npm,pypi,cargo,maven) + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-patch-scan-help.sequence.json b/test/native-snapshots/socket-patch-scan-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-patch-scan-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-patch-setup-help.help.svg b/test/native-snapshots/socket-patch-setup-help.help.svg new file mode 100644 index 0000000000..1e37b13fad --- /dev/null +++ b/test/native-snapshots/socket-patch-setup-help.help.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + +Configure package.json postinstall scripts to apply patches + + + + + +Usage: + +socket-patch setup + [OPTIONS] + + + + + +Options: + + + + +--cwd + <CWD> Working directory [default: .] + + + +-d +, +--dry-run + Preview changes without modifying files + + + +-y +, +--yes + Skip confirmation prompt + + + +--json + Output results as JSON + + + +-h +, +--help + Print help + + + +-V +, +--version + Print version + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-patch-setup-help.sequence.json b/test/native-snapshots/socket-patch-setup-help.sequence.json new file mode 100644 index 0000000000..8cc77578a7 --- /dev/null +++ b/test/native-snapshots/socket-patch-setup-help.sequence.json @@ -0,0 +1 @@ +["help"] diff --git a/test/native-snapshots/socket-scan-setup.canceled.svg b/test/native-snapshots/socket-scan-setup.canceled.svg new file mode 100644 index 0000000000..94233fa3fd --- /dev/null +++ b/test/native-snapshots/socket-scan-setup.canceled.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan setup | org: (not set) | token: (not set) + + + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + + + +Note: This tool will set up flag and argument defaults for certain + + + CLI commands. You can still override them by explicitly + + + setting the flag. It is meant to be a convenience tool. + + + + + +This command will generate a `socket.json` file in the target cwd. + + +You can choose to add this file to your repo (handy for collab) + + +or to add it to the ignored files, or neither. This file is only + + +used in CLI workflows. + + + + + +Note: For details on a flag you can run `socket <cmd> --help` + + + + + +✔ + +Which scan command do you want to configure? + +(cancel) + + + + + + +ℹ + User canceled + + + + + + + + +Finished + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-scan-setup.github-selected.svg b/test/native-snapshots/socket-scan-setup.github-selected.svg new file mode 100644 index 0000000000..c69751e1b7 --- /dev/null +++ b/test/native-snapshots/socket-scan-setup.github-selected.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan setup | org: (not set) | token: (not set) + + + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + + + +Note: This tool will set up flag and argument defaults for certain + + + CLI commands. You can still override them by explicitly + + + setting the flag. It is meant to be a convenience tool. + + + + + +This command will generate a `socket.json` file in the target cwd. + + +You can choose to add this file to your repo (handy for collab) + + +or to add it to the ignored files, or neither. This file is only + + +used in CLI workflows. + + + + + +Note: For details on a flag you can run `socket <cmd> --help` + + + + + +? + +Which scan command do you want to configure? + + + + socket scan create + + +❯ socket scan github + + + + (cancel) + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-scan-setup.menu.svg b/test/native-snapshots/socket-scan-setup.menu.svg new file mode 100644 index 0000000000..562500b341 --- /dev/null +++ b/test/native-snapshots/socket-scan-setup.menu.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +CLI: v2.1.0-pre-release.0 | cmd: socket scan setup | org: (not set) | token: (not set) + + + + + +ℹ + No socket.json found at <NATIVE_FIXTURE>, will generate a new one + + + + + +Note: This tool will set up flag and argument defaults for certain + + + CLI commands. You can still override them by explicitly + + + setting the flag. It is meant to be a convenience tool. + + + + + +This command will generate a `socket.json` file in the target cwd. + + +You can choose to add this file to your repo (handy for collab) + + +or to add it to the ignored files, or neither. This file is only + + +used in CLI workflows. + + + + + +Note: For details on a flag you can run `socket <cmd> --help` + + + + + +? + +Which scan command do you want to configure? + +(Use arrow keys) + + + +❯ socket scan create + + + + socket scan github + + + (cancel) + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/native-snapshots/socket-scan-setup.sequence.json b/test/native-snapshots/socket-scan-setup.sequence.json new file mode 100644 index 0000000000..7084b172a2 --- /dev/null +++ b/test/native-snapshots/socket-scan-setup.sequence.json @@ -0,0 +1 @@ +["menu","github-selected","canceled"] diff --git a/test/native-snapshots/socket-version.sequence.json b/test/native-snapshots/socket-version.sequence.json new file mode 100644 index 0000000000..353b63fd1c --- /dev/null +++ b/test/native-snapshots/socket-version.sequence.json @@ -0,0 +1 @@ +["version"] diff --git a/test/native-snapshots/socket-version.version.svg b/test/native-snapshots/socket-version.version.svg new file mode 100644 index 0000000000..1f873f2ddc --- /dev/null +++ b/test/native-snapshots/socket-version.version.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + +2.1.0-pre-release.0 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/package-environment.npm-version.test.mts b/test/package-environment.npm-version.test.mts index 825f44f2c8..7c019ee3d0 100644 --- a/test/package-environment.npm-version.test.mts +++ b/test/package-environment.npm-version.test.mts @@ -4,16 +4,17 @@ const spawnMock = vi.fn(async () => ({ stdout: '11.6.0' })) const resolveBinPathSyncMock = vi.fn(() => '/fake/npm-cli.js') const whichBinMock = vi.fn(async () => 'npm') -vi.mock('@socketsecurity/registry/lib/spawn', () => ({ +vi.mock('../src/utils/spawn.mts', () => ({ spawn: spawnMock, })) -vi.mock('@socketsecurity/registry/lib/bin', () => ({ +vi.mock('../src/utils/bin.mts', () => ({ resolveBinPathSync: resolveBinPathSyncMock, whichBin: whichBinMock, })) -vi.mock('../src/utils/fs.mts', () => ({ +vi.mock('../src/utils/fs.mts', async importOriginal => ({ + ...(await importOriginal()), findUp: vi.fn(async () => undefined), })) diff --git a/test/package-matrix.test.mts b/test/package-matrix.test.mts new file mode 100644 index 0000000000..ac2dde587a --- /dev/null +++ b/test/package-matrix.test.mts @@ -0,0 +1,250 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import os from 'node:os' +import path from 'node:path' +import { gzipSync } from 'node:zlib' + +import { describe, expect, it } from 'vitest' + +import { + createPrivatePnpmEnvironment, + currentPrereleaseVersion, + makeVariants, + readPackagedManifest, + run, + updateRootManifest, + validatePackagedManifest, + validatePrivateVirtualStore, + verifyPrivateVirtualStore, +} from '../scripts/build-package-matrix.mts' + +function tarEntry(name: string, contents: string): Buffer { + const data = Buffer.from(contents) + const header = Buffer.alloc(512) + header.write(name, 0, 100) + header.write('0000644\0', 100, 8) + header.write('0000000\0', 108, 8) + header.write('0000000\0', 116, 8) + header.write(data.length.toString(8).padStart(11, '0') + '\0', 124, 12) + header.write('00000000000\0', 136, 12) + header.fill(32, 148, 156) + header.write('0', 156, 1) + header.write('ustar\0', 257, 6) + header.write('00', 263, 2) + const checksum = header.reduce((sum, byte) => sum + byte, 0) + header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8) + const padded = Buffer.alloc(Math.ceil(data.length / 512) * 512) + data.copy(padded) + return gzipSync(Buffer.concat([header, padded, Buffer.alloc(1024)])) +} + +describe('package matrix helpers', () => { + it('accepts only the current root prerelease version format', () => { + expect(currentPrereleaseVersion('2.1.0-pre-release.17')).toBe( + '2.1.0-pre-release.17', + ) + expect(() => currentPrereleaseVersion('2.1.0')).toThrow( + 'Expected package.json version', + ) + }) + + it('defines the four stable and prerelease package variants', () => { + expect( + makeVariants('2.1.0-pre-release.3').map(({ artifact }) => artifact), + ).toEqual([ + 'socket-1.14.1.tgz', + 'socket-2.1.0-pre-release.3.tgz', + '@socketsecurity-cli-1.14.1.tgz', + '@socketsecurity-cli-with-sentry-1.14.1.tgz', + ]) + }) + + it('changes only the package name and version and reads packed metadata', () => { + const raw = + '{\n "name": "socket",\n "version": "2.1.0-pre-release.0",\n "private": true\n}\n' + expect(JSON.parse(updateRootManifest(raw, '1.14.1'))).toEqual({ + name: 'socket', + version: '1.14.1', + private: true, + }) + const bytes = tarEntry( + 'package/package.json', + '{"name":"socket","version":"1.14.1","bin":{"socket":"bin/cli.js","socket-npm":"bin/npm-cli.js","socket-npx":"bin/npx-cli.js","socket-pnpm":"bin/pnpm-cli.js","socket-yarn":"bin/yarn-cli.js"}}', + ) + const manifest = readPackagedManifest(bytes) + expect(manifest).toMatchObject({ + name: 'socket', + version: '1.14.1', + }) + expect(() => + validatePackagedManifest( + { ...manifest, bin: { socket: 'wrong.js' } }, + makeVariants('2.1.0-pre-release.0')[0]!, + ), + ).toThrow('CLI bin map mismatch') + expect(() => + validatePackagedManifest( + manifest, + makeVariants('2.1.0-pre-release.0')[0]!, + ), + ).not.toThrow() + }) + + it('validates the legacy CLI bin alias on the scoped package', () => { + const variant = makeVariants('2.1.0-pre-release.0')[2]! + const standardBins = { + socket: 'bin/cli.js', + 'socket-npm': 'bin/npm-cli.js', + 'socket-npx': 'bin/npx-cli.js', + 'socket-pnpm': 'bin/pnpm-cli.js', + 'socket-yarn': 'bin/yarn-cli.js', + } + expect(() => + validatePackagedManifest( + { + name: '@socketsecurity/cli', + version: '1.14.1', + bin: { cli: 'bin/cli.js', ...standardBins }, + }, + variant, + ), + ).not.toThrow() + }) + + it('requires pnpm to keep its virtual store inside the private workspace', () => { + const workspace = '/tmp/private-package-workspace' + expect(() => validatePrivateVirtualStore(workspace, '.pnpm')).not.toThrow() + expect(() => + validatePrivateVirtualStore( + workspace, + '../../../../shared/pnpm/store/links', + ), + ).toThrow('pnpm virtual store escaped the private workspace') + }) + + it('keeps a nested pnpm install local despite user-level global-store settings', async () => { + const workspace = await mkdtemp( + path.join(os.tmpdir(), 'matrix-private-pnpm-test-'), + ) + const userConfig = path.join(workspace, 'user.npmrc') + const fixturePackage = path.join(workspace, 'fixture-package') + await mkdir(fixturePackage) + await writeFile( + path.join(fixturePackage, 'package.json'), + JSON.stringify({ name: 'matrix-local-fixture', version: '1.0.0' }), + ) + await writeFile( + path.join(fixturePackage, 'index.js'), + 'module.exports = 1\n', + ) + await writeFile( + path.join(workspace, 'package.json'), + JSON.stringify({ + name: 'matrix-private-pnpm-test', + version: '1.0.0', + packageManager: 'pnpm@10.33.0', + dependencies: { 'matrix-local-fixture': 'file:./fixture-package' }, + }), + ) + await writeFile(path.join(workspace, '.npmrc'), 'audit=false\n') + await writeFile(userConfig, 'enable-global-virtual-store=true\n') + const env = createPrivatePnpmEnvironment(workspace, { + ...process.env, + npm_config_userconfig: userConfig, + NPM_CONFIG_USERCONFIG: userConfig, + }) + try { + run( + workspace, + 'pnpm', + [ + 'install', + '--offline', + '--ignore-scripts', + '--config.enable-global-virtual-store=false', + '--virtual-store-dir=node_modules/.pnpm', + ], + env, + ) + await verifyPrivateVirtualStore(workspace) + run( + workspace, + 'pnpm', + [ + 'install', + '--offline', + '--frozen-lockfile=false', + '--config.confirmModulesPurge=false', + ], + env, + ) + await verifyPrivateVirtualStore(workspace) + } finally { + await rm(workspace, { recursive: true, force: true }) + } + }, 30_000) + + it.skipIf(process.platform !== 'win32')( + 'launches a pnpm.cmd shim through cross-spawn', + async () => { + const directory = await mkdtemp( + path.join(os.tmpdir(), 'matrix-pnpm-shim-test-'), + ) + const marker = path.join(directory, 'launched.txt') + const shim = path.join(directory, 'pnpm.cmd') + await writeFile( + shim, + '@echo off\r\necho launched> "%MATRIX_PNPM_SHIM_MARKER%"\r\n', + ) + const env = Object.fromEntries( + Object.entries(process.env).filter( + ([key]) => key.toLowerCase() !== 'path', + ), + ) as NodeJS.ProcessEnv + env.PATH = `${directory}${path.delimiter}${process.env['PATH'] ?? ''}` + env.MATRIX_PNPM_SHIM_MARKER = marker + try { + run(directory, 'pnpm', ['--version'], env) + await expect(readFile(marker, 'utf8')).resolves.toContain('launched') + } finally { + await rm(directory, { recursive: true, force: true }) + } + }, + 30_000, + ) + + it('stages tracked and intentional untracked source without ignored outputs', () => { + const require = createRequire(import.meta.url) + const { execFileSync } = + require('node:child_process') as typeof import('node:child_process') + const { fileURLToPath } = require('node:url') as typeof import('node:url') + const root = fileURLToPath(new URL('..', import.meta.url)) + const ignored = execFileSync( + 'git', + ['check-ignore', '-v', 'build/sea/check'], + { + cwd: root, + encoding: 'utf8', + }, + ) + expect(ignored).toContain('/build/') + const allowlisted = execFileSync( + 'git', + ['check-ignore', '--no-index', '-v', 'scripts/build-package-matrix.mts'], + { + cwd: root, + encoding: 'utf8', + }, + ) + expect(allowlisted).toContain('!/scripts/build-package-matrix.mts') + const workflowAllowlisted = execFileSync( + 'git', + ['check-ignore', '--no-index', '-v', '.github/workflows/ci.yml'], + { + cwd: root, + encoding: 'utf8', + }, + ) + expect(workflowAllowlisted).toContain('!/.github/workflows/ci.yml') + }) +}) diff --git a/test/unit/utils/native-cli-harness.test.mts b/test/unit/utils/native-cli-harness.test.mts new file mode 100644 index 0000000000..78803a378e --- /dev/null +++ b/test/unit/utils/native-cli-harness.test.mts @@ -0,0 +1,132 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + createNativeTerminalFixture, + nativeFixturePathReplacements, + nativeSpawnEnvironment, + readNativeTestEnvironment, +} from '../../utils/native-cli-harness.mts' + +const temporaryDirectories: string[] = [] + +afterEach(async () => { + vi.unstubAllEnvs() + await Promise.all( + temporaryDirectories + .splice(0) + .map(directory => rm(directory, { recursive: true, force: true })), + ) +}) + +const makeStage = async (): Promise => { + const stage = await mkdtemp( + path.join(process.env['RUNNER_TEMP'] ?? os.tmpdir(), 'sn-'), + ) + temporaryDirectories.push(stage) + return stage +} + +describe('native CLI harness utilities', () => { + it('validates native runner environment lazily', async () => { + expect(() => readNativeTestEnvironment({})).toThrow( + /binary, Node version, and stage/, + ) + vi.stubEnv('SOCKET_CLI_NATIVE_TEST_BINARY', '') + vi.stubEnv('SOCKET_CLI_NATIVE_TEST_NODE_VERSION', '') + vi.stubEnv('SOCKET_CLI_NATIVE_TEST_STAGE', '') + await expect( + import('../../utils/native-cli-harness.mts').then(({ captureFlow }) => + captureFlow({ + flow: 'missing-env', + args: [], + expectedText: '', + exitCode: 0, + }), + ), + ).rejects.toThrow(/binary, Node version, and stage/) + }) + + it('reads runner values and keeps Socket config override precedence explicit', () => { + expect( + readNativeTestEnvironment({ + SOCKET_CLI_NATIVE_TEST_BINARY: '/tmp/socket', + SOCKET_CLI_NATIVE_TEST_NODE_VERSION: '26.13.0', + SOCKET_CLI_NATIVE_TEST_STAGE: '/tmp/stage', + }), + ).toEqual({ + binary: '/tmp/socket', + nodeVersion: '26.13.0', + stage: '/tmp/stage', + }) + expect(nativeSpawnEnvironment(['--help'])).toEqual({ + NO_COLOR: '', + CI: '1', + SOCKET_CLI_CONFIG: '{}', + }) + expect(nativeSpawnEnvironment(['--config', '{}'])).toEqual({ + NO_COLOR: '', + CI: '1', + }) + expect( + nativeSpawnEnvironment(['--help'], { + CI: '0', + SOCKET_CLI_CONFIG: 'explicit', + }), + ).toEqual({ NO_COLOR: '', CI: '0', SOCKET_CLI_CONFIG: 'explicit' }) + }) + + it('normalizes full socket.json paths before shorter fixture prefixes', () => { + const posixPaths = nativeFixturePathReplacements( + '/tmp/stage/f', + '/private/tmp/stage/f', + ) + expect(posixPaths).toContainEqual({ + search: '/tmp/stage/f/socket.json', + replacement: '/socket.json', + }) + expect(posixPaths).toContainEqual({ + search: '/private/tmp/stage/f', + replacement: '', + }) + const windowsPaths = nativeFixturePathReplacements( + 'C:\\stage\\f', + 'C:\\stage\\f', + ) + expect(windowsPaths).toContainEqual({ + search: 'C:\\stage\\f\\socket.json', + replacement: '/socket.json', + }) + }) + + it('creates an isolated 100-column terminal fixture with requested rows', async () => { + const stage = await makeStage() + const env = { + SOCKET_CLI_NATIVE_TEST_BINARY: '/tmp/socket', + SOCKET_CLI_NATIVE_TEST_NODE_VERSION: '26.13.0', + SOCKET_CLI_NATIVE_TEST_STAGE: stage, + } + const fixture = await createNativeTerminalFixture(17, env) + expect(fixture.cwd).toBe(path.join(stage, 'f')) + expect(fixture.terminal.cols).toBe(100) + expect(fixture.terminal.rows).toBe(17) + }) + + it('supports wider captures for native help output', async () => { + const stage = await makeStage() + const fixture = await createNativeTerminalFixture( + 20, + { + SOCKET_CLI_NATIVE_TEST_BINARY: '/tmp/socket', + SOCKET_CLI_NATIVE_TEST_NODE_VERSION: '26.13.0', + SOCKET_CLI_NATIVE_TEST_STAGE: stage, + }, + 140, + ) + expect(fixture.terminal.cols).toBe(140) + expect(fixture.terminal.rows).toBe(20) + }) +}) diff --git a/test/unit/utils/native-command-inventory.test.mts b/test/unit/utils/native-command-inventory.test.mts new file mode 100644 index 0000000000..331b39cd55 --- /dev/null +++ b/test/unit/utils/native-command-inventory.test.mts @@ -0,0 +1,128 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + declarationFor, + discoverNativeCommandPaths, + literal, + parse, +} from '../../utils/native-command-inventory.mts' + +const temporaryDirectories: string[] = [] + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map(directory => rm(directory, { recursive: true, force: true })), + ) +}) + +async function fixtureRoot(): Promise { + const root = await mkdtemp(path.join(os.tmpdir(), 'sn-command-inventory-')) + temporaryDirectories.push(root) + await mkdir(path.join(root, 'commands'), { recursive: true }) + return root +} + +async function writeSource( + root: string, + file: string, + source: string, +): Promise { + await writeFile(path.join(root, file), source) +} + +describe('native command inventory utilities', () => { + it('discovers and sorts root and nested registrations from an isolated source tree', async () => { + const root = await fixtureRoot() + await writeSource( + root, + 'commands.mts', + ` + import { cmdZed } from './commands/zed.mts' + import { cmdAlpha } from './commands/alpha.mts' + export const rootCommands = { zed: cmdZed, alpha: cmdAlpha } + `, + ) + await writeSource( + root, + 'commands/zed.mts', + ` + import { cmdChild } from './child.mts' + export const cmdZed = { subcommands: { child: cmdChild } } + `, + ) + await writeSource(root, 'commands/child.mts', 'export const cmdChild = {}') + await writeSource(root, 'commands/alpha.mts', 'export const cmdAlpha = {}') + + expect(await discoverNativeCommandPaths(root)).toEqual([ + 'alpha', + 'zed', + 'zed child', + ]) + const ast = await parse(path.join(root, 'commands.mts')) + expect(literal({ type: 'Identifier', name: 'cmdZed' })).toBe('cmdZed') + expect(declarationFor(ast, 'rootCommands').type).toBe('ObjectExpression') + }) + + it('fails closed for unresolved exports and unsupported registration references', async () => { + const root = await fixtureRoot() + await writeSource( + root, + 'commands.mts', + ` + import { missing } from './commands/item.mts' + export const rootCommands = { item: missing } + `, + ) + await writeSource(root, 'commands/item.mts', 'export const other = {}') + await expect(discoverNativeCommandPaths(root)).rejects.toThrow( + /Could not resolve registered command missing/, + ) + + await writeSource( + root, + 'commands.mts', + ` + export const rootCommands = { item: notImported } + `, + ) + await expect(discoverNativeCommandPaths(root)).rejects.toThrow( + /Unsupported command registration reference: notImported/, + ) + }) + + it('fails closed when a command module has ambiguous subcommand registrations', async () => { + const root = await fixtureRoot() + await writeSource( + root, + 'commands.mts', + ` + import { cmdItem } from './commands/item.mts' + export const rootCommands = { item: cmdItem } + `, + ) + await writeSource( + root, + 'commands/item.mts', + ` + export const cmdItem = {} + const unrelated = { subcommands: {} } + const alsoUnrelated = { nested: { subcommands: {} } } + `, + ) + await expect(discoverNativeCommandPaths(root)).rejects.toThrow( + /Ambiguous subcommand registrations for item: 2/, + ) + }) + + it('discovers the complete sorted production registration set', async () => { + const paths = await discoverNativeCommandPaths() + expect(paths).toHaveLength(71) + expect(paths).toEqual([...paths].sort((a, b) => a.localeCompare(b))) + }) +}) diff --git a/test/unit/utils/optimize-svg.test.mts b/test/unit/utils/optimize-svg.test.mts new file mode 100644 index 0000000000..254a427dee --- /dev/null +++ b/test/unit/utils/optimize-svg.test.mts @@ -0,0 +1,57 @@ +import { selectRasterizer } from '@termless/core' +import { describe, expect, it } from 'vitest' + +import { optimizeSvg } from '../../utils/optimize-svg.mts' + +const extractStyle = (svg: string) => + svg.match(/ + frame + ` + const optimized = optimizeSvg(source) + expect(extractStyle(optimized)?.trim()).toBe( + extractStyle(source)?.replaceAll("'", ''').trim(), + ) + expect(optimized).toContain('class="f0"') + expect(optimized).toContain('animation: f0 2s steps(1) infinite') + expect(optimized).toContain('font-family: 'Terminal'') + }) +}) diff --git a/test/unit/utils/terminal-animation.test.mts b/test/unit/utils/terminal-animation.test.mts new file mode 100644 index 0000000000..77d3010572 --- /dev/null +++ b/test/unit/utils/terminal-animation.test.mts @@ -0,0 +1,317 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' + +import { createTerminal, selectRasterizer } from '@termless/core' +import { createXtermBackend } from '@termless/xtermjs' +import { describe, expect, it, onTestFinished } from 'vitest' + +import { + assertSamePixels, + captureCheckpoint, + createBlankCell, + createRecording, + createTerminalAnimationSvg, + emptyCell, + expectTerminalAnimation, + findLastOccupiedRow, + isVisiblyOccupied, + renderCaptured, +} from '../../utils/terminal-animation.mts' + +import type { AnimationFrame } from '@termless/core' + +describe('terminal animation recorder', () => { + it('repairs keyframe separators while preserving every frame and duration', () => { + const frames: AnimationFrame[] = [ + { + svg: 'frame-one', + duration: 100, + }, + { + svg: 'frame-two', + duration: 250, + }, + { + svg: 'frame-three', + duration: 400, + }, + ] + const svg = createTerminalAnimationSvg(frames) + for (const text of ['frame-one', 'frame-two', 'frame-three']) { + expect(svg).toContain(text) + } + expect(svg).toContain('f0 0.75s') + expect(svg).toContain('f1 0.75s') + expect(svg).toContain('f2 0.75s') + const css = [...svg.matchAll(/` + }), + ) +} + +export function emptyCell(): Cell { + return { + char: '', + fg: null, + bg: null, + bold: false, + dim: false, + italic: false, + underline: false, + underlineColor: null, + strikethrough: false, + inverse: false, + blink: false, + overline: false, + hidden: false, + wide: false, + continuation: false, + hyperlink: null, + } +} + +export function createRecording( + checkpoints: readonly Checkpoint[], +): TerminalRecording { + if (checkpoints.length === 0) { + throw new Error('Recording needs a checkpoint') + } + const seen = new Set() + const frozen: Checkpoint[] = [] + for (const checkpoint of checkpoints) { + if (!safeName.test(checkpoint.name)) { + throw new Error(`Unsafe checkpoint name: ${checkpoint.name}`) + } + if (seen.has(checkpoint.name)) { + throw new Error(`Duplicate checkpoint: ${checkpoint.name}`) + } + seen.add(checkpoint.name) + if (typeof checkpoint.svg !== 'string') { + throw new Error('Invalid checkpoint data') + } + const captured = checkpoint.captured + if (captured) { + if ( + frozen.some( + item => item.captured && item.captured.cols !== captured.cols, + ) + ) { + throw new Error( + 'Captured checkpoints must have the same terminal width', + ) + } + frozen.push( + Object.freeze({ name: checkpoint.name, svg: checkpoint.svg, captured }), + ) + } else { + frozen.push(Object.freeze({ name: checkpoint.name, svg: checkpoint.svg })) + } + } + const sharedHeight = Math.max( + 0, + ...frozen.map(item => item.captured?.height ?? 0), + ) + const normalized = frozen.map(checkpoint => + checkpoint.captured + ? Object.freeze({ + ...checkpoint, + svg: renderCaptured(checkpoint.captured, sharedHeight), + }) + : checkpoint, + ) + return Object.freeze({ checkpoints: Object.freeze(normalized) }) +} + +export async function expectTerminalAnimation( + recording: TerminalRecording, + options: RecordingOptions, +): Promise { + if (!safeName.test(options.flow)) { + throw new Error(`Unsafe flow name: ${options.flow}`) + } + const names = recording.checkpoints.map(({ name }) => name) + if ( + names.length === 0 || + new Set(names).size !== names.length || + names.some(name => !safeName.test(name)) + ) { + throw new Error( + 'Recording checkpoint names must be safe, unique, and non-empty', + ) + } + const durations = options.durations ?? names.map(() => 900) + if ( + durations.length !== names.length || + durations.some(value => !Number.isFinite(value) || value <= 0) + ) { + throw new Error('Each checkpoint needs a finite positive duration') + } + const flowDir = path.join(options.artifactsDir, options.flow) + await mkdir(flowDir, { recursive: true }) + const checkpoints = recording.checkpoints.map(checkpoint => ({ + ...checkpoint, + svg: optimizeSvg(checkpoint.svg), + })) + const rasterizer = await selectRasterizer('resvg') + const animationFrames: AnimationFrame[] = [] + try { + for (const checkpoint of checkpoints) { + animationFrames.push({ + svg: checkpoint.svg, + duration: durations[names.indexOf(checkpoint.name)]!, + }) + } + await Promise.all( + checkpoints.map(async checkpoint => { + const png = await rasterizer.toPng(checkpoint.svg, 1) + await Promise.all([ + writeFile( + path.join(flowDir, `${checkpoint.name}.svg`), + checkpoint.svg, + ), + writeFile(path.join(flowDir, `${checkpoint.name}.png`), png), + ]) + }), + ) + if (animationFrames.length > 1) { + await writeFile( + path.join(flowDir, 'animation.svg'), + createTerminalAnimationSvg(animationFrames), + ) + } + await writeFile( + path.join(flowDir, 'sequence.json'), + JSON.stringify({ checkpoints: names, durations }, null, 2) + '\n', + ) + } finally { + await rasterizer.dispose?.() + } + if ( + names.length !== options.expectedSequence.length || + names.some((name, index) => name !== options.expectedSequence[index]) + ) { + throw new Error( + `Checkpoint sequence mismatch: expected ${options.expectedSequence.join(', ')}, received ${names.join(', ')}`, + ) + } + await expect(JSON.stringify(names) + '\n').toMatchFileSnapshot( + path.join(options.snapshotsDir, `${options.flow}.sequence.json`), + ) + const isSnapshotUpdate = + process.env['SOCKET_CLI_NATIVE_TEST_UPDATE_SNAPSHOTS'] === 'true' + const baselineRasterizer = !isSnapshotUpdate + ? await selectRasterizer('resvg') + : undefined + try { + await Promise.all( + checkpoints.map(async checkpoint => { + const svgPath = path.join( + options.snapshotsDir, + `${options.flow}.${checkpoint.name}.svg`, + ) + if (baselineRasterizer) { + const expectedSvg = await readFile(svgPath, 'utf8') + try { + await assertSamePixels( + baselineRasterizer, + expectedSvg, + checkpoint.svg, + ) + } catch (error) { + await writeFile( + path.join(flowDir, `${checkpoint.name}.expected.png`), + await baselineRasterizer.toPng(expectedSvg, 1), + ) + throw error + } + } else { + await expect(checkpoint.svg).toMatchFileSnapshot(svgPath) + } + }), + ) + } finally { + await baselineRasterizer?.dispose?.() + } +} diff --git a/test/utils/terminal-output.mts b/test/utils/terminal-output.mts new file mode 100644 index 0000000000..ed034a6cd6 --- /dev/null +++ b/test/utils/terminal-output.mts @@ -0,0 +1,93 @@ +import type { TerminalBackend } from '@termless/core' + +export interface LiteralReplacement { + readonly search: string + readonly replacement: string +} + +/** Replace known fixture paths before terminal layout, including split PTY chunks. */ +export function normalizeBackendPaths( + backend: TerminalBackend, + paths: readonly string[], + replacement = '', +): { flush(): void; matched(search: string): boolean } { + return normalizeBackendOutput( + backend, + paths.map(search => ({ search, replacement })), + ) +} + +/** Normalize exact known strings before the emulator calculates terminal layout. */ +export function normalizeBackendOutput( + backend: TerminalBackend, + literals: readonly LiteralReplacement[], +): { flush(): void; matched(search: string): boolean } { + const replacements = literals + .filter(({ search }) => search.length > 0) + .map( + ({ replacement, search }) => + [Buffer.from(search), Buffer.from(replacement)] as const, + ) + .sort(([a], [b]) => b.length - a.length) + if (!replacements.length) { + return { flush() {}, matched: () => false } + } + + const feed = backend.feed.bind(backend) + const matches = new Set() + let pending = Buffer.alloc(0) + const write = (data: Buffer, flush: boolean) => { + const combined = Buffer.concat([pending, data]) + const output: Buffer[] = [] + let cursor = 0 + while (cursor < combined.length) { + const exact = replacements.find(([needle]) => + combined.subarray(cursor, cursor + needle.length).equals(needle), + ) + if (exact) { + matches.add(exact[0].toString()) + output.push(exact[1]) + cursor += exact[0].length + continue + } + let partialLength = 0 + if (!flush) { + for (const [needle] of replacements) { + const length = Math.min(needle.length - 1, combined.length - cursor) + if ( + length > partialLength && + combined + .subarray(cursor, cursor + length) + .equals(needle.subarray(0, length)) + ) { + partialLength = length + } + } + } + if (partialLength === combined.length - cursor) { + break + } + if (partialLength > 0) { + output.push(combined.subarray(cursor, cursor + partialLength)) + cursor += partialLength + } else { + output.push(combined.subarray(cursor, cursor + 1)) + cursor += 1 + } + } + pending = Buffer.from(combined.subarray(cursor)) + const normalized = Buffer.concat(output) + if (normalized.length > 0) { + feed(normalized) + } + } + backend.feed = data => write(Buffer.from(data), false) + return { + flush() { + write(Buffer.alloc(0), true) + }, + matched(search) { + return matches.has(search) + }, + } +} diff --git a/tsconfig.dts.json b/tsconfig.dts.json index f7122e96d0..41071fdb0f 100644 --- a/tsconfig.dts.json +++ b/tsconfig.dts.json @@ -14,6 +14,6 @@ // @typescript/native-preview currently cannot resolve paths for "include" if // the config is not in the root of the repository. This is why tsconfig.dts.json // is in the repository root with ./tsconfig.json instead of the ./config folder. - "include": ["src/**/*.mts"], + "include": ["src/**/*.mts", "src/**/*.json"], "exclude": ["test", "**/*.test.mts"] } diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json index e1d3d72839..aec4a63459 100644 --- a/tsconfig.scripts.json +++ b/tsconfig.scripts.json @@ -15,6 +15,6 @@ // to two tsc projects — they are separate invocations — and the entry scripts // import from it, so leaving it out is a TS6307 "not listed within the file // list of project" error. - "include": ["scripts/*.mts", "scripts/lib/**/*.mts"], + "include": ["scripts/*.mts", "scripts/lib/**/*.mts", "scripts/test/**/*.mts"], "exclude": ["node_modules/**"] } diff --git a/vitest.config.mts b/vitest.config.mts index fc26f18cf5..6480013120 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -5,6 +5,10 @@ export default defineConfig({ preserveSymlinks: false, }, test: { + // Keep the package unit lane scoped to this CLI. The checkout also contains + // ignored upstream repositories and fleet tooling with independent deps and + // Vitest configs; those are exercised by their own workflows. + include: ['src/**/*.test.mts', 'test/**/*.test.mts'], exclude: [ '**/node_modules/**', '**/dist/**', @@ -14,10 +18,20 @@ export default defineConfig({ '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*', // Exclude E2E tests from regular test runs. '**/*.e2e.test.mts', + // Process-level CLI tests run in the serialized integration lane. + '**/*.integration.test.mts', + // Native process tests require the explicitly built SEA artifact. + '**/*.native.test.mts', + // Termless requires Node 23.6+; these focused UI captures run in the Node 26 lane. + 'test/unit/utils/terminal-animation.test.mts', + 'test/unit/utils/optimize-svg.test.mts', + 'test/unit/utils/native-cli-harness.test.mts', // Exclude test fixtures — directories like test/fixtures/commands/optimize/pnpm{8,9}/ // contain full repo copies populated by `socket optimize` runs; their .test.mts // files would otherwise be globbed and run as if they were the real suite. 'test/fixtures/**', + 'test/fleet/**', + 'upstream/**', ], coverage: { exclude: [ diff --git a/vitest.integration.config.mts b/vitest.integration.config.mts new file mode 100644 index 0000000000..c204188f28 --- /dev/null +++ b/vitest.integration.config.mts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + preserveSymlinks: false, + }, + test: { + // CLI integration tests may exercise npm exec/npx and pnpm dlx. Keep files + // serial so they never mutate the same package-manager cache concurrently. + fileParallelism: false, + include: ['**/*.integration.test.mts'], + exclude: [ + '**/node_modules/**', + '**/dist/**', + '.claude/worktrees/**', + '.pnpm-store/**', + '**/.{idea,git,cache,output,temp}/**', + 'test/fixtures/**', + ], + }, +}) diff --git a/vitest.native.config.mts b/vitest.native.config.mts new file mode 100644 index 0000000000..4297d6553a --- /dev/null +++ b/vitest.native.config.mts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + preserveSymlinks: false, + }, + test: { + // These tests run a native executable and may invoke npm exec/npx or pnpm + // dlx. Serialize files to protect both package managers' shared caches. + fileParallelism: false, + // The integration files already execute complete CLI processes through + // spawnSocketCli, which switches to the SEA binary in this lane. + include: ['**/*.native.test.mts', '**/*.integration.test.mts'], + exclude: [ + '**/node_modules/**', + '**/dist/**', + '.claude/worktrees/**', + '.pnpm-store/**', + '**/.{idea,git,cache,output,temp}/**', + 'test/fixtures/**', + ], + }, +}) diff --git a/vitest.terminal.config.mts b/vitest.terminal.config.mts new file mode 100644 index 0000000000..d10adbb524 --- /dev/null +++ b/vitest.terminal.config.mts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { preserveSymlinks: false }, + test: { + include: [ + 'test/unit/utils/terminal-animation.test.mts', + 'test/unit/utils/terminal-output.test.mts', + 'test/unit/utils/optimize-svg.test.mts', + 'test/unit/utils/native-cli-harness.test.mts', + ], + exclude: ['**/node_modules/**', '**/dist/**', '.claude/worktrees/**'], + }, +})