@trustify-da/trustify-da-javascript-client 0.3.0-ea.9988076 → 0.3.0-ea.9a6adf7

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 (69) hide show
  1. package/README.md +127 -10
  2. package/dist/package.json +27 -23
  3. package/dist/src/analysis.d.ts +21 -5
  4. package/dist/src/analysis.js +53 -4
  5. package/dist/src/batch_opts.d.ts +24 -0
  6. package/dist/src/batch_opts.js +35 -0
  7. package/dist/src/cli.js +171 -4
  8. package/dist/src/cyclone_dx_sbom.d.ts +14 -1
  9. package/dist/src/cyclone_dx_sbom.js +43 -8
  10. package/dist/src/index.d.ts +137 -6
  11. package/dist/src/index.js +350 -5
  12. package/dist/src/license/index.d.ts +3 -3
  13. package/dist/src/license/index.js +4 -1
  14. package/dist/src/license/license_utils.js +4 -1
  15. package/dist/src/license/licenses_api.d.ts +2 -2
  16. package/dist/src/oci_image/images.d.ts +1 -1
  17. package/dist/src/oci_image/utils.d.ts +5 -5
  18. package/dist/src/oci_image/utils.js +11 -2
  19. package/dist/src/provider.d.ts +8 -4
  20. package/dist/src/provider.js +13 -4
  21. package/dist/src/providers/base_java.d.ts +14 -12
  22. package/dist/src/providers/base_java.js +18 -44
  23. package/dist/src/providers/base_javascript.d.ts +37 -10
  24. package/dist/src/providers/base_javascript.js +108 -20
  25. package/dist/src/providers/base_pyproject.d.ts +158 -0
  26. package/dist/src/providers/base_pyproject.js +322 -0
  27. package/dist/src/providers/golang_gomodules.d.ts +23 -13
  28. package/dist/src/providers/golang_gomodules.js +165 -119
  29. package/dist/src/providers/gomod_parser.d.ts +4 -0
  30. package/dist/src/providers/gomod_parser.js +16 -0
  31. package/dist/src/providers/java_gradle.d.ts +19 -0
  32. package/dist/src/providers/java_gradle.js +116 -2
  33. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  34. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  35. package/dist/src/providers/java_maven.d.ts +10 -2
  36. package/dist/src/providers/java_maven.js +165 -1
  37. package/dist/src/providers/javascript_bun.d.ts +10 -0
  38. package/dist/src/providers/javascript_bun.js +100 -0
  39. package/dist/src/providers/javascript_npm.d.ts +6 -0
  40. package/dist/src/providers/javascript_npm.js +21 -0
  41. package/dist/src/providers/javascript_pnpm.d.ts +1 -1
  42. package/dist/src/providers/javascript_pnpm.js +8 -4
  43. package/dist/src/providers/manifest.d.ts +2 -0
  44. package/dist/src/providers/manifest.js +22 -4
  45. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  46. package/dist/src/providers/marker_evaluator.js +191 -0
  47. package/dist/src/providers/processors/yarn_berry_processor.d.ts +2 -2
  48. package/dist/src/providers/processors/yarn_berry_processor.js +88 -5
  49. package/dist/src/providers/processors/yarn_classic_processor.d.ts +2 -2
  50. package/dist/src/providers/python_controller.d.ts +5 -1
  51. package/dist/src/providers/python_controller.js +65 -7
  52. package/dist/src/providers/python_pip.d.ts +5 -0
  53. package/dist/src/providers/python_pip.js +6 -6
  54. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  55. package/dist/src/providers/python_pip_pyproject.js +146 -0
  56. package/dist/src/providers/python_poetry.d.ts +98 -0
  57. package/dist/src/providers/python_poetry.js +286 -0
  58. package/dist/src/providers/python_uv.d.ts +46 -0
  59. package/dist/src/providers/python_uv.js +269 -0
  60. package/dist/src/providers/rust_cargo.d.ts +12 -3
  61. package/dist/src/providers/rust_cargo.js +41 -6
  62. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  63. package/dist/src/sbom.d.ts +14 -1
  64. package/dist/src/sbom.js +13 -2
  65. package/dist/src/tools.d.ts +28 -2
  66. package/dist/src/tools.js +58 -0
  67. package/dist/src/workspace.d.ts +70 -0
  68. package/dist/src/workspace.js +256 -0
  69. package/package.json +28 -24
