wyvrnpm 2.12.2 → 2.13.11

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.
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const { normalizeDependencies } = require('../manifest');
7
7
  const { resolveDependencies } = require('../resolve');
8
8
  const { downloadDependencies } = require('../download');
9
+ const { resolveSigningContext } = require('../signing/resolve-context');
9
10
  const { wyvrnFetch } = require('../http-fetch');
10
11
  const { resolveEffectiveConf, cmakeCacheVariables, unflattenConf } = require('../conf');
11
12
  const { resolveBinaryDirRoot } = require('../binary-dir');
@@ -350,6 +351,47 @@ async function install(argv) {
350
351
 
351
352
  const razerDir = path.join(rootDir, 'wyvrn_internal');
352
353
 
354
+ // ── Resolve requestConfigs for per-config dep slicing ─────────────────────
355
+ // Precedence: CLI > wyvrn.local.json > consumer's recipe build.configs.
356
+ // When all three are absent, falls through to null which means "download
357
+ // every config the dep author published" (preserves pre-feature semantics
358
+ // for fat-zip deps; for sliced deps, equivalent to "no override, fetch all").
359
+ let requestConfigs = null;
360
+ const cliRaw = typeof argv.requestConfigs === 'string' ? argv.requestConfigs.trim() : null;
361
+ if (cliRaw) {
362
+ requestConfigs = cliRaw.split(',').map((s) => s.trim()).filter(Boolean);
363
+ if (requestConfigs.length === 0) {
364
+ log.error('--request-configs was empty after parsing');
365
+ process.exit(1);
366
+ }
367
+ } else if (effectiveConfResult.localRequestConfigs) {
368
+ requestConfigs = effectiveConfResult.localRequestConfigs.slice();
369
+ } else if (manifest && manifest.build && (Array.isArray(manifest.build.configs) || typeof manifest.build.configs === 'string')) {
370
+ // Read configs directly from the consumer's manifest. Avoid normalizeRecipe
371
+ // here because it can throw on `${options.X}` references that this caller
372
+ // hasn't resolved yet — request-configs only depends on the configs field.
373
+ const raw = manifest.build.configs;
374
+ requestConfigs = (typeof raw === 'string' ? [raw] : raw)
375
+ .filter((s) => typeof s === 'string' && s.trim())
376
+ .slice();
377
+ if (requestConfigs.length === 0) requestConfigs = null;
378
+ }
379
+ if (requestConfigs) {
380
+ log.info(`Request configs: ${requestConfigs.join(', ')}`);
381
+ }
382
+
383
+ // Per-dep override map (wyvrn.local.json: depRequestConfigs). When a
384
+ // dep is named in this map, its slice selection is the listed configs;
385
+ // otherwise it inherits the global `requestConfigs`. Use case: "fetch
386
+ // all four configs of fmt + zlib, but only MinSizeRel of grpc."
387
+ const depRequestConfigs = effectiveConfResult.localDepRequestConfigs ?? null;
388
+ if (depRequestConfigs && Object.keys(depRequestConfigs).length > 0) {
389
+ const summary = Object.entries(depRequestConfigs)
390
+ .map(([n, c]) => `${n}=[${c.join(',')}]`)
391
+ .join(' ');
392
+ log.info(`Per-dep request configs: ${summary}`);
393
+ }
394
+
353
395
  // Extract auth from the first install source (if any). Prefer
354
396
  // `tokenEnv` over a literal `token` stored in config — closes
355
397
  // EVALUATION.md S8 (no plaintext secret persisted, value is looked up
@@ -360,6 +402,12 @@ async function install(argv) {
360
402
  token: ctx.auth.for(firstInstallSrc).token,
361
403
  buildMode,
362
404
  profileName,
405
+ // S1: forwarded straight through to download.js's verify tail.
406
+ requireSignatures: Boolean(argv.requireSignatures),
407
+ // Per-config slice selection. null = "no override, fetch all available".
408
+ requestConfigs,
409
+ // Per-dep override; null when the overlay omits it.
410
+ depRequestConfigs,
363
411
  };
364
412
 
365
413
  // ── Resolve upload destination up-front when --upload-built is set ──────
@@ -387,6 +435,33 @@ async function install(argv) {
387
435
  downloadOptions.uploadAuth = resolved.uploadAuth;
388
436
  downloadOptions.uploadStats = createUploadStats();
389
437
 
438
+ // S1: resolve signing context for the consumer-upload tail. Same
439
+ // precedence as `wyvrnpm publish` — per-source signing on the
440
+ // resolved upload entry, then defaultSigning, then CLI overrides.
441
+ // When null, the upload is unsigned and downstream consumers warn /
442
+ // abort per the destination registry's mode.json.
443
+ const uploadSourceEntry = (config.publishSources ?? [])
444
+ .find((s) => s.url === resolved.uploadSource || s.name === argv.uploadSource) ?? null;
445
+ let uploadSigningContext = null;
446
+ try {
447
+ const ctxOrSkip = resolveSigningContext(
448
+ {
449
+ noSign: argv.noSign,
450
+ signingKeyEnv: argv.signingKeyEnv,
451
+ signingKeyFile: argv.signingKeyFile,
452
+ },
453
+ config,
454
+ uploadSourceEntry,
455
+ );
456
+ // `skip: true` (--no-sign) is treated identically to "no context"
457
+ // here — the consumer just doesn't sign.
458
+ uploadSigningContext = (ctxOrSkip && !ctxOrSkip.skip) ? ctxOrSkip : null;
459
+ } catch (err) {
460
+ log.error(`upload-built signing config invalid: ${err.message}`);
461
+ process.exit(1);
462
+ }
463
+ downloadOptions.uploadSigningContext = uploadSigningContext;
464
+
390
465
  if (resolved.uploadSource !== (packageSources[0] ?? '')) {
391
466
  log.info(
392
467
  `upload-built: install source(s) and upload source differ —\n` +
@@ -569,7 +644,7 @@ async function install(argv) {
569
644
  // here so CI doesn't take "Done — N installed" at face value when one of
570
645
  // the deps actually failed.
571
646
  const SUCCESS_RESOLVED = new Set([
572
- 'v1', 'v2', 'v2-compat', 'v2-source-build', 'cached', 'link',
647
+ 'v1', 'v2', 'v2-slice', 'v2-compat', 'v2-source-build', 'cached', 'link',
573
648
  ]);
574
649
  const failed = [];
575
650
  for (const [name, entry] of lockEntries ?? []) {
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const ed25519 = require('../signing/ed25519');
7
+ const log = require('../logger');
8
+
9
+ /**
10
+ * `wyvrnpm key gen --out <dir> [--id <publisher-id>]`
11
+ *
12
+ * Generates a fresh Ed25519 keypair and writes:
13
+ * <dir>/<id>.priv.pem — PKCS#8 private key (publisher keeps secret)
14
+ * <dir>/<id>.pub.pem — SPKI PEM public key (paste into trust anchor)
15
+ * <dir>/<id>.pub.b64 — Base64 SPKI DER (the literal value for keys.json)
16
+ *
17
+ * Refuses to overwrite existing files — rotation events are rare and
18
+ * destructive enough that the user should explicitly remove the old keys
19
+ * (or pick a fresh `--id`) rather than have the command stomp them.
20
+ */
21
+ async function gen(argv) {
22
+ const outDir = argv.out;
23
+ const id = argv.id ?? 'wyvrnpm';
24
+ if (!outDir) { log.error('--out <dir> is required'); process.exit(1); }
25
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id)) {
26
+ log.error(`--id must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/ (got "${id}")`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const privPath = path.join(outDir, `${id}.priv.pem`);
31
+ const pubPath = path.join(outDir, `${id}.pub.pem`);
32
+ const b64Path = path.join(outDir, `${id}.pub.b64`);
33
+
34
+ for (const p of [privPath, pubPath, b64Path]) {
35
+ if (fs.existsSync(p)) {
36
+ log.error(`refusing to overwrite existing file: ${p}`);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ fs.mkdirSync(outDir, { recursive: true });
42
+ const k = ed25519.generateKeyPair();
43
+ // Private key permissions: chmod 0600 on POSIX so a stray process or
44
+ // backup tool doesn't accidentally include it. No-op on Windows.
45
+ fs.writeFileSync(privPath, k.privateKeyPem, { mode: 0o600 });
46
+ fs.writeFileSync(pubPath, k.publicKeyPem);
47
+ fs.writeFileSync(b64Path, k.publicKeyB64 + '\n');
48
+
49
+ if (argv.format === 'json') {
50
+ process.stdout.write(JSON.stringify({
51
+ command: 'key gen',
52
+ wyvrnpmVersion: require('../../package.json').version,
53
+ id,
54
+ out: outDir,
55
+ privateKeyFile: privPath,
56
+ publicKeyPem: pubPath,
57
+ publicKeyB64: b64Path,
58
+ publicKey: k.publicKeyB64,
59
+ }, null, 2) + '\n');
60
+ } else {
61
+ log.success(`Generated ed25519 keypair "${id}"`);
62
+ log.info(` private key : ${privPath}`);
63
+ log.info(` public PEM : ${pubPath}`);
64
+ log.info(` public b64 : ${b64Path}`);
65
+ console.log();
66
+ console.log('Add this entry to your registry\'s `.trust/keys.json`:');
67
+ console.log(JSON.stringify({
68
+ id, alg: 'ed25519',
69
+ publicKey: k.publicKeyB64,
70
+ identityHint: '<your.email@example.com>',
71
+ addedAt: new Date().toISOString(),
72
+ retiredAt: null,
73
+ }, null, 2));
74
+ }
75
+ }
76
+
77
+ /**
78
+ * `wyvrnpm key show-pub --in <priv-file>`
79
+ *
80
+ * Re-derives the public key from a private key file. Handy for verifying
81
+ * that a CI-supplied key matches the keyId pinned in `keys.json` before
82
+ * publishing — derives + prints, without ever writing back to disk.
83
+ */
84
+ async function showPub(argv) {
85
+ const inPath = argv.in;
86
+ if (!inPath) { log.error('--in <priv-file> is required'); process.exit(1); }
87
+ if (!fs.existsSync(inPath)) {
88
+ log.error(`private key file not found: ${inPath}`);
89
+ process.exit(1);
90
+ }
91
+ let derived;
92
+ try {
93
+ derived = ed25519.derivePublicKey(fs.readFileSync(inPath, 'utf8'));
94
+ } catch (err) {
95
+ log.error(err.message);
96
+ process.exit(1);
97
+ }
98
+ if (argv.format === 'json') {
99
+ process.stdout.write(JSON.stringify({
100
+ command: 'key show-pub',
101
+ wyvrnpmVersion: require('../../package.json').version,
102
+ in: inPath,
103
+ publicKeyPem: derived.publicKeyPem,
104
+ publicKey: derived.publicKeyB64,
105
+ }, null, 2) + '\n');
106
+ } else {
107
+ process.stdout.write(derived.publicKeyPem);
108
+ process.stdout.write(`\nbase64: ${derived.publicKeyB64}\n`);
109
+ }
110
+ }
111
+
112
+ module.exports = { gen, showPub };