Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fc1fbf0
feat: add trust downgrade policy helper
isaacsamual994-lang Aug 20, 2026
b634094
test: cover trust downgrade policy
isaacsamual994-lang Aug 20, 2026
9ae5cbe
feat(arborist): verify trust downgrade policy before reify
isaacsamual994-lang Aug 20, 2026
9ef5481
test(arborist): cover registry trust verification
isaacsamual994-lang Aug 20, 2026
e0545f0
refactor(arborist): isolate trust policy verification
isaacsamual994-lang Aug 20, 2026
d0810f4
Update trust-policy.js
isaacsamual994-lang Aug 20, 2026
36e0d0b
feat: add trust policy preflight
isaacsamual994-lang Aug 20, 2026
6bab1c2
feat(config): add trust policy definitions
isaacsamual994-lang Aug 20, 2026
79da16c
feat(config): define trust policy options
isaacsamual994-lang Aug 20, 2026
3e9f692
test(config): cover trust policy options
isaacsamual994-lang Aug 20, 2026
ad4fe2b
feat: enforce trust policy during update
isaacsamual994-lang Aug 20, 2026
b4d016e
feat: enforce trust policy during install
isaacsamual994-lang Aug 20, 2026
45c4b5f
feat: enforce trust policy during ci
isaacsamual994-lang Aug 20, 2026
34f5b65
test: cover trust policy preflight
isaacsamual994-lang Aug 20, 2026
029b878
fix(arborist): harden trust downgrade errors
isaacsamual994-lang Aug 20, 2026
10537d5
test(arborist): cover trust policy integration
isaacsamual994-lang Aug 20, 2026
8ff78b1
test(arborist): harden trust policy verifier
isaacsamual994-lang Aug 20, 2026
8b1dee9
test(arborist): cover trust downgrade policy
isaacsamual994-lang Aug 20, 2026
d9a7117
style: remove editor-added blank line
isaacsamual994-lang Aug 20, 2026
3708cc3
chore: replace editor-corrupted update file
isaacsamual994-lang Aug 20, 2026
8ee38a2
chore: restore update command
isaacsamual994-lang Aug 20, 2026
24ffd02
chore: replace formatted ci command
isaacsamual994-lang Aug 20, 2026
3bfe1e6
chore: restore ci command
isaacsamual994-lang Aug 20, 2026
faa1031
chore: replace formatted install command
isaacsamual994-lang Aug 20, 2026
67316a6
chore: restore install command
isaacsamual994-lang Aug 20, 2026
fffac15
chore: replace formatted trust policy helper
isaacsamual994-lang Aug 20, 2026
7f92d8b
chore: restore trust policy helper
isaacsamual994-lang Aug 20, 2026
f79fb53
chore: replace formatted trust policy test
isaacsamual994-lang Aug 20, 2026
364b23a
chore: restore trust policy test
isaacsamual994-lang Aug 20, 2026
6c123fe
chore: replace formatted config definitions index
isaacsamual994-lang Aug 20, 2026
8154a93
chore: restore config definitions index
isaacsamual994-lang Aug 20, 2026
89c2ac4
chore: replace formatted config definitions test
isaacsamual994-lang Aug 20, 2026
5e83ed7
chore: restore config definitions test
isaacsamual994-lang Aug 20, 2026
759a86c
fix(arborist): scope trust downgrade to major release line
isaacsamual994-lang Aug 22, 2026
03a0143
test(arborist): cover major release-line trust policy
isaacsamual994-lang Aug 22, 2026
13e4062
test(arborist): keep integration downgrade within major
isaacsamual994-lang Aug 22, 2026
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
6 changes: 6 additions & 0 deletions lib/commands/ci.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js')
const trustPolicyPreflight = require('../utils/trust-policy-preflight.js')
const runScript = require('@npmcli/run-script')
const fs = require('node:fs/promises')
const path = require('node:path')
Expand Down Expand Up @@ -31,6 +32,9 @@ class CI extends ArboristWorkspaceCmd {
'allow-scripts',
'strict-allow-scripts',
'dangerously-allow-all-scripts',
'trust-policy',
'trust-policy-exclude',
'trust-policy-ignore-after',
'audit',
'bin-links',
'fund',
Expand Down Expand Up @@ -113,6 +117,8 @@ class CI extends ArboristWorkspaceCmd {
)
}

await trustPolicyPreflight({ arb, options: opts })