@@ -4,8 +4,8 @@ import { PackageURL } from 'packageurl-js';
4
4
  import { parse as parseToml } from 'smol-toml';
5
5
  import { getLicense } from '../license/license_utils.js';
6
6
  import Sbom from '../sbom.js';
7
- import { getCustomPath, invokeCommand } from '../tools.js';
8
- export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
7
+ import { getCustom, getCustomPath, invokeCommand } from '../tools.js';
8
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'cargo'; } };
9
9
  /** @typedef {import('../provider').Provider} */
10
10
  /** @typedef {import('../provider').Provided} Provided */
11
11
  /**
@@ -71,15 +71,22 @@ function isSupported(manifestName) {
71
71
  * Read project license from Cargo.toml, with fallback to LICENSE file.
72
72
  * Supports the `license` field under `[package]` (single crate / workspace
73
73
  * with root) and under `[workspace.package]` (virtual workspaces).
74
+ * When a member crate uses `license = { workspace = true }`, the actual
75
+ * license is resolved from the workspace root Cargo.toml.
74
76
  * @param {string} manifestPath - path to Cargo.toml
77
+ * @param {object} [metadata] - cargo metadata output (needed for workspace license resolution)
75
78
  * @returns {string|null} SPDX identifier or null
76
79
  */
77
- function readLicenseFromManifest(manifestPath) {
80
+ function readLicenseFromManifest(manifestPath, metadata) {
78
81
  let fromManifest = null;
79
82
  try {
80
83
  let content = fs.readFileSync(manifestPath, 'utf-8');
81
84
  let parsed = parseToml(content);
82
- fromManifest = parsed.package?.license
85
+ let packageLicense = parsed.package?.license;
86
+ if (typeof packageLicense === 'object' && packageLicense?.workspace === true) {
87
+ packageLicense = resolveWorkspaceLicense(parsed, metadata);
88
+ }
89
+ fromManifest = packageLicense
83
90
  || parsed.workspace?.package?.license
84
91
  || null;
85
92
  }
@@ -88,6 +95,26 @@ function readLicenseFromManifest(manifestPath) {
88
95
  }
89
96
  return getLicense(fromManifest, manifestPath);
90
97
  }
98
+ /**
99
+ * Resolve a workspace-inherited license from the workspace root Cargo.toml.
100
+ * @param {object} parsed - the parsed member crate Cargo.toml
101
+ * @param {object} [metadata] - cargo metadata output with workspace_root
102
+ * @returns {string|null} resolved license string or null
103
+ */
104
+ function resolveWorkspaceLicense(parsed, metadata) {
105
+ if (metadata?.workspace_root) {
106
+ let cargoTomlPath = path.join(metadata.workspace_root, 'Cargo.toml');
107
+ try {
108
+ let content = fs.readFileSync(cargoTomlPath, 'utf-8');
109
+ let workspaceParsed = parseToml(content);
110
+ return workspaceParsed.workspace?.package?.license || null;
111
+ }
112
+ catch (_) {
113
+ // fall through
114
+ }
115
+ }
116
+ return parsed.workspace?.package?.license || null;
117
+ }
91
118
  /**
92
119
  * Validates that Cargo.lock exists in the manifest directory or in a parent
93
120
  * workspace root directory. In Cargo workspaces the lock file always lives at
@@ -95,10 +122,18 @@ function readLicenseFromManifest(manifestPath) {
95
122
  * up the directory tree looking for Cargo.lock (stopping when we find a
96
123
  * Cargo.toml that contains a [workspace] section, or when we reach the
97
124
  * filesystem root).
125
+ * When TRUSTIFY_DA_WORKSPACE_DIR is provided (via env var or opts),
126
+ * checks only that directory for Cargo.lock — no walk-up.
98
127
  * @param {string} manifestDir - the directory where the manifest lies
128
+ * @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
99
129
  * @returns {boolean} true if Cargo.lock is found
100
130
  */
101
- function validateLockFile(manifestDir) {
131
+ function validateLockFile(manifestDir, opts = {}) {
132
+ const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts);
133
+ if (workspaceDir) {
134
+ const dir = path.resolve(workspaceDir);
135
+ return fs.existsSync(path.join(dir, 'Cargo.lock'));
136
+ }
102
137
  let dir = path.resolve(manifestDir);
103
138
  let parent = dir;
104
139
  do {
@@ -165,7 +200,7 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
165
200
  let metadata = executeCargoMetadata(cargoBin, manifestDir);
166
201
  let ignoredDeps = getIgnoredDeps(manifest, metadata);
167
202
  let crateType = detectCrateType(metadata);
168
- let license = readLicenseFromManifest(manifest);
203
+ let license = readLicenseFromManifest(manifest, metadata);
169
204
  let sbom;
170
205
  if (crateType === CrateType.WORKSPACE_VIRTUAL) {
171
206
  sbom = handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license);
@@ -25,9 +25,14 @@ export default class Sbom {
25
25
  /**
26
26
  * @param {component} sourceRef current source Component ( Starting from root component by clients)
27
27
  * @param {PackageURL} targetRef current dependency to add to Dependencies list of component sourceRef
28
+ * @param {string} [scope] - Scope of the dependency
29
+ * @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
28
30
  * @return Sbom
29
31
  */
30
- addDependency(sourceRef: component, targetRef: PackageURL, scope: any): CycloneDxSbom;
32
+ addDependency(sourceRef: component, targetRef: PackageURL, scope?: string, targetHashes?: Array<{
33
+ alg: string;
34
+ content: string;
35
+ }>): CycloneDxSbom;
31
36
  /**
32
37
  * @return String sbom json in a string format
33
38
  */
@@ -45,6 +50,7 @@ export default class Sbom {
45
50
  version: any;
46
51
  scope: any;
47
52
  licenses?: any;
53
+ hashes?: any;
48
54
  };
49
55
  /** This method gets a component object, and a string name, and checks if the name is a substring of the component' purl.
50
56
  * @param {} component to search in its dependencies
@@ -53,6 +59,13 @@ export default class Sbom {
53
59
  * @return {boolean}
54
60
  */
55
61
  checkIfPackageInsideDependsOnList(component: any, name: string): boolean;
62
+ /**
63
+ * Checks if any entry in the dependsOn list of sourceRef starts with the given purl prefix.
64
+ * @param {PackageURL} sourceRef - The source component to check
65
+ * @param {string} purlPrefix - The purl prefix to match (e.g. "pkg:npm/minimist@")
66
+ * @return {boolean}
67
+ */
68
+ checkDependsOnByPurlPrefix(sourceRef: PackageURL, purlPrefix: string): boolean;
56
69
  /** Removes the root component from the sbom
57
70
  */
58
71
  removeRootComponent(): void;
package/dist/src/sbom.js CHANGED
@@ -43,10 +43,12 @@ export default class Sbom {
43
43
  /**
44
44
  * @param {component} sourceRef current source Component ( Starting from root component by clients)
45
45
  * @param {PackageURL} targetRef current dependency to add to Dependencies list of component sourceRef
46
+ * @param {string} [scope] - Scope of the dependency
47
+ * @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
46
48
  * @return Sbom
47
49
  */
48
- addDependency(sourceRef, targetRef, scope) {
49
- return this.sbomModel.addDependency(sourceRef, targetRef, scope);
50
+ addDependency(sourceRef, targetRef, scope, targetHashes) {
51
+ return this.sbomModel.addDependency(sourceRef, targetRef, scope, targetHashes);
50
52
  }
51
53
  /**
52
54
  * @return String sbom json in a string format
@@ -77,6 +79,15 @@ export default class Sbom {
77
79
  checkIfPackageInsideDependsOnList(component, name) {
78
80
  return this.sbomModel.checkIfPackageInsideDependsOnList(component, name);
79
81
  }
82
+ /**
83
+ * Checks if any entry in the dependsOn list of sourceRef starts with the given purl prefix.
84
+ * @param {PackageURL} sourceRef - The source component to check
85
+ * @param {string} purlPrefix - The purl prefix to match (e.g. "pkg:npm/minimist@")
86
+ * @return {boolean}
87
+ */
88
+ checkDependsOnByPurlPrefix(sourceRef, purlPrefix) {
89
+ return this.sbomModel.checkDependsOnByPurlPrefix(sourceRef, purlPrefix);
90
+ }
80
91
  /** Removes the root component from the sbom
81
92
  */
82
93
  removeRootComponent() {
@@ -61,6 +61,32 @@ export function toPurlFromString(strPurl: any): PackageURL | null;
61
61
  * @param {string} cwd - directory for which to find the root of the git repository.
62
62
  */
63
63
  export function getGitRootDir(cwd: string): string | undefined;
64
+ /**
65
+ * Normalize a filesystem path, lowercasing on Windows for case-insensitive comparison.
66
+ *
67
+ * @param {string} thePath
68
+ * @returns {string}
69
+ */
70
+ export function normalizePath(thePath: string): string;
71
+ /**
72
+ * Walk up from `startDir` to `repoRoot` looking for an executable wrapper script.
73
+ *
74
+ * @param {string} startDir - Absolute directory to start from
75
+ * @param {string} wrapperName - Wrapper filename (e.g. `mvnw`, `gradlew`)
76
+ * @param {string} [repoRoot] - Stop boundary (defaults to git root or filesystem root)
77
+ * @returns {string | undefined}
78
+ */
79
+ export function traverseForWrapper(startDir: string, wrapperName: string, repoRoot?: string): string | undefined;
80
+ /**
81
+ * Resolve a build-tool binary, preferring a wrapper when configured.
82
+ *
83
+ * @param {string} globalBinary - Global binary name (e.g. `mvn`, `gradle`)
84
+ * @param {string} localWrapper - Wrapper filename (e.g. `mvnw`, `gradlew.bat`)
85
+ * @param {string} startDir - Directory from which to start the wrapper search
86
+ * @param {import('./index.js').Options} [opts={}]
87
+ * @returns {string} Path to the resolved binary
88
+ */
89
+ export function resolveBinary(globalBinary: string, localWrapper: string, startDir: string, opts?: import("./index.js").Options): string;
64
90
  /** this method invokes command string in a process in a synchronous way.
65
91
  * @param {string} bin - the command to be invoked
66
92
  * @param {Array<string>} args - the args to pass to the binary
@@ -74,13 +100,13 @@ export function invokeCommand(bin: string, args: Array<string>, opts?: import("c
74
100
  * @param {import("index.js").Options} opts - The trustify DA options that may contain proxy configuration
75
101
  * @returns {RequestInit} The fetch options with proxy agent if applicable
76
102
  */
77
- export function addProxyAgent(options: RequestInit, opts: import("index.js").Options): RequestInit;
103
+ export function addProxyAgent(options: RequestInit, opts: any): RequestInit;
78
104
  /**
79
105
  * Utility function for fetching vendor tokens
80
106
  * @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
81
107
  * @returns {{}}
82
108
  */
83
- export function getTokenHeaders(opts?: import("index.js").Options): {};
109
+ export function getTokenHeaders(opts?: any): {};
84
110
  export const RegexNotToBeLogged: RegExp;
85
111
  export const TRUSTIFY_DA_TOKEN_HEADER: "trust-da-token";
86
112
  export const TRUSTIFY_DA_TELEMETRY_ID_HEADER: "telemetry-anonymous-id";
package/dist/src/tools.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { execFileSync } from "child_process";
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
2
4
  import { EOL } from "os";
3
5
  import { HttpsProxyAgent } from "https-proxy-agent";
4
6
  import { PackageURL } from "packageurl-js";
@@ -128,6 +130,62 @@ export function getGitRootDir(cwd) {
128
130
  return undefined;
129
131
  }
130
132
  }
133
+ /**
134
+ * Normalize a filesystem path, lowercasing on Windows for case-insensitive comparison.
135
+ *
136
+ * @param {string} thePath
137
+ * @returns {string}
138
+ */
139
+ export function normalizePath(thePath) {
140
+ const normalized = path.resolve(thePath).normalize();
141
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
142
+ }
143
+ /**
144
+ * Walk up from `startDir` to `repoRoot` looking for an executable wrapper script.
145
+ *
146
+ * @param {string} startDir - Absolute directory to start from
147
+ * @param {string} wrapperName - Wrapper filename (e.g. `mvnw`, `gradlew`)
148
+ * @param {string} [repoRoot] - Stop boundary (defaults to git root or filesystem root)
149
+ * @returns {string | undefined}
150
+ */
151
+ export function traverseForWrapper(startDir, wrapperName, repoRoot = undefined) {
152
+ const currentDir = normalizePath(startDir);
153
+ repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(currentDir).root;
154
+ const wrapperPath = path.join(currentDir, wrapperName);
155
+ try {
156
+ fs.accessSync(wrapperPath, fs.constants.X_OK);
157
+ return wrapperPath;
158
+ }
159
+ catch {
160
+ const rootDir = path.parse(currentDir).root;
161
+ if (currentDir === repoRoot || currentDir === rootDir) {
162
+ return undefined;
163
+ }
164
+ const parentDir = path.dirname(currentDir);
165
+ if (parentDir === currentDir || parentDir === rootDir) {
166
+ return undefined;
167
+ }
168
+ return traverseForWrapper(parentDir, wrapperName, repoRoot);
169
+ }
170
+ }
171
+ /**
172
+ * Resolve a build-tool binary, preferring a wrapper when configured.
173
+ *
174
+ * @param {string} globalBinary - Global binary name (e.g. `mvn`, `gradle`)
175
+ * @param {string} localWrapper - Wrapper filename (e.g. `mvnw`, `gradlew.bat`)
176
+ * @param {string} startDir - Directory from which to start the wrapper search
177
+ * @param {import('./index.js').Options} [opts={}]
178
+ * @returns {string} Path to the resolved binary
179
+ */
180
+ export function resolveBinary(globalBinary, localWrapper, startDir, opts = {}) {
181
+ if (getWrapperPreference(globalBinary, opts)) {
182
+ const wrapper = traverseForWrapper(startDir, localWrapper);
183
+ if (wrapper !== undefined) {
184
+ return wrapper;
185
+ }
186
+ }
187
+ return getCustomPath(globalBinary, opts);
188
+ }
131
189
  /** this method invokes command string in a process in a synchronous way.
132
190
  * @param {string} bin - the command to be invoked
133
191
  * @param {Array<string>} args - the args to pass to the binary
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Resolve ignore globs for workspace discovery: defaults + `TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE` + `opts.workspaceDiscoveryIgnore`.
3
+ * Patterns are fast-glob / micromatch style, relative to the workspace root (forward slashes).
4
+ *
5
+ * @param {{ workspaceDiscoveryIgnore?: string[], TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string, [key: string]: unknown }} [opts={}]
6
+ * @returns {string[]}
7
+ */
8
+ export function resolveWorkspaceDiscoveryIgnore(opts?: {
9
+ workspaceDiscoveryIgnore?: string[];
10
+ TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string;
11
+ [key: string]: unknown;
12
+ }): string[];
13
+ /**
14
+ * Drop manifest paths whose location matches an ignore pattern (e.g. root unshift, Cargo paths).
15
+ *
16
+ * @param {string[]} manifestPaths
17
+ * @param {string} root
18
+ * @param {string[]} ignorePatterns
19
+ * @returns {string[]}
20
+ */
21
+ export function filterManifestPathsByDiscoveryIgnore(manifestPaths: string[], root: string, ignorePatterns: string[]): string[];
22
+ /**
23
+ * @typedef {{ valid: true, name: string, version: string } | { valid: false, error: string }} ValidatePackageJsonResult
24
+ */
25
+ /**
26
+ * Validate a package.json has non-empty `name` and `version` (required for stable SBOM root identity in batch).
27
+ *
28
+ * @param {string} packageJsonPath - Absolute or relative path to package.json
29
+ * @returns {ValidatePackageJsonResult}
30
+ */
31
+ export function validatePackageJson(packageJsonPath: string): ValidatePackageJsonResult;
32
+ /**
33
+ * Discover all package.json paths in a JS/TS workspace.
34
+ * Reads pnpm-workspace.yaml or package.json workspaces.
35
+ *
36
+ * @param {string} workspaceRoot - Absolute or relative path to workspace root
37
+ * @param {{ workspaceDiscoveryIgnore?: string[], TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string, [key: string]: unknown }} [opts={}] - optional `workspaceDiscoveryIgnore` globs (merged with defaults and env)
38
+ * @returns {Promise<string[]>} Paths to package.json files (absolute)
39
+ */
40
+ export function discoverWorkspacePackages(workspaceRoot: string, opts?: {
41
+ workspaceDiscoveryIgnore?: string[];
42
+ TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string;
43
+ [key: string]: unknown;
44
+ }): Promise<string[]>;
45
+ /**
46
+ * Convert workspace glob patterns to manifest-file glob patterns,
47
+ * correctly handling negation prefixes.
48
+ *
49
+ * @param {string[]} patterns - Workspace glob patterns (may include negations)
50
+ * @param {string} manifestFileName - e.g. 'package.json' or 'Cargo.toml'
51
+ * @returns {string[]}
52
+ */
53
+ export function toManifestGlobPatterns(patterns: string[], manifestFileName: string): string[];
54
+ /**
55
+ * Discover all Cargo.toml manifest paths in a Cargo workspace.
56
+ * Uses `cargo metadata` to get workspace members.
57
+ *
58
+ * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain Cargo.toml and Cargo.lock)
59
+ * @param {import('./index.js').Options} [opts={}]
60
+ * @returns {Promise<string[]>} Paths to Cargo.toml files (absolute)
61
+ */
62
+ export function discoverWorkspaceCrates(workspaceRoot: string, opts?: import("./index.js").Options): Promise<string[]>;
63
+ export type ValidatePackageJsonResult = {
64
+ valid: true;
65
+ name: string;
66
+ version: string;
67
+ } | {
68
+ valid: false;
69
+ error: string;
70
+ };
@@ -0,0 +1,256 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { load as yamlLoad } from 'js-yaml';
5
+ import micromatch from 'micromatch';
6
+ import { getCustom, getCustomPath, invokeCommand } from './tools.js';
7
+ /** Default paths skipped during JS workspace discovery (merged with user patterns). */
8
+ const DEFAULT_WORKSPACE_DISCOVERY_IGNORE = [
9
+ '**/node_modules/**',
10
+ '**/.git/**',
11
+ ];
12
+ /**
13
+ * Resolve ignore globs for workspace discovery: defaults + `TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE` + `opts.workspaceDiscoveryIgnore`.
14
+ * Patterns are fast-glob / micromatch style, relative to the workspace root (forward slashes).
15
+ *
16
+ * @param {{ workspaceDiscoveryIgnore?: string[], TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string, [key: string]: unknown }} [opts={}]
17
+ * @returns {string[]}
18
+ */
19
+ export function resolveWorkspaceDiscoveryIgnore(opts = {}) {
20
+ const merged = [...DEFAULT_WORKSPACE_DISCOVERY_IGNORE];
21
+ const fromEnv = getCustom('TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE', null, opts);
22
+ if (fromEnv && String(fromEnv).trim()) {
23
+ merged.push(...String(fromEnv).split(',').map(s => s.trim()).filter(Boolean));
24
+ }
25
+ const extra = opts.workspaceDiscoveryIgnore;
26
+ if (Array.isArray(extra)) {
27
+ for (const p of extra) {
28
+ if (typeof p === 'string' && p.trim()) {
29
+ merged.push(p.trim());
30
+ }
31
+ }
32
+ }
33
+ return [...new Set(merged)];
34
+ }
35
+ /**
36
+ * @param {string} root - Workspace root (absolute)
37
+ * @param {string[]} ignorePatterns
38
+ */
39
+ function buildWorkspaceDiscoveryGlobOptions(root, ignorePatterns) {
40
+ return {
41
+ cwd: root,
42
+ absolute: true,
43
+ onlyFiles: true,
44
+ ignore: ignorePatterns,
45
+ followSymbolicLinks: false,
46
+ };
47
+ }
48
+ /**
49
+ * @param {string} filePath - Absolute path
50
+ * @param {string} root - Workspace root (absolute)
51
+ * @returns {string} Relative path with forward slashes
52
+ */
53
+ function relativePathForGlobMatch(filePath, root) {
54
+ const rel = path.relative(root, filePath);
55
+ return rel.split(path.sep).join('/');
56
+ }
57
+ /**
58
+ * Drop manifest paths whose location matches an ignore pattern (e.g. root unshift, Cargo paths).
59
+ *
60
+ * @param {string[]} manifestPaths
61
+ * @param {string} root
62
+ * @param {string[]} ignorePatterns
63
+ * @returns {string[]}
64
+ */
65
+ export function filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) {
66
+ if (!ignorePatterns.length) {
67
+ return manifestPaths;
68
+ }
69
+ const resolvedRoot = path.resolve(root);
70
+ return manifestPaths.filter(absPath => {
71
+ const rel = relativePathForGlobMatch(absPath, resolvedRoot);
72
+ if (rel === '') {
73
+ return true;
74
+ }
75
+ return !ignorePatterns.some(pattern => micromatch.isMatch(rel, pattern, { dot: true }));
76
+ });
77
+ }
78
+ /**
79
+ * @typedef {{ valid: true, name: string, version: string } | { valid: false, error: string }} ValidatePackageJsonResult
80
+ */
81
+ /**
82
+ * Validate a package.json has non-empty `name` and `version` (required for stable SBOM root identity in batch).
83
+ *
84
+ * @param {string} packageJsonPath - Absolute or relative path to package.json
85
+ * @returns {ValidatePackageJsonResult}
86
+ */
87
+ export function validatePackageJson(packageJsonPath) {
88
+ let content;
89
+ try {
90
+ const raw = fs.readFileSync(packageJsonPath, 'utf-8');
91
+ content = JSON.parse(raw);
92
+ }
93
+ catch (err) {
94
+ const msg = err instanceof Error ? err.message : String(err);
95
+ return { valid: false, error: `Invalid package.json: ${msg}` };
96
+ }
97
+ if (!content || typeof content !== 'object') {
98
+ return { valid: false, error: 'package.json must be a JSON object' };
99
+ }
100
+ const name = content.name;
101
+ const version = content.version;
102
+ if (typeof name !== 'string' || !name.trim()) {
103
+ return { valid: false, error: 'Missing or invalid name' };
104
+ }
105
+ if (typeof version !== 'string' || !version.trim()) {
106
+ return { valid: false, error: 'Missing or invalid version' };
107
+ }
108
+ return { valid: true, name: name.trim(), version: version.trim() };
109
+ }
110
+ /**
111
+ * Discover all package.json paths in a JS/TS workspace.
112
+ * Reads pnpm-workspace.yaml or package.json workspaces.
113
+ *
114
+ * @param {string} workspaceRoot - Absolute or relative path to workspace root
115
+ * @param {{ workspaceDiscoveryIgnore?: string[], TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string, [key: string]: unknown }} [opts={}] - optional `workspaceDiscoveryIgnore` globs (merged with defaults and env)
116
+ * @returns {Promise<string[]>} Paths to package.json files (absolute)
117
+ */
118
+ export async function discoverWorkspacePackages(workspaceRoot, opts = {}) {
119
+ const root = path.resolve(workspaceRoot);
120
+ const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts);
121
+ const globOpts = buildWorkspaceDiscoveryGlobOptions(root, ignorePatterns);
122
+ const pnpmWorkspace = path.join(root, 'pnpm-workspace.yaml');
123
+ const packageJson = path.join(root, 'package.json');
124
+ if (fs.existsSync(pnpmWorkspace)) {
125
+ return discoverFromPnpmWorkspace(root, pnpmWorkspace, globOpts, ignorePatterns);
126
+ }
127
+ if (fs.existsSync(packageJson)) {
128
+ return discoverFromPackageJsonWorkspaces(root, packageJson, globOpts, ignorePatterns);
129
+ }
130
+ return [];
131
+ }
132
+ /**
133
+ * @param {string} root
134
+ * @param {string} pnpmWorkspacePath
135
+ * @param {object} globOpts - fast-glob options (cwd, ignore, followSymbolicLinks, …)
136
+ * @param {string[]} ignorePatterns - for post-filter
137
+ * @returns {Promise<string[]>}
138
+ */
139
+ async function discoverFromPnpmWorkspace(root, pnpmWorkspacePath, globOpts, ignorePatterns) {
140
+ const content = fs.readFileSync(pnpmWorkspacePath, 'utf-8');
141
+ const packages = parsePnpmPackages(content);
142
+ if (packages.length === 0) {
143
+ return [];
144
+ }
145
+ const patterns = toManifestGlobPatterns(packages, 'package.json');
146
+ const manifestPaths = await fg(patterns, globOpts);
147
+ return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns);
148
+ }
149
+ /**
150
+ * Parse the `packages` array from pnpm-workspace.yaml content.
151
+ * @param {string} content - Raw YAML content
152
+ * @returns {string[]}
153
+ */
154
+ function parsePnpmPackages(content) {
155
+ let doc;
156
+ try {
157
+ doc = yamlLoad(content);
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ if (!doc || typeof doc !== 'object' || !Array.isArray(doc.packages)) {
163
+ return [];
164
+ }
165
+ return doc.packages.filter(p => typeof p === 'string' && p.trim()).map(p => p.trim());
166
+ }
167
+ /**
168
+ * Convert workspace glob patterns to manifest-file glob patterns,
169
+ * correctly handling negation prefixes.
170
+ *
171
+ * @param {string[]} patterns - Workspace glob patterns (may include negations)
172
+ * @param {string} manifestFileName - e.g. 'package.json' or 'Cargo.toml'
173
+ * @returns {string[]}
174
+ */
175
+ export function toManifestGlobPatterns(patterns, manifestFileName) {
176
+ return patterns.map(p => {
177
+ if (p.startsWith('!')) {
178
+ return `!${p.slice(1)}/${manifestFileName}`;
179
+ }
180
+ return `${p}/${manifestFileName}`;
181
+ });
182
+ }
183
+ /**
184
+ * @param {string} root
185
+ * @param {string} packageJsonPath
186
+ * @param {object} globOpts
187
+ * @param {string[]} ignorePatterns
188
+ * @returns {Promise<string[]>}
189
+ */
190
+ async function discoverFromPackageJsonWorkspaces(root, packageJsonPath, globOpts, ignorePatterns) {
191
+ let pkg;
192
+ try {
193
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
194
+ }
195
+ catch {
196
+ return [];
197
+ }
198
+ const workspaces = pkg.workspaces;
199
+ if (!workspaces) {
200
+ return [];
201
+ }
202
+ const raw = Array.isArray(workspaces) ? workspaces : workspaces.packages || [];
203
+ const patterns = toManifestGlobPatterns(raw.filter(p => typeof p === 'string'), 'package.json');
204
+ if (patterns.length === 0) {
205
+ return [];
206
+ }
207
+ const manifestPaths = await fg(patterns, globOpts);
208
+ const rootPkg = path.join(root, 'package.json');
209
+ if (fs.existsSync(rootPkg) && !manifestPaths.includes(rootPkg)) {
210
+ manifestPaths.unshift(rootPkg);
211
+ }
212
+ return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns);
213
+ }
214
+ /**
215
+ * Discover all Cargo.toml manifest paths in a Cargo workspace.
216
+ * Uses `cargo metadata` to get workspace members.
217
+ *
218
+ * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain Cargo.toml and Cargo.lock)
219
+ * @param {import('./index.js').Options} [opts={}]
220
+ * @returns {Promise<string[]>} Paths to Cargo.toml files (absolute)
221
+ */
222
+ export async function discoverWorkspaceCrates(workspaceRoot, opts = {}) {
223
+ const root = path.resolve(workspaceRoot);
224
+ const cargoToml = path.join(root, 'Cargo.toml');
225
+ const cargoLock = path.join(root, 'Cargo.lock');
226
+ if (!fs.existsSync(cargoToml) || !fs.existsSync(cargoLock)) {
227
+ return [];
228
+ }
229
+ const cargoBin = getCustomPath('cargo', opts);
230
+ let output;
231
+ try {
232
+ output = invokeCommand(cargoBin, ['metadata', '--format-version', '1', '--no-deps'], { cwd: root });
233
+ }
234
+ catch {
235
+ return [];
236
+ }
237
+ let metadata;
238
+ try {
239
+ metadata = JSON.parse(output.toString().trim());
240
+ }
241
+ catch {
242
+ return [];
243
+ }
244
+ const memberIds = new Set(metadata.workspace_members || []);
245
+ const manifestPaths = [];
246
+ for (const pkg of metadata.packages || []) {
247
+ if (memberIds.has(pkg.id) && pkg.manifest_path) {
248
+ const manifestPath = path.resolve(pkg.manifest_path);
249
+ if (fs.existsSync(manifestPath)) {
250
+ manifestPaths.push(manifestPath);
251
+ }
252
+ }
253
+ }
254
+ const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts);
255
+ return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns);
256
+ }