@verdaccio/plugin-verifier 1.0.0-next-9.1

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +255 -0
  4. package/bin/verdaccio-plugin-verifier +6 -0
  5. package/build/_virtual/_rolldown/runtime.cjs +23 -0
  6. package/build/cli.cjs +15 -0
  7. package/build/cli.cjs.map +1 -0
  8. package/build/cli.d.ts +2 -0
  9. package/build/cli.d.ts.map +1 -0
  10. package/build/cli.js +14 -0
  11. package/build/cli.js.map +1 -0
  12. package/build/commands/verify.cjs +76 -0
  13. package/build/commands/verify.cjs.map +1 -0
  14. package/build/commands/verify.d.ts +11 -0
  15. package/build/commands/verify.d.ts.map +1 -0
  16. package/build/commands/verify.js +74 -0
  17. package/build/commands/verify.js.map +1 -0
  18. package/build/diagnostics.cjs +209 -0
  19. package/build/diagnostics.cjs.map +1 -0
  20. package/build/diagnostics.d.ts +13 -0
  21. package/build/diagnostics.d.ts.map +1 -0
  22. package/build/diagnostics.js +207 -0
  23. package/build/diagnostics.js.map +1 -0
  24. package/build/index.cjs +11 -0
  25. package/build/index.d.ts +5 -0
  26. package/build/index.d.ts.map +1 -0
  27. package/build/index.js +4 -0
  28. package/build/sanity-checks.cjs +27 -0
  29. package/build/sanity-checks.cjs.map +1 -0
  30. package/build/sanity-checks.d.ts +11 -0
  31. package/build/sanity-checks.d.ts.map +1 -0
  32. package/build/sanity-checks.js +22 -0
  33. package/build/sanity-checks.js.map +1 -0
  34. package/build/types.d.ts +80 -0
  35. package/build/types.d.ts.map +1 -0
  36. package/build/verify-plugin.cjs +87 -0
  37. package/build/verify-plugin.cjs.map +1 -0
  38. package/build/verify-plugin.d.ts +19 -0
  39. package/build/verify-plugin.d.ts.map +1 -0
  40. package/build/verify-plugin.js +85 -0
  41. package/build/verify-plugin.js.map +1 -0
  42. package/package.json +63 -0
  43. package/src/cli.ts +14 -0
  44. package/src/commands/verify.ts +100 -0
  45. package/src/diagnostics.ts +274 -0
  46. package/src/index.ts +10 -0
  47. package/src/sanity-checks.ts +27 -0
  48. package/src/types.ts +83 -0
  49. package/src/verify-plugin.ts +115 -0
  50. package/tests/diagnostics.spec.ts +323 -0
  51. package/tests/fixtures/entry-point-bad-json/package.json +1 -0
  52. package/tests/fixtures/entry-point-exports-default/package.json +7 -0
  53. package/tests/fixtures/entry-point-exports-import-default/package.json +9 -0
  54. package/tests/fixtures/entry-point-exports-import-string/package.json +7 -0
  55. package/tests/fixtures/entry-point-exports-string/package.json +5 -0
  56. package/tests/fixtures/entry-point-main/package.json +3 -0
  57. package/tests/fixtures/entry-point-module/package.json +3 -0
  58. package/tests/fixtures/package.json +3 -0
  59. package/tests/fixtures/verdaccio-invalid-plugin/index.js +2 -0
  60. package/tests/fixtures/verdaccio-null-plugin/index.js +4 -0
  61. package/tests/fixtures/verdaccio-throwing-plugin/index.js +4 -0
  62. package/tests/fixtures/verdaccio-valid-auth-es6-plugin/index.js +14 -0
  63. package/tests/fixtures/verdaccio-valid-auth-plugin/index.js +20 -0
  64. package/tests/fixtures/verdaccio-valid-filter-plugin/index.js +12 -0
  65. package/tests/fixtures/verdaccio-valid-middleware-plugin/index.js +10 -0
  66. package/tests/fixtures/verdaccio-valid-storage-plugin/index.js +30 -0
  67. package/tests/fixtures/verdaccio-wrong-category-plugin/index.js +11 -0
  68. package/tests/verify-plugin.spec.ts +218 -0
  69. package/tsconfig.build.json +4 -0
  70. package/tsconfig.json +24 -0
  71. package/vite.config.mjs +69 -0
  72. package/vitest.config.mjs +11 -0
