Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,43 @@ jobs:
with:
setup-script: pnpm run build
main-script: pnpm run test:npm

# Repo-owned: every override declares `engines.node: >=24`, but the jobs
# above run a newer current-line Node — nothing executes the FLOOR, so a
# too-new syntax/API in an override stays green here and only breaks in a
# consumer pinned to the floor. This job side-installs the exact floor
# release (sha256-verified against nodejs.org's published SHASUMS256 before
# extraction) and dynamic-imports every override's node entry under it.
floor-node-smoke:
name: 🧪 NPM Overrides on Floor Node
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Bootstrap checkout
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
TRIGGER_REF: ${{ github.ref }}
run: |
set -euo pipefail
git init -q
git config --local advice.detachedHead false
git remote remove origin 2>/dev/null || true
git remote add origin "${SERVER_URL}/${REPOSITORY}"
FETCH_ARGS=(--no-tags --prune --depth 1 origin "${TRIGGER_REF}")
if [ -n "${GITHUB_TOKEN}" ]; then
AUTH_B64="$(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 | tr -d '\n')"
git -c "http.${SERVER_URL}/.extraheader=AUTHORIZATION: basic ${AUTH_B64}" fetch "${FETCH_ARGS[@]}"
else
git fetch "${FETCH_ARGS[@]}"
fi
git checkout -q --detach FETCH_HEAD
- uses: ./.github/actions/fleet/setup-and-install
with:
node-version: 24
socket-api-token: ${{ secrets.SOCKET_API_TOKEN }}
- uses: ./.github/actions/fleet/run-script
with:
main-script: pnpm run test:npm:floor
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"test": "node scripts/fleet/test.mts",
"test:fuzz": "node scripts/repo/fuzz.mts",
"test:npm": "node scripts/npm/run-vitest-npm.mts",
"test:npm:floor": "node scripts/npm/floor-node-smoke.mts",
"type": "node node_modules/typescript/bin/tsc --noEmit -p .config/fleet/tsconfig.check.json",
"update": "node scripts/fleet/update.mts",
"update:external-tools": "node scripts/repo/update-external-tools.mts",
Expand Down
216 changes: 216 additions & 0 deletions scripts/npm/floor-node-smoke.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
#!/usr/bin/env node
/**
* @file Prove every npm override runs on the FLOOR Node its engines declare.
* All packages/npm overrides ship `engines.node: >=24`, but regular CI runs
* a newer current-line Node — nothing executes the floor, so an override
* that quietly uses newer-than-floor syntax/APIs stays green here and only
* breaks inside a consumer pinned to the floor. This gate side-installs the
* EXACT floor release (pinned version + sha256, verified before extraction —
* a mismatched download hard-fails) and dynamic-imports every override's
* node entry under that binary.
* The floor binary is a side install: it never touches PATH — the harness
* keeps running on the repo's own Node and spawns the floor binary
* explicitly, so the two runtimes can't be confused.
* Usage: node scripts/npm/floor-node-smoke.mts
* Env: FLOOR_NODE_DIR overrides the install dir (default: RUNNER_TEMP,
* falling back to os.tmpdir()).
*/
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'

import { safeDeleteSync } from '@socketsecurity/lib-stable/fs/safe'
import { httpDownload } from '@socketsecurity/lib-stable/http-request/download'
import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'

const logger = getDefaultLogger()

