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.
- package/README.md +163 -2
- package/bin/wyvrnpm.js +167 -11
- package/cmake/cpp.cmake +6 -8
- package/package.json +1 -1
- package/src/build/recipe.js +25 -3
- package/src/commands/configure.js +61 -1
- package/src/commands/install.js +76 -1
- package/src/commands/key.js +112 -0
- package/src/commands/publish.js +402 -49
- package/src/commands/show.js +23 -0
- package/src/commands/trust.js +136 -0
- package/src/conf/index.js +131 -11
- package/src/config.js +35 -4
- package/src/download.js +408 -11
- package/src/providers/base.js +110 -15
- package/src/providers/file.js +90 -29
- package/src/providers/http.js +109 -32
- package/src/providers/s3.js +103 -31
- package/src/publish/slice.js +414 -0
- package/src/signing/attestation.js +301 -0
- package/src/signing/canonical.js +80 -0
- package/src/signing/ed25519.js +123 -0
- package/src/signing/index.js +201 -0
- package/src/signing/resolve-context.js +73 -0
- package/src/signing/trust.js +215 -0
- package/src/toolchain/template.cmake +7 -5
- package/src/upload-built.js +88 -14
- package/src/v2-meta.js +98 -0
package/src/providers/base.js
CHANGED
|
@@ -12,13 +12,24 @@
|
|
|
12
12
|
* v1 (legacy): {source}/{platform}/{name}/{version}/wyvrn.{json,zip}
|
|
13
13
|
* {source}/{platform}/{name}/latest.json
|
|
14
14
|
*
|
|
15
|
-
* v2: {source}/v2/{name}/{version}/{profileHash}/wyvrn.
|
|
15
|
+
* v2: {source}/v2/{name}/{version}/{profileHash}/wyvrn.json
|
|
16
|
+
* {source}/v2/{name}/{version}/{profileHash}/wyvrn.zip (fat-zip publish, default)
|
|
17
|
+
* {source}/v2/{name}/{version}/{profileHash}/wyvrn-<Config>.zip (per-config slice publish — recipe build.publishPerConfig: true)
|
|
18
|
+
* {source}/v2/{name}/{version}/{profileHash}/wyvrn.att.json (S1)
|
|
19
|
+
* {source}/v2/{name}/{version}/{profileHash}/wyvrn.sig (S1)
|
|
16
20
|
* {source}/v2/{name}/{version}/source.json
|
|
17
21
|
* {source}/v2/{name}/versions.json
|
|
18
22
|
* {source}/v2/{name}/latest.json
|
|
23
|
+
* {source}/v2/.trust/keys.json (S1)
|
|
24
|
+
* {source}/v2/.trust/mode.json (S1)
|
|
19
25
|
*
|
|
20
26
|
* Publishing with v2 tooling writes both v2 AND v1 paths by default so that
|
|
21
27
|
* v1 clients can still consume the packages.
|
|
28
|
+
*
|
|
29
|
+
* The signing files (`wyvrn.att.json` + `wyvrn.sig`) and the per-registry
|
|
30
|
+
* trust anchor (`.trust/`) are additive — pre-S1 clients ignore them and
|
|
31
|
+
* S1 clients treat their absence as `mode: off` (CLAUDE.md §8.S1 +
|
|
32
|
+
* claude/PLAN-SIGNING.md / echo-off-setlocal-velvet-lagoon plan).
|
|
22
33
|
*/
|
|
23
34
|
class BaseProvider {
|
|
24
35
|
/**
|
|
@@ -84,20 +95,47 @@ class BaseProvider {
|
|
|
84
95
|
* (author publish); populated by the upload-built flow. Readers ignore
|
|
85
96
|
* the field — it is informational only.
|
|
86
97
|
*
|
|
87
|
-
*
|
|
98
|
+
* `files` may also carry `attPath` and `sigPath` — paths to the
|
|
99
|
+
* `wyvrn.att.json` (signed attestation body) and `wyvrn.sig` (detached
|
|
100
|
+
* signature envelope) produced by the publish flow's signing tail. Both
|
|
101
|
+
* optional; when present the provider MUST upload them in the order
|
|
102
|
+
* `zip(s) → manifest → att → sig → versions.json` so a partial-failure
|
|
103
|
+
* crash never leaves a sig-less entry visible in versions.json
|
|
104
|
+
* (claude/echo-off-setlocal-velvet-lagoon §3 + Risks §write-order).
|
|
105
|
+
*
|
|
106
|
+
* Per-config slicing (recipe `build.publishPerConfig: true`):
|
|
107
|
+
* `options.artefactConfigs: { config → { sha256, sizeBytes } }` and
|
|
108
|
+
* `files.artefactZips: { config → path }` together replace the single
|
|
109
|
+
* `files.zip` upload. The provider uploads each slice as
|
|
110
|
+
* `wyvrn-<Config>.zip` and emits a `configs` array (instead of
|
|
111
|
+
* `contentSha256`) into the versions.json profile entry + latest.json.
|
|
112
|
+
* `options.contentSha256` is null/absent in this branch — the
|
|
113
|
+
* per-slice SHAs live in the published manifest's `artefactConfigs`
|
|
114
|
+
* block (canonical source of truth) and in the v2 attestation's
|
|
115
|
+
* `artefactSha256` field. Pre-feature publishes (no
|
|
116
|
+
* `artefactConfigs`) take the byte-identical legacy path.
|
|
117
|
+
*
|
|
118
|
+
* @param {{
|
|
119
|
+
* manifest: string,
|
|
120
|
+
* zip?: string,
|
|
121
|
+
* artefactZips?: Record<string, string>,
|
|
122
|
+
* attPath?: string,
|
|
123
|
+
* sigPath?: string,
|
|
124
|
+
* }} files
|
|
88
125
|
* @param {{
|
|
89
|
-
* source:
|
|
90
|
-
* name:
|
|
91
|
-
* version:
|
|
92
|
-
* profileHash:
|
|
93
|
-
* buildSettings:
|
|
94
|
-
* contentSha256
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
126
|
+
* source: string,
|
|
127
|
+
* name: string,
|
|
128
|
+
* version: string,
|
|
129
|
+
* profileHash: string,
|
|
130
|
+
* buildSettings: import('../profile').WyvrnProfile, // stored in metadata for debugging
|
|
131
|
+
* contentSha256?: string, // fat-zip publishes only
|
|
132
|
+
* artefactConfigs?: Record<string, { sha256: string, sizeBytes: number }>, // slice publishes only
|
|
133
|
+
* gitSha: string|null,
|
|
134
|
+
* gitRepo: string|null,
|
|
135
|
+
* updateLatest?: boolean, // default true; false for upload-built
|
|
136
|
+
* uploadedBy?: object, // optional provenance block, written into wyvrn.json
|
|
137
|
+
* awsProfile?: string,
|
|
138
|
+
* token?: string,
|
|
101
139
|
* }} options
|
|
102
140
|
* @returns {Promise<void>}
|
|
103
141
|
*/
|
|
@@ -105,6 +143,49 @@ class BaseProvider {
|
|
|
105
143
|
throw new Error(`v2Publish() is not implemented in ${this.constructor.name}`);
|
|
106
144
|
}
|
|
107
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Fetch the signed attestation body (`wyvrn.att.json`) for a v2 build.
|
|
148
|
+
* Returns the raw bytes (as a Buffer) so the verifier sees exactly what
|
|
149
|
+
* the publisher signed — never a JSON.parse → JSON.stringify round trip,
|
|
150
|
+
* which would re-canonicalise and could change byte content.
|
|
151
|
+
*
|
|
152
|
+
* Returns null when the file does not exist (registry hasn't adopted S1,
|
|
153
|
+
* or this build was published with `--no-sign`).
|
|
154
|
+
*
|
|
155
|
+
* @param {{ source: string, name: string, version: string, profileHash: string, awsProfile?: string, token?: string }} _options
|
|
156
|
+
* @returns {Promise<Buffer|null>}
|
|
157
|
+
*/
|
|
158
|
+
async v2GetAttestation(_options) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Fetch the signature envelope (`wyvrn.sig`) for a v2 build. Same
|
|
164
|
+
* Buffer-not-parsed contract as `v2GetAttestation`.
|
|
165
|
+
*
|
|
166
|
+
* @param {{ source: string, name: string, version: string, profileHash: string, awsProfile?: string, token?: string }} _options
|
|
167
|
+
* @returns {Promise<Buffer|null>}
|
|
168
|
+
*/
|
|
169
|
+
async v2GetSignature(_options) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fetch the per-registry trust anchor: `.trust/keys.json` and
|
|
175
|
+
* `.trust/mode.json`. Returns `{ keys, mode }` as parsed objects, or
|
|
176
|
+
* `null` when the registry has no `.trust/` directory at all (treated
|
|
177
|
+
* as mode `off` by the install flow — current behaviour).
|
|
178
|
+
*
|
|
179
|
+
* Either inner field may itself be null if only one of the two files
|
|
180
|
+
* exists (mid-rollout state). Callers default missing `mode` to `off`.
|
|
181
|
+
*
|
|
182
|
+
* @param {{ source: string, awsProfile?: string, token?: string }} _options
|
|
183
|
+
* @returns {Promise<{ keys: object|null, mode: object|null }|null>}
|
|
184
|
+
*/
|
|
185
|
+
async v2GetTrustAnchor(_options) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
108
189
|
/**
|
|
109
190
|
* Fetch the wyvrn.json metadata object for a specific v2 build.
|
|
110
191
|
* Returns null if the build does not exist.
|
|
@@ -130,7 +211,21 @@ class BaseProvider {
|
|
|
130
211
|
/**
|
|
131
212
|
* Download the prebuilt binary zip for a specific v2 build to destPath.
|
|
132
213
|
*
|
|
133
|
-
*
|
|
214
|
+
* For per-config slice publishes, set `options.sliceConfig` to the
|
|
215
|
+
* canonical CMake config name (e.g. `"Release"`). The provider downloads
|
|
216
|
+
* `wyvrn-<sliceConfig>.zip` instead of the fat `wyvrn.zip`. When unset,
|
|
217
|
+
* downloads the fat `wyvrn.zip` (today's path, byte-identical for
|
|
218
|
+
* pre-feature publishes).
|
|
219
|
+
*
|
|
220
|
+
* @param {{
|
|
221
|
+
* source: string,
|
|
222
|
+
* name: string,
|
|
223
|
+
* version: string,
|
|
224
|
+
* profileHash: string,
|
|
225
|
+
* sliceConfig?: string,
|
|
226
|
+
* awsProfile?: string,
|
|
227
|
+
* token?: string,
|
|
228
|
+
* }} options
|
|
134
229
|
* @param {string} destPath - absolute path to write the zip to
|
|
135
230
|
* @param {number} timeoutMs
|
|
136
231
|
* @returns {Promise<boolean>} true on success
|
package/src/providers/file.js
CHANGED
|
@@ -91,35 +91,62 @@ class FileProvider extends BaseProvider {
|
|
|
91
91
|
async v2Publish(files, options) {
|
|
92
92
|
const {
|
|
93
93
|
source, name, version, profileHash, buildSettings,
|
|
94
|
-
contentSha256, gitSha, gitRepo,
|
|
95
|
-
updateLatest = true,
|
|
94
|
+
contentSha256, gitSha, gitRepo,
|
|
95
|
+
updateLatest = true,
|
|
96
|
+
artefactConfigs,
|
|
96
97
|
} = options;
|
|
98
|
+
// `publishedLock` and `uploadedBy` are no longer consumed here —
|
|
99
|
+
// both are baked into `files.manifest` by the publisher (via
|
|
100
|
+
// src/v2-meta.js) before this call. They remain in the v2Publish
|
|
101
|
+
// contract on BaseProvider only for backward compatibility.
|
|
97
102
|
|
|
98
103
|
const v2root = this._v2Root(source, name);
|
|
99
104
|
const buildDir = path.join(v2root, version, profileHash);
|
|
100
105
|
fs.mkdirSync(buildDir, { recursive: true });
|
|
101
106
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
// Slice publish iff artefactConfigs is supplied non-empty. List the
|
|
108
|
+
// configs in alphabetical order so versions.json round-trips
|
|
109
|
+
// deterministically across publishers.
|
|
110
|
+
const sliceConfigs =
|
|
111
|
+
(artefactConfigs && Object.keys(artefactConfigs).length > 0)
|
|
112
|
+
? Object.keys(artefactConfigs).sort()
|
|
113
|
+
: null;
|
|
114
|
+
|
|
115
|
+
// 1) zip(s) — fat or per-config slices
|
|
116
|
+
if (sliceConfigs) {
|
|
117
|
+
if (!files.artefactZips || typeof files.artefactZips !== 'object') {
|
|
118
|
+
throw new Error('v2Publish: artefactConfigs supplied but files.artefactZips missing');
|
|
119
|
+
}
|
|
120
|
+
for (const config of sliceConfigs) {
|
|
121
|
+
const slicePath = files.artefactZips[config];
|
|
122
|
+
if (!slicePath) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`v2Publish: artefactConfigs declares "${config}" but files.artefactZips["${config}"] is missing`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
this._copy(slicePath, path.join(buildDir, `wyvrn-${config}.zip`));
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
if (!files.zip) {
|
|
131
|
+
throw new Error('v2Publish: files.zip required for fat-zip publish (or pass artefactConfigs + files.artefactZips for slice publish)');
|
|
132
|
+
}
|
|
133
|
+
this._copy(files.zip, path.join(buildDir, 'wyvrn.zip'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2) wyvrn.json — uploaded VERBATIM from `files.manifest`. The
|
|
137
|
+
// publisher (commands/publish.js, upload-built.js) builds the v2Meta
|
|
138
|
+
// body via src/v2-meta.js so its signed manifestSha256 matches
|
|
139
|
+
// exactly what consumers compute from v2GetMeta. Providers MUST
|
|
140
|
+
// NOT mutate the manifest here — that would invalidate every
|
|
141
|
+
// signature.
|
|
142
|
+
this._copy(files.manifest, path.join(buildDir, 'wyvrn.json'));
|
|
143
|
+
|
|
144
|
+
// 2.5) signing files (S1) — written between manifest and versions.json
|
|
145
|
+
// so a partial-failure crash never leaves a sig-less entry visible
|
|
146
|
+
// in versions.json. Both files optional; when absent the registry's
|
|
147
|
+
// mode.json governs whether consumers warn or abort.
|
|
148
|
+
if (files.attPath) this._copy(files.attPath, path.join(buildDir, 'wyvrn.att.json'));
|
|
149
|
+
if (files.sigPath) this._copy(files.sigPath, path.join(buildDir, 'wyvrn.sig'));
|
|
123
150
|
|
|
124
151
|
// 3) source.json
|
|
125
152
|
if (gitSha || gitRepo) {
|
|
@@ -130,16 +157,23 @@ class FileProvider extends BaseProvider {
|
|
|
130
157
|
});
|
|
131
158
|
}
|
|
132
159
|
|
|
133
|
-
// 4) versions.json index
|
|
160
|
+
// 4) versions.json index. Per-profile entries hold either
|
|
161
|
+
// `contentSha256` (fat zip) or `configs` (slice publish);
|
|
162
|
+
// both fields can coexist for hybrid publishes but phase 1
|
|
163
|
+
// emits one or the other.
|
|
134
164
|
const versionsPath = path.join(v2root, 'versions.json');
|
|
135
165
|
let versionsIdx = this._readJson(versionsPath) ?? { name, latest: version, versions: {} };
|
|
136
166
|
if (!versionsIdx.versions) versionsIdx.versions = {};
|
|
137
167
|
if (!versionsIdx.versions[version]) versionsIdx.versions[version] = { profiles: {} };
|
|
138
|
-
|
|
139
|
-
|
|
168
|
+
|
|
169
|
+
const profileEntry = {
|
|
140
170
|
publishedAt: new Date().toISOString(),
|
|
141
171
|
buildSettings,
|
|
142
172
|
};
|
|
173
|
+
if (sliceConfigs) profileEntry.configs = sliceConfigs;
|
|
174
|
+
else profileEntry.contentSha256 = contentSha256;
|
|
175
|
+
versionsIdx.versions[version].profiles[profileHash] = profileEntry;
|
|
176
|
+
|
|
143
177
|
if (gitSha || gitRepo) {
|
|
144
178
|
versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
145
179
|
}
|
|
@@ -149,7 +183,10 @@ class FileProvider extends BaseProvider {
|
|
|
149
183
|
// 5) v2 latest.json — skipped for consumer uploads (upload-built) so a
|
|
150
184
|
// historical-version rebuild cannot clobber the real "latest" pointer.
|
|
151
185
|
if (updateLatest) {
|
|
152
|
-
|
|
186
|
+
const latestEntry = { version, profileHash };
|
|
187
|
+
if (sliceConfigs) latestEntry.configs = sliceConfigs;
|
|
188
|
+
else latestEntry.contentSha256 = contentSha256;
|
|
189
|
+
this._writeJson(path.join(v2root, 'latest.json'), latestEntry);
|
|
153
190
|
}
|
|
154
191
|
}
|
|
155
192
|
|
|
@@ -162,12 +199,36 @@ class FileProvider extends BaseProvider {
|
|
|
162
199
|
return this._readJson(path.join(this._v2Root(source, name), 'versions.json'));
|
|
163
200
|
}
|
|
164
201
|
|
|
165
|
-
async v2DownloadZip({ source, name, version, profileHash }, destPath) {
|
|
166
|
-
const
|
|
202
|
+
async v2DownloadZip({ source, name, version, profileHash, sliceConfig }, destPath) {
|
|
203
|
+
const filename = sliceConfig ? `wyvrn-${sliceConfig}.zip` : 'wyvrn.zip';
|
|
204
|
+
const src = path.join(this._v2Root(source, name), version, profileHash, filename);
|
|
167
205
|
if (!fs.existsSync(src)) return false;
|
|
168
206
|
fs.copyFileSync(src, destPath);
|
|
169
207
|
return true;
|
|
170
208
|
}
|
|
209
|
+
|
|
210
|
+
// ── S1 signing surface ────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async v2GetAttestation({ source, name, version, profileHash }) {
|
|
213
|
+
const p = path.join(this._v2Root(source, name), version, profileHash, 'wyvrn.att.json');
|
|
214
|
+
if (!fs.existsSync(p)) return null;
|
|
215
|
+
return fs.readFileSync(p); // raw bytes — verifier needs them unmodified
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async v2GetSignature({ source, name, version, profileHash }) {
|
|
219
|
+
const p = path.join(this._v2Root(source, name), version, profileHash, 'wyvrn.sig');
|
|
220
|
+
if (!fs.existsSync(p)) return null;
|
|
221
|
+
return fs.readFileSync(p);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async v2GetTrustAnchor({ source }) {
|
|
225
|
+
const trustDir = path.join(this._resolvePath(source), 'v2', '.trust');
|
|
226
|
+
if (!fs.existsSync(trustDir)) return null;
|
|
227
|
+
return {
|
|
228
|
+
keys: this._readJson(path.join(trustDir, 'keys.json')),
|
|
229
|
+
mode: this._readJson(path.join(trustDir, 'mode.json')),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
171
232
|
}
|
|
172
233
|
|
|
173
234
|
module.exports = FileProvider;
|
package/src/providers/http.js
CHANGED
|
@@ -123,6 +123,37 @@ class HttpProvider extends BaseProvider {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* GET url → raw bytes (Buffer), or null on 404 / error. Used for the
|
|
128
|
+
* S1 attestation + signature fetches where the verifier must see the
|
|
129
|
+
* exact bytes the publisher signed (any JSON.parse → JSON.stringify
|
|
130
|
+
* round-trip would re-canonicalise and could change content).
|
|
131
|
+
*/
|
|
132
|
+
_getBytes(url, token) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
const parsed = new URL(url);
|
|
135
|
+
const headers = { 'User-Agent': USER_AGENT };
|
|
136
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
137
|
+
const req = this._lib(url).request(
|
|
138
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'GET', headers },
|
|
139
|
+
(res) => {
|
|
140
|
+
if (res.statusCode === 404) { res.resume(); resolve(null); return; }
|
|
141
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
142
|
+
log.warn(`HTTP GET ${url} → ${res.statusCode}; treating as missing`);
|
|
143
|
+
res.resume();
|
|
144
|
+
resolve(null);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const chunks = [];
|
|
148
|
+
res.on('data', (c) => chunks.push(c));
|
|
149
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
req.on('error', () => resolve(null));
|
|
153
|
+
req.end();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
/** GET url → stream download to destPath. Returns true on success. */
|
|
127
158
|
async _download(url, destPath, token, timeoutMs) {
|
|
128
159
|
const controller = new AbortController();
|
|
@@ -174,41 +205,57 @@ class HttpProvider extends BaseProvider {
|
|
|
174
205
|
async v2Publish(files, options) {
|
|
175
206
|
const {
|
|
176
207
|
source, name, version, profileHash, buildSettings,
|
|
177
|
-
contentSha256, gitSha, gitRepo,
|
|
178
|
-
updateLatest = true,
|
|
208
|
+
contentSha256, gitSha, gitRepo, token,
|
|
209
|
+
updateLatest = true,
|
|
210
|
+
artefactConfigs,
|
|
179
211
|
} = options;
|
|
180
212
|
const base = source.replace(/\/$/, '');
|
|
181
213
|
const v2root = `${base}/v2/${name}`;
|
|
182
214
|
const buildRoot = `${v2root}/${version}/${profileHash}`;
|
|
183
215
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
const sliceConfigs =
|
|
217
|
+
(artefactConfigs && Object.keys(artefactConfigs).length > 0)
|
|
218
|
+
? Object.keys(artefactConfigs).sort()
|
|
219
|
+
: null;
|
|
220
|
+
|
|
221
|
+
// 1) Upload the binary zip(s) — fat or per-config slices.
|
|
222
|
+
if (sliceConfigs) {
|
|
223
|
+
if (!files.artefactZips || typeof files.artefactZips !== 'object') {
|
|
224
|
+
throw new Error('v2Publish: artefactConfigs supplied but files.artefactZips missing');
|
|
225
|
+
}
|
|
226
|
+
for (const config of sliceConfigs) {
|
|
227
|
+
const slicePath = files.artefactZips[config];
|
|
228
|
+
if (!slicePath) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`v2Publish: artefactConfigs declares "${config}" but files.artefactZips["${config}"] is missing`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
await this._put(`${buildRoot}/wyvrn-${config}.zip`, slicePath, 'application/zip', token);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
if (!files.zip) {
|
|
237
|
+
throw new Error('v2Publish: files.zip required for fat-zip publish');
|
|
238
|
+
}
|
|
239
|
+
await this._put(`${buildRoot}/wyvrn.zip`, files.zip, 'application/zip', token);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 2) Upload wyvrn.json VERBATIM from `files.manifest`. The publisher
|
|
243
|
+
// (commands/publish.js, upload-built.js) builds the v2Meta body
|
|
244
|
+
// via src/v2-meta.js so the signed manifestSha256 matches what
|
|
245
|
+
// consumers compute from v2GetMeta. Providers MUST NOT mutate.
|
|
246
|
+
await this._put(
|
|
206
247
|
`${buildRoot}/wyvrn.json`,
|
|
207
|
-
|
|
248
|
+
files.manifest,
|
|
208
249
|
'application/json',
|
|
209
250
|
token,
|
|
210
251
|
);
|
|
211
252
|
|
|
253
|
+
// 2.5) signing files (S1) — written between manifest and versions.json
|
|
254
|
+
// so a partial-failure crash never leaves a sig-less entry visible
|
|
255
|
+
// in versions.json. Both files optional; mode.json governs.
|
|
256
|
+
if (files.attPath) await this._put(`${buildRoot}/wyvrn.att.json`, files.attPath, 'application/json', token);
|
|
257
|
+
if (files.sigPath) await this._put(`${buildRoot}/wyvrn.sig`, files.sigPath, 'application/json', token);
|
|
258
|
+
|
|
212
259
|
// 3) Upload source.json (git metadata for build-from-source)
|
|
213
260
|
if (gitSha || gitRepo) {
|
|
214
261
|
const srcMeta = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
@@ -220,17 +267,23 @@ class HttpProvider extends BaseProvider {
|
|
|
220
267
|
);
|
|
221
268
|
}
|
|
222
269
|
|
|
223
|
-
// 4) Read-modify-write versions.json index (non-atomic, best-effort)
|
|
270
|
+
// 4) Read-modify-write versions.json index (non-atomic, best-effort).
|
|
271
|
+
// Per-profile entries hold either `contentSha256` (fat zip) or
|
|
272
|
+
// `configs` (slice publish).
|
|
224
273
|
let versionsIdx = await this._getJson(`${v2root}/versions.json`, token) ?? {
|
|
225
274
|
name, latest: version, versions: {},
|
|
226
275
|
};
|
|
227
276
|
if (!versionsIdx.versions) versionsIdx.versions = {};
|
|
228
277
|
if (!versionsIdx.versions[version]) versionsIdx.versions[version] = { profiles: {} };
|
|
229
|
-
|
|
230
|
-
|
|
278
|
+
|
|
279
|
+
const profileEntry = {
|
|
231
280
|
publishedAt: new Date().toISOString(),
|
|
232
281
|
buildSettings,
|
|
233
282
|
};
|
|
283
|
+
if (sliceConfigs) profileEntry.configs = sliceConfigs;
|
|
284
|
+
else profileEntry.contentSha256 = contentSha256;
|
|
285
|
+
versionsIdx.versions[version].profiles[profileHash] = profileEntry;
|
|
286
|
+
|
|
234
287
|
if (gitSha || gitRepo) {
|
|
235
288
|
versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
236
289
|
}
|
|
@@ -244,9 +297,12 @@ class HttpProvider extends BaseProvider {
|
|
|
244
297
|
|
|
245
298
|
// 5) v2 latest.json — skipped for consumer uploads (see FileProvider note).
|
|
246
299
|
if (updateLatest) {
|
|
300
|
+
const latestEntry = { version, profileHash };
|
|
301
|
+
if (sliceConfigs) latestEntry.configs = sliceConfigs;
|
|
302
|
+
else latestEntry.contentSha256 = contentSha256;
|
|
247
303
|
await this._putBuffer(
|
|
248
304
|
`${v2root}/latest.json`,
|
|
249
|
-
Buffer.from(JSON.stringify(
|
|
305
|
+
Buffer.from(JSON.stringify(latestEntry)),
|
|
250
306
|
'application/json',
|
|
251
307
|
token,
|
|
252
308
|
);
|
|
@@ -263,11 +319,32 @@ class HttpProvider extends BaseProvider {
|
|
|
263
319
|
return this._getJson(`${base}/v2/${name}/versions.json`, token);
|
|
264
320
|
}
|
|
265
321
|
|
|
266
|
-
async v2DownloadZip({ source, name, version, profileHash, token }, destPath, timeoutMs) {
|
|
267
|
-
const base
|
|
268
|
-
const
|
|
322
|
+
async v2DownloadZip({ source, name, version, profileHash, sliceConfig, token }, destPath, timeoutMs) {
|
|
323
|
+
const base = source.replace(/\/$/, '');
|
|
324
|
+
const filename = sliceConfig ? `wyvrn-${sliceConfig}.zip` : 'wyvrn.zip';
|
|
325
|
+
const url = `${base}/v2/${name}/${version}/${profileHash}/${filename}`;
|
|
269
326
|
return this._download(url, destPath, token, timeoutMs);
|
|
270
327
|
}
|
|
328
|
+
|
|
329
|
+
// ── S1 signing surface ────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
async v2GetAttestation({ source, name, version, profileHash, token }) {
|
|
332
|
+
const base = source.replace(/\/$/, '');
|
|
333
|
+
return this._getBytes(`${base}/v2/${name}/${version}/${profileHash}/wyvrn.att.json`, token);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async v2GetSignature({ source, name, version, profileHash, token }) {
|
|
337
|
+
const base = source.replace(/\/$/, '');
|
|
338
|
+
return this._getBytes(`${base}/v2/${name}/${version}/${profileHash}/wyvrn.sig`, token);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async v2GetTrustAnchor({ source, token }) {
|
|
342
|
+
const base = source.replace(/\/$/, '');
|
|
343
|
+
const keys = await this._getJson(`${base}/v2/.trust/keys.json`, token);
|
|
344
|
+
const mode = await this._getJson(`${base}/v2/.trust/mode.json`, token);
|
|
345
|
+
if (keys === null && mode === null) return null; // no .trust dir at all → mode off
|
|
346
|
+
return { keys, mode };
|
|
347
|
+
}
|
|
271
348
|
}
|
|
272
349
|
|
|
273
350
|
module.exports = HttpProvider;
|