ruvnet-brain 4.0.7 → 4.0.8

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.
@@ -3,13 +3,16 @@ import crypto from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
- import { RECEIPT_PREFIX, TERMINAL_STATES } from './release-transaction.mjs';
6
+ import {
7
+ pollObservation, RECEIPT_PREFIX, TERMINAL_STATES, transactionIdFor,
8
+ } from './release-transaction.mjs';
7
9
 
8
10
  const REPO = 'stuinfla/ruvnet-brain';
9
11
  const PACKAGE = 'ruvnet-brain';
12
+ export const ASSET_DOWNLOAD_TIMEOUT_MS = Number(process.env.RUVNET_RELEASE_ASSET_TIMEOUT_MS || 600_000);
10
13
 
11
14
  const command = (name, args, options = {}) => execFileSync(name, args, {
12
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...options,
15
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000, ...options,
13
16
  }).trim();
14
17
  const json = (name, args, options) => JSON.parse(command(name, args, options));
15
18
  const maybe = (callback, fallback = null) => {
@@ -24,31 +27,80 @@ const tagSha = (tag, root) => {
24
27
 
25
28
  const assetBytes = (asset) => {
26
29
  const result = spawnSync('gh', ['api', asset.url, '-H', 'Accept: application/octet-stream'], {
27
- encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'],
30
+ encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], timeout: ASSET_DOWNLOAD_TIMEOUT_MS,
28
31
  });
29
- if (result.error || result.status !== 0) throw new Error(`cannot download transaction asset ${asset.name}`);
32
+ if (result.error || result.signal || result.status !== 0) {
33
+ throw new Error(`cannot download transaction asset ${asset.name}: ${result.error?.message || result.signal || `exit ${result.status}`}`);
34
+ }
30
35
  return Buffer.from(result.stdout);
31
36
  };
32
37
  const assetReceipt = (asset) => JSON.parse(assetBytes(asset).toString('utf8'));
33
38
  const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
39
+ const OBSERVATION_POLICY = {
40
+ maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS || 180_000),
41
+ maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS || 14),
42
+ initialDelayMs: 1_000,
43
+ maxDelayMs: 15_000,
44
+ multiplier: 1.8,
45
+ jitter: (delay) => Math.floor(Math.random() * Math.min(1_000, delay * 0.2)),
46
+ };
47
+
48
+ export function selectCurrentReleaseBytes({ remoteBytes, localBytes, generatedBytes, identity }) {
49
+ const bytes = remoteBytes || localBytes || generatedBytes;
50
+ let value;
51
+ try { value = JSON.parse(Buffer.from(bytes).toString('utf8')); } catch { throw new Error('public receipt is invalid JSON'); }
52
+ if (value.transactionId !== identity.transactionId || value.version !== identity.version
53
+ || value.candidateSha !== identity.candidateSha || value.verdict !== 'PASS'
54
+ || value.hosts?.verdict !== 'PASS') {
55
+ throw new Error('public receipt identity conflict');
56
+ }
57
+ return Buffer.from(bytes);
58
+ }
34
59
 
