wyvrnpm 2.12.3 → 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.
package/src/v2-meta.js ADDED
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Build the canonical v2 `wyvrn.json` body. Single source of truth for the
5
+ * shape so the publisher (publish.js / upload-built.js) and the providers
6
+ * (s3/http/file `v2Publish`) produce byte-identical bytes — required so
7
+ * the consumer's `manifestSha256` over the canonical form matches the
8
+ * value the publisher signed into the attestation (S1 plan §3.1).
9
+ *
10
+ * Inputs are spread on top of the publisher-provided manifest so any
11
+ * field already present on the source `wyvrn.json` survives unchanged.
12
+ * The well-known v2 fields always appear — `schemaVersion`, `profileHash`,
13
+ * `buildSettings`, `gitSha`, `gitRepo`, `publishedAt`.
14
+ *
15
+ * Artefact-shape fields:
16
+ * - `contentSha256` — SHA256 of the single `wyvrn.zip` (fat-zip publishes,
17
+ * the historical default).
18
+ * - `artefactConfigs` — { config → { sha256, sizeBytes } } for per-config
19
+ * slice publishes (recipe `build.publishPerConfig: true`).
20
+ * At least one of the two must be supplied. Both may coexist — that's a
21
+ * future "slice-plus-fat-zip-fallback" option, not used today but the
22
+ * canonical-byte contract permits it.
23
+ *
24
+ * `publishedLock` / `uploadedBy` / `artefactConfigs` only appear when
25
+ * explicitly supplied non-empty; absent-vs-empty discipline matters
26
+ * because the canonical SHA includes them when present and omits them
27
+ * when absent — flipping that bit on or off changes the signed value.
28
+ */
29
+ function buildV2Meta(rawManifest, opts) {
30
+ const {
31
+ profileHash, buildSettings, contentSha256,
32
+ gitSha = null, gitRepo = null,
33
+ publishedAt,
34
+ publishedLock,
35
+ uploadedBy,
36
+ artefactConfigs,
37
+ } = opts;
38
+
39
+ if (typeof profileHash !== 'string') throw new Error('buildV2Meta: profileHash required');
40
+ if (typeof publishedAt !== 'string') throw new Error('buildV2Meta: publishedAt required (ISO 8601 UTC)');
41
+ if (buildSettings === undefined) throw new Error('buildV2Meta: buildSettings required');
42
+
43
+ // Validate artefactConfigs shape when supplied so the manifest never
44
+ // lands on the registry with malformed slice metadata.
45
+ let artefactConfigsNonEmpty = false;
46
+ if (artefactConfigs !== undefined && artefactConfigs !== null) {
47
+ if (typeof artefactConfigs !== 'object' || Array.isArray(artefactConfigs)) {
48
+ throw new Error('buildV2Meta: artefactConfigs must be an object keyed by config name');
49
+ }
50
+ const entries = Object.entries(artefactConfigs);
51
+ for (const [config, entry] of entries) {
52
+ if (typeof config !== 'string' || config.length === 0 || config.trim() !== config) {
53
+ throw new Error(
54
+ `buildV2Meta: artefactConfigs key ${JSON.stringify(config)} must be a non-empty trimmed string`,
55
+ );
56
+ }
57
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
58
+ throw new Error(
59
+ `buildV2Meta: artefactConfigs[${JSON.stringify(config)}] must be an object`,
60
+ );
61
+ }
62
+ if (typeof entry.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(entry.sha256)) {
63
+ throw new Error(
64
+ `buildV2Meta: artefactConfigs[${JSON.stringify(config)}].sha256 must be 64 lowercase hex chars`,
65
+ );
66
+ }
67
+ if (!Number.isInteger(entry.sizeBytes) || entry.sizeBytes < 0) {
68
+ throw new Error(
69
+ `buildV2Meta: artefactConfigs[${JSON.stringify(config)}].sizeBytes must be a non-negative integer`,
70
+ );
71
+ }
72
+ }
73
+ artefactConfigsNonEmpty = entries.length > 0;
74
+ }
75
+
76
+ // At least one artefact-shape field must be supplied. Both is allowed
77
+ // (slice + fat-zip-fallback). Neither is a hard error — without it the
78
+ // manifest doesn't tell consumers what to download.
79
+ if (typeof contentSha256 !== 'string' && !artefactConfigsNonEmpty) {
80
+ throw new Error('buildV2Meta: contentSha256 or non-empty artefactConfigs required');
81
+ }
82
+
83
+ return {
84
+ ...rawManifest,
85
+ schemaVersion: 2,
86
+ profileHash,
87
+ buildSettings,
88
+ ...(typeof contentSha256 === 'string' ? { contentSha256 } : {}),
89
+ gitSha,
90
+ gitRepo,
91
+ publishedAt,
92
+ ...(publishedLock && Object.keys(publishedLock).length > 0 ? { publishedLock } : {}),
93
+ ...(uploadedBy ? { uploadedBy } : {}),
94
+ ...(artefactConfigsNonEmpty ? { artefactConfigs } : {}),
95
+ };
96
+ }
97
+
98
+ module.exports = { buildV2Meta };