ohrisk 0.127.0 → 0.128.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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.128.0 - 2026-06-20
4
+
5
+ ### Added
6
+
7
+ - Yarn Berry `yarn.lock` files are now parsed alongside Yarn classic lockfiles,
8
+ including `npm:` protocol descriptors, patched npm packages, and workspace
9
+ package roots.
10
+
11
+ ### Fixed
12
+
13
+ - Real-world Yarn workspace scans now ignore local `workspace:` packages as npm
14
+ package evidence while still scanning each workspace package manifest as a
15
+ dependency root.
16
+
17
+ ## 0.127.1 - 2026-06-20
18
+
19
+ ### Fixed
20
+
21
+ - The default Node artifact fetcher now returns DNS lookup results in the shape
22
+ requested by Node's HTTP client, fixing `Invalid IP address: undefined`
23
+ failures when scanning real remote package tarballs.
24
+ - Package tarball evidence now accepts npm tarballs that use a custom
25
+ top-level directory, such as `bun/package.json`, instead of only
26
+ `package/package.json`.
27
+ - Registry metadata fallback now requests the exact package version endpoint
28
+ instead of the full package metadata document, avoiding oversized metadata
29
+ failures for packages with long histories such as `@types/node`.
30
+ - Remote package tarballs that exceed Ohrisk's size limits now produce
31
+ unavailable package evidence instead of aborting the whole repository scan.
32
+ - Package metadata now uses npm's normalized `bin` path form, avoiding publish
33
+ auto-correction warnings for the CLI entry.
34
+
3
35
  ## 0.127.0 - 2026-06-20
4
36
 
5
37
  ### Fixed
package/README.md CHANGED
@@ -62,18 +62,18 @@ Ohrisk is distributed as an npm package, and the packaged CLI runs on Node.js
62
62
  `>=20.0.0`. Bun is used for Ohrisk development, tests, and packaging, but users
63
63
  do not need Bun installed to run the published CLI.
64
64
 
65
- Ohrisk scans Bun, npm package-lock/shrinkwrap, pnpm, Deno npm, and Yarn v1
65
+ Ohrisk scans Bun, npm package-lock/shrinkwrap, pnpm, Deno npm, and Yarn
66
66
  lockfiles regardless of which package manager you use to install the CLI.
67
67
 
68
68
  ## Current Scope
69
69
 
70
70
  The current implementation is the first npm-style vertical slice:
71
71
 
72
- - Bun `bun.lock`, npm `package-lock.json`, npm `npm-shrinkwrap.json`, pnpm `pnpm-lock.yaml`, Deno `deno.lock`, and Yarn v1 `yarn.lock` project discovery
72
+ - Bun `bun.lock`, npm `package-lock.json`, npm `npm-shrinkwrap.json`, pnpm `pnpm-lock.yaml`, Deno `deno.lock`, and Yarn classic/Berry `yarn.lock` project discovery
73
73
  - Node-compatible packaged CLI entrypoint for npm, pnpm, Yarn, npx, pnpm dlx, and yarn dlx users
74
74
  - explicit lockfile selection with `--lockfile <path>` for projects that contain more than one supported lockfile
75
75
  - direct and transitive dependency graph extraction
76
- - Bun, npm, pnpm, and Yarn v1 workspace projects are scanned from every workspace/importer package root
76
+ - Bun, npm, pnpm, and Yarn classic/Berry workspace projects are scanned from every workspace/importer package root
77
77
  - Deno `deno.lock` projects are scanned for npm package dependencies recorded in `npm:` specifiers; remote URL imports and JSR packages are not scanned yet
78
78
  - npm alias dependency resolution, including pnpm alias package keys, with alias context preserved in dependency paths
79
79
  - production, development, optional, and peer dependency classification
@@ -159,7 +159,7 @@ Supported lockfiles:
159
159
  - `npm-shrinkwrap.json` with the same package-lock parser support
160
160
  - `pnpm-lock.yaml` with `importers`, `packages`, and `snapshots` sections
