wyvrnpm 2.3.2 → 2.8.1
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/README.md +465 -19
- package/bin/wyvrn.js +175 -9
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/macros.cmake +51 -0
- package/cmake/variables.cmake +13 -3
- package/package.json +1 -1
- package/src/auth.js +66 -0
- package/src/build/cache.js +196 -0
- package/src/build/index.js +23 -2
- package/src/build/msvc-env.js +50 -7
- package/src/build/recipe.js +36 -3
- package/src/commands/add.js +140 -0
- package/src/commands/build.js +111 -7
- package/src/commands/cache.js +189 -0
- package/src/commands/configure.js +15 -4
- package/src/commands/init.js +36 -0
- package/src/commands/install-skill.js +8 -0
- package/src/commands/install.js +271 -5
- package/src/commands/profile.js +70 -7
- package/src/commands/publish.js +41 -4
- package/src/commands/show.js +14 -4
- package/src/compat.js +21 -9
- package/src/conf/index.js +391 -0
- package/src/conf/namespaces.js +94 -0
- package/src/download.js +136 -148
- package/src/logger.js +30 -2
- package/src/manifest.js +34 -7
- package/src/profile.js +148 -12
- package/src/resolve.js +47 -7
- package/src/settings-overrides.js +152 -0
- package/src/toolchain/index.js +71 -0
- package/src/toolchain/presets.js +108 -31
- package/src/toolchain/template.cmake +11 -0
- package/src/upload-built.js +11 -2
- package/src/zip-safe.js +52 -0
package/src/download.js
CHANGED
|
@@ -12,6 +12,7 @@ const { getProvider } = require('./providers');
|
|
|
12
12
|
const { evaluateCompat, pickBestCandidate, summarizeReasons } = require('./compat');
|
|
13
13
|
const { buildFromSource } = require('./build');
|
|
14
14
|
const { uploadSourceBuiltArtefact } = require('./upload-built');
|
|
15
|
+
const { assertAllSafeZipEntryNames } = require('./zip-safe');
|
|
15
16
|
const log = require('./logger');
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -59,17 +60,11 @@ async function tryDownloadFromSources(name, version, packageSources, platform, h
|
|
|
59
60
|
//
|
|
60
61
|
// PLAN-BUILD-MISSING phase 1: strict-by-default resolution.
|
|
61
62
|
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// profile" from "v2 doesn't know this package at all").
|
|
68
|
-
//
|
|
69
|
-
// tryV2Compat — the old OS+arch compatibility fallback. Now dead code on
|
|
70
|
-
// the default path; only reachable via the escape-hatch env
|
|
71
|
-
// var WYVRNPM_ALLOW_LOOSE_COMPAT=1 (deprecated — prints a
|
|
72
|
-
// warning when used). Will be deleted in a future release.
|
|
63
|
+
// tryV2Exact attempts the exact profile-hash match and, when it misses,
|
|
64
|
+
// returns the versions index so the caller can distinguish "v2 knows this
|
|
65
|
+
// package but not your profile" from "v2 doesn't know this package at all".
|
|
66
|
+
// The declarative compat path (findCompatibleBuild) and the source-build
|
|
67
|
+
// path consume that index.
|
|
73
68
|
//
|
|
74
69
|
|
|
75
70
|
/**
|
|
@@ -94,9 +89,43 @@ async function tryDownloadFromSources(name, version, packageSources, platform, h
|
|
|
94
89
|
* result?: { contentSha256: string|null, gitSha: string|null, gitRepo: string|null, profileHash: string, source: string },
|
|
95
90
|
* indexFoundInSource: string|null, // which source returned a versions index (null = none did)
|
|
96
91
|
* availableProfiles: string[], // profile hashes the registry HAS, for the error message
|
|
97
|
-
* versionsIndex?: object, // pre-fetched index,
|
|
92
|
+
* versionsIndex?: object, // pre-fetched index, reused by findCompatibleBuild + source-build
|
|
98
93
|
* }>}
|
|
99
94
|
*/
|
|
95
|
+
/**
|
|
96
|
+
* Locate a v2 versions.json index for (name, version) across the configured
|
|
97
|
+
* sources WITHOUT attempting to download the exact-match artefact. Used by
|
|
98
|
+
* the `--build=always` path (F9) where the registry index is needed purely
|
|
99
|
+
* to look up `gitRepo` / `gitSha` for source-build.
|
|
100
|
+
*
|
|
101
|
+
* Returns the same shape as tryV2Exact's miss case.
|
|
102
|
+
*
|
|
103
|
+
* @param {{ name: string, version: string }} dep
|
|
104
|
+
* @param {string[]} packageSources
|
|
105
|
+
* @param {object} authOptions { awsProfile?, token? }
|
|
106
|
+
*/
|
|
107
|
+
async function findV2Index(dep, packageSources, authOptions = {}) {
|
|
108
|
+
const { name, version } = dep;
|
|
109
|
+
const { awsProfile, token } = authOptions;
|
|
110
|
+
|
|
111
|
+
for (const source of packageSources) {
|
|
112
|
+
let provider;
|
|
113
|
+
try { provider = getProvider(source); } catch { continue; }
|
|
114
|
+
|
|
115
|
+
const idx = await provider.v2GetVersionsIndex({ source, name, awsProfile, token });
|
|
116
|
+
if (idx?.versions?.[version]?.profiles) {
|
|
117
|
+
return {
|
|
118
|
+
found: false,
|
|
119
|
+
indexFoundInSource: source,
|
|
120
|
+
versionsIndex: idx,
|
|
121
|
+
availableProfiles: Object.keys(idx.versions[version].profiles),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { found: false, indexFoundInSource: null, versionsIndex: null, availableProfiles: [] };
|
|
127
|
+
}
|
|
128
|
+
|
|
100
129
|
async function tryV2Exact(dep, packageSources, destZipPath, timeoutMs, authOptions = {}) {
|
|
101
130
|
const { name, version, profileHash } = dep;
|
|
102
131
|
const { awsProfile, token } = authOptions;
|
|
@@ -209,7 +238,7 @@ async function findCompatibleBuild({ name, version, consumerProfile, consumerOpt
|
|
|
209
238
|
: packageProfile);
|
|
210
239
|
|
|
211
240
|
const rawCompat = meta.compatibility ?? null;
|
|
212
|
-
const { compatible, reasons } = evaluateCompat(consumerFull, packageFull, rawCompat);
|
|
241
|
+
const { compatible, reasons } = evaluateCompat(consumerFull, packageFull, rawCompat, meta.kind);
|
|
213
242
|
if (compatible) {
|
|
214
243
|
candidates.push({ profileHash, packageProfile, reasons, buildEntry, meta });
|
|
215
244
|
}
|
|
@@ -229,69 +258,20 @@ async function findCompatibleBuild({ name, version, consumerProfile, consumerOpt
|
|
|
229
258
|
};
|
|
230
259
|
}
|
|
231
260
|
|
|
232
|
-
/**
|
|
233
|
-
* LEGACY compat fallback — scan versions.json for a build with the same OS +
|
|
234
|
-
* arch as the consumer's profile and use it. Reachable only via
|
|
235
|
-
* WYVRNPM_ALLOW_LOOSE_COMPAT=1. Prints a deprecation warning whenever it
|
|
236
|
-
* succeeds. Will be removed after one release cycle.
|
|
237
|
-
*
|
|
238
|
-
* @param {{ name: string, version: string, profile: import('./profile').WyvrnProfile }} dep
|
|
239
|
-
* @param {object} versionsIndex pre-fetched from tryV2Exact
|
|
240
|
-
* @param {string} source source URL that returned the index
|
|
241
|
-
* @param {string} destZipPath
|
|
242
|
-
* @param {number} timeoutMs
|
|
243
|
-
* @param {object} authOptions
|
|
244
|
-
* @returns {Promise<object|null>} same shape as tryV2Exact.result, or null
|
|
245
|
-
*/
|
|
246
|
-
async function tryV2Compat(dep, versionsIndex, source, destZipPath, timeoutMs, authOptions = {}) {
|
|
247
|
-
const { name, version, profile } = dep;
|
|
248
|
-
const { awsProfile, token } = authOptions;
|
|
249
|
-
|
|
250
|
-
let provider;
|
|
251
|
-
try { provider = getProvider(source); } catch { return null; }
|
|
252
|
-
|
|
253
|
-
const versionEntry = versionsIndex?.versions?.[version];
|
|
254
|
-
if (!versionEntry?.profiles) return null;
|
|
255
|
-
|
|
256
|
-
for (const [ph, buildEntry] of Object.entries(versionEntry.profiles)) {
|
|
257
|
-
const bp = buildEntry.buildSettings ?? buildEntry.profile;
|
|
258
|
-
if (!bp) continue;
|
|
259
|
-
if (bp.os !== profile.os) continue;
|
|
260
|
-
if (bp.arch !== profile.arch) continue;
|
|
261
|
-
|
|
262
|
-
log.warn(
|
|
263
|
-
`⚠ DEPRECATED loose-compat fallback (WYVRNPM_ALLOW_LOOSE_COMPAT=1):\n` +
|
|
264
|
-
` ${name}@${version} — using build [${ph}] (${bp.compiler} ${bp['compiler.version']})\n` +
|
|
265
|
-
` cppstd/runtime may differ from your profile. This fallback will be removed in the next release.`,
|
|
266
|
-
);
|
|
267
|
-
const ok = await provider.v2DownloadZip({ source, name, version, profileHash: ph, awsProfile, token }, destZipPath, timeoutMs);
|
|
268
|
-
if (ok) {
|
|
269
|
-
return {
|
|
270
|
-
contentSha256: buildEntry.contentSha256 ?? null,
|
|
271
|
-
gitSha: versionEntry.source?.gitSha ?? null,
|
|
272
|
-
gitRepo: versionEntry.source?.gitRepo ?? null,
|
|
273
|
-
profileHash: ph,
|
|
274
|
-
source,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
261
|
/**
|
|
282
262
|
* Build the structured error message shown when a package exists in the v2
|
|
283
263
|
* registry but no build matches the consumer's profileHash.
|
|
284
264
|
*/
|
|
285
|
-
function buildStrictMissMessage({ name, version, profileHash, availableProfiles, buildMode
|
|
265
|
+
function buildStrictMissMessage({ name, version, profileHash, availableProfiles, buildMode }) {
|
|
286
266
|
const lines = [
|
|
287
267
|
`no v2 build of ${name}@${version} matches profile [${profileHash}]`,
|
|
288
268
|
availableProfiles.length > 0
|
|
289
269
|
? ` Available builds on the registry: ${availableProfiles.map((p) => `[${p}]`).join(' ')}`
|
|
290
270
|
: ` The registry lists no builds for this version.`,
|
|
291
271
|
];
|
|
292
|
-
if (buildMode === 'missing') {
|
|
272
|
+
if (buildMode === 'missing' || buildMode === 'always') {
|
|
293
273
|
lines.push(
|
|
294
|
-
` --build
|
|
274
|
+
` --build=${buildMode} source-build was attempted but did not produce a usable binary. ` +
|
|
295
275
|
`See the error above for details (missing git metadata, missing "build" section in source manifest, or compile failure).`,
|
|
296
276
|
);
|
|
297
277
|
} else {
|
|
@@ -299,11 +279,6 @@ function buildStrictMissMessage({ name, version, profileHash, availableProfiles,
|
|
|
299
279
|
` Fix: publish a build for your profile, switch to a compatible one, or rerun with --build=missing to source-build.`,
|
|
300
280
|
);
|
|
301
281
|
}
|
|
302
|
-
if (!loose) {
|
|
303
|
-
lines.push(
|
|
304
|
-
` Escape hatch: WYVRNPM_ALLOW_LOOSE_COMPAT=1 (deprecated — accepts same OS+arch with possibly-different cppstd/runtime).`,
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
282
|
return lines.join('\n');
|
|
308
283
|
}
|
|
309
284
|
|
|
@@ -313,8 +288,17 @@ function buildStrictMissMessage({ name, version, profileHash, availableProfiles,
|
|
|
313
288
|
|
|
314
289
|
async function extractZip(destZipPath, extractDir) {
|
|
315
290
|
const zip = new StreamZip.async({ file: destZipPath });
|
|
316
|
-
|
|
317
|
-
|
|
291
|
+
try {
|
|
292
|
+
// Zip Slip defense-in-depth (EVALUATION.md S2): pre-scan entry names
|
|
293
|
+
// before handing them to node-stream-zip. Library is currently not
|
|
294
|
+
// known-vulnerable, but registry zips are the single most untrusted
|
|
295
|
+
// input on this path.
|
|
296
|
+
const entries = await zip.entries();
|
|
297
|
+
assertAllSafeZipEntryNames(Object.keys(entries), path.basename(destZipPath));
|
|
298
|
+
await zip.extract(null, extractDir);
|
|
299
|
+
} finally {
|
|
300
|
+
await zip.close();
|
|
301
|
+
}
|
|
318
302
|
}
|
|
319
303
|
|
|
320
304
|
// ---------------------------------------------------------------------------
|
|
@@ -328,12 +312,13 @@ async function extractZip(destZipPath, extractDir) {
|
|
|
328
312
|
* 1. Exact profile-hash match from any configured v2 source → use it.
|
|
329
313
|
* 2. Otherwise, if the package exists in v2 (versions.json returned an
|
|
330
314
|
* entry for this version) but nothing matches our profile:
|
|
331
|
-
* -
|
|
332
|
-
*
|
|
333
|
-
* - `buildMode === 'missing'
|
|
334
|
-
* at
|
|
335
|
-
* -
|
|
336
|
-
*
|
|
315
|
+
* - Try declarative per-package compatibility (PLAN-BUILD-MISSING
|
|
316
|
+
* phase 2); use the best compatible build if any.
|
|
317
|
+
* - With `buildMode === 'missing'` and nothing compatible: clone the
|
|
318
|
+
* source at gitRepo@gitSha and build it locally (phase 3).
|
|
319
|
+
* - Otherwise: STRICT FAIL with a structured error. Does not fall
|
|
320
|
+
* back to v1 (that would silently accept a build with different
|
|
321
|
+
* settings).
|
|
337
322
|
* 3. Otherwise (v2 doesn't know the package at all) → v1 legacy fallback.
|
|
338
323
|
*
|
|
339
324
|
* Legacy deps (plain `Map<name, version>`, no profile info) skip v2 entirely
|
|
@@ -362,15 +347,6 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
362
347
|
uploadStats,
|
|
363
348
|
} = options;
|
|
364
349
|
|
|
365
|
-
if (buildMode === 'always') {
|
|
366
|
-
throw new Error(
|
|
367
|
-
'--build=always is not implemented (requires PLAN-BUILD-MISSING phase 3). ' +
|
|
368
|
-
'Use --build=never (default) or --build=missing.',
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const looseCompatAllowed = process.env.WYVRNPM_ALLOW_LOOSE_COMPAT === '1';
|
|
373
|
-
|
|
374
350
|
if (!fs.existsSync(razerDir)) {
|
|
375
351
|
fs.mkdirSync(razerDir, { recursive: true });
|
|
376
352
|
}
|
|
@@ -385,6 +361,11 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
385
361
|
const profileHash = isLegacyEntry ? null : (depInfoOrVersion.profileHash ?? null);
|
|
386
362
|
const profile = isLegacyEntry ? null : (depInfoOrVersion.profile ?? null);
|
|
387
363
|
const options = isLegacyEntry ? null : (depInfoOrVersion.options ?? null);
|
|
364
|
+
const pinnedSource = isLegacyEntry ? null : (depInfoOrVersion.pinnedSource ?? null);
|
|
365
|
+
// Per-dep source pin (EVALUATION.md S4). When set, restrict every
|
|
366
|
+
// lookup for this package to the pinned URL; no fan-out across
|
|
367
|
+
// configured sources.
|
|
368
|
+
const depSources = pinnedSource ? [pinnedSource.url] : packageSources;
|
|
388
369
|
|
|
389
370
|
const extractDir = path.join(razerDir, name);
|
|
390
371
|
const versionFile = path.join(extractDir, '.wyvrn-version');
|
|
@@ -398,36 +379,43 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
398
379
|
continue;
|
|
399
380
|
}
|
|
400
381
|
|
|
401
|
-
// Skip if already at correct version + profile hash
|
|
382
|
+
// Skip if already at correct version + profile hash. `--build=always`
|
|
383
|
+
// (F9) bypasses this cache so the forced rebuild actually fires.
|
|
402
384
|
if (fs.existsSync(extractDir)) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
:
|
|
406
|
-
const installedMeta = fs.existsSync(metaFile)
|
|
407
|
-
? JSON.parse(fs.readFileSync(metaFile, 'utf8'))
|
|
408
|
-
: null;
|
|
409
|
-
|
|
410
|
-
const sameVersion = installedVersion === version;
|
|
411
|
-
const sameProfile = !profileHash || installedMeta?.profileHash === profileHash;
|
|
412
|
-
|
|
413
|
-
if (sameVersion && sameProfile) {
|
|
414
|
-
log.info(`Already present: ${name}@${version}${profileHash ? ` [${installedMeta?.profileHash}]` : ''} — skipping`);
|
|
415
|
-
lockEntries.set(name, {
|
|
416
|
-
version,
|
|
417
|
-
profileHash: installedMeta?.profileHash ?? null,
|
|
418
|
-
contentSha256: installedMeta?.contentSha256 ?? null,
|
|
419
|
-
gitSha: installedMeta?.gitSha ?? null,
|
|
420
|
-
resolvedFrom: installedMeta?.resolvedFrom ?? 'cached',
|
|
421
|
-
...(options ? { options } : {}),
|
|
422
|
-
});
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
if (!sameVersion) {
|
|
426
|
-
log.info(`Version changed: ${name} ${installedVersion} → ${version}, re-downloading`);
|
|
385
|
+
if (buildMode === 'always') {
|
|
386
|
+
log.info(`--build=always: removing existing ${name}@${version} to force source-build`);
|
|
387
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
427
388
|
} else {
|
|
428
|
-
|
|
389
|
+
const installedVersion = fs.existsSync(versionFile)
|
|
390
|
+
? fs.readFileSync(versionFile, 'utf8').trim()
|
|
391
|
+
: null;
|
|
392
|
+
const installedMeta = fs.existsSync(metaFile)
|
|
393
|
+
? JSON.parse(fs.readFileSync(metaFile, 'utf8'))
|
|
394
|
+
: null;
|
|
395
|
+
|
|
396
|
+
const sameVersion = installedVersion === version;
|
|
397
|
+
const sameProfile = !profileHash || installedMeta?.profileHash === profileHash;
|
|
398
|
+
|
|
399
|
+
if (sameVersion && sameProfile) {
|
|
400
|
+
log.info(`Already present: ${name}@${version}${profileHash ? ` [${installedMeta?.profileHash}]` : ''} — skipping`);
|
|
401
|
+
lockEntries.set(name, {
|
|
402
|
+
version,
|
|
403
|
+
profileHash: installedMeta?.profileHash ?? null,
|
|
404
|
+
contentSha256: installedMeta?.contentSha256 ?? null,
|
|
405
|
+
gitSha: installedMeta?.gitSha ?? null,
|
|
406
|
+
resolvedFrom: installedMeta?.resolvedFrom ?? 'cached',
|
|
407
|
+
...(options ? { options } : {}),
|
|
408
|
+
...(pinnedSource ? { source: pinnedSource.name } : {}),
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (!sameVersion) {
|
|
413
|
+
log.info(`Version changed: ${name} ${installedVersion} → ${version}, re-downloading`);
|
|
414
|
+
} else {
|
|
415
|
+
log.info(`Profile changed for ${name}@${version}, re-downloading`);
|
|
416
|
+
}
|
|
417
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
429
418
|
}
|
|
430
|
-
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
431
419
|
}
|
|
432
420
|
|
|
433
421
|
log.info(`Downloading: ${name}@${version}`);
|
|
@@ -439,26 +427,38 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
439
427
|
|
|
440
428
|
// ── Try v2 exact ────────────────────────────────────────────────────────
|
|
441
429
|
if (profileHash && profile) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
430
|
+
// --build=always skips every registry-binary path (exact match, compat
|
|
431
|
+
// match) and goes straight to source-build. Fetch just the versions
|
|
432
|
+
// index so source-build can find gitRepo/gitSha. EVALUATION.md F9.
|
|
433
|
+
const v2 = (buildMode === 'always')
|
|
434
|
+
? await findV2Index(
|
|
435
|
+
{ name, version },
|
|
436
|
+
depSources,
|
|
437
|
+
{ awsProfile, token },
|
|
438
|
+
)
|
|
439
|
+
: await tryV2Exact(
|
|
440
|
+
{ name, version, profileHash },
|
|
441
|
+
depSources,
|
|
442
|
+
destZipPath,
|
|
443
|
+
timeoutMs,
|
|
444
|
+
{ awsProfile, token },
|
|
445
|
+
);
|
|
449
446
|
|
|
450
447
|
if (v2.found) {
|
|
451
448
|
downloadResult = v2.result;
|
|
452
449
|
resolvedFrom = 'v2';
|
|
453
450
|
} else if (v2.indexFoundInSource) {
|
|
454
|
-
// v2 knows this package but no exact profile match
|
|
455
|
-
// to v1 — that would silently
|
|
456
|
-
//
|
|
457
|
-
//
|
|
451
|
+
// v2 knows this package but no exact profile match (or --build=always
|
|
452
|
+
// forced the path here). Do NOT fall back to v1 — that would silently
|
|
453
|
+
// accept a binary with different build settings. Try declarative
|
|
454
|
+
// compatibility first (phase 2) unless always-mode, then source-build
|
|
455
|
+
// when --build=missing|always, then fail strictly.
|
|
458
456
|
skipV1Fallback = true;
|
|
459
457
|
|
|
460
458
|
// ── Phase 2: declarative per-package compatibility ─────────────────
|
|
461
|
-
|
|
459
|
+
// Skip under --build=always: the whole point of that flag is to
|
|
460
|
+
// produce a fresh source-build, not to accept a registry binary.
|
|
461
|
+
const compatResult = (buildMode === 'always') ? null : await findCompatibleBuild({
|
|
462
462
|
name, version,
|
|
463
463
|
consumerProfile: profile,
|
|
464
464
|
consumerOptions: options,
|
|
@@ -490,8 +490,8 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
// ── Phase 3: source-build when `--build=missing`
|
|
494
|
-
if (!downloadResult && buildMode === 'missing') {
|
|
493
|
+
// ── Phase 3: source-build when `--build=missing` or `--build=always` ──
|
|
494
|
+
if (!downloadResult && (buildMode === 'missing' || buildMode === 'always')) {
|
|
495
495
|
const sourceInfo = v2.versionsIndex?.versions?.[version]?.source;
|
|
496
496
|
if (sourceInfo?.gitRepo && sourceInfo?.gitSha) {
|
|
497
497
|
try {
|
|
@@ -531,33 +531,16 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
// ── Deprecated escape hatch: WYVRNPM_ALLOW_LOOSE_COMPAT=1 ──────────
|
|
535
|
-
if (!downloadResult && looseCompatAllowed) {
|
|
536
|
-
const compat = await tryV2Compat(
|
|
537
|
-
{ name, version, profile },
|
|
538
|
-
v2.versionsIndex,
|
|
539
|
-
v2.indexFoundInSource,
|
|
540
|
-
destZipPath,
|
|
541
|
-
timeoutMs,
|
|
542
|
-
{ awsProfile, token },
|
|
543
|
-
);
|
|
544
|
-
if (compat) {
|
|
545
|
-
downloadResult = compat;
|
|
546
|
-
resolvedFrom = 'v2-compat-deprecated';
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
534
|
if (!downloadResult) {
|
|
551
535
|
log.error(buildStrictMissMessage({
|
|
552
536
|
name, version, profileHash,
|
|
553
537
|
availableProfiles: v2.availableProfiles,
|
|
554
538
|
buildMode,
|
|
555
|
-
loose: looseCompatAllowed,
|
|
556
539
|
}));
|
|
557
540
|
lockEntries.set(name, {
|
|
558
541
|
version,
|
|
559
542
|
profileHash,
|
|
560
|
-
resolvedFrom: buildMode === 'missing' ? 'source-build-failed' : 'no-exact-match',
|
|
543
|
+
resolvedFrom: (buildMode === 'missing' || buildMode === 'always') ? 'source-build-failed' : 'no-exact-match',
|
|
561
544
|
availableProfiles: v2.availableProfiles,
|
|
562
545
|
});
|
|
563
546
|
continue;
|
|
@@ -568,7 +551,7 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
568
551
|
|
|
569
552
|
// ── Fall back to v1 ─────────────────────────────────────────────────────
|
|
570
553
|
if (!downloadResult && !skipV1Fallback) {
|
|
571
|
-
const ok = await tryDownloadFromSources(name, version,
|
|
554
|
+
const ok = await tryDownloadFromSources(name, version, depSources, platform, httpClient, timeoutMs, destZipPath);
|
|
572
555
|
if (!ok) {
|
|
573
556
|
log.warn(`package ${name}@${version} was not found in any configured source`);
|
|
574
557
|
lockEntries.set(name, { version, resolvedFrom: 'not-found' });
|
|
@@ -631,7 +614,12 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
631
614
|
profileHash: downloadResult?.profileHash ?? null,
|
|
632
615
|
contentSha256: downloadResult?.contentSha256 ?? null,
|
|
633
616
|
gitSha: downloadResult?.gitSha ?? null,
|
|
634
|
-
...(options
|
|
617
|
+
...(options ? { options } : {}),
|
|
618
|
+
// S4: record the pinned source name on the lock entry so a
|
|
619
|
+
// reviewer reading wyvrn.lock can confirm the artefact came
|
|
620
|
+
// from the declared source. Absent on non-pinned deps so
|
|
621
|
+
// pre-S4 lockfiles remain byte-identical.
|
|
622
|
+
...(pinnedSource ? { source: pinnedSource.name } : {}),
|
|
635
623
|
});
|
|
636
624
|
|
|
637
625
|
// ── Upload freshly source-built artefact back to the registry ────────
|
package/src/logger.js
CHANGED
|
@@ -11,6 +11,32 @@
|
|
|
11
11
|
|
|
12
12
|
const PREFIX = '[wyvrn]';
|
|
13
13
|
|
|
14
|
+
// ── Sensitive-value redaction (EVALUATION.md S8) ──────────────────────────────
|
|
15
|
+
// Defense-in-depth against a token leaking into logs or error traces. Nothing
|
|
16
|
+
// in wyvrnpm deliberately logs tokens today, but callers sometimes paste full
|
|
17
|
+
// commands + error output into bug reports, and some future error path might
|
|
18
|
+
// stringify argv. Two narrow patterns cover every format we handle:
|
|
19
|
+
//
|
|
20
|
+
// `--token <value>` — CLI leak
|
|
21
|
+
// `Authorization: Bearer <value>` — HTTP header leak
|
|
22
|
+
// `Bearer <value>` (standalone) — header fragment leak
|
|
23
|
+
//
|
|
24
|
+
// Redaction is a last-line mitigation — the primary fix is `--token-env` and
|
|
25
|
+
// per-source `tokenEnv` so the literal value never lands in argv to begin with.
|
|
26
|
+
const TOKEN_PATTERNS = [
|
|
27
|
+
[/(--token(?:-env)?[= ]+)\S+/g, '$1***'],
|
|
28
|
+
[/(Authorization\s*:\s*Bearer\s+)\S+/gi, '$1***'],
|
|
29
|
+
[/(\bBearer\s+)[A-Za-z0-9._\-+/=]{16,}/g, '$1***'],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function redactSensitive(text) {
|
|
33
|
+
let out = String(text);
|
|
34
|
+
for (const [pattern, replacement] of TOKEN_PATTERNS) {
|
|
35
|
+
out = out.replace(pattern, replacement);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
const ANSI = {
|
|
15
41
|
reset: '\x1b[0m',
|
|
16
42
|
bold: '\x1b[1m',
|
|
@@ -49,7 +75,9 @@ function paint(text, code, stream) {
|
|
|
49
75
|
function emit(stream, consoleFn, coloredPrefix, message) {
|
|
50
76
|
// Every line in a multi-line message gets the prefix — so multi-line errors
|
|
51
77
|
// line up visually without the caller having to repeat `[wyvrn]` per line.
|
|
52
|
-
|
|
78
|
+
// Redaction runs before the prefix/colour is applied so patterns like
|
|
79
|
+
// `--token xyz` match regardless of how the caller formatted the line.
|
|
80
|
+
const out = redactSensitive(message)
|
|
53
81
|
.split('\n')
|
|
54
82
|
.map((line) => `${coloredPrefix} ${line}`)
|
|
55
83
|
.join('\n');
|
|
@@ -91,4 +119,4 @@ function debug(message) {
|
|
|
91
119
|
emit(process.stderr, console.error, `${p} ${tag}`, message);
|
|
92
120
|
}
|
|
93
121
|
|
|
94
|
-
module.exports = { info, warn, error, success, debug, setJsonMode, isJsonMode };
|
|
122
|
+
module.exports = { info, warn, error, success, debug, setJsonMode, isJsonMode, redactSensitive };
|
package/src/manifest.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
+
const { readRecipeConf } = require('./conf');
|
|
7
|
+
|
|
6
8
|
/** Current manifest schema version emitted by v2 tooling. */
|
|
7
9
|
const SCHEMA_VERSION = 2;
|
|
8
10
|
|
|
@@ -52,18 +54,21 @@ function defaultManifest(name) {
|
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Normalise the raw `dependencies` field from a manifest into a consistent
|
|
55
|
-
* `{ name → { version, settings, options } }` map, regardless of
|
|
57
|
+
* `{ name → { version, settings, options, source? } }` map, regardless of
|
|
58
|
+
* input format:
|
|
56
59
|
*
|
|
57
|
-
* • String value : "1.0.0"
|
|
58
|
-
* • Object value : { version, settings?, options? } → kept (missing keys default to {})
|
|
59
|
-
* • Array format : [{ Name, Version }]
|
|
60
|
+
* • String value : "1.0.0" → { version: "1.0.0", settings: {}, options: {} }
|
|
61
|
+
* • Object value : { version, settings?, options?, source? } → kept (missing keys default to {})
|
|
62
|
+
* • Array format : [{ Name, Version }] → converted (no settings/options)
|
|
60
63
|
*
|
|
61
64
|
* `settings` merges on top of the active profile for this dep only.
|
|
62
65
|
* `options` maps to the recipe's declared options; validated against
|
|
63
66
|
* the recipe's `allowed` list at resolve time (see src/options.js).
|
|
67
|
+
* `source` (EVALUATION.md S4) pins the dep to a named install source
|
|
68
|
+
* (e.g. `"primary-s3"`); absent = fan out across all configured sources.
|
|
64
69
|
*
|
|
65
70
|
* @param {object|Array|undefined} rawDeps
|
|
66
|
-
* @returns {Record<string, { version: string, settings: object, options: object }>}
|
|
71
|
+
* @returns {Record<string, { version: string, settings: object, options: object, source?: string }>}
|
|
67
72
|
*/
|
|
68
73
|
function normalizeDependencies(rawDeps) {
|
|
69
74
|
if (!rawDeps || typeof rawDeps !== 'object') return {};
|
|
@@ -83,11 +88,17 @@ function normalizeDependencies(rawDeps) {
|
|
|
83
88
|
if (typeof value === 'string') {
|
|
84
89
|
result[n] = { version: value, settings: {}, options: {} };
|
|
85
90
|
} else if (value && typeof value === 'object') {
|
|
86
|
-
|
|
91
|
+
const entry = {
|
|
87
92
|
version: value.version ?? value.Version ?? '',
|
|
88
93
|
settings: value.settings ?? {},
|
|
89
94
|
options: value.options ?? {},
|
|
90
95
|
};
|
|
96
|
+
// Per-package source pin (S4). Only keep it if it's a non-empty
|
|
97
|
+
// string so {} / null / 0 don't silently pin to a nonsense value.
|
|
98
|
+
if (typeof value.source === 'string' && value.source.length > 0) {
|
|
99
|
+
entry.source = value.source;
|
|
100
|
+
}
|
|
101
|
+
result[n] = entry;
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
return result;
|
|
@@ -106,11 +117,27 @@ function readManifest(manifestPath) {
|
|
|
106
117
|
throw new Error(`Manifest not found: ${resolved}`);
|
|
107
118
|
}
|
|
108
119
|
const raw = fs.readFileSync(resolved, 'utf8');
|
|
120
|
+
let parsed;
|
|
109
121
|
try {
|
|
110
|
-
|
|
122
|
+
parsed = JSON.parse(raw);
|
|
111
123
|
} catch (err) {
|
|
112
124
|
throw new Error(`Failed to parse manifest at ${resolved}: ${err.message}`);
|
|
113
125
|
}
|
|
126
|
+
|
|
127
|
+
// Validate the `conf` block (PLAN-CONF.md phase 1.2). Only runs for
|
|
128
|
+
// local manifests — registry-side manifests fetched via resolve.js /
|
|
129
|
+
// download.js go through fetch().json() and deliberately skip this so
|
|
130
|
+
// a consumer with an older wyvrnpm doesn't fail on a publisher's
|
|
131
|
+
// forward-compatible conf keys.
|
|
132
|
+
if (parsed && typeof parsed === 'object' && parsed.conf !== undefined) {
|
|
133
|
+
try {
|
|
134
|
+
readRecipeConf(parsed);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(`Invalid \`conf\` block in ${resolved}: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return parsed;
|
|
114
141
|
}
|
|
115
142
|
|
|
116
143
|
/**
|