// The floor is the low edge of packages/npm/*'s `engines.node: >=24` range.
// Bump BOTH the version and every sha256 together when the engines floor
// moves — hashes come from https://nodejs.org/dist/v<version>/SHASUMS256.txt.
// floor-node-pin-matches-engines (check --all) enforces the version half of
// that lock-step: it fails when any override's engines floor drifts from
// this pin.
export const FLOOR_NODE_VERSION = '24.0.0'
const FLOOR_NODE_PLATFORMS: Record<string, { asset: string; sha256: string }> =
{
'darwin-arm64': {
asset: `node-v${FLOOR_NODE_VERSION}-darwin-arm64.tar.gz`,
sha256:
'194e2f3dd3ec8c2adcaa713ed40f44c5ca38467880e160974ceac1659be60121',
},
'darwin-x64': {
asset: `node-v${FLOOR_NODE_VERSION}-darwin-x64.tar.gz`,
sha256:
'f716b3ce14a7e37a6cbf97c9de10d444d7da07ef833cd8da81dd944d111e6a4a',
},
'linux-x64': {
asset: `node-v${FLOOR_NODE_VERSION}-linux-x64.tar.xz`,
sha256:
'59b8af617dccd7f9f68cc8451b2aee1e86d6bd5cb92cd51dd6216a31b707efd7',
},
'win-x64': {
asset: `node-v${FLOOR_NODE_VERSION}-win-x64.zip`,
sha256:
'3d0fff80c87bb9a8d7f49f2f27832aa34a1477d137af46f5b14df5498be81304',
},
}

const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../..',
)
const npmPackagesPath = path.join(repoRoot, 'packages/npm')

