@dependably/npm-check 1.8.0 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dependably/npm-check",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "A comprehensive tool for validating, migrating, and updating npm package-lock.json files across versions 1, 2, and 3.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -80,7 +80,7 @@
80
80
  "eslint": "10.5.0",
81
81
  "globals": "17.6.0",
82
82
  "jest": "29.7.0",
83
- "sonarqube-scanner": "3.5.0"
83
+ "sonarqube-scanner": "5.0.0"
84
84
  },
85
85
  "allowScripts": {
86
86
  "fsevents": true,
@@ -89,15 +89,16 @@
89
89
  "overrides": {
90
90
  "@babel/plugin-transform-modules-systemjs": "7.29.7",
91
91
  "@ungap/structured-clone": "1.3.1",
92
- "js-yaml": "4.2.0",
92
+ "brace-expansion": "5.0.9",
93
+ "js-yaml": "4.3.1",
93
94
  "picomatch": "4.0.4",
94
95
  "glob": {
95
96
  "minimatch": "3.1.5",
96
- "brace-expansion": "1.1.15"
97
+ "brace-expansion": "1.1.18"
97
98
  },
98
99
  "test-exclude": {
99
100
  "minimatch": "3.1.5",
100
- "brace-expansion": "1.1.15"
101
+ "brace-expansion": "1.1.18"
101
102
  }
102
103
  }
103
104
  }
@@ -16,7 +16,8 @@ export const CONFIG_FILENAMES = ['.npm-checkrc.json', 'npm-check.config.json'];
16
16
 
17
17
  // Shared, cross-tool config file (JSON, no extension) discovered by walking up
18
18
  // from the working directory. `.dependably` is canonical; `.dependably-check` is
19
- // a deprecated alias kept for the migration window (docs/dependably-config-spec.md §7).
19
+ // a deprecated alias kept for the migration window (config spec §7, at
20
+ // https://gitlab.northwardlabs.ca/moonlitlabs/dependably-spec).
20
21
  export const SHARED_CONFIG_FILENAME = '.dependably';
21
22
  export const DEPRECATED_SHARED_CONFIG_FILENAME = '.dependably-check';
22
23
  // Checked in this order at each directory level (canonical wins).
@@ -34,8 +35,9 @@ export const SUPPORTED_CONFIG_VERSION = 1;
34
35
  // own section but tolerated (ignored) in `common`.
35
36
  export const APPLICABLE_SELECTORS = ['package', 'id'];
36
37
 