35
60
  export function liveReleaseProvider({
36
61
  root = process.cwd(), candidateReceipt = null, publicationReceipt = null,
37
62
  } = {}) {
38
63
  let releases = [];
39
64
  let activeDraft = null;
65
+ const assetDigestCache = new Map();
40
66
  const refresh = () => {
41
- releases = json('gh', ['api', `repos/${REPO}/releases?per_page=100`]);
67
+ const pages = json('gh', ['api', `repos/${REPO}/releases?per_page=100`, '--paginate', '--slurp']);
68
+ releases = (Array.isArray(pages?.[0]) ? pages : [pages]).flat();
42
69
  return releases;
43
70
  };
44
71
  const releaseById = (id) => json('gh', ['api', `repos/${REPO}/releases/${id}`]);
72
+ const assetsForRelease = (id) => {
73
+ const pages = json('gh', ['api', `repos/${REPO}/releases/${id}/assets?per_page=100`, '--paginate', '--slurp']);
74
+ return (Array.isArray(pages?.[0]) ? pages : [pages]).flat();
75
+ };
76
+ const hydratedRelease = (release) => ({ ...release, assets: assetsForRelease(release.id) });
45
77
  const receiptsFor = (release) => (release?.assets || [])
46
78
  .filter(({ name }) => name.startsWith(RECEIPT_PREFIX) && name.endsWith('.json'))
47
79
  .map(assetReceipt);
48
- const materializeRemoteAssets = (draft, identity) => {
80
+ const expectedAssets = (identity) => ({
81
+ 'ruvnet-brain.zip': identity.bundleSha256,
82
+ 'ruvnet-brain.zip.sig': identity.bundleSignatureSha256,
83
+ 'ruvnet-brain.zip.sha256': identity.bundleDigestSha256,
84
+ [identity.packageAssetName || '']: identity.packageSha256 || null,
85
+ });
86
+ const digestAsset = (asset, force = false) => {
87
+ const key = `${asset.id}:${asset.size}`;
88
+ if (!force && assetDigestCache.has(key)) return assetDigestCache.get(key);
89
+ const digest = sha256(assetBytes(asset));
90
+ assetDigestCache.set(key, digest);
91
+ return digest;
92
+ };
93
+ const assetsExactFor = (release, identity, force = false) => {
94
+ const expected = expectedAssets(identity);
95
+ return Object.entries(expected).filter(([name, digest]) => name && digest).every(([name, digest]) => {
96
+ const asset = release?.assets?.find((item) => item.name === name);
97
+ return asset && digestAsset(asset, force) === digest;
98
+ });
99
+ };
100
+ const materializeRemoteAssets = async (draft, identity) => {
49
101
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-release-staged-'));
50
102
  try {
51
- const release = releaseById(draft.id);
103
+ const release = hydratedRelease(releaseById(draft.id));
52
104
  const required = [
53
105
  ['bundlePath', 'ruvnet-brain.zip'],
54
106
  ['bundleSignaturePath', 'ruvnet-brain.zip.sig'],
@@ -65,13 +117,25 @@ export function liveReleaseProvider({
65
117
  if (sha256(fs.readFileSync(assets.bundlePath)) !== identity.bundleSha256) {
66
118
  throw new Error('staged GitHub bundle digest does not match transaction identity');
67
119
  }
68
- const packed = json('npm', [
69
- 'pack', `${PACKAGE}@candidate-v${identity.version}`, '--json', '--pack-destination', temp,
70
- ]);
71
- if (!Array.isArray(packed) || packed.length !== 1 || !packed[0].filename) {
72
- throw new Error('npm candidate pack did not return exactly one artifact');
120
+ if (identity.bundleSignatureSha256
121
+ && sha256(fs.readFileSync(assets.bundleSignaturePath)) !== identity.bundleSignatureSha256) {
122
+ throw new Error('staged GitHub bundle signature bytes do not match transaction identity');
123
+ }
124
+ if (identity.bundleDigestSha256
125
+ && sha256(fs.readFileSync(assets.bundleDigestPath)) !== identity.bundleDigestSha256) {
126
+ throw new Error('staged GitHub digest sidecar bytes do not match transaction identity');
73
127
  }
74
- assets.packagePath = path.join(temp, packed[0].filename);
128
+ const { value: metadata } = await pollObservation(
129
+ async () => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']),
130
+ (value) => value?.version === identity.version
131
+ && value?.dist?.integrity === identity.packageIntegrity
132
+ && typeof value?.dist?.tarball === 'string',
133
+ OBSERVATION_POLICY,
134
+ );
135
+ const response = await fetch(metadata.dist.tarball, { signal: AbortSignal.timeout(30_000) });
136
+ if (!response.ok) throw new Error(`npm candidate tarball download failed: HTTP ${response.status}`);
137
+ assets.packagePath = path.join(temp, identity.packageAssetName || `${PACKAGE}-${identity.version}.tgz`);
138
+ fs.writeFileSync(assets.packagePath, Buffer.from(await response.arrayBuffer()), { flag: 'wx', mode: 0o600 });
75
139
  const integrity = `sha512-${crypto.createHash('sha512').update(fs.readFileSync(assets.packagePath)).digest('base64')}`;
76
140
  if (integrity !== identity.packageIntegrity) throw new Error('staged npm package integrity mismatch');
77
141
  return { assets, cleanup: () => fs.rmSync(temp, { recursive: true, force: true }) };
@@ -82,24 +146,42 @@ export function liveReleaseProvider({
82
146
  };
83
147
 
84
148
  return {
149
+ observationPolicy: OBSERVATION_POLICY,
150
+
85
151
  async discover(identity) {
86
152
  const all = refresh();
87
- const matchingDrafts = all.filter((release) => release.tag_name === identity.tag
88
- && release.target_commitish === identity.candidateSha).map((release) => ({
89
- id: release.id, tag: release.tag_name, sha: release.target_commitish, draft: release.draft,
90
- }));
153
+ const matchingDrafts = all.filter((release) => release.tag_name === identity.tag)
154
+ .map((release) => ({
155
+ id: release.id,
156
+ tag: release.tag_name,
157
+ sha: release.draft ? release.target_commitish : tagSha(identity.tag, root),
158
+ draft: release.draft,
159
+ }))
160
+ .filter((release) => release.sha === identity.candidateSha);
91
161
  activeDraft = matchingDrafts[0] || null;
92
- const receipts = matchingDrafts.length === 1 ? receiptsFor(releaseById(matchingDrafts[0].id)) : [];
162
+ const receipts = matchingDrafts.length === 1
163
+ ? receiptsFor(hydratedRelease(releaseById(matchingDrafts[0].id))) : [];
93
164
  const latestByTransaction = new Map();
94
- for (const receipt of all.flatMap((release) => receiptsFor(release))) {
95
- const prior = latestByTransaction.get(receipt.transactionId);
96
- if (!prior || receipt.sequence > prior.sequence) latestByTransaction.set(receipt.transactionId, receipt);
165
+ for (const release of all) {
166
+ for (const receipt of receiptsFor(hydratedRelease(release))) {
167
+ const prior = latestByTransaction.get(receipt.transactionId);
168
+ if (!prior || receipt.sequence > prior.receipt.sequence) {
169
+ latestByTransaction.set(receipt.transactionId, { receipt, release });
170
+ }
171
+ }
97
172
  }
98
- const pending = [...latestByTransaction.values()]
99
- .filter((receipt) => !TERMINAL_STATES.has(receipt.state));
100
- const npmLatest = maybe(() => command('npm', ['view', `${PACKAGE}@latest`, 'version']), null);
101
- const githubLatest = maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
102
- return { matchingDrafts, receipts, pending, prior: { npmLatest, githubLatest } };
173
+ const npmLatest = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
174
+ const githubLatest = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
175
+ const legacySettled = [];
176
+ const pending = [];
177
+ for (const { receipt, release } of latestByTransaction.values()) {
178
+ if (TERMINAL_STATES.has(receipt.state)) continue;
179
+ const settled = receipt.schemaVersion === 1 && release.draft === false
180
+ && receipt.identity?.version === npmLatest && receipt.identity?.tag === githubLatest;
181
+ if (settled) legacySettled.push(receipt.transactionId);
182
+ else pending.push(receipt);
183
+ }
184
+ return { matchingDrafts, receipts, pending, legacySettled, prior: { npmLatest, githubLatest } };
103
185
  },
104
186
 
105
187
  async createDraft(identity) {
@@ -112,6 +194,63 @@ export function liveReleaseProvider({
112
194
  return activeDraft;
113
195
  },
114
196
 
197
+ async observeSnapshot(identity, draft = activeDraft, { forceAssets = false } = {}) {
198
+ try {
199
+ const release = draft?.id ? hydratedRelease(releaseById(draft.id)) : null;
200
+ const latestTag = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
201
+ const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
202
+ const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
203
+ const latestVersion = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
204
+ const resolvedSha = release && !release.draft ? tagSha(identity.tag, root) : release?.target_commitish || draft?.sha || null;
205
+ const publicReceipt = release?.assets?.find((asset) => asset.name === 'current-release.json');
206
+ let publicReceiptExact = false;
207
+ let publicHostsExact = false;
208
+ if (publicReceipt) {
209
+ const value = assetReceipt(publicReceipt);
210
+ publicReceiptExact = value?.transactionId === transactionIdFor(identity)
211
+ && value?.version === identity.version
212
+ && value?.candidateSha === identity.candidateSha
213
+ && /^[a-f0-9]{64}$/.test(value?.publicationReceiptSha256 || '');
214
+ publicHostsExact = value?.verdict === 'PASS' && value?.hosts?.verdict === 'PASS'
215
+ && ['claudeOnly', 'codexOnly', 'dual'].every((mode) => value.hosts?.[mode]?.status === 'PASS'
216
+ && value.hosts?.[mode]?.doctorExit === 0
217
+ && value.hosts?.[mode]?.artifactSha256 === identity.packageSha256);
218
+ }
219
+ return {
220
+ npm: {
221
+ candidateVersion: candidate?.version || exactVersion?.version || null,
222
+ candidateIntegrity: candidate?.dist?.integrity || exactVersion?.dist?.integrity || null,
223
+ candidateTagVersion: candidate?.version || null,
224
+ latestVersion,
225
+ },
226
+ github: release ? {
227
+ tag: release.tag_name,
228
+ sha: resolvedSha,
229
+ draft: release.draft === true,
230
+ published: release.draft === false,
231
+ latest: latestTag === identity.tag,
232
+ latestTag,
233
+ assetsExact: assetsExactFor(release, identity, forceAssets),
234
+ } : { tag: null, sha: null, draft: false, published: false, latest: false, assetsExact: false },
235
+ publicReceiptExact,
236
+ publicHostsExact,
237
+ };
238
+ } catch (error) {
239
+ return { readError: error.message };
240
+ }
241
+ },
242
+
243
+ async observeNpm(identity) {
244
+ const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
245
+ const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
246
+ return {
247
+ candidateVersion: candidate?.version || exactVersion?.version || null,
248
+ candidateIntegrity: candidate?.dist?.integrity || exactVersion?.dist?.integrity || null,
249
+ candidateTagVersion: candidate?.version || null,
250
+ latestVersion: command('npm', ['view', `${PACKAGE}@latest`, 'version']),
251
+ };
252
+ },
253
+
115
254
  async appendReceipt(draft, receipt, name) {
116
255
  const anchor = draft || activeDraft;
117
256
  if (!anchor) throw new Error('no draft anchor for transaction receipt');
@@ -127,7 +266,7 @@ export function liveReleaseProvider({
127
266
 
128
267
  async readReceipt(draft, sequence) {
129
268
  const name = `${RECEIPT_PREFIX}${String(sequence).padStart(4, '0')}.json`;
130
- const release = releaseById((draft || activeDraft).id);
269
+ const release = hydratedRelease(releaseById((draft || activeDraft).id));
131
270
  const asset = release.assets?.find((item) => item.name === name);
132
271
  if (!asset) throw new Error(`remote receipt ${name} is missing after upload`);
133
272
  return assetReceipt(asset);
@@ -136,7 +275,7 @@ export function liveReleaseProvider({
136
275
  async uploadAssets(draft, assets) {
137
276
  const paths = [assets.bundlePath, assets.bundleSignaturePath, assets.bundleDigestPath, assets.packagePath];
138
277
  for (const file of paths) {
139
- const release = releaseById(draft.id);
278
+ const release = hydratedRelease(releaseById(draft.id));
140
279
  const existing = release.assets?.find(({ name }) => name === path.basename(file));
141
280
  if (existing) {
142
281
  if (sha256(assetBytes(existing)) !== sha256(fs.readFileSync(file))) {
@@ -154,7 +293,11 @@ export function liveReleaseProvider({
154
293
 
155
294
  async stageNpm(identity, packagePath) {
156
295
  const existing = maybe(() => command('npm', ['view', `${PACKAGE}@${identity.version}`, 'dist.integrity']), null);
157
- if (existing) return;
296
+ if (existing) {
297
+ if (existing !== identity.packageIntegrity) throw new Error('existing npm version has different immutable bytes');
298
+ command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, `candidate-v${identity.version}`]);
299
+ return;
300
+ }
158
301
  command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`]);
159
302
  },
160
303
 
@@ -173,13 +316,21 @@ export function liveReleaseProvider({
173
316
  return { tag: release.tag_name, sha: tagSha(identity.tag, root), latest: latest === identity.tag };
174
317
  },
175
318
 
176
- async promoteNpm(identity) {
319
+ async promoteNpm(identity, expectedPrior) {
320
+ const current = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
321
+ if (current !== expectedPrior && current !== identity.version) {
322
+ throw new Error(`refusing npm promotion: latest is ${current}, expected ${expectedPrior}`);
323
+ }
177
324
  command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, 'latest']);
178
325
  },
179
326
  async observeNpmLatest() {
180
327
  return { version: command('npm', ['view', `${PACKAGE}@latest`, 'version']) };
181
328
  },
182
- async makeGithubLatest(draft) {
329
+ async makeGithubLatest(draft, identity, expectedPrior) {
330
+ const current = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
331
+ if (current !== expectedPrior && current !== identity.tag) {
332
+ throw new Error(`refusing GitHub promotion: latest is ${current}, expected ${expectedPrior}`);
333
+ }
183
334
  command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-f', 'make_latest=true']);
184
335
  },
185
336
  async observeGithubLatest() {
@@ -190,15 +341,7 @@ export function liveReleaseProvider({
190
341
  if (current !== expected) throw new Error(`refusing compensation: npm latest is ${current}, expected ${expected}`);
191
342
  command('npm', ['dist-tag', 'add', `${PACKAGE}@${prior}`, 'latest']);
192
343
  },
193
- async finalize(identity, receipt, hostVerifier) {
194
- const staged = materializeRemoteAssets(activeDraft, identity);
195
- let hosts;
196
- try {
197
- hosts = await hostVerifier.verify({ source: 'final', identity, assets: staged.assets });
198
- } finally {
199
- staged.cleanup();
200
- }
201
- if (hosts.verdict !== 'PASS') return { verdict: 'FAIL', hosts };
344
+ async finalize(identity, receipt, _hostVerifier) {
202
345
  if (!candidateReceipt || !publicationReceipt) {
203
346
  throw new Error('final convergence requires candidate and publication receipt paths');
204
347
  }
@@ -207,30 +350,64 @@ export function liveReleaseProvider({
207
350
  'scripts/publication-receipt.mjs', '--candidate', candidateReceipt, '--out', publicationReceipt,
208
351
  ], { cwd: root, encoding: 'utf8', timeout: 1_200_000 });
209
352
  if (publication.error || publication.status !== 0) {
210
- return { verdict: 'FAIL', hosts, publicationError: String(publication.stderr || publication.error?.message) };
353
+ return { verdict: 'FAIL', publicationError: String(publication.stderr || publication.error?.message) };
211
354
  }
212
355
  }
213
356
  const seal = spawnSync(process.execPath, [
214
357
  'scripts/release-proof.mjs', '--candidate', candidateReceipt, '--publication', publicationReceipt,
215
358
  ], { cwd: root, encoding: 'utf8', timeout: 300_000 });
216
359
  if (seal.error || seal.status !== 0) {
217
- return { verdict: 'FAIL', hosts, sealError: String(seal.stderr || seal.error?.message) };
218
- }
219
- const channels = spawnSync(process.execPath, ['scripts/verify-channels.mjs'], {
220
- cwd: root, encoding: 'utf8', timeout: 600_000,
221
- });
222
- if (channels.error || channels.status !== 0) {
223
- return { verdict: 'FAIL', hosts, channelError: String(channels.stderr || channels.error?.message) };
224
- }
225
- const probe = spawnSync(process.execPath, ['scripts/published-surface-probe.mjs', '--json'], {
226
- cwd: root, encoding: 'utf8', timeout: 600_000,
227
- });
228
- let observed = null;
229
- try { observed = JSON.parse(probe.stdout || ''); } catch {}
230
- return {
231
- verdict: probe.status === 0 && observed?.verdict === 'PASS' ? 'PASS' : 'FAIL',
360
+ return { verdict: 'FAIL', sealError: String(seal.stderr || seal.error?.message) };
361
+ }
362
+ const publication = JSON.parse(fs.readFileSync(path.resolve(root, publicationReceipt), 'utf8'));
363
+ const hosts = {
364
+ verdict: 'PASS',
365
+ claudeOnly: publication.installed?.claudeOnly,
366
+ codexOnly: publication.installed?.codexOnly,
367
+ dual: publication.installed?.dual,
368
+ };
369
+ const observed = publication.postPublicationChecks?.find(({ name }) => name === 'published-surface-probe');
370
+ const result = {
371
+ verdict: observed?.status === 'completed' && observed?.conclusion === 'success' ? 'PASS' : 'FAIL',
232
372
  hosts, surface: observed, publicationReceipt, previousReceiptDigest: receipt.receiptDigest,
233
373
  };
374
+ if (result.verdict === 'PASS') {
375
+ const currentRelease = path.join(path.dirname(path.resolve(root, publicationReceipt)), 'current-release.json');
376
+ const generatedCurrentRelease = Buffer.from(`${JSON.stringify({
377
+ schemaVersion: 1,
378
+ transactionId: receipt.transactionId,
379
+ version: identity.version,
380
+ tag: identity.tag,
381
+ candidateSha: identity.candidateSha,
382
+ verdict: 'PASS',
383
+ publicationReceiptSha256: sha256(fs.readFileSync(path.resolve(root, publicationReceipt))),
384
+ hosts,
385
+ surface: observed,
386
+ }, null, 2)}\n`);
387
+ let remote = hydratedRelease(releaseById(activeDraft.id)).assets
388
+ ?.find((asset) => asset.name === 'current-release.json');
389
+ let currentReleaseBytes;
390
+ try {
391
+ currentReleaseBytes = selectCurrentReleaseBytes({
392
+ remoteBytes: remote ? assetBytes(remote) : null,
393
+ localBytes: !remote && fs.existsSync(currentRelease) ? fs.readFileSync(currentRelease) : null,
394
+ generatedBytes: generatedCurrentRelease,
395
+ identity: { transactionId: receipt.transactionId, version: identity.version, candidateSha: identity.candidateSha },
396
+ });
397
+ } catch (error) {
398
+ return { ...result, verdict: 'FAIL', currentReleaseError: error.message };
399
+ }
400
+ if (!fs.existsSync(currentRelease)) fs.writeFileSync(currentRelease, currentReleaseBytes, { flag: 'wx', mode: 0o600 });
401
+ if (!remote) {
402
+ command('gh', ['release', 'upload', activeDraft.tag, currentRelease, '--repo', REPO]);
403
+ remote = hydratedRelease(releaseById(activeDraft.id)).assets
404
+ ?.find((asset) => asset.name === 'current-release.json');
405
+ }
406
+ if (!remote || sha256(assetBytes(remote)) !== sha256(currentReleaseBytes)) {
407
+ return { ...result, verdict: 'FAIL', currentReleaseError: 'public receipt verification failed' };
408
+ }
409
+ }
410
+ return result;
234
411
  },
235
412
  };
236
413
  }