@@ -0,0 +1,323 @@
1
+ import { join } from 'node:path';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { PLUGIN_CATEGORY } from '@verdaccio/core';
5
+
6
+ import { resolveEntryPoint, runDiagnostics } from '../src/diagnostics';
7
+
8
+ const fixturesPath = join(__dirname, 'fixtures');
9
+
10
+ describe('runDiagnostics', () => {
11
+ describe('resolve phase', () => {
12
+ it('should fail when plugins folder does not exist', async () => {
13
+ const steps = await runDiagnostics({
14
+ pluginPath: 'some-plugin',
15
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
16
+ pluginsFolder: '/does/not/exist',
17
+ });
18
+
19
+ expect(steps).toHaveLength(1);
20
+ expect(steps[0].phase).toBe('resolve');
21
+ expect(steps[0].pass).toBe(false);
22
+ expect(steps[0].message).toContain('does not exist');
23
+ });
24
+
25
+ it('should fail when plugin directory is not found in plugins folder', async () => {
26
+ const steps = await runDiagnostics({
27
+ pluginPath: 'non-existent',
28
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
29
+ pluginsFolder: fixturesPath,
30
+ });
31
+
32
+ expect(steps).toHaveLength(1);
33
+ expect(steps[0].phase).toBe('resolve');
34
+ expect(steps[0].pass).toBe(false);
35
+ expect(steps[0].message).toContain('verdaccio-non-existent');
36
+ });
37
+
38
+ it('should resolve from plugins folder successfully', async () => {
39
+ const steps = await runDiagnostics({
40
+ pluginPath: 'valid-auth-plugin',
41
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
42
+ pluginsFolder: fixturesPath,
43
+ });
44
+
45
+ const resolveStep = steps.find((s) => s.phase === 'resolve');
46
+ expect(resolveStep?.pass).toBe(true);
47
+ expect(resolveStep?.message).toContain('resolved from');
48
+ });
49
+
50
+ it('should resolve from node_modules when no plugins folder given', async () => {
51
+ const steps = await runDiagnostics({
52
+ pluginPath: '@verdaccio/core',
53
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
54
+ });
55
+
56
+ const resolveStep = steps.find((s) => s.phase === 'resolve');
57
+ expect(resolveStep?.pass).toBe(true);
58
+ });
59
+
60
+ it('should fail resolution from node_modules for missing package', async () => {
61
+ const steps = await runDiagnostics({
62
+ pluginPath: 'totally-nonexistent-package-xyz',
63
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
64
+ });
65
+
66
+ expect(steps).toHaveLength(1);
67
+ expect(steps[0].phase).toBe('resolve');
68
+ expect(steps[0].pass).toBe(false);
69
+ expect(steps[0].message).toContain('not found');
70
+ });
71
+
72
+ it('should handle scoped plugin names without adding prefix', async () => {
73
+ const steps = await runDiagnostics({
74
+ pluginPath: '@scope/my-plugin',
75
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
76
+ });
77
+
78
+ expect(steps).toHaveLength(1);
79
+ expect(steps[0].phase).toBe('resolve');
80
+ expect(steps[0].pass).toBe(false);
81
+ // Scoped names should not get the verdaccio- prefix
82
+ expect(steps[0].message).toContain('@scope/my-plugin');
83
+ expect(steps[0].message).not.toContain('verdaccio-@scope');
84
+ });
85
+ });
86
+
87
+ describe('export phase', () => {
88
+ it('should fail when module does not export a function', async () => {
89
+ const steps = await runDiagnostics({
90
+ pluginPath: 'invalid-plugin',
91
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
92
+ pluginsFolder: fixturesPath,
93
+ });
94
+
95
+ const exportStep = steps.find((s) => s.phase === 'export');
96
+ expect(exportStep).toBeDefined();
97
+ expect(exportStep?.pass).toBe(false);
98
+ expect(exportStep?.message).toContain('does not export a function');
99
+ expect(exportStep?.message).toContain('Exported keys');
100
+ });
101
+
102
+ it('should detect CommonJS factory function export', async () => {
103
+ const steps = await runDiagnostics({
104
+ pluginPath: 'valid-auth-plugin',
105
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
106
+ pluginsFolder: fixturesPath,
107
+ });
108
+
109
+ const exportStep = steps.find((s) => s.phase === 'export');
110
+ expect(exportStep?.pass).toBe(true);
111
+ expect(exportStep?.message).toContain('CommonJS');
112
+ });
113
+
114
+ it('should detect ES6 default export', async () => {
115
+ const steps = await runDiagnostics({
116
+ pluginPath: 'valid-auth-es6-plugin',
117
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
118
+ pluginsFolder: fixturesPath,
119
+ });
120
+
121
+ const exportStep = steps.find((s) => s.phase === 'export');
122
+ expect(exportStep?.pass).toBe(true);
123
+ expect(exportStep?.message).toContain('ES6');
124
+ });
125
+ });
126
+
127
+ describe('instantiate phase', () => {
128
+ it('should fail when plugin throws during instantiation', async () => {
129
+ const steps = await runDiagnostics({
130
+ pluginPath: 'throwing-plugin',
131
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
132
+ pluginsFolder: fixturesPath,
133
+ });
134
+
135
+ const instantiateStep = steps.find((s) => s.phase === 'instantiate');
136
+ expect(instantiateStep).toBeDefined();
137
+ expect(instantiateStep?.pass).toBe(false);
138
+ expect(instantiateStep?.message).toContain('threw during instantiation');
139
+ });
140
+
141
+ it('should fail when plugin factory returns null', async () => {
142
+ const steps = await runDiagnostics({
143
+ pluginPath: 'null-plugin',
144
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
145
+ pluginsFolder: fixturesPath,
146
+ });
147
+
148
+ const instantiateStep = steps.find((s) => s.phase === 'instantiate');
149
+ expect(instantiateStep).toBeDefined();
150
+ expect(instantiateStep?.pass).toBe(false);
151
+ expect(instantiateStep?.message).toContain('null');
152
+ });
153
+
154
+ it('should pass instantiation for a valid plugin', async () => {
155
+ const steps = await runDiagnostics({
156
+ pluginPath: 'valid-auth-plugin',
157
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
158
+ pluginsFolder: fixturesPath,
159
+ });
160
+
161
+ const instantiateStep = steps.find((s) => s.phase === 'instantiate');
162
+ expect(instantiateStep?.pass).toBe(true);
163
+ expect(instantiateStep?.message).toContain('successfully');
164
+ });
165
+ });
166
+
167
+ describe('sanity-check phase', () => {
168
+ it('should fail when plugin does not implement required methods', async () => {
169
+ const steps = await runDiagnostics({
170
+ pluginPath: 'wrong-category-plugin',
171
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
172
+ pluginsFolder: fixturesPath,
173
+ });
174
+
175
+ const sanityStep = steps.find((s) => s.phase === 'sanity-check');
176
+ expect(sanityStep).toBeDefined();
177
+ expect(sanityStep?.pass).toBe(false);
178
+ expect(sanityStep?.message).toContain('authentication');
179
+ expect(sanityStep?.message).toContain('Available methods');
180
+ });
181
+
182
+ it('should pass all phases for a valid plugin', async () => {
183
+ const steps = await runDiagnostics({
184
+ pluginPath: 'valid-auth-plugin',
185
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
186
+ pluginsFolder: fixturesPath,
187
+ });
188
+
189
+ expect(steps).toHaveLength(4);
190
+ expect(steps.every((s) => s.pass)).toBe(true);
191
+ expect(steps.map((s) => s.phase)).toEqual([
192
+ 'resolve',
193
+ 'export',
194
+ 'instantiate',
195
+ 'sanity-check',
196
+ ]);
197
+ });
198
+
199
+ it('should use custom sanity check when provided', async () => {
200
+ const steps = await runDiagnostics({
201
+ pluginPath: 'valid-auth-plugin',
202
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
203
+ pluginsFolder: fixturesPath,
204
+ sanityCheck: () => false,
205
+ });
206
+
207
+ const sanityStep = steps.find((s) => s.phase === 'sanity-check');
208
+ expect(sanityStep?.pass).toBe(false);
209
+ });
210
+
211
+ it('should verify each plugin category', async () => {
212
+ const categoryFixtures = [
213
+ { category: PLUGIN_CATEGORY.STORAGE, fixture: 'valid-storage-plugin' },
214
+ { category: PLUGIN_CATEGORY.MIDDLEWARE, fixture: 'valid-middleware-plugin' },
215
+ { category: PLUGIN_CATEGORY.FILTER, fixture: 'valid-filter-plugin' },
216
+ ];
217
+
218
+ for (const { category, fixture } of categoryFixtures) {
219
+ const steps = await runDiagnostics({
220
+ pluginPath: fixture,
221
+ category,
222
+ pluginsFolder: fixturesPath,
223
+ });
224
+
225
+ const sanityStep = steps.find((s) => s.phase === 'sanity-check');
226
+ expect(sanityStep?.pass).toBe(true);
227
+ }
228
+ });
229
+ });
230
+
231
+ describe('early return behavior', () => {
232
+ it('should stop at resolve phase on failure', async () => {
233
+ const steps = await runDiagnostics({
234
+ pluginPath: 'non-existent',
235
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
236
+ pluginsFolder: fixturesPath,
237
+ });
238
+
239
+ expect(steps).toHaveLength(1);
240
+ expect(steps[0].phase).toBe('resolve');
241
+ });
242
+
243
+ it('should stop at export phase on failure', async () => {
244
+ const steps = await runDiagnostics({
245
+ pluginPath: 'invalid-plugin',
246
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
247
+ pluginsFolder: fixturesPath,
248
+ });
249
+
250
+ expect(steps).toHaveLength(2);
251
+ expect(steps[0].phase).toBe('resolve');
252
+ expect(steps[1].phase).toBe('export');
253
+ });
254
+
255
+ it('should stop at instantiate phase on failure', async () => {
256
+ const steps = await runDiagnostics({
257
+ pluginPath: 'throwing-plugin',
258
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
259
+ pluginsFolder: fixturesPath,
260
+ });
261
+
262
+ expect(steps).toHaveLength(3);
263
+ expect(steps[0].phase).toBe('resolve');
264
+ expect(steps[1].phase).toBe('export');
265
+ expect(steps[2].phase).toBe('instantiate');
266
+ });
267
+ });
268
+
269
+ describe('resolveEntryPoint', () => {
270
+ it('should resolve exports["."] as a string', () => {
271
+ const dir = join(fixturesPath, 'entry-point-exports-string');
272
+ expect(resolveEntryPoint(dir)).toBe(join(dir, './lib/main.js'));
273
+ });
274
+
275
+ it('should resolve exports["."].import.default', () => {
276
+ const dir = join(fixturesPath, 'entry-point-exports-import-default');
277
+ expect(resolveEntryPoint(dir)).toBe(join(dir, './lib/entry.mjs'));
278
+ });
279
+
280
+ it('should resolve exports["."].import as a string', () => {
281
+ const dir = join(fixturesPath, 'entry-point-exports-import-string');
282
+ expect(resolveEntryPoint(dir)).toBe(join(dir, './lib/entry.mjs'));
283
+ });
284
+
285
+ it('should resolve exports["."].default', () => {
286
+ const dir = join(fixturesPath, 'entry-point-exports-default');
287
+ expect(resolveEntryPoint(dir)).toBe(join(dir, './lib/fallback.js'));
288
+ });
289
+
290
+ it('should resolve pkg.module', () => {
291
+ const dir = join(fixturesPath, 'entry-point-module');
292
+ expect(resolveEntryPoint(dir)).toBe(join(dir, './esm/index.mjs'));
293
+ });
294
+
295
+ it('should resolve pkg.main', () => {
296
+ const dir = join(fixturesPath, 'entry-point-main');
297
+ expect(resolveEntryPoint(dir)).toBe(join(dir, './dist/index.cjs'));
298
+ });
299
+
300
+ it('should fallback to index.js when no package.json exists', () => {
301
+ const dir = join(fixturesPath, 'entry-point-fallback');
302
+ expect(resolveEntryPoint(dir)).toBe(join(dir, 'index.js'));
303
+ });
304
+
305
+ it('should fallback to index.js when package.json is invalid', () => {
306
+ const dir = join(fixturesPath, 'entry-point-bad-json');
307
+ expect(resolveEntryPoint(dir)).toBe(join(dir, 'index.js'));
308
+ });
309
+ });
310
+
311
+ describe('plugin config forwarding', () => {
312
+ it('should pass plugin config to the plugin constructor', async () => {
313
+ const steps = await runDiagnostics({
314
+ pluginPath: 'valid-auth-plugin',
315
+ category: PLUGIN_CATEGORY.AUTHENTICATION,
316
+ pluginsFolder: fixturesPath,
317
+ pluginConfig: { custom: 'value' },
318
+ });
319
+
320
+ expect(steps.every((s) => s.pass)).toBe(true);
321
+ });
322
+ });
323
+ });
@@ -0,0 +1 @@
1
+ { invalid json !!!
@@ -0,0 +1,7 @@
1
+ {
2
+ "exports": {
3
+ ".": {
4
+ "default": "./lib/fallback.js"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "exports": {
3
+ ".": {
4
+ "import": {
5
+ "default": "./lib/entry.mjs"
6
+ }
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "exports": {
3
+ ".": {
4
+ "import": "./lib/entry.mjs"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "exports": {
3
+ ".": "./lib/main.js"
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "main": "./dist/index.cjs"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "module": "./esm/index.mjs"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,2 @@
1
+ // This plugin exports an object, not a function or class — invalid export shape
2
+ module.exports = { notAPlugin: true };
@@ -0,0 +1,4 @@
1
+ // This plugin factory returns null instead of an object
2
+ module.exports = function () {
3
+ return null;
4
+ };
@@ -0,0 +1,4 @@
1
+ // This plugin throws during instantiation
2
+ module.exports = function () {
3
+ throw new Error('Plugin initialization failed');
4
+ };
@@ -0,0 +1,14 @@
1
+ // ES6-style plugin with default export
2
+ class ValidAuthES6Plugin {
3
+ constructor(config, options) {
4
+ this.config = config;
5
+ this.options = options;
6
+ }
7
+
8
+ authenticate(_user, _password, cb) {
9
+ cb(null, [_user]);
10
+ }
11
+ }
12
+
13
+ Object.defineProperty(exports, '__esModule', { value: true });
14
+ exports.default = ValidAuthES6Plugin;
@@ -0,0 +1,20 @@
1
+ class ValidAuthPlugin {
2
+ constructor(config, options) {
3
+ this.config = config;
4
+ this.options = options;
5
+ }
6
+
7
+ authenticate(_user, _password, cb) {
8
+ cb(null, [_user]);
9
+ }
10
+
11
+ allow_access(_user, _pkg, cb) {
12
+ cb(null, true);
13
+ }
14
+
15
+ allow_publish(_user, _pkg, cb) {
16
+ cb(null, true);
17
+ }
18
+ }
19
+
20
+ module.exports = (...args) => new ValidAuthPlugin(...args);
@@ -0,0 +1,12 @@
1
+ class ValidFilterPlugin {
2
+ constructor(config, options) {
3
+ this.config = config;
4
+ this.options = options;
5
+ }
6
+
7
+ async filter_metadata(manifest) {
8
+ return manifest;
9
+ }
10
+ }
11
+
12
+ module.exports = (...args) => new ValidFilterPlugin(...args);
@@ -0,0 +1,10 @@
1
+ class ValidMiddlewarePlugin {
2
+ constructor(config, options) {
3
+ this.config = config;
4
+ this.options = options;
5
+ }
6
+
7
+ register_middlewares(_app, _auth, _storage) {}
8
+ }
9
+
10
+ module.exports = (...args) => new ValidMiddlewarePlugin(...args);
@@ -0,0 +1,30 @@
1
+ class ValidStoragePlugin {
2
+ constructor(config, options) {
3
+ this.config = config;
4
+ this.options = options;
5
+ }
6
+
7
+ async init() {}
8
+ async getSecret() {
9
+ return 'secret';
10
+ }
11
+ async setSecret() {}
12
+ getPackageStorage() {
13
+ return {};
14
+ }
15
+ async add() {}
16
+ async remove() {}
17
+ async get() {
18
+ return [];
19
+ }
20
+ async search() {
21
+ return [];
22
+ }
23
+ async saveToken() {}
24
+ async deleteToken() {}
25
+ async readTokens() {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ module.exports = (...args) => new ValidStoragePlugin(...args);
@@ -0,0 +1,11 @@
1
+ // This is a valid middleware plugin, but will fail sanity check if tested as auth
2
+ class WrongCategoryPlugin {
3
+ constructor(config, options) {
4
+ this.config = config;
5
+ this.options = options;
6
+ }
7
+
8
+ register_middlewares(_app, _auth, _storage) {}
9
+ }
10
+
11
+ module.exports = (...args) => new WrongCategoryPlugin(...args);