npq 3.5.5 → 3.7.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 +4 -0
- package/__tests__/marshalls.newBin.test.js +441 -0
- package/__tests__/marshalls.typosquatting.test.js +9 -7
- package/__tests__/marshalls.version-maturity.test.js +194 -0
- package/lib/marshalls/newbin.marshall.js +88 -0
- package/lib/marshalls/version-maturity.marshall.js +67 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,8 @@ Note: `npq` by default will offload all commands and their arguments to the `npm
|
|
|
97
97
|
| expired domains | Will show a warning if a package has been found with one of its maintainers having an email address that includes an expired domain | Checks a dependency version for a maintainer with an expired domain
|
|
98
98
|
| signatures | Will compare the package's signature as it shows on the registry's pakument with the keys published on the npmjs.com registry
|
|
99
99
|
| provenance | Will verify the package's attestations of provenance metadata for the published package
|
|
100
|
+
| version-maturity | Will show a warning if the specific version being installed was published less than 7 days ago | Helps identify recently published versions that may not have been reviewed by the community yet
|
|
101
|
+
| 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.
|
|
100
102
|
|
|
101
103
|
### Disabling Marshalls
|
|
102
104
|
|
|
@@ -125,6 +127,8 @@ Here are all the available environment variable names for disabling specific mar
|
|
|
125
127
|
| signatures | `MARSHALL_DISABLE_SIGNATURES` | Disable registry signature verification |
|
|
126
128
|
| snyk | `MARSHALL_DISABLE_SNYK` | Disable Snyk vulnerability checks |
|
|
127
129
|
| typosquatting | `MARSHALL_DISABLE_TYPOSQUATTING` | Disable typosquatting detection |
|
|
130
|
+
| version-maturity | `MARSHALL_DISABLE_VERSION_MATURITY` | Disable version maturity checks |
|
|
131
|
+
| newBin | `MARSHALL_DISABLE_NEWBIN` | Disable new binary introduction checks |
|
|
128
132
|
|
|
129
133
|
### Run checks on package without installing it:
|
|
130
134
|
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const NewBinMarshall = require('../lib/marshalls/newbin.marshall')
|
|
4
|
+
const PackageRepoUtils = require('../lib/helpers/packageRepoUtils')
|
|
5
|
+
|
|
6
|
+
jest.mock('../lib/helpers/packageRepoUtils')
|
|
7
|
+
|
|
8
|
+
describe('NewBinMarshall', () => {
|
|
9
|
+
let packageRepoUtilsMock
|
|
10
|
+
let newBinMarshall
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
packageRepoUtilsMock = new PackageRepoUtils()
|
|
14
|
+
newBinMarshall = new NewBinMarshall({ packageRepoUtils: packageRepoUtilsMock })
|
|
15
|
+
|
|
16
|
+
// Mock context and task for the marshall
|
|
17
|
+
newBinMarshall.init(
|
|
18
|
+
{
|
|
19
|
+
marshalls: {
|
|
20
|
+
newBin: {
|
|
21
|
+
status: null,
|
|
22
|
+
errors: [],
|
|
23
|
+
warnings: [],
|
|
24
|
+
data: {}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
pkgs: [] // Not directly used by validate but part of ctx
|
|
28
|
+
},
|
|
29
|
+
{ output: '' } // Mock task object
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
jest.clearAllMocks()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const mockPackageInfo = (packageName, versions) => {
|
|
38
|
+
const fullPackageData = {
|
|
39
|
+
name: packageName,
|
|
40
|
+
versions: {},
|
|
41
|
+
'dist-tags': {}
|
|
42
|
+
}
|
|
43
|
+
let latestTag = null
|
|
44
|
+
Object.keys(versions).forEach((vStr) => {
|
|
45
|
+
fullPackageData.versions[vStr] = {
|
|
46
|
+
name: packageName,
|
|
47
|
+
version: vStr,
|
|
48
|
+
bin: versions[vStr].bin
|
|
49
|
+
// other fields like scripts, dependencies might be needed if other marshalls run
|
|
50
|
+
}
|
|
51
|
+
if (!latestTag || require('semver').gt(vStr, latestTag)) {
|
|
52
|
+
latestTag = vStr
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
if (latestTag) {
|
|
56
|
+
fullPackageData['dist-tags'].latest = latestTag
|
|
57
|
+
}
|
|
58
|
+
return fullPackageData
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('should return correct title', () => {
|
|
62
|
+
const newBinMarshall = new NewBinMarshall({
|
|
63
|
+
packageRepoUtils: null
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const title = newBinMarshall.title()
|
|
67
|
+
expect(title).toBe('Checking for new binaries introduced in package.json')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should pass if no previous version exists', async () => {
|
|
71
|
+
const pkg = {
|
|
72
|
+
packageName: 'test-pkg',
|
|
73
|
+
packageVersion: '1.0.0',
|
|
74
|
+
packageString: 'test-pkg@1.0.0'
|
|
75
|
+
}
|
|
76
|
+
const versions = {
|
|
77
|
+
'1.0.0': { bin: { 'my-cli': 'cli.js' } }
|
|
78
|
+
}
|
|
79
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
80
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
81
|
+
|
|
82
|
+
await expect(newBinMarshall.validate(pkg)).resolves.toBeUndefined()
|
|
83
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(0)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should pass if bin field is the same', async () => {
|
|
87
|
+
const pkg = {
|
|
88
|
+
packageName: 'test-pkg',
|
|
89
|
+
packageVersion: '1.0.1',
|
|
90
|
+
packageString: 'test-pkg@1.0.1'
|
|
91
|
+
}
|
|
92
|
+
const versions = {
|
|
93
|
+
'1.0.0': { bin: { 'my-cli': 'cli.js' } },
|
|
94
|
+
'1.0.1': { bin: { 'my-cli': 'cli.js' } }
|
|
95
|
+
}
|
|
96
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
97
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
98
|
+
|
|
99
|
+
await expect(newBinMarshall.validate(pkg)).resolves.toBeUndefined()
|
|
100
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(0)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should warn if a new binary is introduced (object format)', async () => {
|
|
104
|
+
const pkg = {
|
|
105
|
+
packageName: 'test-pkg',
|
|
106
|
+
packageVersion: '1.0.1',
|
|
107
|
+
packageString: 'test-pkg@1.0.1'
|
|
108
|
+
}
|
|
109
|
+
const versions = {
|
|
110
|
+
'1.0.0': { bin: { 'old-cli': 'old.js' } },
|
|
111
|
+
'1.0.1': { bin: { 'old-cli': 'old.js', 'new-cli': 'new.js' } }
|
|
112
|
+
}
|
|
113
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
114
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
115
|
+
|
|
116
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
117
|
+
'New binaries detected for test-pkg@1.0.1'
|
|
118
|
+
)
|
|
119
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(1)
|
|
120
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
121
|
+
"introduces a new binary 'new-cli'"
|
|
122
|
+
)
|
|
123
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
124
|
+
"compared to version '1.0.0'"
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should warn if a new binary is introduced (string format to object)', async () => {
|
|
129
|
+
const pkg = {
|
|
130
|
+
packageName: 'test-pkg',
|
|
131
|
+
packageVersion: '1.0.1',
|
|
132
|
+
packageString: 'test-pkg@1.0.1'
|
|
133
|
+
}
|
|
134
|
+
// Previous version has string bin, new one has object bin
|
|
135
|
+
const versions = {
|
|
136
|
+
'1.0.0': { bin: 'old.js' }, // This will be normalized to { 'test-pkg': 'old.js' }
|
|
137
|
+
'1.0.1': { bin: { 'test-pkg': 'old.js', 'new-cli': 'new.js' } }
|
|
138
|
+
}
|
|
139
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
140
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
141
|
+
|
|
142
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
143
|
+
'New binaries detected for test-pkg@1.0.1'
|
|
144
|
+
)
|
|
145
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(1)
|
|
146
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
147
|
+
"introduces a new binary 'new-cli'"
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should warn if a new binary is introduced (string format for both, new name)', async () => {
|
|
152
|
+
// This case implies the package name itself changed or the bin refers to a different command name
|
|
153
|
+
// For this marshall, we primarily care about new keys in the (normalized) bin object.
|
|
154
|
+
// If package name changes, it's a different package, not really a "new bin" for the *same* package.
|
|
155
|
+
// However, if `bin` was a string `old-bin-name.js` and becomes `new-bin-name.js`,
|
|
156
|
+
// and assuming the package name (key) remains the same, this is not a *new* binary key.
|
|
157
|
+
// This test will check introduction of a string bin when there was none.
|
|
158
|
+
const pkg = {
|
|
159
|
+
packageName: 'test-pkg',
|
|
160
|
+
packageVersion: '1.0.1',
|
|
161
|
+
packageString: 'test-pkg@1.0.1'
|
|
162
|
+
}
|
|
163
|
+
const versions = {
|
|
164
|
+
'1.0.0': { bin: null }, // No bin before
|
|
165
|
+
'1.0.1': { bin: 'new.js' } // New bin (string)
|
|
166
|
+
}
|
|
167
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
168
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
169
|
+
|
|
170
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
171
|
+
'New binaries detected for test-pkg@1.0.1'
|
|
172
|
+
)
|
|
173
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(1)
|
|
174
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
175
|
+
"introduces a new binary 'test-pkg'"
|
|
176
|
+
)
|
|
177
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain("command: 'new.js'")
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should pass if a binary is removed', async () => {
|
|
181
|
+
const pkg = {
|
|
182
|
+
packageName: 'test-pkg',
|
|
183
|
+
packageVersion: '1.0.1',
|
|
184
|
+
packageString: 'test-pkg@1.0.1'
|
|
185
|
+
}
|
|
186
|
+
const versions = {
|
|
187
|
+
'1.0.0': { bin: { 'old-cli': 'old.js', 'to-remove': 'remove.js' } },
|
|
188
|
+
'1.0.1': { bin: { 'old-cli': 'old.js' } }
|
|
189
|
+
}
|
|
190
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
191
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
192
|
+
|
|
193
|
+
await expect(newBinMarshall.validate(pkg)).resolves.toBeUndefined()
|
|
194
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(0)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should pass if bin changes from string to object but represents the same binary', async () => {
|
|
198
|
+
const pkg = {
|
|
199
|
+
packageName: 'test-pkg',
|
|
200
|
+
packageVersion: '1.0.1',
|
|
201
|
+
packageString: 'test-pkg@1.0.1'
|
|
202
|
+
}
|
|
203
|
+
const versions = {
|
|
204
|
+
'1.0.0': { bin: 'cli.js' }, // Normalized: { 'test-pkg': 'cli.js' }
|
|
205
|
+
'1.0.1': { bin: { 'test-pkg': 'cli.js' } }
|
|
206
|
+
}
|
|
207
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
208
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
209
|
+
|
|
210
|
+
await expect(newBinMarshall.validate(pkg)).resolves.toBeUndefined()
|
|
211
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(0)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should pass if bin changes from object to string but represents the same binary', async () => {
|
|
215
|
+
const pkg = {
|
|
216
|
+
packageName: 'test-pkg',
|
|
217
|
+
packageVersion: '1.0.1',
|
|
218
|
+
packageString: 'test-pkg@1.0.1'
|
|
219
|
+
}
|
|
220
|
+
const versions = {
|
|
221
|
+
'1.0.0': { bin: { 'test-pkg': 'cli.js' } },
|
|
222
|
+
'1.0.1': { bin: 'cli.js' } // Normalized: { 'test-pkg': 'cli.js' }
|
|
223
|
+
}
|
|
224
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
225
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
226
|
+
|
|
227
|
+
await expect(newBinMarshall.validate(pkg)).resolves.toBeUndefined()
|
|
228
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(0)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should handle package version being a dist-tag like "latest"', async () => {
|
|
232
|
+
const pkg = {
|
|
233
|
+
packageName: 'test-pkg',
|
|
234
|
+
packageVersion: 'latest',
|
|
235
|
+
packageString: 'test-pkg@latest'
|
|
236
|
+
}
|
|
237
|
+
const versions = {
|
|
238
|
+
'1.0.0': { bin: { 'my-cli': 'cli.js' } },
|
|
239
|
+
'1.0.1': { bin: { 'my-cli': 'cli.js', 'new-cli': 'new.js' } } // latest points to 1.0.1
|
|
240
|
+
}
|
|
241
|
+
const mockPkgInfo = mockPackageInfo('test-pkg', versions)
|
|
242
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPkgInfo)
|
|
243
|
+
// Mock getSemVer to resolve 'latest' to '1.0.1'
|
|
244
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => {
|
|
245
|
+
if (version === 'latest') return '1.0.1'
|
|
246
|
+
return version
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
250
|
+
'New binaries detected for test-pkg@latest'
|
|
251
|
+
)
|
|
252
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(1)
|
|
253
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
254
|
+
"introduces a new binary 'new-cli'"
|
|
255
|
+
)
|
|
256
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
257
|
+
"compared to version '1.0.0'"
|
|
258
|
+
)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should pass if package info cannot be fetched', async () => {
|
|
262
|
+
const pkg = {
|
|
263
|
+
packageName: 'test-pkg',
|
|
264
|
+
packageVersion: '1.0.0',
|
|
265
|
+
packageString: 'test-pkg@1.0.0'
|
|
266
|
+
}
|
|
267
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(null) // Simulate failed fetch
|
|
268
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
269
|
+
|
|
270
|
+
await expect(newBinMarshall.validate(pkg)).resolves.toBeUndefined()
|
|
271
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(0)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should pass if target version string cannot be resolved', async () => {
|
|
275
|
+
const pkg = {
|
|
276
|
+
packageName: 'test-pkg',
|
|
277
|
+
packageVersion: 'nonexistent-tag',
|
|
278
|
+
packageString: 'test-pkg@nonexistent-tag'
|
|
279
|
+
}
|
|
280
|
+
const versions = { '1.0.0': { bin: 'cli.js' } }
|
|
281
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
282
|
+
packageRepoUtilsMock.getSemVer.mockResolvedValue(null) // Simulate tag not resolving
|
|
283
|
+
|
|
284
|
+
await expect(newBinMarshall.validate(pkg)).resolves.toBeUndefined()
|
|
285
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(0)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should handle multiple new binaries being introduced', async () => {
|
|
289
|
+
const pkg = {
|
|
290
|
+
packageName: 'test-pkg',
|
|
291
|
+
packageVersion: '1.0.1',
|
|
292
|
+
packageString: 'test-pkg@1.0.1'
|
|
293
|
+
}
|
|
294
|
+
const versions = {
|
|
295
|
+
'1.0.0': { bin: { 'old-cli': 'old.js' } },
|
|
296
|
+
'1.0.1': { bin: { 'old-cli': 'old.js', 'new-cli-1': 'new1.js', 'new-cli-2': 'new2.js' } }
|
|
297
|
+
}
|
|
298
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
299
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
300
|
+
|
|
301
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
302
|
+
'New binaries detected for test-pkg@1.0.1'
|
|
303
|
+
)
|
|
304
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(2)
|
|
305
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
306
|
+
"introduces a new binary 'new-cli-1'"
|
|
307
|
+
)
|
|
308
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[1].message).toContain(
|
|
309
|
+
"introduces a new binary 'new-cli-2'"
|
|
310
|
+
)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('should pass if the new version has no bin field but old one did', async () => {
|
|
314
|
+
const pkg = {
|
|
315
|
+
packageName: 'test-pkg',
|
|
316
|
+
packageVersion: '1.0.1',
|
|
317
|
+
packageString: 'test-pkg@1.0.1'
|
|
318
|
+
}
|
|
319
|
+
const versions = {
|
|
320
|
+
'1.0.0': { bin: { 'old-cli': 'old.js' } },
|
|
321
|
+
'1.0.1': { bin: null } // No bin in new version
|
|
322
|
+
}
|
|
323
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPackageInfo('test-pkg', versions))
|
|
324
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
325
|
+
|
|
326
|
+
await expect(newBinMarshall.validate(pkg)).resolves.toBeUndefined()
|
|
327
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(0)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should correctly use package name from version data for string bin normalization', async () => {
|
|
331
|
+
// Scenario: pkg.packageName might be different from the actual name in package.json (e.g. alias)
|
|
332
|
+
// The marshall should use the name from the fetched package.json for normalization.
|
|
333
|
+
const pkg = {
|
|
334
|
+
packageName: 'alias-pkg',
|
|
335
|
+
packageVersion: '1.0.1',
|
|
336
|
+
packageString: 'alias-pkg@1.0.1'
|
|
337
|
+
}
|
|
338
|
+
const actualPackageName = 'actual-pkg-name'
|
|
339
|
+
|
|
340
|
+
// Mock getPackageInfo to return versions with the actual package name
|
|
341
|
+
const versions = {
|
|
342
|
+
'1.0.0': { bin: null }, // No bin in old version
|
|
343
|
+
'1.0.1': { bin: 'cli.js' } // Bin is a string, should be keyed by actualPackageName
|
|
344
|
+
}
|
|
345
|
+
const mockPkgData = mockPackageInfo(actualPackageName, versions) // mockPackageInfo uses the name for versions too
|
|
346
|
+
|
|
347
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPkgData)
|
|
348
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
349
|
+
|
|
350
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
351
|
+
`New binaries detected for ${pkg.packageString}`
|
|
352
|
+
)
|
|
353
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(1)
|
|
354
|
+
// Check that the binary name used in the warning is the actualPackageName
|
|
355
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
356
|
+
`introduces a new binary '${actualPackageName}'`
|
|
357
|
+
)
|
|
358
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(`command: 'cli.js'`)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('should use pkg.packageName when newVersionData.name is falsy (line 40 coverage)', async () => {
|
|
362
|
+
const pkg = {
|
|
363
|
+
packageName: 'fallback-pkg',
|
|
364
|
+
packageVersion: '1.0.1',
|
|
365
|
+
packageString: 'fallback-pkg@1.0.1'
|
|
366
|
+
}
|
|
367
|
+
const versions = {
|
|
368
|
+
'1.0.0': { bin: null },
|
|
369
|
+
'1.0.1': { bin: 'cli.js' } // This version will have name: falsy
|
|
370
|
+
}
|
|
371
|
+
const mockPkgData = mockPackageInfo('fallback-pkg', versions)
|
|
372
|
+
// Set name to falsy to test the fallback in line 40: newVersionData.name || pkg.packageName
|
|
373
|
+
mockPkgData.versions['1.0.1'].name = ''
|
|
374
|
+
|
|
375
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPkgData)
|
|
376
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
377
|
+
|
|
378
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
379
|
+
'New binaries detected for fallback-pkg@1.0.1'
|
|
380
|
+
)
|
|
381
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(1)
|
|
382
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
383
|
+
"introduces a new binary 'fallback-pkg'"
|
|
384
|
+
)
|
|
385
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain("command: 'cli.js'")
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('should use pkg.packageName when previousVersionData.name is falsy (line 56 coverage)', async () => {
|
|
389
|
+
const pkg = {
|
|
390
|
+
packageName: 'fallback-pkg',
|
|
391
|
+
packageVersion: '1.0.1',
|
|
392
|
+
packageString: 'fallback-pkg@1.0.1'
|
|
393
|
+
}
|
|
394
|
+
const versions = {
|
|
395
|
+
'1.0.0': { bin: 'old.js' }, // This version will have name: falsy
|
|
396
|
+
'1.0.1': { bin: { 'fallback-pkg': 'old.js', 'new-cli': 'new.js' } }
|
|
397
|
+
}
|
|
398
|
+
const mockPkgData = mockPackageInfo('fallback-pkg', versions)
|
|
399
|
+
// Set name to falsy to test the fallback in line 56: previousVersionData.name || pkg.packageName
|
|
400
|
+
mockPkgData.versions['1.0.0'].name = undefined
|
|
401
|
+
|
|
402
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPkgData)
|
|
403
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
404
|
+
|
|
405
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
406
|
+
'New binaries detected for fallback-pkg@1.0.1'
|
|
407
|
+
)
|
|
408
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(1)
|
|
409
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
410
|
+
"introduces a new binary 'new-cli'"
|
|
411
|
+
)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('should use pkg.packageName when both version data names are falsy (covers both lines)', async () => {
|
|
415
|
+
const pkg = {
|
|
416
|
+
packageName: 'double-fallback-pkg',
|
|
417
|
+
packageVersion: '1.0.1',
|
|
418
|
+
packageString: 'double-fallback-pkg@1.0.1'
|
|
419
|
+
}
|
|
420
|
+
const versions = {
|
|
421
|
+
'1.0.0': { bin: null },
|
|
422
|
+
'1.0.1': { bin: 'cli.js' }
|
|
423
|
+
}
|
|
424
|
+
const mockPkgData = mockPackageInfo('double-fallback-pkg', versions)
|
|
425
|
+
// Set both names to falsy values to test both fallbacks
|
|
426
|
+
mockPkgData.versions['1.0.0'].name = null
|
|
427
|
+
mockPkgData.versions['1.0.1'].name = ''
|
|
428
|
+
|
|
429
|
+
packageRepoUtilsMock.getPackageInfo.mockResolvedValue(mockPkgData)
|
|
430
|
+
packageRepoUtilsMock.getSemVer.mockImplementation(async (name, version) => version)
|
|
431
|
+
|
|
432
|
+
await expect(newBinMarshall.validate(pkg)).rejects.toThrow(
|
|
433
|
+
'New binaries detected for double-fallback-pkg@1.0.1'
|
|
434
|
+
)
|
|
435
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings).toHaveLength(1)
|
|
436
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain(
|
|
437
|
+
"introduces a new binary 'double-fallback-pkg'"
|
|
438
|
+
)
|
|
439
|
+
expect(newBinMarshall.ctx.marshalls.newBin.warnings[0].message).toContain("command: 'cli.js'")
|
|
440
|
+
})
|
|
441
|
+
})
|
|
@@ -8,7 +8,7 @@ describe('Typosquatting Marshall', () => {
|
|
|
8
8
|
|
|
9
9
|
// Mock the top packages data to include duplicates
|
|
10
10
|
const originalTopPackages = require('../data/top-packages.json')
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
// Create a test package that would match multiple similar packages
|
|
13
13
|
const pkg = {
|
|
14
14
|
packageName: 'ghtml' // This should match 'html' which appears multiple times in the data
|
|
@@ -21,18 +21,20 @@ describe('Typosquatting Marshall', () => {
|
|
|
21
21
|
} catch (error) {
|
|
22
22
|
// Check that the error message doesn't contain duplicate entries
|
|
23
23
|
const errorMessage = error.message
|
|
24
|
-
expect(errorMessage).toContain(
|
|
25
|
-
|
|
24
|
+
expect(errorMessage).toContain(
|
|
25
|
+
'Package name could be a typosquatting attempt for popular package(s):'
|
|
26
|
+
)
|
|
27
|
+
|
|
26
28
|
// Extract the package names from the error message
|
|
27
29
|
const packagesList = errorMessage.split('popular package(s): ')[1]
|
|
28
30
|
const packages = packagesList.split(', ')
|
|
29
|
-
|
|
31
|
+
|
|
30
32
|
// Check that there are no duplicates
|
|
31
33
|
const uniquePackages = [...new Set(packages)]
|
|
32
34
|
expect(packages.length).toBe(uniquePackages.length)
|
|
33
|
-
|
|
35
|
+
|
|
34
36
|
// Verify that 'html' appears only once even though it exists multiple times in the data
|
|
35
|
-
const htmlCount = packages.filter(pkg => pkg === 'html').length
|
|
37
|
+
const htmlCount = packages.filter((pkg) => pkg === 'html').length
|
|
36
38
|
expect(htmlCount).toBe(1)
|
|
37
39
|
}
|
|
38
40
|
})
|
|
@@ -64,4 +66,4 @@ describe('Typosquatting Marshall', () => {
|
|
|
64
66
|
const result = await typosquattingMarshall.validate(pkg)
|
|
65
67
|
expect(result).toEqual([])
|
|
66
68
|
})
|
|
67
|
-
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Marshall = require('../lib/marshalls/version-maturity.marshall')
|
|
4
|
+
|
|
5
|
+
describe('Version Maturity Marshall', () => {
|
|
6
|
+
test('should have the correct title', () => {
|
|
7
|
+
const testMarshall = new Marshall({
|
|
8
|
+
packageRepoUtils: null
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
expect(testMarshall.title()).toEqual('Checking version maturity')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('should throw error for recently published version (same day)', async () => {
|
|
15
|
+
const now = new Date()
|
|
16
|
+
const hoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000) // 2 hours ago
|
|
17
|
+
|
|
18
|
+
const testMarshall = new Marshall({
|
|
19
|
+
packageRepoUtils: {
|
|
20
|
+
getPackageInfo: () => {
|
|
21
|
+
return Promise.resolve({
|
|
22
|
+
time: {
|
|
23
|
+
'1.0.0': hoursAgo.toISOString()
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await expect(
|
|
32
|
+
testMarshall.validate({
|
|
33
|
+
packageName: 'test-package',
|
|
34
|
+
packageVersion: '1.0.0'
|
|
35
|
+
})
|
|
36
|
+
).rejects.toThrow(
|
|
37
|
+
'detected a recently published version (published 2 hours ago) - consider waiting for community review'
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should throw error for recently published version (within threshold)', async () => {
|
|
42
|
+
const now = new Date()
|
|
43
|
+
const daysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) // 3 days ago
|
|
44
|
+
|
|
45
|
+
const testMarshall = new Marshall({
|
|
46
|
+
packageRepoUtils: {
|
|
47
|
+
getPackageInfo: () => {
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
time: {
|
|
50
|
+
'1.0.0': daysAgo.toISOString()
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
testMarshall.validate({
|
|
60
|
+
packageName: 'test-package',
|
|
61
|
+
packageVersion: '1.0.0'
|
|
62
|
+
})
|
|
63
|
+
).rejects.toThrow(
|
|
64
|
+
'detected a recently published version (published 3 days ago) - consider waiting for community review'
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should throw error for version published exactly 1 day ago', async () => {
|
|
69
|
+
const now = new Date()
|
|
70
|
+
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000) // 1 day ago
|
|
71
|
+
|
|
72
|
+
const testMarshall = new Marshall({
|
|
73
|
+
packageRepoUtils: {
|
|
74
|
+
getPackageInfo: () => {
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
time: {
|
|
77
|
+
'1.0.0': oneDayAgo.toISOString()
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await expect(
|
|
86
|
+
testMarshall.validate({
|
|
87
|
+
packageName: 'test-package',
|
|
88
|
+
packageVersion: '1.0.0'
|
|
89
|
+
})
|
|
90
|
+
).rejects.toThrow(
|
|
91
|
+
'detected a recently published version (published 1 day ago) - consider waiting for community review'
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('should pass for version published beyond threshold', async () => {
|
|
96
|
+
const now = new Date()
|
|
97
|
+
const weekAgo = new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000) // 8 days ago
|
|
98
|
+
|
|
99
|
+
const testMarshall = new Marshall({
|
|
100
|
+
packageRepoUtils: {
|
|
101
|
+
getPackageInfo: () => {
|
|
102
|
+
return Promise.resolve({
|
|
103
|
+
time: {
|
|
104
|
+
'1.0.0': weekAgo.toISOString()
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
},
|
|
108
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const result = await testMarshall.validate({
|
|
113
|
+
packageName: 'test-package',
|
|
114
|
+
packageVersion: '1.0.0'
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
packageName: 'test-package',
|
|
119
|
+
packageVersion: '1.0.0'
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('should throw error when package time information is missing', async () => {
|
|
124
|
+
const testMarshall = new Marshall({
|
|
125
|
+
packageRepoUtils: {
|
|
126
|
+
getPackageInfo: () => {
|
|
127
|
+
return Promise.resolve({})
|
|
128
|
+
},
|
|
129
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await expect(
|
|
134
|
+
testMarshall.validate({
|
|
135
|
+
packageName: 'test-package',
|
|
136
|
+
packageVersion: '1.0.0'
|
|
137
|
+
})
|
|
138
|
+
).rejects.toThrow('could not determine package version information')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('should throw error when version release date is missing', async () => {
|
|
142
|
+
const testMarshall = new Marshall({
|
|
143
|
+
packageRepoUtils: {
|
|
144
|
+
getPackageInfo: () => {
|
|
145
|
+
return Promise.resolve({
|
|
146
|
+
time: {
|
|
147
|
+
'2.0.0': new Date().toISOString()
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await expect(
|
|
156
|
+
testMarshall.validate({
|
|
157
|
+
packageName: 'test-package',
|
|
158
|
+
packageVersion: '1.0.0'
|
|
159
|
+
})
|
|
160
|
+
).rejects.toThrow('could not determine release date for version 1.0.0')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('should handle version aliases correctly', async () => {
|
|
164
|
+
const now = new Date()
|
|
165
|
+
const daysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000) // 2 days ago
|
|
166
|
+
|
|
167
|
+
const testMarshall = new Marshall({
|
|
168
|
+
packageRepoUtils: {
|
|
169
|
+
getPackageInfo: () => {
|
|
170
|
+
return Promise.resolve({
|
|
171
|
+
time: {
|
|
172
|
+
'1.2.3': daysAgo.toISOString()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
},
|
|
176
|
+
getSemVer: (packageName, packageVersion) => {
|
|
177
|
+
if (packageVersion === 'latest') {
|
|
178
|
+
return Promise.resolve('1.2.3')
|
|
179
|
+
}
|
|
180
|
+
return Promise.resolve(packageVersion)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await expect(
|
|
186
|
+
testMarshall.validate({
|
|
187
|
+
packageName: 'test-package',
|
|
188
|
+
packageVersion: 'latest'
|
|
189
|
+
})
|
|
190
|
+
).rejects.toThrow(
|
|
191
|
+
'detected a recently published version (published 2 days ago) - consider waiting for community review'
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const semver = require('semver')
|
|
4
|
+
const BaseMarshall = require('./baseMarshall')
|
|
5
|
+
const Warning = require('../helpers/warning')
|
|
6
|
+
const { marshallCategories } = require('./constants')
|
|
7
|
+
|
|
8
|
+
const MARSHALL_NAME = 'newBin' // Or a more descriptive name like 'newBinary'
|
|
9
|
+
|
|
10
|
+
class NewBinMarshall extends BaseMarshall {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
super(options)
|
|
13
|
+
this.name = MARSHALL_NAME
|
|
14
|
+
// Decide on a category, e.g., PackageHealth or a new one if appropriate
|
|
15
|
+
this.categoryId = marshallCategories.PackageHealth.id
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
title() {
|
|
19
|
+
return 'Checking for new binaries introduced in package.json'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async validate(pkg) {
|
|
23
|
+
const fullPackageInfo = await this.packageRepoUtils.getPackageInfo(pkg.packageName)
|
|
24
|
+
if (!fullPackageInfo || !fullPackageInfo.versions) {
|
|
25
|
+
// If we can't get package info or versions, skip
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const targetVersionString = await this.packageRepoUtils.getSemVer(
|
|
30
|
+
pkg.packageName,
|
|
31
|
+
pkg.packageVersion
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if (!targetVersionString || !fullPackageInfo.versions[targetVersionString]) {
|
|
35
|
+
// If target version string can't be resolved or doesn't exist in versions
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const newVersionData = fullPackageInfo.versions[targetVersionString]
|
|
40
|
+
const newBin = this.normalizeBin(newVersionData.bin, newVersionData.name || pkg.packageName)
|
|
41
|
+
|
|
42
|
+
const allVersions = Object.keys(fullPackageInfo.versions)
|
|
43
|
+
const olderVersions = allVersions
|
|
44
|
+
.filter((v) => semver.valid(v) && semver.lt(v, targetVersionString))
|
|
45
|
+
.sort(semver.rcompare) // Sorts descending, so first is latest older
|
|
46
|
+
|
|
47
|
+
if (olderVersions.length === 0) {
|
|
48
|
+
// No previous version to compare against
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const previousVersionString = olderVersions[0]
|
|
53
|
+
const previousVersionData = fullPackageInfo.versions[previousVersionString]
|
|
54
|
+
const oldBin = this.normalizeBin(
|
|
55
|
+
previousVersionData.bin,
|
|
56
|
+
previousVersionData.name || pkg.packageName
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const newBinaries = Object.keys(newBin).filter((key) => !oldBin.hasOwnProperty(key))
|
|
60
|
+
|
|
61
|
+
if (newBinaries.length > 0) {
|
|
62
|
+
newBinaries.forEach((binaryName) => {
|
|
63
|
+
const message = `Package '${pkg.packageString}' introduces a new binary '${binaryName}' (command: '${newBin[binaryName]}') compared to version '${previousVersionString}'.`
|
|
64
|
+
this.setMessage({ pkg: pkg.packageString, message }, true) // true for warning
|
|
65
|
+
})
|
|
66
|
+
// Throw a warning to signal that messages have been set.
|
|
67
|
+
// The base class's checkPackage will catch this and use the messages.
|
|
68
|
+
// Note: We only need to throw one Warning, even if multiple binaries are new.
|
|
69
|
+
// The messages have already been added.
|
|
70
|
+
|
|
71
|
+
throw new Warning(`New binaries detected for ${pkg.packageString}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
normalizeBin(binField, packageJSONName) {
|
|
76
|
+
if (!binField) {
|
|
77
|
+
return {}
|
|
78
|
+
}
|
|
79
|
+
if (typeof binField === 'string') {
|
|
80
|
+
// If bin is a string, the key is the package name from package.json
|
|
81
|
+
return { [packageJSONName]: binField }
|
|
82
|
+
}
|
|
83
|
+
// If it's already an object, return as is
|
|
84
|
+
return binField
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = NewBinMarshall
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const BaseMarshall = require('./baseMarshall')
|
|
4
|
+
const { marshallCategories } = require('./constants')
|
|
5
|
+
|
|
6
|
+
const MARSHALL_NAME = 'version_maturity'
|
|
7
|
+
const VERSION_AGE_THRESHOLD = 7 // specified in days
|
|
8
|
+
|
|
9
|
+
class Marshall extends BaseMarshall {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options)
|
|
12
|
+
this.name = MARSHALL_NAME
|
|
13
|
+
this.categoryId = marshallCategories.PackageHealth.id
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
title() {
|
|
17
|
+
return 'Checking version maturity'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
validate(pkg) {
|
|
21
|
+
let packageData = null
|
|
22
|
+
return this.packageRepoUtils
|
|
23
|
+
.getPackageInfo(pkg.packageName)
|
|
24
|
+
.then((data) => {
|
|
25
|
+
if (data && data.time) {
|
|
26
|
+
packageData = data
|
|
27
|
+
return pkg
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error('could not determine package version information')
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.then((pkg) => {
|
|
33
|
+
return this.packageRepoUtils.getSemVer(pkg.packageName, pkg.packageVersion)
|
|
34
|
+
})
|
|
35
|
+
.then((versionResolved) => {
|
|
36
|
+
const versionReleaseDate = packageData.time[versionResolved]
|
|
37
|
+
|
|
38
|
+
if (!versionReleaseDate) {
|
|
39
|
+
throw new Error(`could not determine release date for version ${versionResolved}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const versionDateDiff = new Date() - new Date(versionReleaseDate)
|
|
43
|
+
const versionDateDiffInDays = Math.round(versionDateDiff / (1000 * 60 * 60 * 24))
|
|
44
|
+
|
|
45
|
+
if (versionDateDiffInDays < VERSION_AGE_THRESHOLD) {
|
|
46
|
+
let timeAgoText = 'days'
|
|
47
|
+
let timeAgoNumber = versionDateDiffInDays
|
|
48
|
+
|
|
49
|
+
if (versionDateDiffInDays === 0) {
|
|
50
|
+
timeAgoText = 'hours'
|
|
51
|
+
const versionDateDiffInHours = Math.round(versionDateDiff / (1000 * 60 * 60))
|
|
52
|
+
timeAgoNumber = versionDateDiffInHours
|
|
53
|
+
} else if (versionDateDiffInDays === 1) {
|
|
54
|
+
timeAgoText = 'day'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error(
|
|
58
|
+
`detected a recently published version (published ${timeAgoNumber} ${timeAgoText} ago) - consider waiting for community review`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return pkg
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = Marshall
|