if (!dryRun) {
const workspacePaths = await getWorkspaces([], {
path: this.npm.localPrefix,
Expand Down
5 changes: 5 additions & 0 deletions lib/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const checks = require('npm-install-checks')
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js')
const trustPolicyPreflight = require('../utils/trust-policy-preflight.js')
const { patchRelaxOpts } = require('../utils/cli-only-flag.js')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')

Expand Down Expand Up @@ -41,6 +42,9 @@ class Install extends ArboristWorkspaceCmd {
'before',
'min-release-age',
'min-release-age-exclude',
'trust-policy',
'trust-policy-exclude',
'trust-policy-ignore-after',
'bin-links',
'fund',
'dry-run',
Expand Down Expand Up @@ -173,6 +177,7 @@ class Install extends ArboristWorkspaceCmd {

const arb = new Arborist(opts)
await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: opts })
await trustPolicyPreflight({ arb, options: opts })
await arb.reify(opts)

if (runRootLifecycle) {
Expand Down
5 changes: 5 additions & 0 deletions lib/commands/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { log } = require('proc-log')
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js')
const trustPolicyPreflight = require('../utils/trust-policy-preflight.js')
const { patchRelaxOpts } = require('../utils/cli-only-flag.js')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')

Expand All @@ -29,6 +30,9 @@ class Update extends ArboristWorkspaceCmd {
'before',
'min-release-age',
'min-release-age-exclude',
'trust-policy',
'trust-policy-exclude',
'trust-policy-ignore-after',
'bin-links',
'fund',
'dry-run',
Expand Down Expand Up @@ -71,6 +75,7 @@ class Update extends ArboristWorkspaceCmd {

const reifyOpts = { ...opts, update }
await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: reifyOpts })
await trustPolicyPreflight({ arb, options: reifyOpts })
await arb.reify(reifyOpts)
await reifyFinish(this.npm, arb)
}
Expand Down
17 changes: 17 additions & 0 deletions lib/utils/trust-policy-preflight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { verifyTrustPolicy } = require('@npmcli/arborist/lib/trust-policy-verifier.js')

const trustPolicyPreflight = async ({ arb, options }) => {
const effectiveOptions = { ...arb.options, ...options }

if (effectiveOptions.trustPolicy !== 'no-downgrade') {
return
}

if (!arb.idealTree) {
await arb.buildIdealTree(options)
}

await verifyTrustPolicy(arb.idealTree, effectiveOptions)
}

module.exports = trustPolicyPreflight
83 changes: 83 additions & 0 deletions test/lib/utils/trust-policy-preflight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const t = require('tap')

const load = t => {
const calls = []
const preflight = t.mock('../../../lib/utils/trust-policy-preflight.js', {
'@npmcli/arborist/lib/trust-policy-verifier.js': {
verifyTrustPolicy: async (tree, options) => calls.push({ tree, options }),
},
})
return { preflight, calls }
}

t.test('no-op when trust policy is disabled', async t => {
const { preflight, calls } = load(t)
let builds = 0
const arb = { idealTree: null,
buildIdealTree: async () => {
builds++
} }
await preflight({ arb, options: {} })
t.equal(builds, 0)
t.equal(calls.length, 0)
})

t.test('builds and verifies the ideal tree for install-style calls', async t => {
const { preflight, calls } = load(t)
const idealTree = { inventory: new Map() }
let builds = 0
const arb = {
idealTree: null,
buildIdealTree: async options => {
builds++
t.equal(options.trustPolicy, 'no-downgrade')
arb.idealTree = idealTree
},
}
const options = { trustPolicy: 'no-downgrade' }
await preflight({ arb, options })
t.equal(builds, 1)
t.equal(calls.length, 1)
t.equal(calls[0].tree, idealTree)
t.strictSame(calls[0].options, options)
})

t.test('reuses a prebuilt ideal tree for ci-style calls', async t => {
const { preflight, calls } = load(t)
const idealTree = { inventory: new Map() }
let builds = 0
const arb = { idealTree,
buildIdealTree: async () => {
builds++
} }
const options = { trustPolicy: 'no-downgrade', trustPolicyExclude: ['pkg@1'] }
await preflight({ arb, options })
t.equal(builds, 0)
t.equal(calls.length, 1)
t.equal(calls[0].tree, idealTree)
t.strictSame(calls[0].options, options)
})

t.test('uses Arborist constructor options for ci-style calls', async t => {
const { preflight, calls } = load(t)
const idealTree = { inventory: new Map() }
const arb = {
idealTree,
options: {
trustPolicy: 'no-downgrade',
trustPolicyExclude: ['pkg@1'],
registry: 'https://registry.example.test/',
},
}

await preflight({ arb, options: { packageLock: true } })

t.equal(calls.length, 1)
t.equal(calls[0].tree, idealTree)
t.strictSame(calls[0].options, {
trustPolicy: 'no-downgrade',
trustPolicyExclude: ['pkg@1'],
registry: 'https://registry.example.test/',
packageLock: true,
})
})
66 changes: 66 additions & 0 deletions workspaces/arborist/lib/trust-policy-verifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const npa = require('npm-package-arg')
const pacote = require('pacote')
const { callLimit: promiseCallLimit } = require('promise-call-limit')
const { checkTrustDowngrade, isTrustPolicyExcluded } = require('./trust-policy.js')

const registryVersions = tree => {
const packages = new Map()
for (const node of tree.inventory.values()) {
if (node.isProjectRoot || node.isWorkspace || node.isLink || node.inDepBundle || !node.version) {
continue
}

// An edgeless node can still be a registry dependency. Only skip when
// every actual consumer edge proves the package came from file:, git:,
// or remote. If any registry edge reaches the node, verify it.
const incomingEdges = [...node.edgesIn]
if (incomingEdges.length &&
incomingEdges.every(edge => edge.spec && !npa(edge.spec).registry)) {
continue
}
const name = node.packageName || node.name
if (!name) {
continue
}

if (!packages.has(name)) {
packages.set(name, new Set())
}
packages.get(name).add(node.version)
}
return packages
}

const verifyTrustPolicy = async (tree, opts = {}) => {
if (opts.trustPolicy !== 'no-downgrade') {
return
}

const queue = []
for (const [name, versions] of registryVersions(tree)) {
const versionsToCheck = [...versions].filter(version =>
!isTrustPolicyExcluded(opts.trustPolicyExclude, name, version))
if (!versionsToCheck.length) {
continue
}

queue.push(async () => {
const packument = await pacote.packument(name, {
...opts,
fullMetadata: true,
})
for (const version of versionsToCheck) {
checkTrustDowngrade(packument, version, {
exclude: opts.trustPolicyExclude,
ignoreAfter: opts.trustPolicyIgnoreAfter,
})
}
})
}
await promiseCallLimit(queue)
}

module.exports = {
registryVersions,
verifyTrustPolicy,
}
Loading