zyrk 0.5.0 → 0.5.2

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 (49) hide show
  1. package/README.md +10 -2
  2. package/VERSION +1 -1
  3. package/dist/commands/deploy.d.ts.map +1 -1
  4. package/dist/commands/deploy.js +7 -11
  5. package/dist/commands/maintenance.d.ts.map +1 -1
  6. package/dist/commands/maintenance.js +22 -2
  7. package/dist/fetch/package.d.ts.map +1 -1
  8. package/dist/fetch/package.js +2 -1
  9. package/dist/fetch/registry.d.ts +5 -0
  10. package/dist/fetch/registry.d.ts.map +1 -1
  11. package/dist/fetch/registry.js +84 -10
  12. package/dist/install/import-npm-workspace.d.ts +2 -1
  13. package/dist/install/import-npm-workspace.d.ts.map +1 -1
  14. package/dist/install/import-npm-workspace.js +34 -12
  15. package/dist/install/import-npm.d.ts +3 -1
  16. package/dist/install/import-npm.d.ts.map +1 -1
  17. package/dist/install/import-npm.js +14 -9
  18. package/dist/install/registry-policy.d.ts +9 -0
  19. package/dist/install/registry-policy.d.ts.map +1 -0
  20. package/dist/install/registry-policy.js +30 -0
  21. package/dist/install/registry.d.ts.map +1 -1
  22. package/dist/install/registry.js +18 -13
  23. package/dist/install/run.d.ts.map +1 -1
  24. package/dist/install/run.js +22 -5
  25. package/dist/install/workspace-graph.d.ts +7 -1
  26. package/dist/install/workspace-graph.d.ts.map +1 -1
  27. package/dist/install/workspace-graph.js +53 -9
  28. package/dist/install/workspace-install.d.ts.map +1 -1
  29. package/dist/install/workspace-install.js +16 -6
  30. package/dist/install/workspace-lock.d.ts +1 -1
  31. package/dist/install/workspace-lock.d.ts.map +1 -1
  32. package/dist/install/workspace-lock.js +5 -1
  33. package/dist/install/workspace-mutation.d.ts.map +1 -1
  34. package/dist/install/workspace-mutation.js +4 -1
  35. package/dist/install/workspace-overrides.d.ts +11 -0
  36. package/dist/install/workspace-overrides.d.ts.map +1 -0
  37. package/dist/install/workspace-overrides.js +67 -0
  38. package/dist/install/workspaces.d.ts.map +1 -1
  39. package/dist/install/workspaces.js +4 -1
  40. package/dist/lockfile/fingerprints.d.ts +1 -0
  41. package/dist/lockfile/fingerprints.d.ts.map +1 -1
  42. package/dist/lockfile/fingerprints.js +16 -1
  43. package/dist/resolve/overrides.d.ts +19 -0
  44. package/dist/resolve/overrides.d.ts.map +1 -0
  45. package/dist/resolve/overrides.js +142 -0
  46. package/dist/resolve/resolver.d.ts +3 -0
  47. package/dist/resolve/resolver.d.ts.map +1 -1
  48. package/dist/resolve/resolver.js +62 -18
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -116,8 +116,10 @@ Default registry and authentication settings are loaded from user/project
116
116
  token placeholders are supported. Credentials are restricted to their configured
117
117
  scheme, host, port, and path; redirects cannot expand that scope. Use
118
118
  `--registry=https://your-registry.example/` on install for an explicit registry.
119
- Mixed scoped routing and disabled TLS verification are rejected before public
120
- lookup. Git sources, non-workspace local sources, and overrides remain unsupported;
119
+ Scoped registries route metadata, exact sources, imports and tool commands by
120
+ the real package name, including aliases. Advisory requests are partitioned by
121
+ registry; private names never fall back to a public endpoint. Disabled TLS
122
+ verification is rejected before lookup. Git sources and non-workspace local sources remain unsupported;
121
123
  they fail explicitly. Optional dependencies incompatible with the selected platform
122
124
  are omitted; missing packages, integrity failures, and other optional failures
123
125
  remain errors. Platform selection is recorded in the lock. macOS, Windows, and
@@ -354,3 +356,9 @@ Keep existing data until an explicit migration is supported.
354
356
 
355
357
  [MIT](LICENSE). If you want to support development,
356
358
  [sponsor the maintainer](https://github.com/sponsors/kingjethro999).
359
+
360
+ Root `package.json` dependency overrides support exact specifications, nested and
361
+ version-qualified selectors, self rules (`.`), and `$direct` references. Direct
362
+ dependency overrides must match the declared specification. Workspace member
363
+ overrides are ignored, matching npm root-only policy. Override rule order is
364
+ recorded in the manifest fingerprint; frozen installs reject policy drift.
package/VERSION CHANGED
@@ -1 +1 @@
1
- v0.5.0
1
+ v0.5.2
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAmEA,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAwEF,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA0ZjB"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAoEA,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAwEF,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAqZjB"}
@@ -1,3 +1,4 @@
1
+ import { installedRegistryConfig } from "../install/registry-policy.js";
1
2
  /** Self-contained runtime deployment and explicit local Docker commands. */
