npq 3.22.0 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/lib/cli.js +6 -16
- package/lib/helpers/packageManagerArgumentParser.js +39 -0
- package/lib/helpers/reportResults.js +49 -2
- package/lib/helpers/versionAge.js +65 -0
- package/lib/marshalls/author.marshall.js +2 -1
- package/lib/marshalls/baseMarshall.js +12 -0
- package/lib/marshalls/version-maturity.marshall.js +24 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,7 @@ NPQ_PKG_MGR=pnpm npx npq install fastify
|
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
132
|
alias pnpm="NPQ_PKG_MGR=pnpm npq-hero"
|
|
133
|
+
pnpm --filter workspace... add express
|
|
133
134
|
```
|
|
134
135
|
|
|
135
136
|
Note: `npq` by default will offload all commands and their arguments to the `npm` (or other package manager as specified) after it finished its due-diligence checks for the respective packages.
|
|
@@ -149,11 +150,32 @@ Note: `npq` by default will offload all commands and their arguments to the `npm
|
|
|
149
150
|
| expired domains | Normalizes and checks complete maintainer email hosts, then corroborates a simple two-label DNS failure through authoritative RDAP before warning | Multipart and otherwise inconclusive results are reported as not evaluated; see [expired-domain marshall documentation](docs/feature/expired-domains.md)
|
|
150
151
|
| signatures | Will compare the package's signature as it shows on the registry's packument with signing keys published by the selected registry | Reports `not evaluated` if the selected registry does not expose signing keys
|
|
151
152
|
| provenance | Will verify the package's attestations of provenance metadata for the published package, and **error** on [provenance regression](docs/feature/provenance.md) (an older semver had registry provenance metadata but the version you install does not) | Reports `not evaluated` if the selected registry does not expose signing keys or attestations
|
|
152
|
-
| version-maturity | Will show
|
|
153
|
+
| version-maturity | Will show an error if the specific version being installed was published less than 7 days ago | Suggests the highest older semver outside npq's 30-day version-recency window when one is available
|
|
153
154
|
| newBin | Will show a warning if the package version being installed introduces a new command-line binary (via the `bin` field in `package.json`) that was not present in its previous version. | Helps identify potentially unexpected new executables being added to your `node_modules/.bin/` directory.
|
|
154
155
|
| typosquatting | Will show a warning if the package name is similar to a popular package name, which could indicate a potential typosquatting attack. | Helps identify packages that may be trying to trick users into installing them by mimicking popular package names.
|
|
155
156
|
| deprecation | Will show a warning if the package version is deprecated on npm or if its GitHub repository has been archived. | Helps identify packages that are no longer maintained or recommended for use. Set `GITHUB_TOKEN` environment variable for higher GitHub API rate limits.
|
|
156
157
|
|
|
158
|
+
### Older release suggestions
|
|
159
|
+
|
|
160
|
+
When a requested version fails the version-maturity check, npq may show the highest older
|
|
161
|
+
semver published outside its 30-day package-version recency window:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
│
|
|
165
|
+
│ ℹ Suggested older release · @openai/codex@0.121.0 — published 33 days ago
|
|
166
|
+
│ Clears npq's package-version recency windows. Re-run npq to audit this
|
|
167
|
+
│ version; compatibility is not evaluated.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The suggestion:
|
|
171
|
+
|
|
172
|
+
- can cross a major-version boundary for tags such as `latest`;
|
|
173
|
+
- is based on cached package publication metadata and is not automatically installed;
|
|
174
|
+
- is not a statement that the older release is safe or that it passes every npq check;
|
|
175
|
+
- does not evaluate API or runtime compatibility.
|
|
176
|
+
|
|
177
|
+
Run npq again with the suggested version and review its compatibility before installing it.
|
|
178
|
+
|
|
157
179
|
### Disabling Marshalls
|
|
158
180
|
|
|
159
181
|
To disable a marshall altogether, set an environment variable using with the marshall's shortname.
|
package/lib/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { parseArgs } = require('node:util')
|
|
4
4
|
const npa = require('npm-package-arg')
|
|
5
5
|
const pkg = require('../package.json')
|
|
6
|
+
const { parsePackageManagerArguments } = require('./helpers/packageManagerArgumentParser')
|
|
6
7
|
|
|
7
8
|
const INSTALL_SUBCOMMANDS = new Set([
|
|
8
9
|
'install',
|
|
@@ -145,22 +146,11 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
static parseArgsMinimal() {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
type: 'string',
|
|
154
|
-
short: 'i',
|
|
155
|
-
default: 'install'
|
|
156
|
-
},
|
|
157
|
-
registry: { type: 'string' },
|
|
158
|
-
userconfig: { type: 'string' },
|
|
159
|
-
globalconfig: { type: 'string' }
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const { values, positionals } = parseArgs(config)
|
|
149
|
+
const packageManager = process.env.NPQ_PKG_MGR || 'npm'
|
|
150
|
+
const { values, positionals } = parsePackageManagerArguments({
|
|
151
|
+
packageManager,
|
|
152
|
+
args: process.argv.slice(2)
|
|
153
|
+
})
|
|
164
154
|
|
|
165
155
|
const earlyExitNoInstall = true
|
|
166
156
|
const normalizedPackages = this._extractPackagesFromPositionals(positionals, earlyExitNoInstall)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { parseArgs } = require('node:util')
|
|
4
|
+
|
|
5
|
+
const BASE_OPTIONS = {
|
|
6
|
+
install: {
|
|
7
|
+
type: 'string',
|
|
8
|
+
short: 'i',
|
|
9
|
+
default: 'install'
|
|
10
|
+
},
|
|
11
|
+
registry: { type: 'string' },
|
|
12
|
+
userconfig: { type: 'string' },
|
|
13
|
+
globalconfig: { type: 'string' }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parsePackageManagerArguments({ packageManager, args }) {
|
|
17
|
+
const options =
|
|
18
|
+
packageManager === 'pnpm'
|
|
19
|
+
? {
|
|
20
|
+
...BASE_OPTIONS,
|
|
21
|
+
filter: { type: 'string', short: 'F', multiple: true },
|
|
22
|
+
'filter-prod': { type: 'string', multiple: true }
|
|
23
|
+
}
|
|
24
|
+
: BASE_OPTIONS
|
|
25
|
+
|
|
26
|
+
const config = {
|
|
27
|
+
allowPositionals: true,
|
|
28
|
+
strict: false,
|
|
29
|
+
options
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (args !== undefined) {
|
|
33
|
+
config.args = args
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return parseArgs(config)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports.parsePackageManagerArguments = parsePackageManagerArguments
|
|
@@ -160,6 +160,7 @@ function reportResults(marshallResults, { plain = false } = {}) {
|
|
|
160
160
|
|
|
161
161
|
if (isPackageMalicious) {
|
|
162
162
|
countErrors = 1
|
|
163
|
+
result.suggestions = []
|
|
163
164
|
|
|
164
165
|
// Rich formatting version
|
|
165
166
|
const topicRich = styleText(
|
|
@@ -242,6 +243,38 @@ function reportResults(marshallResults, { plain = false } = {}) {
|
|
|
242
243
|
})
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
if (!isPackageMalicious && result.suggestions && result.suggestions.length > 0) {
|
|
247
|
+
result.suggestions.forEach((suggestion) => {
|
|
248
|
+
const iconRich = styleText(['cyan'], 'ℹ')
|
|
249
|
+
const labelRich = styleText(['cyan', 'bold'], 'Suggested older release')
|
|
250
|
+
const headline = `${suggestion.packageSpec} — published ${suggestion.ageDays} days ago`
|
|
251
|
+
const detail =
|
|
252
|
+
"Clears npq's package-version recency windows. Re-run npq to audit this version; compatibility is not evaluated."
|
|
253
|
+
const suggestionIndent = ` ${styleText(['gray'], '│')} `
|
|
254
|
+
const headlineWidth = Math.max(
|
|
255
|
+
20,
|
|
256
|
+
terminalWidth - getDisplayLength(' │ ℹ Suggested older release · ') - 2
|
|
257
|
+
)
|
|
258
|
+
const detailWidth = Math.max(20, terminalWidth - getDisplayLength(' │ ') - 2)
|
|
259
|
+
const wrappedHeadline = wrapTextWithIndent(
|
|
260
|
+
headline,
|
|
261
|
+
headlineWidth,
|
|
262
|
+
suggestionIndent
|
|
263
|
+
)
|
|
264
|
+
const wrappedDetail = wrapTextWithIndent(detail, detailWidth, suggestionIndent)
|
|
265
|
+
|
|
266
|
+
resultsForPrettyPrint += `\n ${prefixRich}`
|
|
267
|
+
resultsForPrettyPrint +=
|
|
268
|
+
`\n ${prefixRich} ${iconRich} ${labelRich} ${separatorRich} ${wrappedHeadline}`
|
|
269
|
+
resultsForPrettyPrint += `\n${suggestionIndent}${wrappedDetail}`
|
|
270
|
+
|
|
271
|
+
resultsForPlainTextPrint +=
|
|
272
|
+
`\n SUGGESTION: ${suggestion.packageSpec} was published ${suggestion.ageDays} days ago ` +
|
|
273
|
+
'and clears the package-version recency windows. Re-run npq to audit it; ' +
|
|
274
|
+
'compatibility is not evaluated.'
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
245
278
|
// Close rich formatting box
|
|
246
279
|
resultsForPrettyPrint += `\n ${styleText(['dim'], '└─')}`
|
|
247
280
|
})
|
|
@@ -297,8 +330,10 @@ function marshallResultsToIssuesPerPackage(results) {
|
|
|
297
330
|
pkg,
|
|
298
331
|
errors: [],
|
|
299
332
|
warnings: [],
|
|
300
|
-
notEvaluated: []
|
|
333
|
+
notEvaluated: [],
|
|
334
|
+
suggestions: []
|
|
301
335
|
}
|
|
336
|
+
const suggestionKeys = new Set()
|
|
302
337
|
for (const marshall of allMarshallResultsList) {
|
|
303
338
|
for (const value of Object.values(marshall)) {
|
|
304
339
|
if (value.errors && value.errors.length > 0) {
|
|
@@ -333,12 +368,24 @@ function marshallResultsToIssuesPerPackage(results) {
|
|
|
333
368
|
}))
|
|
334
369
|
issues.notEvaluated.push(skippedChecks)
|
|
335
370
|
}
|
|
371
|
+
if (value.suggestions && value.suggestions.length > 0) {
|
|
372
|
+
value.suggestions.forEach((entry) => {
|
|
373
|
+
const suggestion = { ...entry }
|
|
374
|
+
delete suggestion.pkg
|
|
375
|
+
const key = `${suggestion.type}:${suggestion.packageSpec}`
|
|
376
|
+
if (!suggestionKeys.has(key)) {
|
|
377
|
+
suggestionKeys.add(key)
|
|
378
|
+
issues.suggestions.push(suggestion)
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
}
|
|
336
382
|
}
|
|
337
383
|
}
|
|
338
384
|
if (
|
|
339
385
|
issues.errors.length > 0 ||
|
|
340
386
|
issues.warnings.length > 0 ||
|
|
341
|
-
issues.notEvaluated.length > 0
|
|
387
|
+
issues.notEvaluated.length > 0 ||
|
|
388
|
+
issues.suggestions.length > 0
|
|
342
389
|
) {
|
|
343
390
|
issuesPerPackage.push(issues)
|
|
344
391
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const semver = require('semver')
|
|
4
|
+
|
|
5
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
6
|
+
const MAX_VERSION_RECENCY_DAYS = 30
|
|
7
|
+
|
|
8
|
+
function getRoundedVersionAgeDays(publishedAt, now = Date.now()) {
|
|
9
|
+
const publishedAtMs = Date.parse(publishedAt)
|
|
10
|
+
const nowMs = now instanceof Date ? now.getTime() : Number(now)
|
|
11
|
+
|
|
12
|
+
if (Number.isNaN(publishedAtMs) || Number.isNaN(nowMs)) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ageMs = nowMs - publishedAtMs
|
|
17
|
+
return ageMs > 0 ? Math.round(ageMs / DAY_MS) : 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findAlternativeVersion({
|
|
21
|
+
packageInfo,
|
|
22
|
+
targetVersion,
|
|
23
|
+
requestedVersion,
|
|
24
|
+
now = Date.now()
|
|
25
|
+
}) {
|
|
26
|
+
if (
|
|
27
|
+
!packageInfo ||
|
|
28
|
+
!packageInfo.versions ||
|
|
29
|
+
!packageInfo.time ||
|
|
30
|
+
!semver.valid(targetVersion)
|
|
31
|
+
) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const targetIsStable = semver.prerelease(targetVersion) === null
|
|
36
|
+
const requestedRange =
|
|
37
|
+
typeof requestedVersion === 'string' && !semver.valid(requestedVersion)
|
|
38
|
+
? semver.validRange(requestedVersion)
|
|
39
|
+
: null
|
|
40
|
+
|
|
41
|
+
const candidates = Object.keys(packageInfo.versions)
|
|
42
|
+
.filter((version) => semver.valid(version) && semver.lt(version, targetVersion))
|
|
43
|
+
.filter((version) => !targetIsStable || semver.prerelease(version) === null)
|
|
44
|
+
.filter((version) => !requestedRange || semver.satisfies(version, requestedRange))
|
|
45
|
+
.map((version) => {
|
|
46
|
+
const publishedAt = packageInfo.time[version]
|
|
47
|
+
const ageDays = getRoundedVersionAgeDays(publishedAt, now)
|
|
48
|
+
return { version, publishedAt, ageDays }
|
|
49
|
+
})
|
|
50
|
+
.filter(
|
|
51
|
+
({ publishedAt, ageDays }) =>
|
|
52
|
+
typeof publishedAt === 'string' &&
|
|
53
|
+
ageDays !== null &&
|
|
54
|
+
ageDays > MAX_VERSION_RECENCY_DAYS
|
|
55
|
+
)
|
|
56
|
+
.sort((left, right) => semver.rcompare(left.version, right.version))
|
|
57
|
+
|
|
58
|
+
return candidates[0] || null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
MAX_VERSION_RECENCY_DAYS,
|
|
63
|
+
findAlternativeVersion,
|
|
64
|
+
getRoundedVersionAgeDays
|
|
65
|
+
}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
const BaseMarshall = require('./baseMarshall')
|
|
36
36
|
const Warning = require('../helpers/warning')
|
|
37
|
+
const { MAX_VERSION_RECENCY_DAYS } = require('../helpers/versionAge')
|
|
37
38
|
const { marshallCategories } = require('./constants')
|
|
38
39
|
|
|
39
40
|
const MARSHALL_NAME = 'author'
|
|
@@ -202,7 +203,7 @@ class Marshall extends BaseMarshall {
|
|
|
202
203
|
)
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
if (dateDiffVersionPublished <=
|
|
206
|
+
if (dateDiffVersionPublished <= MAX_VERSION_RECENCY_DAYS) {
|
|
206
207
|
throw new Warning(
|
|
207
208
|
`This version was published only ${dateDiffVersionPublished} days ago by ${npmUser.name} <${npmUser.email}>`
|
|
208
209
|
)
|
|
@@ -22,6 +22,7 @@ class BaseMarshall {
|
|
|
22
22
|
status: null,
|
|
23
23
|
errors: [],
|
|
24
24
|
warnings: [],
|
|
25
|
+
suggestions: [],
|
|
25
26
|
notEvaluated: [],
|
|
26
27
|
data: {},
|
|
27
28
|
marshall: this.name,
|
|
@@ -60,6 +61,13 @@ class BaseMarshall {
|
|
|
60
61
|
return undefined
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
if (err.suggestion && typeof err.suggestion === 'object') {
|
|
65
|
+
this.setSuggestion({
|
|
66
|
+
pkg: pkg.packageString,
|
|
67
|
+
...err.suggestion
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
this.setMessage(message, Boolean(err instanceof Warning))
|
|
64
72
|
return undefined
|
|
65
73
|
})
|
|
@@ -89,6 +97,10 @@ class BaseMarshall {
|
|
|
89
97
|
})
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
setSuggestion(suggestion) {
|
|
101
|
+
this.ctx.marshalls[this.name].suggestions.push(suggestion)
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
/**
|
|
93
105
|
* Resolves a package version string to an actual semver version
|
|
94
106
|
* Handles dist-tags (latest, beta, etc.) and direct semver versions
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const BaseMarshall = require('./baseMarshall')
|
|
4
|
+
const { findAlternativeVersion } = require('../helpers/versionAge')
|
|
4
5
|
const { marshallCategories } = require('./constants')
|
|
5
6
|
|
|
6
7
|
const MARSHALL_NAME = 'version_maturity'
|
|
@@ -35,7 +36,8 @@ class Marshall extends BaseMarshall {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const versionReleaseDate = data.time[packageVersion]
|
|
38
|
-
const
|
|
39
|
+
const now = Date.now()
|
|
40
|
+
const versionDateDiff = now - new Date(versionReleaseDate)
|
|
39
41
|
const versionDateDiffInDays = Math.round(versionDateDiff / (1000 * 60 * 60 * 24))
|
|
40
42
|
|
|
41
43
|
if (versionDateDiffInDays < VERSION_AGE_THRESHOLD) {
|
|
@@ -50,9 +52,29 @@ class Marshall extends BaseMarshall {
|
|
|
50
52
|
timeAgoText = 'day'
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
const error = new Error(
|
|
54
56
|
`Detected a recently published version: published ${timeAgoNumber} ${timeAgoText} ago. Consider waiting for community review.`
|
|
55
57
|
)
|
|
58
|
+
const alternative = findAlternativeVersion({
|
|
59
|
+
packageInfo: data,
|
|
60
|
+
targetVersion: packageVersion,
|
|
61
|
+
requestedVersion: pkg.packageVersion,
|
|
62
|
+
now
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (alternative) {
|
|
66
|
+
error.suggestion = {
|
|
67
|
+
type: 'alternative-version',
|
|
68
|
+
packageName: pkg.packageName,
|
|
69
|
+
version: alternative.version,
|
|
70
|
+
packageSpec: `${pkg.packageName}@${alternative.version}`,
|
|
71
|
+
publishedAt: alternative.publishedAt,
|
|
72
|
+
ageDays: alternative.ageDays,
|
|
73
|
+
reason: 'version-recency'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw error
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
return pkg
|