37
- // Keys npm-check recognizes inside `common` / its own section. Unknown keys warn.
38
- const KNOWN_SECTION_KEYS = new Set([
38
+ // Keys npm-check recognizes inside its own section. An unrecognized key there warns;
39
+ // the same key in `common` is ignored, since it may belong to a sibling tool.
40
+ export const KNOWN_SECTION_KEYS = new Set([
39
41
  'rules', 'exceptions', 'exclude', 'failOn',
40
42
  'allowedRegistryHosts', 'allowedLocalFeeds', 'maxWarnings'
41
43
  ]);
@@ -58,6 +60,12 @@ export const DEFAULT_CONFIG = {
58
60
  'install-scripts': ['warn', { allow: [] }],
59
61
  'no-git-deps': 'warn',
60
62
  'no-remote-deps': ['warn', { allowedHosts: ['registry.npmjs.org', 'npm.pkg.github.com'] }],
63
+ // Lockfile portability, distinct from the trust question the two rules above
64
+ // ask. Empty `hosts` == off: pinning is a per-project decision (a project
65
+ // that genuinely installs from a private registry must not be flagged), and
66
+ // unlike `allowedRegistryHosts` this list is NOT unioned from shared config —
67
+ // a pin that only ever widens would not be a pin.
68
+ 'resolved-registry-pin': ['error', { hosts: [] }],
61
69
  'pinned-versions': ['error', {
62
70
  sections: ['dependencies', 'devDependencies', 'optionalDependencies'],
63
71
  ignore: []
@@ -188,11 +196,26 @@ function unionList(a, b) {
188
196
  // id replaces common's wholesale (no cross-section option deep-merge, §B.3).
189
197
  function mergeRuleMaps(commonRules, toolRules) {
190
198
  if (!commonRules && !toolRules) return undefined;
191
- return { ...(commonRules && typeof commonRules === 'object' ? commonRules : {}),
192
- ...(toolRules && typeof toolRules === 'object' ? toolRules : {}) };
199
+
200
+ // A rule id in `common` that npm-check does not know belongs to a sibling tool, so it is
201
+ // dropped rather than merged (§8). Without this it reached mergeConfig's registry check and
202
+ // raised UNKNOWN_RULE, which made a shared config unusable the moment any sibling configured
203
+ // one of its own rules. Ids from the tool's own section are passed through untouched, so an
204
+ // unknown id there still errors — that one is a typo, not a sibling's.
205
+ const fromCommon = {};
206
+ if (commonRules && typeof commonRules === 'object') {
207
+ for (const [ruleId, value] of Object.entries(commonRules)) {
208
+ if (KNOWN_RULES.includes(ruleId)) fromCommon[ruleId] = value;
209
+ }
210
+ }
211
+
212
+ return { ...fromCommon, ...(toolRules && typeof toolRules === 'object' ? toolRules : {}) };
193
213
  }
194
214
 
195
- // Warn about keys npm-check does not recognize inside a read section (§8).
215
+ // Warn about keys npm-check does not recognize inside its own section (§8). `common`
216
+ // is deliberately not checked: it is shared with the sibling tools, so a key npm-check
217
+ // does not know there belongs to one of them, not to a typo. This matches how unknown
218
+ // rule ids are already treated — tolerated in `common`, an error in the own section.
196
219
  function unknownKeyWarnings(section, label, warnings) {
197
220
  if (!section || typeof section !== 'object') return;
198
221
  for (const key of Object.keys(section)) {
@@ -218,7 +241,6 @@ function resolveToolSection(parsed, warnings) {
218
241
  warnings.push({ code: 'DEPRECATED_ALIAS_SECTION', message: `both "${SECTION_KEY}" and "${DEPRECATED_SECTION_KEY}" sections present; using "${SECTION_KEY}"` });
219
242
  }
220
243
 
221
- unknownKeyWarnings(common, 'common', warnings);
222
244
  unknownKeyWarnings(tool, SECTION_KEY, warnings);
223
245
 
224
246
  const settings = {};
package/src/audit.js CHANGED
@@ -666,6 +666,65 @@ const validPnpmFieldRule = {
666
666
  }
667
667
  };
668
668
 
669
+ /**
670
+ * Lockfile portability: every `resolved` URL must point at a host this project
671
+ * pins to.
672
+ *
673
+ * This is deliberately NOT the same question as `secure-resolved` /
674
+ * `no-remote-deps`, which both consult `allowedRegistryHosts` to ask "is this
675
+ * host a legitimate, trusted registry?". Trust and portability are orthogonal:
676
+ * an org's own private mirror is entirely trusted, yet a lockfile resolving
677
+ * from it cannot be installed by anyone outside that network (a public CI
678
+ * runner, an external contributor, a GitHub build). A shared
679
+ * `allowedRegistryHosts` also unions across config levels, so it can only ever
680
+ * grow more permissive — correct for a trust allowlist, but useless as a pin,
681
+ * which must be able to narrow.
682
+ *
683
+ * Opt-in: with no `hosts` configured the rule is a no-op, so projects that
684
+ * genuinely install from a private registry are unaffected.
685
+ */
686
+ const resolvedRegistryPinRule = {
687
+ id: 'resolved-registry-pin',
688
+ description: 'Resolved URLs must point only at the registry hosts this project pins to',
689
+ defaultSeverity: 'error',
690
+ // pnpm lockfiles carry no `resolved` URLs (the registry is implied by config),
691
+ // so there is nothing to pin.
692
+ flavors: ['npm'],
693
+ check({ lockfile, options }) {
694
+ const { hosts = [] } = options;
695
+ const findings = [];
696
+ // Unconfigured == off. Pinning is a per-project decision, not a default.
697
+ if (!Array.isArray(hosts) || hosts.length === 0) return findings;
698
+ if (!lockfile.packages) return findings;
699
+
700
+ const pinned = hosts
701
+ .filter((h) => typeof h === 'string' && h.trim())
702
+ .map((h) => h.trim().toLowerCase());
703
+ if (pinned.length === 0) return findings;
704
+
705
+ forEachPackageEntry(lockfile, ({ key, entry, name, isRoot, isWorkspaceSource, isLink, isGitDep, isFileDep }) => {
706
+ // Git/file/link/workspace entries resolve outside the registry by
707
+ // definition — no-git-deps / secure-resolved own those.
708
+ if (isRoot || isWorkspaceSource || isLink || isGitDep || isFileDep) return;
709
+ const resolved = entry && entry.resolved;
710
+ if (!resolved || !/^https?:/i.test(resolved)) return;
711
+ let hostname;
712
+ try {
713
+ hostname = new URL(resolved).hostname.toLowerCase();
714
+ } catch {
715
+ // Unparseable URL — secure-resolved flags it; not this rule's job.
716
+ return;
717
+ }
718
+ if (pinned.includes(hostname)) return;
719
+ findings.push({
720
+ packagePath: key,
721
+ message: `${name || key} resolves from "${hostname}", which is not a pinned registry host (${pinned.join(', ')}) — the lockfile will not install where that host is unreachable`
722
+ });
723
+ });
724
+ return findings;
725
+ }
726
+ };
727
+
669
728
  export const rules = [
670
729
  lockfileVersionRule,
671
730
  validStructureRule,
@@ -675,6 +734,7 @@ export const rules = [
675
734
  installScriptsRule,
676
735
  noGitDepsRule,
677
736
  noRemoteDepsRule,
737
+ resolvedRegistryPinRule,
678
738
  pinnedVersionsRule,
679
739
  lockfileSyncRule,
680
740
  noOrphanPackagesRule,
package/src/exceptions.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // src/exceptions.js
2
2
  //
3
- // Reference implementation of the `.dependably` exception grammar
4
- // (docs/dependably-config-spec.md §6). This is the module the C# and Python
5
- // ports mirror; keep it behavior-compatible with the conformance fixtures under
6
- // conformance/dependably/.
3
+ // Reference implementation of the `.dependably` exception grammar, specified in
4
+ // §6 of the config spec at
5
+ // https://gitlab.northwardlabs.ca/moonlitlabs/dependably-spec. This is the module
6
+ // the C# and Python ports mirror; keep it behavior-compatible with the vendored
7
+ // conformance fixtures under conformance/dependably/.
7
8
  //
8
9
  // An exception suppresses SPECIFIC findings so a run does not fail on them,
9
10
  // without excluding whole files (`exclude`) or disabling a rule globally
@@ -59,7 +59,7 @@ const KNOWN_KEYS = new Set([
59
59
  'fetch-retry-maxtimeout', 'fetch-timeout', 'access', 'tag', 'lockfile-version',
60
60
  'omit', 'include', 'ignore-scripts', 'foreground-scripts', 'node-options',
61
61
  'progress', 'prefer-offline', 'prefer-online', 'offline', 'global', 'unsafe-perm',
62
- 'user-agent', 'maxsockets', 'before', 'workspaces', 'workspace'
62
+ 'update-notifier', 'user-agent', 'maxsockets', 'before', 'workspaces', 'workspace'
63
63
  ]);
64
64
 
65
65
  // Plaintext-credential keys: bare `_auth`/`_authtoken`/`_password`, or the
package/src/report.js CHANGED
@@ -43,6 +43,7 @@ const RULE_SECTION = {
43
43
  'install-scripts': 'install-scripts',
44
44
  'no-git-deps': 'git',
45
45
  'no-remote-deps': 'remote',
46
+ 'resolved-registry-pin': 'registry-pin',
46
47
  'pinned-versions': 'pinned',
47
48
  'no-orphan-packages': 'orphans',
48
49
  'unused-dependencies': 'unused',
@@ -72,6 +73,7 @@ const SECTIONS = [
72
73
  { id: 'install-scripts', title: 'Install scripts' },
73
74
  { id: 'git', title: 'Git dependencies' },
74
75
  { id: 'remote', title: 'Remote-URL deps' },
76
+ { id: 'registry-pin', title: 'Registry pin' },
75
77
  { id: 'pinned', title: 'Pinned versions' },
76
78
  { id: 'orphans', title: 'Orphaned packages' },
77
79
  { id: 'unused', title: 'Unused dependencies' },