2
3
  import { constants } from "node:fs";
3
4
  import { open, copyFile, chmod, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile, } from "node:fs/promises";
@@ -141,16 +142,14 @@ export async function runDeployCommand(options) {
141
142
  manifestFingerprint(manifest))
142
143
  throw error("ZYRK_LOCK_MISMATCH", "Deployment manifest, lock, and installed generation disagree.");
143
144
  if (resolved.nodes.length) {
144
- const origins = new Set(resolved.nodes.map((node) => node.origin));
145
- if (origins.size !== 1)
146
- throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Mixed-origin deployment requires a validated registry adapter.");
145
+ const registryConfig = installedRegistryConfig(state, resolved.nodes);
147
146
  const patches = await loadProjectPatches(project);
148
147
  const policy = object(manifest.zyrk) ? manifest.zyrk : {};
149
148
  assertFrozenCompatible(doc, {
150
149
  manifest: manifestFingerprint(manifest),
151
150
  scripts: scriptPolicyFingerprint(policy.scripts),
152
151
  config: configFingerprint({
153
- registry: [...origins][0],
152
+ ...registryConfig,
154
153
  linker: "shared",
155
154
  platform: currentPlatform(),
156
155
  ...(Array.isArray(policy.mutablePackages)
@@ -800,15 +799,12 @@ async function exportWorkspace(options, project, manifest, state, out, parent, f
800
799
  const policy = object(manifest.zyrk) ? manifest.zyrk : {};
801
800
  if (locked.doc.zyrk.scripts !== scriptPolicyFingerprint(policy.scripts))
802
801
  throw error("ZYRK_LOCK_MISMATCH", "Workspace deployment build policy changed after installation.");
803
- const origins = new Set(locked.graph.nodes
804
- .filter((n) => n.kind === "registry")
805
- .map((n) => n.origin));
806
- if (origins.size > 1)
807
- throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Mixed-origin workspace export requires a validated registry adapter.");
808
- if (origins.size === 1) {
802
+ const registryNodesForPolicy = locked.graph.nodes.filter((n) => n.kind === "registry");
803
+ if (registryNodesForPolicy.length || state.registryConfig !== undefined) {
804
+ const registryConfig = installedRegistryConfig(state, registryNodesForPolicy);
809
805
  const patches = await loadProjectPatches(project);
810
806
  const expected = configFingerprint({
811
- registry: [...origins][0],
807
+ ...registryConfig,
812
808
  linker: "shared",
813
809
  platform: currentPlatform(),
814
810
  ...(Array.isArray(policy.mutablePackages)
@@ -1 +1 @@
1
- {"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/commands/maintenance.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AASvE,QAAA,MAAM,UAAU,YAAI,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,CAAU,CAAC;AAC5E,KAAK,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;IAChD,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,QAAQ,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC,CAAC;IACH,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAUF,wBAAsB,qBAAqB,CAAC,OAAO,EAAE;IACnD,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;IACtC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,aAAa,GAAG,WAAW,CAAC,CAwOvC"}
1
+ {"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/commands/maintenance.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AASvE,QAAA,MAAM,UAAU,YAAI,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,CAAU,CAAC;AAC5E,KAAK,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;IAChD,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,QAAQ,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC,CAAC;IACH,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAUF,wBAAsB,qBAAqB,CAAC,OAAO,EAAE;IACnD,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;IACtC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,aAAa,GAAG,WAAW,CAAC,CAgQvC"}
@@ -126,6 +126,7 @@ export async function runMaintenanceCommand(options) {
126
126
  scripts: scriptPolicyFingerprint(object(manifest.zyrk) ? manifest.zyrk.scripts : undefined),
127
127
  config: configFingerprint({
128
128
  registry: options.registry.origin,
129
+ registries: options.registry.scopedRegistries,
129
130
  linker: "shared",
130
131
  platform: currentPlatform(),
131
132
  ...(object(manifest.zyrk) && Array.isArray(manifest.zyrk.mutablePackages)
@@ -146,7 +147,7 @@ export async function runMaintenanceCommand(options) {
146
147
  : lockedGraph(lock.doc).nodes;
147
148
  const packages = Object.create(null);
148
149
  for (const node of graphNodes) {
149
- if (node.origin !== options.registry.origin)
150
+ if (node.origin !== options.registry.originFor(node.name))
150
151
  throw new ZyrkError("ZYRK_POLICY_DENIED", "Cannot audit a graph from a different registry origin.", {
151
152
  remedy: "Select the registry recorded by the lockfile before sending dependency names.",
152
153
  });
@@ -158,7 +159,26 @@ export async function runMaintenanceCommand(options) {
158
159
  return base;
159
160
  let response;
160
161
  try {
161
- response = await options.registry.auditAdvisories(packages);
162
+ const groups = new Map();
163
+ for (const [name, versions] of Object.entries(packages)) {
164
+ const client = options.registry.forPackage(name);
165
+ let selected = groups.get(client);
166
+ if (!selected) {
167
+ selected = Object.create(null);
168
+ groups.set(client, selected);
169
+ }
170
+ selected[name] = versions;
171
+ }
172
+ const replies = await Promise.all([...groups].map(async ([client, selected]) => {
173
+ const reply = await client.auditAdvisories(selected);
174
+ if (!object(reply) ||
175
+ Object.keys(reply).some((name) => !Object.hasOwn(selected, name)))
176
+ return null;
177
+ return reply;
178
+ }));
179
+ response = replies.some((reply) => reply === null)
180
+ ? null
181
+ : Object.assign({}, ...replies);
162
182
  }
163
183
  catch {
164
184
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/fetch/package.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGxC,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAA6B,MAAM,eAAe,CAAC;AAE1E;;2EAE2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,IAAI,IAAI,CAAC;IACnB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3C;AAED,wBAAgB,oBAAoB,CAClC,OAAO,GAAE;IACP,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACjB,GACL;IAAE,SAAS,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAAC,KAAK,IAAI,IAAI,CAAA;CAAE,CAuDzE;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,WAAW,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,wEAAwE;IACxE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CA+IxB"}
1
+ {"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/fetch/package.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGxC,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAA6B,MAAM,eAAe,CAAC;AAE1E;;2EAE2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,IAAI,IAAI,CAAC;IACnB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3C;AAED,wBAAgB,oBAAoB,CAClC,OAAO,GAAE;IACP,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACjB,GACL;IAAE,SAAS,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAAC,KAAK,IAAI,IAAI,CAAA;CAAE,CAuDzE;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,WAAW,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,wEAAwE;IACxE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAgJxB"}
@@ -72,7 +72,8 @@ export function createFetchOperation(options = {}) {
72
72
  * Callers resolve the version first; this function never guesses.
73
73
  */
74
74
  export async function fetchAndCommitSource(options) {
75
- const { store, registry, txn, name, version } = options;
75
+ const { store, txn, name, version } = options;
76
+ const registry = options.registry.forPackage(name);
76
77
  const label = `${name}@${version}`;
77
78
  // ---- 1. Metadata: exact version entry for the resolved identity ----
78
79
  options.operation?.checkpoint();
@@ -48,6 +48,7 @@ export interface Packument {
48
48
  modified?: string;
49
49
  }
50
50
  export interface RegistryClientOptions {
51
+ scopedRegistries?: Readonly<Record<string, string>>;
51
52
  /** Registry base URL; must be http(s). Trailing slash optional. */
52
53
  registry: string;
53
54
  userAgent: string;
@@ -83,6 +84,10 @@ export declare class RegistryClient {
83
84
  /** Credential-free registry origin; participates in source identity (§8.1). */
84
85
  get origin(): string;
85
86
  get registry(): string;
87
+ /** Routing always follows the real package name, including npm aliases. */
88
+ forPackage(name: string): RegistryClient;
89
+ originFor(name: string): string;
90
+ get scopedRegistries(): Readonly<Record<string, string>>;
86
91
  /** Fetch the (abbreviated) packument for an exact package name. */
87
92
  packument(name: string, options?: {
88
93
  signal?: AbortSignal;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/fetch/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/fetch/registry.ts"],"names":[],"mappings":"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"}
@@ -22,6 +22,8 @@ export const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
22
22
  /** npm's abbreviated packument media type, used for installs. */
23
23
  const INSTALL_V1_ACCEPT = "application/vnd.npm.install-v1+json";
24
24
  export class RegistryClient {
25
+ #routeBoundaries = [];
26
+ #scopedClients = new Map();
25
27
  #registry;
26
28
  #origin;
27
29
  #userAgent;
@@ -30,7 +32,7 @@ export class RegistryClient {
30
32
  #maxTarballBytes;
31
33
  #maxMetadataBytes;
32
34
  #maxTotalMetadataBytes;
33
- #totalMetadataBytes = 0;
35
+ #metadataBudget = { bytes: 0 };
34
36
  #maxMetadataVersions;
35
37
  #requestDeadlineMs;
36
38
  #signal;
@@ -59,13 +61,51 @@ export class RegistryClient {
59
61
  base.timeout = options.timeoutMs;
60
62
  const parsed = safeTransportOptions(options.fetchOptions ?? {}, registry);
61
63
  this.#fetchOptions = { ...base, ...parsed.transport, strictSSL: true };
62
- this.#credentials = parsed.credentials;
64
+ const boundary = new URL(registry);
65
+ this.#credentials = parsed.credentials.filter(({ scope }) => scope.origin === boundary.origin &&
66
+ (scope.pathname.startsWith(boundary.pathname) ||
67
+ boundary.pathname.startsWith(scope.pathname)));
63
68
  this.#maxMetadataBytes = positiveLimit(options.maxMetadataBytes ?? 32 * 1024 * 1024, "maxMetadataBytes");
64
69
  this.#maxTotalMetadataBytes = positiveLimit(options.maxTotalMetadataBytes ?? 128 * 1024 * 1024, "maxTotalMetadataBytes");
65
70
  this.#maxMetadataVersions = positiveLimit(options.maxMetadataVersions ?? 100_000, "maxMetadataVersions");
66
71
  this.#requestDeadlineMs = positiveLimit(options.requestDeadlineMs ?? 120_000, "requestDeadlineMs");
67
72
  positiveLimit(this.#maxTarballBytes, "maxTarballBytes");
68
73
  this.#signal = options.signal;
74
+ const { scopedRegistries, ...baseOptions } = options;
75
+ for (const [scope, selected] of Object.entries(scopedRegistries ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
76
+ if (!/^@[a-z0-9][a-z0-9._-]*$/.test(scope))
77
+ throw new ZyrkError("ZYRK_POLICY_DENIED", "Invalid scoped registry name.", { remedy: "Use an npm @scope registry setting." });
78
+ const { headers, ...childFetchOptions } = options.fetchOptions ?? {};
79
+ const client = new RegistryClient({
80
+ ...baseOptions,
81
+ fetchOptions: childFetchOptions,
82
+ registry: selected,
83
+ });
84
+ // Raw Authorization headers are scoped once to the original registry.
85
+ // Cloning must never reinterpret them as credentials for a child host.
86
+ for (const authorization of Object.values((headers ?? {}))) {
87
+ const originalScope = new URL(registry), childScope = new URL(client.registry);
88
+ if (originalScope.origin === childScope.origin &&
89
+ (originalScope.pathname.startsWith(childScope.pathname) ||
90
+ childScope.pathname.startsWith(originalScope.pathname)))
91
+ client.#credentials.push({ scope: originalScope, authorization });
92
+ }
93
+ client.#credentials.sort((a, b) => b.scope.pathname.length - a.scope.pathname.length);
94
+ client.#metadataBudget = this.#metadataBudget;
95
+ this.#scopedClients.set(scope, client);
96
+ }
97
+ // A containing public registry must not borrow a credential belonging to a
98
+ // narrower configured private registry on the same host. Explicit broad
99
+ // credentials retain their declared scope, including use by child routes.
100
+ const clients = [this, ...this.#scopedClients.values()];
101
+ const boundaries = clients.map((client) => new URL(client.registry));
102
+ for (const client of clients) {
103
+ const own = new URL(client.registry);
104
+ client.#routeBoundaries = boundaries;
105
+ client.#credentials = client.#credentials.filter(({ scope }) => !boundaries.some((boundary) => boundary.origin === scope.origin &&
106
+ boundary.pathname.length > own.pathname.length &&
107
+ scope.pathname.startsWith(boundary.pathname)));
108
+ }
69
109
  }
70
110
  /** Credential-free registry origin; participates in source identity (§8.1). */
71
111
  get origin() {
@@ -74,9 +114,24 @@ export class RegistryClient {
74
114
  get registry() {
75
115
  return this.#registry;
76
116
  }
117
+ /** Routing always follows the real package name, including npm aliases. */
118
+ forPackage(name) {
119
+ validateRegistryPackageName(name);
120
+ return name.startsWith("@")
121
+ ? (this.#scopedClients.get(name.slice(0, name.indexOf("/"))) ?? this)
122
+ : this;
123
+ }
124
+ originFor(name) {
125
+ return this.forPackage(name).origin;
126
+ }
127
+ get scopedRegistries() {
128
+ return Object.fromEntries([...this.#scopedClients].map(([scope, client]) => [scope, client.origin]));
129
+ }
77
130
  /** Fetch the (abbreviated) packument for an exact package name. */
78
131
  async packument(name, options = {}) {
79
- validateRegistryPackageName(name);
132
+ const selected = this.forPackage(name);
133
+ if (selected !== this)
134
+ return selected.packument(name, options);
80
135
  const url = `${this.#registry}${escapePackageName(name)}`;
81
136
  this.#logger?.debug("fetching packument", { name, url });
82
137
  let raw;
@@ -96,6 +151,14 @@ export class RegistryClient {
96
151
  versions.some((version) => typeof version !== "string"))
97
152
  throw new ZyrkError("ZYRK_POLICY_DENIED", "Invalid registry advisory package versions.");
98
153
  }
154
+ const routes = new Set(Object.keys(packages).map((name) => this.forPackage(name)));
155
+ if (routes.size > 1)
156
+ throw new ZyrkError("ZYRK_POLICY_DENIED", "Mixed-registry audit requests must be partitioned by registry.", {
157
+ remedy: "Audit each selected registry independently so private names are never disclosed across origins.",
158
+ });
159
+ const selected = [...routes][0];
160
+ if (selected && selected !== this)
161
+ return selected.auditAdvisories(packages, options);
99
162
  const body = JSON.stringify(packages);
100
163
  if (Buffer.byteLength(body) > this.#maxMetadataBytes)
101
164
  throw resourceLimit("advisory request bytes", this.#maxMetadataBytes, "maxMetadataBytes");
@@ -107,10 +170,10 @@ export class RegistryClient {
107
170
  }
108
171
  }
109
172
  #countMetadataBytes(bytes, responseBytes) {
110
- this.#totalMetadataBytes += bytes;
173
+ this.#metadataBudget.bytes += bytes;
111
174
  if (responseBytes > this.#maxMetadataBytes)
112
175
  throw resourceLimit("metadata bytes", this.#maxMetadataBytes, "maxMetadataBytes");
113
- if (this.#totalMetadataBytes > this.#maxTotalMetadataBytes)
176
+ if (this.#metadataBudget.bytes > this.#maxTotalMetadataBytes)
114
177
  throw resourceLimit("total metadata bytes", this.#maxTotalMetadataBytes, "maxTotalMetadataBytes");
115
178
  }
116
179
  async #readMetadata(response) {
@@ -226,6 +289,15 @@ export class RegistryClient {
226
289
  throw mapFetchError(error, `download tarball for ${options.label}`);
227
290
  }
228
291
  }
292
+ #insideAdvisoryBoundary(url) {
293
+ const registryScope = new URL(this.#registry);
294
+ return (url.origin === registryScope.origin &&
295
+ url.pathname.startsWith(registryScope.pathname) &&
296
+ !/%(?:2f|5c|2e)/i.test(url.pathname) &&
297
+ !this.#routeBoundaries.some((boundary) => boundary.origin === url.origin &&
298
+ boundary.pathname.length > registryScope.pathname.length &&
299
+ url.pathname.startsWith(boundary.pathname)));
300
+ }
229
301
  async #withRequest(url, accept, consume, signal, request = {}) {
230
302
  const controller = new AbortController();
231
303
  let active;
@@ -246,6 +318,10 @@ export class RegistryClient {
246
318
  cancel();
247
319
  try {
248
320
  let current = parseTarballUrl(url, "registry request");
321
+ if (request.method === "POST" && !this.#insideAdvisoryBoundary(current))
322
+ throw new ZyrkError("ZYRK_FETCH_REFUSED", "Registry advisory endpoint overlaps another configured registry or has an ambiguous path.", {
323
+ remedy: "Use a distinct advisory endpoint within the selected registry's own route.",
324
+ });
249
325
  let crossedBoundary = false;
250
326
  for (let hop = 0; hop <= 10; hop++) {
251
327
  if (controller.signal.aborted)
@@ -317,10 +393,8 @@ export class RegistryClient {
317
393
  if (current.protocol === "https:" && next.protocol !== "https:")
318
394
  throw new ZyrkError("ZYRK_FETCH_REFUSED", "Registry HTTPS downgrade redirect refused.", { remedy: "Use HTTPS for every registry redirect." });
319
395
  if (request.method === "POST") {
320
- const registryScope = new URL(this.#registry);
321
396
  if (![307, 308].includes(active.status) ||
322
- next.origin !== registryScope.origin ||
323
- !next.pathname.startsWith(registryScope.pathname))
397
+ !this.#insideAdvisoryBoundary(next))
324
398
  throw new ZyrkError("ZYRK_FETCH_REFUSED", "Registry advisory redirect crosses its authorized endpoint boundary or changes POST semantics.", {
325
399
  remedy: "Configure the advisory service directly within the selected registry; private package names are never forwarded outside that boundary.",
326
400
  });
@@ -421,8 +495,8 @@ export function validateRegistryPackageName(name) {
421
495
  // eslint-disable-next-line no-control-regex
422
496
  if (/[\u0000-\u0020\u007f]/.test(name))
423
497
  bad("control or whitespace character");
424
- if (!/^(?:@[^/@]+\/)?[^/@]+$/.test(name))
425
- bad("shape is not `name` or `@scope/name`");
498
+ if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name))
499
+ bad("name must use supported lowercase npm package characters");
426
500
  for (const segment of name.split("/")) {
427
501
  if (segment === "." || segment === "..")
428
502
  bad("path-like segment");
@@ -1,5 +1,6 @@
1
1
  import type { NpmLockDoc } from "../lockfile/types.ts";
2
2
  import type { WorkspaceProject } from "./workspaces.ts";
3
3
  import type { WorkspaceGraph } from "./workspace-graph.ts";
4
- export declare function importNpmWorkspaceLock(doc: NpmLockDoc, project: WorkspaceProject, origin: string): WorkspaceGraph;
4
+ /** Import stays pure while live-node scope refinement converges. */
5
+ export declare function importNpmWorkspaceLock(doc: NpmLockDoc, project: WorkspaceProject, origin: string | ((name: string) => string)): WorkspaceGraph;
5
6
  //# sourceMappingURL=import-npm-workspace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"import-npm-workspace.d.ts","sourceRoot":"","sources":["../../src/install/import-npm-workspace.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAQvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,sBAAsB,CAAC;AAE9B,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,MAAM,GACb,cAAc,CAyThB"}
1
+ {"version":3,"file":"import-npm-workspace.d.ts","sourceRoot":"","sources":["../../src/install/import-npm-workspace.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAQvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,sBAAsB,CAAC;AA0V9B,oEAAoE;AACpE,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,GAC1C,cAAc,CAWhB"}
@@ -1,5 +1,6 @@
1
1
  /** Translate npm's physical virtual tree without selecting new registry versions. */
2
2
  import * as path from "node:path";
3
+ import { workspaceOverrides, refineWorkspaceOverride, WorkspaceOverrideRestart, } from "./workspace-overrides.js";
3
4
  import { satisfies } from "semver";
4
5
  import { validateLockDoc } from "../lockfile/read.js";
5
6
  import { sha512Hex } from "../util/hash.js";
@@ -8,7 +9,7 @@ import { importNpmLock } from "./import-npm.js";
8
9
  import { lockMismatch, cleanUrl } from "./lock.js";
9
10
  import { ZyrkError } from "../errors.js";
10
11
  import { assertWorkspacePlatform } from "./workspace-graph.js";
11
- export function importNpmWorkspaceLock(doc, project, origin) {
12
+ function importNpmWorkspaceLockPass(doc, project, origin, overrideState) {
12
13
  if (!doc ||
13
14
  typeof doc !== "object" ||
14
15
  Array.isArray(doc) ||
@@ -62,9 +63,11 @@ export function importNpmWorkspaceLock(doc, project, origin) {
62
63
  if (doc.zyrk ||
63
64
  ![2, 3].includes(doc.lockfileVersion ?? 0) ||
64
65
  !doc.packages ||
65
- !cleanUrl(origin))
66
+ (typeof origin === "string" && !cleanUrl(origin)))
66
67
  throw lockMismatch("workspace adoption needs an unannotated npm v2/v3 lock");
67
68
  const packages = doc.packages;
69
+ const overrides = overrideState.policy;
70
+ const contexts = new Map(overrideState.contexts);
68
71
  const graph = { nodes: [], importers: {}, omissions: [] };
69
72
  const locations = new Map();
70
73
  const local = new Map(project.importers.map((i) => [i.id === "." ? "" : i.id, i]));
@@ -131,7 +134,10 @@ export function importNpmWorkspaceLock(doc, project, origin) {
131
134
  }
132
135
  };
133
136
  const populated = new Set();
134
- const load = (location, name, spec, from) => {
137
+ const load = (location, name, spec, from, parentContext) => {
138
+ const selectedOverride = overrides.select(parentContext, name, spec);
139
+ spec = selectedOverride.spec;
140
+ const contextKey = `${location}\0${selectedOverride.context.id}`;
135
141
  const entry = packages[location];
136
142
  if (entry.link === true) {
137
143
  const target = locations.get(entry.resolved);
@@ -143,6 +149,7 @@ export function importNpmWorkspaceLock(doc, project, origin) {
143
149
  (!["*", "^", "~"].includes(range) &&
144
150
  !satisfies(target.version, range)))
145
151
  throw lockMismatch("npm workspace link disagrees with requirement");
152
+ refineWorkspaceOverride(overrideState.contexts, target.key, selectedOverride.context);
146
153
  return target;
147
154
  }
148
155
  // Reuse the strict single-selection adapter's archive, alias, source and
@@ -155,12 +162,12 @@ export function importNpmWorkspaceLock(doc, project, origin) {
155
162
  lockfileVersion: 3,
156
163
  packages: { "": {}, [`node_modules/${name}`]: shallow },
157
164
  }, { [name]: spec }, origin).nodes[0];
158
- let node = locations.get(location);
165
+ let node = locations.get(contextKey);
159
166
  if (!node) {
160
167
  const { peerBindings: _bindings, ...source } = selected;
161
168
  node = {
162
169
  ...source,
163
- key: `${selected.name}@${selected.version}#${sha512Hex(location).slice(0, 16)}`,
170
+ key: `${selected.name}@${selected.version}#${sha512Hex(selectedOverride.context.id ? contextKey : location).slice(0, 16)}`,
164
171
  kind: "registry",
165
172
  edges: [],
166
173
  peers: {},
@@ -168,25 +175,26 @@ export function importNpmWorkspaceLock(doc, project, origin) {
168
175
  };
169
176
  assertWorkspacePlatform({ nodes: [node], importers: {}, omissions: [] });
170
177
  graph.nodes.push(node);
171
- locations.set(location, node);
178
+ locations.set(contextKey, node);
179
+ contexts.set(node.key, selectedOverride.context);
172
180
  }
173
181
  try {
174
182
  populate(location, node);
175
183
  }
176
184
  catch (cause) {
177
- populated.delete(location);
178
- locations.delete(location);
185
+ populated.delete(node.key);
186
+ locations.delete(contextKey);
179
187
  graph.nodes = graph.nodes.filter((n) => n !== node);
180
188
  throw cause;
181
189
  }
182
190
  return node;
183
191
  };
184
192
  const populate = (location, node) => {
185
- if (populated.has(location))
193
+ if (populated.has(node.key))
186
194
  return;
187
195
  if (populated.size >= 10000)
188
196
  throw lockMismatch("npm import resource limit exceeded");
189
- populated.add(location);
197
+ populated.add(node.key);
190
198
  const entry = packages[location];
191
199
  const requirements = new Map();
192
200
  for (const [field, kind] of [
@@ -227,7 +235,7 @@ export function importNpmWorkspaceLock(doc, project, origin) {
227
235
  }
228
236
  let target;
229
237
  try {
230
- target = load(targetLocation, name, requirement.spec, location);
238
+ target = load(targetLocation, name, requirement.spec, location, contexts.get(node.key));
231
239
  }
232
240
  catch (cause) {
233
241
  if (requirement.kind === "optional" &&
@@ -259,7 +267,7 @@ export function importNpmWorkspaceLock(doc, project, origin) {
259
267
  node.peers[name] = null;
260
268
  continue;
261
269
  }
262
- const target = load(targetLocation, name, spec, location);
270
+ const target = load(targetLocation, name, spec, location, contexts.get(node.key));
263
271
  node.peers[name] = target.key;
264
272
  if (!node.edges.some((edge) => edge.name === name))
265
273
  node.edges.push({ name, target: target.key, kind: "peer", spec });
@@ -281,3 +289,17 @@ export function importNpmWorkspaceLock(doc, project, origin) {
281
289
  graph.nodes.sort((a, b) => a.key.localeCompare(b.key));
282
290
  return graph;
283
291
  }
292
+ /** Import stays pure while live-node scope refinement converges. */
293
+ export function importNpmWorkspaceLock(doc, project, origin) {
294
+ const state = workspaceOverrides(project), expires = Date.now() + 30000;
295
+ for (let round = 0; round < 128 && Date.now() < expires; round++) {
296
+ try {
297
+ return importNpmWorkspaceLockPass(doc, project, origin, state);
298
+ }
299
+ catch (cause) {
300
+ if (!(cause instanceof WorkspaceOverrideRestart))
301
+ throw cause;
302
+ }
303
+ }
304
+ throw lockMismatch("npm workspace override import resource limit exceeded");
305
+ }
@@ -1,4 +1,6 @@
1
+ /** Adopt npm v2/v3 exact selections without asking the registry to pick versions. */
2
+ import { type OverridePolicy } from "../resolve/overrides.ts";
1
3
  import type { NpmLockDoc } from "../lockfile/types.ts";
2
4
  import type { FinalizedGraph } from "../resolve/resolver.ts";
3
- export declare function importNpmLock(doc: NpmLockDoc, requirements: Record<string, string>, origin: string): FinalizedGraph;
5
+ export declare function importNpmLock(doc: NpmLockDoc, requirements: Record<string, string>, origin: string | ((name: string) => string), overrides?: OverridePolicy): FinalizedGraph;
4
6
  //# sourceMappingURL=import-npm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"import-npm.d.ts","sourceRoot":"","sources":["../../src/install/import-npm.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EACV,cAAc,EAEf,MAAM,wBAAwB,CAAC;AAKhC,wBAAgB,aAAa,CAC3B,GAAG,EAAE,UAAU,EACf,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,MAAM,EAAE,MAAM,GACb,cAAc,CA0KhB"}
1
+ {"version":3,"file":"import-npm.d.ts","sourceRoot":"","sources":["../../src/install/import-npm.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EACV,cAAc,EAEf,MAAM,wBAAwB,CAAC;AAKhC,wBAAgB,aAAa,CAC3B,GAAG,EAAE,UAAU,EACf,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,EAC3C,SAAS,GAAE,cAAwC,GAClD,cAAc,CAqLhB"}
@@ -1,11 +1,12 @@
1
1
  /** Adopt npm v2/v3 exact selections without asking the registry to pick versions. */
2
+ import { createRootOverrides, } from "../resolve/overrides.js";
2
3
  import { validateLockDoc } from "../lockfile/read.js";
3
4
  import { canonicalStrongSri } from "../util/integrity.js";
4
5
  import { valid, satisfies } from "semver";
5
6
  import { parseRequirement } from "../resolve/spec.js";
6
7
  import { sourceIdOf, sha512Hex } from "../util/hash.js";
7
8
  import { cleanUrl, lockMismatch, packageName } from "./lock.js";
8
- export function importNpmLock(doc, requirements, origin) {
9
+ export function importNpmLock(doc, requirements, origin, overrides = createRootOverrides({})) {
9
10
  validateLockDoc(doc, "package-lock.json");
10
11
  const packages = doc.packages;
11
12
  if (!packages || !packages[""])
@@ -26,12 +27,16 @@ export function importNpmLock(doc, requirements, origin) {
26
27
  : "";
27
28
  }
28
29
  };
29
- const load = (location, edgeName, spec) => {
30
- const existing = locations.get(location);
30
+ const load = (location, edgeName, spec, parentContext) => {
31
+ const selected = overrides.select(parentContext, edgeName, spec);
32
+ spec = selected.spec;
33
+ const contextKey = `${location}\0${selected.context.id}`;
34
+ const existing = locations.get(contextKey);
31
35
  const entry = packages[location];
32
36
  if (!entry || entry.link === true || entry.inBundle === true)
33
37
  throw lockMismatch("npm lock contains an unsupported linked or bundled selection");
34
38
  const request = parseRequirement(edgeName, spec);
39
+ const selectedOrigin = typeof origin === "string" ? origin : origin(request.realName);
35
40
  const name = typeof entry.name === "string" ? entry.name : request.realName;
36
41
  const integrity = typeof entry.integrity === "string"
37
42
  ? canonicalStrongSri(entry.integrity)
@@ -52,16 +57,16 @@ export function importNpmLock(doc, requirements, origin) {
52
57
  if (existing)
53
58
  return existing;
54
59
  const node = {
55
- key: `${name}@${entry.version}#${sha512Hex(location).slice(0, 16)}`,
60
+ key: `${name}@${entry.version}#${sha512Hex(selected.context.id ? contextKey : location).slice(0, 16)}`,
56
61
  name,
57
62
  version: entry.version,
58
- origin,
63
+ origin: selectedOrigin,
59
64
  integrity,
60
65
  sourceId: sourceIdOf({
61
66
  sourceType: "registry",
62
67
  name,
63
68
  version: entry.version,
64
- origin,
69
+ origin: selectedOrigin,
65
70
  integrity,
66
71
  }),
67
72
  tarballUrl: entry.resolved,
@@ -95,7 +100,7 @@ export function importNpmLock(doc, requirements, origin) {
95
100
  node.platform[field] = value;
96
101
  }
97
102
  }
98
- locations.set(location, node);
103
+ locations.set(contextKey, node);
99
104
  const dependencies = Object.create(null);
100
105
  for (const [group, kind] of [
101
106
  ["dependencies", "prod"],
@@ -127,7 +132,7 @@ export function importNpmLock(doc, requirements, origin) {
127
132
  }
128
133
  throw lockMismatch("npm lock is missing a required dependency");
129
134
  }
130
- const target = load(targetLocation, dep, requirement.spec);
135
+ const target = load(targetLocation, dep, requirement.spec, selected.context);
131
136
  node.edges.push({
132
137
  name: dep,
133
138
  target: target.key,
@@ -147,7 +152,7 @@ export function importNpmLock(doc, requirements, origin) {
147
152
  const location = find("", name);
148
153
  if (!location)
149
154
  throw lockMismatch("npm lock is missing a direct selection");
150
- return { name, target: load(location, name, spec).key };
155
+ return { name, target: load(location, name, spec, overrides.root).key };
151
156
  });
152
157
  return {
153
158
  nodes: [...locations.values()].sort((a, b) => a.key.localeCompare(b.key)),
@@ -0,0 +1,9 @@
1
+ export type InstalledRegistryConfig = {
2
+ registry: string;
3
+ registries?: Readonly<Record<string, string>>;
4
+ };
5
+ export declare function installedRegistryConfig(state: Record<string, unknown>, nodes: readonly {
6
+ name: string;
7
+ origin: string;
8
+ }[]): InstalledRegistryConfig;
9
+ //# sourceMappingURL=registry-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-policy.d.ts","sourceRoot":"","sources":["../../src/install/registry-policy.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC/C,CAAC;AACF,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,GACjD,uBAAuB,CAqCzB"}