161
161
  - `deno.lock` npm package entries from Deno v3/v4-style lockfiles
162
- - Yarn v1 `yarn.lock` with root and workspace dependency sets from `package.json` manifests
162
+ - Yarn classic/Berry `yarn.lock` with root and workspace dependency sets from `package.json` manifests
163
163
 
164
164
  Select a specific lockfile when a project contains more than one supported lockfile:
165
165
 
package/dist/cli.js CHANGED
@@ -15175,7 +15175,7 @@ function parseDiffArgs(argv) {
15175
15175
  }
15176
15176
 
15177
15177
  // src/cli/version.ts
15178
- var OHRISK_VERSION = "0.127.0";
15178
+ var OHRISK_VERSION = "0.128.0";
15179
15179
 
15180
15180
  // src/diff/compare.ts
15181
15181
  function diffRiskFindings(input) {
@@ -15480,7 +15480,8 @@ function collectTarballEvidence(input) {
15480
15480
  tarball: unpacked.value,
15481
15481
  maxEntries: input.maxEntries ?? PACKAGE_TARBALL_MAX_ENTRIES
15482
15482
  });
15483
- const packageJsonEntry = entries.find((entry) => normalizePackagePath(entry.path) === "package.json");
15483
+ const packageRoot = findPackageRoot(entries);
15484
+ const packageJsonEntry = packageRoot === undefined ? undefined : entries.find((entry) => normalizePackagePath(entry.path, packageRoot) === "package.json");
15484
15485
  if (!packageJsonEntry) {
15485
15486
  return err(createError({
15486
15487
  code: "PACKAGE_JSON_PARSE_FAILED",
@@ -15498,7 +15499,7 @@ function collectTarballEvidence(input) {
15498
15499
  if (!packageJson.ok) {
15499
15500
  return err(packageJson.error);
15500
15501
  }
15501
- const files = collectTarEvidenceFiles(entries);
15502
+ const files = collectTarEvidenceFiles(entries, packageRoot);
15502
15503
  const warnings = files.length === 0 ? ["No LICENSE, LICENCE, UNLICENSE, COPYING, or NOTICE file found."] : [];
15503
15504
  return ok({
15504
15505
  packageId: input.packageId,
@@ -15589,9 +15590,22 @@ function parseTarEntries(input) {
15589
15590
  }
15590
15591
  return entries;
15591
15592
  }
15592
- function collectTarEvidenceFiles(entries) {
15593
+ function findPackageRoot(entries) {
15594
+ if (entries.some((entry) => entry.path === "package.json")) {
15595
+ return "";
15596
+ }
15597
+ const roots = entries.map((entry) => {
15598
+ const match = /^([^/]+)\/package\.json$/.exec(entry.path);
15599
+ return match?.[1];
15600
+ }).filter((root) => root !== undefined).sort();
15601
+ if (roots.includes("package")) {
15602
+ return "package";
15603
+ }
15604
+ return roots[0];
15605
+ }
15606
+ function collectTarEvidenceFiles(entries, packageRoot) {
15593
15607
  return entries.map((entry) => {
15594
- const normalized = normalizePackagePath(entry.path);
15608
+ const normalized = normalizePackagePath(entry.path, packageRoot);
15595
15609
  if (!isRootPackageFile(normalized)) {
15596
15610
  return;
15597
15611
  }
@@ -15622,8 +15636,11 @@ function readLicenseFields2(packageJson) {
15622
15636
  function isObjectRecord2(value) {
15623
15637
  return typeof value === "object" && value !== null && !Array.isArray(value);
15624
15638
  }
15625
- function normalizePackagePath(path2) {
15626
- return path2.replace(/^package\//, "");
15639
+ function normalizePackagePath(path2, packageRoot) {
15640
+ if (packageRoot === "") {
15641
+ return path2;
15642
+ }
15643
+ return path2.startsWith(`${packageRoot}/`) ? path2.slice(packageRoot.length + 1) : path2;
15627
15644
  }
15628
15645
  function readNullTerminated(buffer, start, length) {
15629
15646
  const slice = buffer.subarray(start, start + length);
@@ -15911,7 +15928,7 @@ function localArtifactTooLargeError(input) {
15911
15928
  });
15912
15929
  }
15913
15930
  async function collectRegistryTarballEvidence(input) {
15914
- const metadataUrl = npmRegistryPackageUrl(input.node.name);
15931
+ const metadataUrl = npmRegistryPackageVersionUrl(input.node.name, input.node.version);
15915
15932
  const metadataUrlPreflight = await preflightRemoteArtifactFetchTarget({
15916
15933
  code: "REGISTRY_METADATA_FETCH_FAILED",
15917
15934
  packageId: input.node.id,
@@ -16250,6 +16267,9 @@ async function collectRemoteTarballEvidence(input) {
16250
16267
  }
16251
16268
  });
16252
16269
  if (!tarball.ok) {
16270
+ if (isPackageTarballTooLargeError(tarball.error)) {
16271
+ return ok(unavailableOversizedTarballEvidence(input.packageId));
16272
+ }
16253
16273
  return err(tarball.error);
16254
16274
  }
16255
16275
  const verified = verifyPackageIntegrity({
@@ -16266,6 +16286,9 @@ async function collectRemoteTarballEvidence(input) {
16266
16286
  tarball: tarball.value
16267
16287
  });
16268
16288
  if (!evidence.ok) {
16289
+ if (isPackageTarballTooLargeError(evidence.error)) {
16290
+ return ok(unavailableOversizedTarballEvidence(input.packageId));
16291
+ }
16269
16292
  return err(evidence.error);
16270
16293
  }
16271
16294
  return ok(addIntegrityWarningWhenUnverified({
@@ -16285,6 +16308,19 @@ async function collectRemoteTarballEvidence(input) {
16285
16308
  }));
16286
16309
  }
16287
16310
  }
16311
+ function isPackageTarballTooLargeError(error) {
16312
+ return error.code === "TARBALL_FETCH_FAILED" && error.message === "Package tarball response exceeded the maximum supported size." || error.code === "TARBALL_PARSE_FAILED" && error.message === "Failed to decompress package tarball evidence." && typeof error.details.maxUnpackedBytes === "number";
16313
+ }
16314
+ function unavailableOversizedTarballEvidence(packageId) {
16315
+ return {
16316
+ packageId,
16317
+ files: [],
16318
+ source: "unavailable",
16319
+ warnings: [
16320
+ "Package tarball evidence exceeded Ohrisk's size limit and was not scanned."
16321
+ ]
16322
+ };
16323
+ }
16288
16324
  function resolveExistingLocalArtifactPath(input) {
16289
16325
  const allowedRoot = realpathSync(resolveLocalArtifactRoot(input.projectRoot));
16290
16326
  const artifactPath = realpathSync(input.artifactPath);
@@ -16967,6 +17003,9 @@ function decodeIntegrityDigest(input) {
16967
17003
  const normalizedDecoded = decoded.toString("base64").replace(/=+$/, "");
16968
17004
  return normalizedDecoded === normalizedInput ? decoded : undefined;
16969
17005
  }
17006
+ function npmRegistryPackageVersionUrl(name, version) {
17007
+ return `${npmRegistryPackageUrl(name)}/${encodeURIComponent(version)}`;
17008
+ }
16970
17009
  function npmRegistryPackageUrl(name) {
16971
17010
  return `https://registry.npmjs.org/${encodeURIComponent(name).replace(/^%40/, "@")}`;
16972
17011
  }
@@ -16974,6 +17013,10 @@ function readRegistryTarballUrl(metadata, version) {
16974
17013
  if (!isRecord(metadata)) {
16975
17014
  return;
16976
17015
  }
17016
+ const dist = metadata.dist;
17017
+ if (isRecord(dist) && typeof dist.tarball === "string") {
17018
+ return dist.tarball;
17019
+ }
16977
17020
  const versions = metadata.versions;
16978
17021
  if (!isRecord(versions)) {
16979
17022
  return;
@@ -16982,11 +17025,11 @@ function readRegistryTarballUrl(metadata, version) {
16982
17025
  if (!isRecord(versionMetadata)) {
16983
17026
  return;
16984
17027
  }
16985
- const dist = versionMetadata.dist;
16986
- if (!isRecord(dist) || typeof dist.tarball !== "string") {
17028
+ const versionDist = versionMetadata.dist;
17029
+ if (!isRecord(versionDist) || typeof versionDist.tarball !== "string") {
16987
17030
  return;
16988
17031
  }
16989
- return dist.tarball;
17032
+ return versionDist.tarball;
16990
17033
  }
16991
17034
  function isRecord(value) {
16992
17035
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -17028,29 +17071,63 @@ async function defaultArtifactHostResolver(hostname) {
17028
17071
  }
17029
17072
  function secureArtifactLookup(hostname, options, callback) {
17030
17073
  defaultArtifactHostResolver(hostname).then((resolutions) => {
17031
- if (resolutions.length === 0) {
17032
- callback(new Error(`Artifact host ${normalizeUrlHostname(hostname)} returned no DNS addresses.`), "", 0);
17074
+ const selection = selectSecureArtifactLookupResponse(hostname, options, resolutions);
17075
+ if (!selection.ok) {
17076
+ respondToSecureArtifactLookupError(callback, options, selection.error);
17033
17077
  return;
17034
17078
  }
17035
- const requestedFamily = options.family === 4 || options.family === 6 ? options.family : undefined;
17036
- const familyResolutions = requestedFamily === undefined ? resolutions : resolutions.filter((resolution) => resolution.family === requestedFamily);
17037
- if (familyResolutions.length === 0) {
17038
- callback(new Error(`Artifact host ${normalizeUrlHostname(hostname)} returned no matching DNS addresses.`), "", 0);
17079
+ if (selection.value.all) {
17080
+ callback(null, selection.value.resolutions);
17039
17081
  return;
17040
17082
  }
17041
- for (const resolution of familyResolutions) {
17042
- const blockedReason = blockedRemoteArtifactHostReason(resolution.address);
17043
- if (blockedReason) {
17044
- callback(new Error(`Blocked artifact host resolution for ${normalizeUrlHostname(hostname)}: ${normalizeUrlHostname(resolution.address)} (${blockedReason}).`), "", 0);
17045
- return;
17046
- }
17047
- }
17048
- const selected = familyResolutions[0];
17049
- callback(null, selected.address, selected.family);
17083
+ callback(null, selection.value.address, selection.value.family);
17050
17084
  }).catch((cause) => {
17051
- callback(cause instanceof Error ? cause : new Error(String(cause)), "", 0);
17085
+ respondToSecureArtifactLookupError(callback, options, cause instanceof Error ? cause : new Error(String(cause)));
17086
+ });
17087
+ }
17088
+ function selectSecureArtifactLookupResponse(hostname, options, resolutions) {
17089
+ const normalizedOptions = normalizeArtifactLookupOptions(options);
17090
+ const normalizedHostname = normalizeUrlHostname(hostname);
17091
+ if (resolutions.length === 0) {
17092
+ return err(new Error(`Artifact host ${normalizedHostname} returned no DNS addresses.`));
17093
+ }
17094
+ const familyResolutions = normalizedOptions.family === undefined ? resolutions : resolutions.filter((resolution) => resolution.family === normalizedOptions.family);
17095
+ if (familyResolutions.length === 0) {
17096
+ return err(new Error(`Artifact host ${normalizedHostname} returned no matching DNS addresses.`));
17097
+ }
17098
+ for (const resolution of familyResolutions) {
17099
+ const blockedReason = blockedRemoteArtifactHostReason(resolution.address);
17100
+ if (blockedReason) {
17101
+ return err(new Error(`Blocked artifact host resolution for ${normalizedHostname}: ${normalizeUrlHostname(resolution.address)} (${blockedReason}).`));
17102
+ }
17103
+ }
17104
+ if (normalizedOptions.all) {
17105
+ return ok({
17106
+ all: true,
17107
+ resolutions: familyResolutions
17108
+ });
17109
+ }
17110
+ const selected = familyResolutions[0];
17111
+ return ok({
17112
+ all: false,
17113
+ address: selected.address,
17114
+ family: selected.family
17052
17115
  });
17053
17116
  }
17117
+ function normalizeArtifactLookupOptions(options) {
17118
+ const family = typeof options === "number" ? options : options?.family;
17119
+ return {
17120
+ all: typeof options === "object" && options?.all === true,
17121
+ family: family === 4 || family === 6 ? family : undefined
17122
+ };
17123
+ }
17124
+ function respondToSecureArtifactLookupError(callback, options, error) {
17125
+ if (normalizeArtifactLookupOptions(options).all) {
17126
+ callback(error, []);
17127
+ return;
17128
+ }
17129
+ callback(error, "", 0);
17130
+ }
17054
17131
  function headersForIncomingMessage(headers) {
17055
17132
  return {
17056
17133
  get: (name) => {
@@ -17189,6 +17266,17 @@ function resolveNpmDependencyReference(requestedName, range) {
17189
17266
  aliased: alias.name !== requestedName
17190
17267
  };
17191
17268
  }
17269
+ if (range.startsWith("npm:")) {
17270
+ const bareRange = range.slice("npm:".length);
17271
+ if (bareRange !== "") {
17272
+ return {
17273
+ requestedName,
17274
+ lookupName: requestedName,
17275
+ lookupRange: bareRange,
17276
+ aliased: false
17277
+ };
17278
+ }
17279
+ }
17192
17280
  return {
17193
17281
  requestedName,
17194
17282
  lookupName: requestedName,
@@ -19064,17 +19152,20 @@ function readPackageName2(packageJson) {
19064
19152
  return typeof packageJson.name === "string" && packageJson.name !== "" ? packageJson.name : undefined;
19065
19153
  }
19066
19154
  function parseLockfile(input, lockfilePath) {
19155
+ if (hasMergeConflictMarkers(input)) {
19156
+ return err(createError({
19157
+ code: "YARN_LOCK_PARSE_FAILED",
19158
+ category: "unsupported_input",
19159
+ message: "Failed to parse yarn.lock because it contains unresolved merge conflicts.",
19160
+ details: {
19161
+ lockfilePath
19162
+ }
19163
+ }));
19164
+ }
19165
+ if (isYarnBerryLockfile(input)) {
19166
+ return parseBerryLockfile(input, lockfilePath);
19167
+ }
19067
19168
  try {
19068
- if (hasMergeConflictMarkers(input)) {
19069
- return err(createError({
19070
- code: "YARN_LOCK_PARSE_FAILED",
19071
- category: "unsupported_input",
19072
- message: "Failed to parse yarn.lock because it contains unresolved merge conflicts.",
19073
- details: {
19074
- lockfilePath
19075
- }
19076
- }));
19077
- }
19078
19169
  const parsed = yarnLockfile.parse(input);
19079
19170
  if (parsed.type === "conflict") {
19080
19171
  return err(createError({
@@ -19086,12 +19177,47 @@ function parseLockfile(input, lockfilePath) {
19086
19177
  }
19087
19178
  }));
19088
19179
  }
19089
- return ok(parsed.object);
19180
+ return ok({
19181
+ format: "classic",
19182
+ entries: parsed.object
19183
+ });
19090
19184
  } catch (cause) {
19091
19185
  return err(createError({
19092
19186
  code: "YARN_LOCK_PARSE_FAILED",
19093
19187
  category: "unsupported_input",
19094
- message: "Failed to parse yarn.lock. Ohrisk currently supports Yarn v1 lockfiles.",
19188
+ message: "Failed to parse yarn.lock. Ohrisk expects a Yarn classic or Berry lockfile.",
19189
+ details: {
19190
+ lockfilePath,
19191
+ cause: cause instanceof Error ? cause.message : String(cause)
19192
+ }
19193
+ }));
19194
+ }
19195
+ }
19196
+ function isYarnBerryLockfile(input) {
19197
+ return /^__metadata:\s*$/m.test(input);
19198
+ }
19199
+ function parseBerryLockfile(input, lockfilePath) {
19200
+ try {
19201
+ const parsed = $parse(input);
19202
+ if (!isObjectRecord8(parsed)) {
19203
+ throw new Error("Expected a YAML mapping at the document root.");
19204
+ }
19205
+ const entries = {};
19206
+ for (const [key, value] of Object.entries(parsed)) {
19207
+ if (key === "__metadata" || !isObjectRecord8(value)) {
19208
+ continue;
19209
+ }
19210
+ entries[key] = value;
19211
+ }
19212
+ return ok({
19213
+ format: "berry",
19214
+ entries
19215
+ });
19216
+ } catch (cause) {
19217
+ return err(createError({
19218
+ code: "YARN_LOCK_PARSE_FAILED",
19219
+ category: "unsupported_input",
19220
+ message: "Failed to parse Yarn Berry lockfile.",
19095
19221
  details: {
19096
19222
  lockfilePath,
19097
19223
  cause: cause instanceof Error ? cause.message : String(cause)
@@ -19104,12 +19230,12 @@ function hasMergeConflictMarkers(input) {
19104
19230
  }
19105
19231
  function parsePackageRecords4(lockfile) {
19106
19232
  const records = [];
19107
- for (const [key, entry] of Object.entries(lockfile)) {
19233
+ for (const [key, entry] of Object.entries(lockfile.entries)) {
19108
19234
  if (typeof entry.version !== "string" || entry.version === "") {
19109
19235
  continue;
19110
19236
  }
19111
19237
  const descriptors = splitDescriptorKey(key);
19112
- const identity2 = descriptors.map(parseDescriptor).find(Boolean);
19238
+ const identity2 = lockfile.format === "berry" ? readBerryPackageIdentity({ key, entry }) : descriptors.map(parseDescriptor).find(Boolean);
19113
19239
  if (!identity2) {
19114
19240
  continue;
19115
19241
  }
@@ -19117,7 +19243,10 @@ function parsePackageRecords4(lockfile) {
19117
19243
  const integrity = typeof entry.integrity === "string" && entry.integrity !== "" ? entry.integrity : undefined;
19118
19244
  records.push({
19119
19245
  key,
19120
- descriptors,
19246
+ descriptors: descriptorIndexKeys({
19247
+ descriptors,
19248
+ format: lockfile.format
19249
+ }),
19121
19250
  name: identity2.name,
19122
19251
  version: entry.version,
19123
19252
  id: `${identity2.name}@${entry.version}`,
@@ -19131,8 +19260,26 @@ function parsePackageRecords4(lockfile) {
19131
19260
  function splitDescriptorKey(key) {
19132
19261
  return key.split(/,\s*/).map((descriptor) => descriptor.trim()).filter(Boolean);
19133
19262
  }
19263
+ function descriptorIndexKeys(input) {
19264
+ const keys = new Set;
19265
+ for (const descriptor of input.descriptors) {
19266
+ const unquoted = unquoteDescriptor(descriptor);
19267
+ keys.add(unquoted);
19268
+ const parsed = input.format === "berry" ? parseBerryDescriptor(unquoted) : parseDescriptor(unquoted);
19269
+ if (!parsed) {
19270
+ continue;
19271
+ }
19272
+ keys.add(`${parsed.name}@${parsed.range}`);
19273
+ keys.add(`${parsed.name}@npm:${parsed.range}`);
19274
+ }
19275
+ return [...keys];
19276
+ }
19134
19277
  function parseDescriptor(descriptor) {
19135
- const unquoted = descriptor.replace(/^"|"$/g, "");
19278
+ const unquoted = unquoteDescriptor(descriptor);
19279
+ const berryDescriptor = parseBerryDescriptor(unquoted);
19280
+ if (berryDescriptor) {
19281
+ return berryDescriptor;
19282
+ }
19136
19283
  const aliasMarker = "@npm:";
19137
19284
  const aliasIndex = unquoted.indexOf(aliasMarker);
19138
19285
  if (aliasIndex > 0) {
@@ -19148,6 +19295,89 @@ function parseDescriptor(descriptor) {
19148
19295
  }
19149
19296
  return { name: parsed.name, range: parsed.reference };
19150
19297
  }
19298
+ function parseBerryDescriptor(descriptor) {
19299
+ const npmLocator = parseBerryNpmLocator(descriptor);
19300
+ if (npmLocator) {
19301
+ const alias = parseNpmPackageReference(`npm:${npmLocator.reference}`);
19302
+ return alias ? { name: alias.name, range: alias.reference } : { name: npmLocator.name, range: npmLocator.reference };
19303
+ }
19304
+ const patchLocator = parseBerryPatchLocator(descriptor);
19305
+ if (patchLocator) {
19306
+ return patchLocator;
19307
+ }
19308
+ return;
19309
+ }
19310
+ function readBerryPackageIdentity(input) {
19311
+ const resolution = typeof input.entry.resolution === "string" ? input.entry.resolution : undefined;
19312
+ const parsedResolution = resolution ? parseBerryResolution(resolution) : undefined;
19313
+ if (parsedResolution) {
19314
+ return parsedResolution;
19315
+ }
19316
+ const descriptorIdentity = splitDescriptorKey(input.key).map((descriptor) => parseBerryDescriptor(unquoteDescriptor(descriptor))).find(Boolean);
19317
+ return descriptorIdentity ? { name: descriptorIdentity.name, version: descriptorIdentity.range } : undefined;
19318
+ }
19319
+ function parseBerryResolution(value) {
19320
+ const unquoted = unquoteDescriptor(value);
19321
+ const npmLocator = parseBerryNpmLocator(unquoted);
19322
+ if (npmLocator) {
19323
+ return {
19324
+ name: npmLocator.name,
19325
+ version: npmLocator.reference
19326
+ };
19327
+ }
19328
+ const patchLocator = parseBerryPatchLocator(unquoted);
19329
+ if (patchLocator) {
19330
+ return {
19331
+ name: patchLocator.name,
19332
+ version: patchLocator.range
19333
+ };
19334
+ }
19335
+ return;
19336
+ }
19337
+ function parseBerryNpmLocator(value) {
19338
+ const marker = "@npm:";
19339
+ const index = value.indexOf(marker);
19340
+ if (index <= 0) {
19341
+ return;
19342
+ }
19343
+ const name = value.slice(0, index);
19344
+ const reference = value.slice(index + marker.length);
19345
+ if (!isValidNpmPackageName(name) || reference === "") {
19346
+ return;
19347
+ }
19348
+ return { name, reference };
19349
+ }
19350
+ function parseBerryPatchLocator(value) {
19351
+ const marker = "@patch:";
19352
+ const index = value.indexOf(marker);
19353
+ if (index <= 0) {
19354
+ return;
19355
+ }
19356
+ const patchedLocator = value.slice(index + marker.length).split("#")[0]?.split("::")[0];
19357
+ if (!patchedLocator) {
19358
+ return;
19359
+ }
19360
+ const decodedLocator = safeDecodeURIComponent(patchedLocator);
19361
+ const npmLocator = parseBerryNpmLocator(decodedLocator);
19362
+ if (!npmLocator) {
19363
+ return;
19364
+ }
19365
+ const alias = parseNpmPackageReference(`npm:${npmLocator.reference}`);
19366
+ return alias ? { name: alias.name, range: alias.reference } : { name: npmLocator.name, range: npmLocator.reference };
19367
+ }
19368
+ function safeDecodeURIComponent(value) {
19369
+ try {
19370
+ return decodeURIComponent(value);
19371
+ } catch {
19372
+ return value;
19373
+ }
19374
+ }
19375
+ function unquoteDescriptor(value) {
19376
+ return value.replace(/^"|"$/g, "");
19377
+ }
19378
+ function isValidNpmPackageName(value) {
19379
+ return /^(?:@[^/]+\/)?[^/@][^@]*$/.test(value);
19380
+ }
19151
19381
  function collectRootDependencies5(packageJson) {
19152
19382
  return [
19153
19383
  ...dependencyEntries4(packageJson.dependencies, "production"),
@@ -19200,10 +19430,28 @@ function indexPackagesByName3(records) {
19200
19430
  return index;
19201
19431
  }
19202
19432
  function resolvePackageRecord4(input) {
19203
- const descriptor = `${input.name}@${input.range}`;
19204
19433
  const reference = resolveNpmDependencyReference(input.name, input.range);
19205
19434
  const candidates = input.nameIndex.get(reference.lookupName) ?? [];
19206
- return input.descriptorIndex.get(descriptor) ?? (candidates.length === 1 ? candidates[0] : undefined) ?? candidates.find((candidate) => candidate.version === reference.lookupRange) ?? undefined;
19435
+ return dependencyDescriptorCandidates({
19436
+ name: input.name,
19437
+ range: input.range,
19438
+ reference
19439
+ }).map((descriptor) => input.descriptorIndex.get(descriptor)).find((record) => record !== undefined) ?? (candidates.length === 1 ? candidates[0] : undefined) ?? candidates.find((candidate) => candidate.version === reference.lookupRange) ?? undefined;
19440
+ }
19441
+ function dependencyDescriptorCandidates(input) {
19442
+ const candidates = new Set;
19443
+ candidates.add(`${input.name}@${input.range}`);
19444
+ candidates.add(`${input.reference.lookupName}@${input.reference.lookupRange}`);
19445
+ candidates.add(`${input.name}@npm:${input.range}`);
19446
+ candidates.add(`${input.reference.lookupName}@npm:${input.reference.lookupRange}`);
19447
+ if (input.range.startsWith("npm:")) {
19448
+ const bareRange = input.range.slice("npm:".length);
19449
+ candidates.add(`${input.name}@${bareRange}`);
19450
+ candidates.add(`${input.name}@npm:${bareRange}`);
19451
+ candidates.add(`${input.reference.lookupName}@${bareRange}`);
19452
+ candidates.add(`${input.reference.lookupName}@npm:${bareRange}`);
19453
+ }
19454
+ return [...candidates].filter((candidate) => !candidate.endsWith("@"));
19207
19455
  }
19208
19456
  function walkDependency5(input) {
19209
19457
  if (input.seen.has(input.record.key)) {
@@ -21123,7 +21371,7 @@ var KNOWN_PROJECT_MANIFESTS = [
21123
21371
  "deno.json",
21124
21372
  "deno.jsonc"
21125
21373
  ];
21126
- var SUPPORTED_LOCKFILE_MESSAGE = "Ohrisk currently supports bun.lock, package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, deno.lock, and Yarn v1 yarn.lock.";
21374
+ var SUPPORTED_LOCKFILE_MESSAGE = "Ohrisk currently supports bun.lock, package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, deno.lock, and Yarn classic/Berry yarn.lock.";
21127
21375
  function discoverProject(options = {}) {
21128
21376
  const startDir = path12.resolve(options.cwd ?? process.cwd());
21129
21377
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohrisk",
3
- "version": "0.127.0",
3
+ "version": "0.128.0",
4
4
  "description": "Catch open-source license risk before your PR ships.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@
9
9
  "node": ">=20.0.0"
10
10
  },
11
11
  "bin": {
12
- "ohrisk": "./dist/cli.js"
12
+ "ohrisk": "dist/cli.js"
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",