/**
* Download + verify + extract the floor Node, returning its binary path.
* `httpDownload` streams to a temp sibling, checks the pinned sha256, and
* atomic-renames on success — a tampered or truncated download never
* materializes at the archive path, let alone extracts.
*/
async function ensureFloorNode(): Promise<string> {
const platKey = `${process.platform}-${process.arch}`
const pin = FLOOR_NODE_PLATFORMS[platKey]
if (!pin) {
throw new Error(
`no floor-node pin for ${platKey} — add its asset + sha256 (from ` +
`https://nodejs.org/dist/v${FLOOR_NODE_VERSION}/SHASUMS256.txt) to FLOOR_NODE_PLATFORMS`,
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows floor pin key mismatch

High Severity

The ensureFloorNode function currently fails on Windows. The platKey it constructs (win32-x64) doesn't align with the win-x64 entry in FLOOR_NODE_PLATFORMS, so it can't find the correct Node.js binary to download and install, leading to a script error.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6ee94a7. Configure here.

const baseDir =
process.env['FLOOR_NODE_DIR'] || process.env['RUNNER_TEMP'] || os.tmpdir()
// Strip the archive suffix (.tar.gz / .tar.xz / .zip) to get the top-level
// directory name the tarball extracts to (node-v<ver>-<os>-<arch>).
const extractDir = path.join(
baseDir,
pin.asset.replace(/\.(tar\.(gz|xz)|zip)$/, ''),
)
const binPath =
process.platform === 'win32'
? path.join(extractDir, 'node.exe')
: path.join(extractDir, 'bin/node')
if (existsSync(binPath)) {
return binPath
}
const url = `https://nodejs.org/dist/v${FLOOR_NODE_VERSION}/${pin.asset}`
logger.log(`downloading floor node ${FLOOR_NODE_VERSION} (${pin.asset})`)
mkdirSync(baseDir, { recursive: true })
const archivePath = path.join(baseDir, pin.asset)
await httpDownload(url, archivePath, { sha256: pin.sha256 })
// `tar -xf` auto-detects gz/xz; bsdtar (macOS + Windows runners) also
// extracts zip through the same flag.
const tar = spawnSync('tar', ['-xf', archivePath, '-C', baseDir], {
stdio: 'inherit',
})
safeDeleteSync(archivePath, { force: true })
if (tar.status !== 0 || !existsSync(binPath)) {
safeDeleteSync(extractDir, { force: true, recursive: true })
throw new Error(`floor node extract failed (${pin.asset})`)
}
return binPath
}

/**
* Resolve a package's node-runtime entry from its `exports` map (the
* overrides ship no `main`). Condition preference mirrors Node's own
* resolution for a `node` consumer; `types` never matches by construction.
*/
function resolveNodeEntry(exp: unknown): string | undefined {
if (typeof exp === 'string') {
return exp
}
if (!exp || typeof exp !== 'object') {
return undefined
}
const conditions = exp as Record<string, unknown>
const conditionOrder = ['node', 'import', 'require', 'default']
for (let i = 0, { length } = conditionOrder; i < length; i += 1) {
const key = conditionOrder[i]!
if (key in conditions) {
const resolved = resolveNodeEntry(conditions[key])
if (resolved) {
return resolved
}
}
}
return undefined
}

async function main(): Promise<void> {
const floorNode = await ensureFloorNode()
const version = spawnSync(floorNode, ['--version'], { encoding: 'utf8' })
logger.log(
`floor node ready: ${String(version.stdout).trim()} at ${floorNode}`,
)

const failures: string[] = []
let imported = 0
let skipped = 0
const dirents = readdirSync(npmPackagesPath, { withFileTypes: true })
for (let i = 0, { length } = dirents; i < length; i += 1) {
const dirent = dirents[i]!
if (!dirent.isDirectory()) {
continue
}
const pkgDir = path.join(npmPackagesPath, dirent.name)
const pkgJsonPath = path.join(pkgDir, 'package.json')
if (!existsSync(pkgJsonPath)) {
continue
}
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
const entryRel = pkgJson.exports
? resolveNodeEntry(pkgJson.exports['.'] ?? pkgJson.exports)
: pkgJson.main
if (!entryRel) {
skipped += 1
logger.log(
`skip ${dirent.name}: no node entry (bin-only or asset package)`,
)
continue
}
const entryUrl = pathToFileURL(path.join(pkgDir, entryRel)).href
// Dynamic import loads every entry kind a floor-pinned ESM consumer can
// reach: .cjs, .js/ESM, and .json (which requires the import attribute
// on every Node — `date` and `es-iterator-helpers` ship JSON entries).
const run = spawnSync(
floorNode,
[
'--input-type=module',
'-e',
"const s = process.argv[1]; await (s.endsWith('.json') ? import(s, { with: { type: 'json' } }) : import(s))",
entryUrl,
],
{ encoding: 'utf8' },
)
if (run.status === 0) {
imported += 1
} else {
failures.push(
`${dirent.name}: ${String(run.stderr).trim().split('\n')[0]}`,
)
}
}

logger.log(
`floor-node smoke: ${imported} imported, ${skipped} skipped, ${failures.length} failed ` +
`(node ${FLOOR_NODE_VERSION})`,
)
if (failures.length > 0) {
for (let i = 0, { length } = failures; i < length; i += 1) {
logger.error(`FAIL ${failures[i]}`)
}
process.exitCode = 1
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smoke passes with zero imports

Medium Severity

The gate only fails when failures is non-empty. If every override is skipped or none are imported, it still exits 0, so a broken resolver or empty package scan stays green while proving nothing — the same silent-empty class this PR fixed for the fleet Test job.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6ee94a7. Configure here.

}

// Entrypoint-guarded: floor-node-pin-matches-engines (check --all) imports
// FLOOR_NODE_VERSION from this module, which must not trigger a download.
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
main().catch((e: unknown) => {
logger.error(e)
process.exitCode = 1
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entrypoint guard never runs main

High Severity

The import.meta.url guard in floor-node-smoke.mts prevents main() from executing when invoked with relative paths, on Windows, or via symlinks. This causes the script to exit successfully without running its checks, resulting in a silent false positive for the floor node smoke test.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6ee94a7. Configure here.

123 changes: 123 additions & 0 deletions scripts/repo/check/floor-node-pin-matches-engines.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env node
/**
* @file The floor-node smoke gate tests the Node version the overrides
* actually declare. The gate side-installs FLOOR_NODE_VERSION and imports
* every packages/npm/* entry under it — its whole premise is that the pin
* IS the low edge of every override's `engines.node` range. That
* lock-step used to be a comment ("bump BOTH together"); this law
* enforces the version half: every override must declare `engines.node`,
* and each range's minimum satisfying version must equal the pin. An
* engines bump without a pin bump (or vice versa) would otherwise leave
* the gate green while proving the WRONG floor — the exact silent drift
* it exists to prevent. (The sha256 half of the lock-step is enforced at
* runtime: httpDownload hard-fails on a hash mismatch.)
* Usage: node scripts/repo/check/floor-node-pin-matches-engines.mts.
*/

import { existsSync, readdirSync, readFileSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
// oxlint-disable-next-line socket/prefer-stable-external-semver -- @socketsecurity/lib-stable has no ./external/semver export at the pinned version; semver is a devDependency (scripts/tests only, not bundled).
import semver from 'semver'

import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'

import { FLOOR_NODE_VERSION } from '../../npm/floor-node-smoke.mts'

const logger = getDefaultLogger()

export interface OverrideEngines {
name: string
enginesNode: string | undefined
}

/**
* Classify each override's declared Node floor against the smoke gate's
* pin. Pure: directory reading happens in the caller so this stays
* unit-testable on synthetic fixtures.
*/
export function findFloorDrift(
overrides: OverrideEngines[],
floorVersion: string,
): string[] {
const findings: string[] = []
for (let i = 0, { length } = overrides; i < length; i += 1) {
const { enginesNode, name } = overrides[i]!
if (!enginesNode) {
findings.push(
`${name}: no engines.node — the floor gate's premise (every ` +
'override declares its floor) does not hold for it',
)
continue
}
let min: semver.SemVer | undefined
try {
// semver throws on a malformed range instead of returning null.
min = semver.minVersion(enginesNode, { loose: true }) ?? undefined
} catch {
min = undefined
}
if (!min) {
findings.push(
`${name}: engines.node ${JSON.stringify(enginesNode)} has no ` +
'resolvable minimum version',
)
continue
}
if (min.version !== floorVersion) {
findings.push(
`${name}: engines.node ${JSON.stringify(enginesNode)} floors at ` +
`${min.version}, but the smoke gate proves ${floorVersion}`,
)
}
}
return findings
}

function collectOverrideEngines(npmPackagesPath: string): OverrideEngines[] {
const out: OverrideEngines[] = []
const dirents = readdirSync(npmPackagesPath, { withFileTypes: true })
for (let i = 0, { length } = dirents; i < length; i += 1) {
const dirent = dirents[i]!
if (!dirent.isDirectory()) {
continue
}
const pkgJsonPath = path.join(npmPackagesPath, dirent.name, 'package.json')
if (!existsSync(pkgJsonPath)) {
continue
}
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as {
engines?: { node?: string | undefined } | undefined
}
out.push({ name: dirent.name, enginesNode: pkgJson.engines?.node })
}
return out
}

async function main(): Promise<number> {
const repoRoot = path.resolve(import.meta.dirname, '../../..')
const overrides = collectOverrideEngines(path.join(repoRoot, 'packages/npm'))
const findings = findFloorDrift(overrides, FLOOR_NODE_VERSION)
if (findings.length) {
logger.fail(
[
`${findings.length} override(s) out of lock-step with the floor-node pin (${FLOOR_NODE_VERSION}):`,
...findings.map(f => ` ${f}`),
'Fix: move engines.node and FLOOR_NODE_VERSION together —',
' scripts/npm/floor-node-smoke.mts (bump the pin + every sha256 from',
' https://nodejs.org/dist/v<version>/SHASUMS256.txt), or correct the',
' drifted engines range.',
].join('\n'),
)
process.exitCode = 1
return 1
}
logger.success(
`all ${overrides.length} overrides floor at ${FLOOR_NODE_VERSION} — the smoke gate proves the declared floor`,
)
return 0
}

if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
void main()
}
Loading