@comergehq/studio 0.1.13 → 0.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comergehq/studio",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Comerge studio",
5
5
  "main": "src/index.ts",
6
6
  "module": "dist/index.mjs",
@@ -68,6 +68,7 @@
68
68
  "react-native": "*",
69
69
  "react-native-safe-area-context": "*",
70
70
  "react-native-svg": "*",
71
+ "react-native-zip-archive": "*",
71
72
  "react-native-view-shot": "*"
72
73
  },
73
74
  "peerDependenciesMeta": {}
@@ -11,6 +11,11 @@ export interface BundlesRemoteDataSource {
11
11
  bundleId: string,
12
12
  options?: { redirect?: boolean }
13
13
  ): Promise<ServiceResponse<{ url: string; redirect: boolean }>>;
14
+ getSignedAssetsDownloadUrl(
15
+ appId: string,
16
+ bundleId: string,
17
+ options?: { redirect?: boolean; kind?: string }
18
+ ): Promise<ServiceResponse<{ url: string; redirect: boolean }>>;
14
19
  }
15
20
 
16
21
  class BundlesRemoteDataSourceImpl extends BaseRemote implements BundlesRemoteDataSource {
@@ -40,6 +45,18 @@ class BundlesRemoteDataSourceImpl extends BaseRemote implements BundlesRemoteDat
40
45
  );
41
46
  return data;
42
47
  }
48
+
49
+ async getSignedAssetsDownloadUrl(
50
+ appId: string,
51
+ bundleId: string,
52
+ options?: { redirect?: boolean; kind?: string }
53
+ ): Promise<ServiceResponse<{ url: string; redirect: boolean }>> {
54
+ const { data } = await api.get<ServiceResponse<{ url: string; redirect: boolean }>>(
55
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download`,
56
+ { params: { redirect: options?.redirect ?? false, kind: options?.kind } }
57
+ );
58
+ return data;
59
+ }
43
60
  }
44
61
 
45
62
  export const bundlesRemoteDataSource: BundlesRemoteDataSource = new BundlesRemoteDataSourceImpl();
@@ -7,6 +7,11 @@ export interface BundlesRepository {
7
7
  initiate(appId: string, payload: InitiateBundleRequest): Promise<Bundle>;
8
8
  getById(appId: string, bundleId: string): Promise<Bundle>;
9
9
  getSignedDownloadUrl(appId: string, bundleId: string, options?: { redirect?: boolean }): Promise<{ url: string; redirect: boolean }>;
10
+ getSignedAssetsDownloadUrl(
11
+ appId: string,
12
+ bundleId: string,
13
+ options?: { redirect?: boolean; kind?: string }
14
+ ): Promise<{ url: string; redirect: boolean }>;
10
15
  }
11
16
 
12
17
  class BundlesRepositoryImpl extends BaseRepository implements BundlesRepository {
@@ -28,6 +33,15 @@ class BundlesRepositoryImpl extends BaseRepository implements BundlesRepository
28
33
  const res = await this.remote.getSignedDownloadUrl(appId, bundleId, options);
29
34
  return this.unwrapOrThrow(res);
30
35
  }
36
+
37
+ async getSignedAssetsDownloadUrl(
38
+ appId: string,
39
+ bundleId: string,
40
+ options?: { redirect?: boolean; kind?: string }
41
+ ): Promise<{ url: string; redirect: boolean }> {
42
+ const res = await this.remote.getSignedAssetsDownloadUrl(appId, bundleId, options);
43
+ return this.unwrapOrThrow(res);
44
+ }
31
45
  }
32
46
 
33
47
  export const bundlesRepository: BundlesRepository = new BundlesRepositoryImpl(bundlesRemoteDataSource);
@@ -2,6 +2,20 @@ export type Platform = 'ios' | 'android';
2
2
 
3
3
  export type BundleStatus = 'pending' | 'building' | 'succeeded' | 'failed';
4
4
 
5
+ export type BundleAssetKind = 'metro-assets' | string;
6
+
7
+ export type BundleAsset = {
8
+ id: string;
9
+ kind: BundleAssetKind;
10
+ storageBucket: string;
11
+ storageKey: string;
12
+ contentType: string | null;
13
+ size: number | null;
14
+ checksumSha256: string | null;
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ };
18
+
5
19
  export type Bundle = {
6
20
  id: string;
7
21
  appId: string;
@@ -15,6 +29,7 @@ export type Bundle = {
15
29
  createdAt: string;
16
30
  updatedAt: string;
17
31
  expiresAt: string | null;
32
+ assets?: BundleAsset[];
18
33
  };
19
34
 
20
35
  export type InitiateBundleRequest = {
@@ -1,8 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import * as FileSystem from 'expo-file-system/legacy';
3
3
  import { Asset } from 'expo-asset';
4
+ import { unzip } from 'react-native-zip-archive';
4
5
 
5
- import type { Platform as BundlePlatform, Bundle } from '../../data/apps/bundles/types';
6
+ import type { Platform as BundlePlatform, Bundle, BundleAsset } from '../../data/apps/bundles/types';
6
7
  import { bundlesRepository } from '../../data/apps/bundles/repository';
7
8
 
8
9
  function sleep(ms: number): Promise<void> {
@@ -90,6 +91,8 @@ export type UseBundleManagerResult = BundleLoadState & {
90
91
  export type EmbeddedBaseBundle = {
91
92
  module: number;
92
93
  meta?: BaseBundleMeta | null;
94
+ assetsModule?: number;
95
+ assetsMeta?: EmbeddedAssetsMeta | null;
93
96
  };
94
97
 
95
98
  export type EmbeddedBaseBundles = {
@@ -97,6 +100,11 @@ export type EmbeddedBaseBundles = {
97
100
  android?: EmbeddedBaseBundle;
98
101
  };
99
102
 
103
+ type EmbeddedAssetsMeta = {
104
+ checksumSha256: string | null;
105
+ size: number | null;
106
+ };
107
+
100
108
  function safeName(s: string) {
101
109
  return s.replace(/[^a-zA-Z0-9._-]/g, '_');
102
110
  }
@@ -114,6 +122,11 @@ async function ensureDir(path: string) {
114
122
  await FileSystem.makeDirectoryAsync(path, { intermediates: true });
115
123
  }
116
124
 
125
+ async function ensureBundleDir(key: string) {
126
+ await ensureDir(bundlesCacheDir());
127
+ await ensureDir(bundleDir(key));
128
+ }
129
+
117
130
  function baseBundleKey(appId: string, platform: BundlePlatform): string {
118
131
  return `base:${appId}:${platform}`;
119
132
  }
@@ -122,16 +135,37 @@ function testBundleKey(appId: string, commitId: string | null | undefined, platf
122
135
  return `test:${appId}:${commitId ?? 'head'}:${platform}:${bundleId}`;
123
136
  }
124
137
 
125
- function toBundleFileUri(key: string): string {
138
+ function legacyBundleFileUri(key: string): string {
126
139
  const dir = bundlesCacheDir();
127
140
  return `${dir}${safeName(key)}.jsbundle`;
128
141
  }
129
142
 
130
- function toBundleMetaFileUri(key: string): string {
143
+ function legacyBundleMetaFileUri(key: string): string {
131
144
  const dir = bundlesCacheDir();
132
145
  return `${dir}${safeName(key)}.meta.json`;
133
146
  }
134
147
 
148
+ function bundleDir(key: string): string {
149
+ const dir = bundlesCacheDir();
150
+ return `${dir}${safeName(key)}/`;
151
+ }
152
+
153
+ function toBundleFileUri(key: string, platform: BundlePlatform): string {
154
+ return `${bundleDir(key)}index.${platform}.jsbundle`;
155
+ }
156
+
157
+ function toBundleMetaFileUri(key: string): string {
158
+ return `${bundleDir(key)}bundle.meta.json`;
159
+ }
160
+
161
+ function toAssetsMetaFileUri(key: string): string {
162
+ return `${bundleDir(key)}assets.meta.json`;
163
+ }
164
+
165
+ function toAssetsDir(key: string): string {
166
+ return `${bundleDir(key)}assets/`;
167
+ }
168
+
135
169
  type BaseBundleMeta = {
136
170
  fingerprint: string;
137
171
  bundleId: string;
@@ -156,7 +190,7 @@ async function writeJsonFile(fileUri: string, value: unknown): Promise<void> {
156
190
  try {
157
191
  await (FileSystem as any).writeAsStringAsync(fileUri, JSON.stringify(value));
158
192
  } catch {
159
-
193
+
160
194
  }
161
195
  }
162
196
 
@@ -170,6 +204,15 @@ async function getExistingNonEmptyFileUri(fileUri: string): Promise<string | nul
170
204
  }
171
205
  }
172
206
 
207
+ async function getExistingBundleFileUri(key: string, platform: BundlePlatform): Promise<string | null> {
208
+ const nextPath = toBundleFileUri(key, platform);
209
+ const next = await getExistingNonEmptyFileUri(nextPath);
210
+ if (next) return next;
211
+ const legacyPath = legacyBundleFileUri(key);
212
+ const legacy = await getExistingNonEmptyFileUri(legacyPath);
213
+ return legacy;
214
+ }
215
+
173
216
  async function downloadIfMissing(url: string, fileUri: string): Promise<string> {
174
217
  const existing = await getExistingNonEmptyFileUri(fileUri);
175
218
  if (existing) return existing;
@@ -189,9 +232,9 @@ async function deleteFileIfExists(fileUri: string) {
189
232
  try {
190
233
  const info = await FileSystem.getInfoAsync(fileUri);
191
234
  if (!info.exists) return;
192
- await FileSystem.deleteAsync(fileUri).catch(() => {});
235
+ await FileSystem.deleteAsync(fileUri).catch(() => { });
193
236
  } catch {
194
-
237
+
195
238
  }
196
239
  }
197
240
 
@@ -202,9 +245,12 @@ async function hydrateBaseFromEmbeddedAsset(
202
245
  ): Promise<{ bundlePath: string; meta?: BaseBundleMeta | null } | null> {
203
246
  if (!embedded?.module) return null;
204
247
  const key = baseBundleKey(appId, platform);
205
- const targetUri = toBundleFileUri(key);
206
- const existing = await getExistingNonEmptyFileUri(targetUri);
207
- if (existing) return { bundlePath: existing, meta: embedded.meta ?? null };
248
+ const existing = await getExistingBundleFileUri(key, platform);
249
+ if (existing) {
250
+ return { bundlePath: existing, meta: embedded.meta ?? null };
251
+ }
252
+ await ensureBundleDir(key);
253
+ const targetUri = toBundleFileUri(key, platform);
208
254
 
209
255
  const asset = Asset.fromModule(embedded.module);
210
256
  await asset.downloadAsync();
@@ -220,8 +266,69 @@ async function hydrateBaseFromEmbeddedAsset(
220
266
  return { bundlePath: finalUri, meta: embedded.meta ?? null };
221
267
  }
222
268
 
223
- async function safeReplaceFileFromUrl(url: string, targetUri: string, tmpKey: string): Promise<string> {
224
- const tmpUri = toBundleFileUri(`tmp:${tmpKey}:${Date.now()}`);
269
+ async function hydrateAssetsFromEmbeddedAsset(
270
+ appId: string,
271
+ platform: BundlePlatform,
272
+ key: string,
273
+ embedded: EmbeddedBaseBundle | undefined
274
+ ): Promise<boolean> {
275
+ const moduleId = embedded?.assetsModule;
276
+ if (!moduleId) return false;
277
+
278
+ const assetsMeta = embedded?.assetsMeta ?? null;
279
+ const assetsDir = toAssetsDir(key);
280
+ const metaUri = toAssetsMetaFileUri(key);
281
+
282
+ const existingMeta = await readJsonFile<AssetsMeta>(metaUri);
283
+ const assetsDirInfo = await FileSystem.getInfoAsync(assetsDir);
284
+ const assetsDirExists = assetsDirInfo.exists && assetsDirInfo.isDirectory;
285
+ const checksumMatches =
286
+ Boolean(existingMeta?.checksumSha256) &&
287
+ Boolean(assetsMeta?.checksumSha256) &&
288
+ existingMeta?.checksumSha256 === assetsMeta?.checksumSha256;
289
+ const embeddedMetaMatches = existingMeta?.storageKey?.startsWith('embedded:');
290
+ if (assetsDirExists && checksumMatches && embeddedMetaMatches) {
291
+ return true;
292
+ }
293
+
294
+ await ensureBundleDir(key);
295
+ await ensureDir(assetsDir);
296
+
297
+ const asset = Asset.fromModule(moduleId);
298
+ await asset.downloadAsync();
299
+ const sourceUri = asset.localUri ?? asset.uri;
300
+ if (!sourceUri) return false;
301
+ const info = await FileSystem.getInfoAsync(sourceUri);
302
+ if (!info.exists) return false;
303
+
304
+ const zipUri = `${bundleDir(key)}assets.zip`;
305
+ await deleteFileIfExists(zipUri);
306
+ await FileSystem.copyAsync({ from: sourceUri, to: zipUri });
307
+
308
+ try {
309
+ await FileSystem.deleteAsync(assetsDir, { idempotent: true }).catch(() => { });
310
+ } catch {
311
+ }
312
+ await ensureDir(assetsDir);
313
+ await unzipArchive(zipUri, assetsDir);
314
+
315
+ await writeJsonFile(metaUri, {
316
+ checksumSha256: assetsMeta?.checksumSha256 ?? null,
317
+ storageKey: `embedded:${assetsMeta?.checksumSha256 ?? 'unknown'}`,
318
+ updatedAt: new Date().toISOString(),
319
+ } satisfies AssetsMeta);
320
+ return true;
321
+ }
322
+
323
+ async function safeReplaceFileFromUrl(
324
+ url: string,
325
+ targetUri: string,
326
+ tmpKey: string,
327
+ platform: BundlePlatform
328
+ ): Promise<string> {
329
+ const tmpKeySafe = `tmp:${tmpKey}:${Date.now()}`;
330
+ const tmpUri = toBundleFileUri(tmpKeySafe, platform);
331
+ await ensureDir(bundleDir(tmpKeySafe));
225
332
  try {
226
333
  await withRetry(
227
334
  async () => {
@@ -244,6 +351,107 @@ async function safeReplaceFileFromUrl(url: string, targetUri: string, tmpKey: st
244
351
  }
245
352
  }
246
353
 
354
+ async function safeReplaceFileFromUrlToPath(url: string, targetUri: string, tmpKey: string): Promise<string> {
355
+ const tmpDir = `${bundlesCacheDir()}tmp/`;
356
+ await ensureDir(tmpDir);
357
+ const tmpUri = `${tmpDir}${safeName(tmpKey)}.tmp`;
358
+ try {
359
+ await withRetry(
360
+ async () => {
361
+ await deleteFileIfExists(tmpUri);
362
+ await FileSystem.downloadAsync(url, tmpUri);
363
+ const tmpOk = await getExistingNonEmptyFileUri(tmpUri);
364
+ if (!tmpOk) throw new Error('Downloaded file is empty.');
365
+ },
366
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4000 }
367
+ );
368
+ await deleteFileIfExists(targetUri);
369
+ await FileSystem.moveAsync({ from: tmpUri, to: targetUri });
370
+ const finalOk = await getExistingNonEmptyFileUri(targetUri);
371
+ if (!finalOk) throw new Error('File replacement failed.');
372
+ return targetUri;
373
+ } finally {
374
+ await deleteFileIfExists(tmpUri);
375
+ }
376
+ }
377
+
378
+ function getMetroAssets(bundle: Bundle): BundleAsset | null {
379
+ const assets = bundle.assets ?? [];
380
+ return assets.find((asset) => asset.kind === 'metro-assets') ?? null;
381
+ }
382
+
383
+ type AssetsMeta = {
384
+ checksumSha256: string | null;
385
+ storageKey: string;
386
+ updatedAt: string;
387
+ };
388
+
389
+ async function ensureAssetsForBundle(
390
+ appId: string,
391
+ bundle: Bundle,
392
+ key: string,
393
+ platform: BundlePlatform
394
+ ): Promise<void> {
395
+ const asset = getMetroAssets(bundle);
396
+ if (!asset?.storageKey) return;
397
+
398
+ await ensureBundleDir(key);
399
+ const assetsDir = toAssetsDir(key);
400
+ await ensureDir(assetsDir);
401
+
402
+ const metaUri = toAssetsMetaFileUri(key);
403
+ const existingMeta = await readJsonFile<AssetsMeta>(metaUri);
404
+ const assetsDirInfo = await FileSystem.getInfoAsync(assetsDir);
405
+ const assetsDirExists = assetsDirInfo.exists && assetsDirInfo.isDirectory;
406
+ if (
407
+ existingMeta?.checksumSha256 &&
408
+ asset.checksumSha256 &&
409
+ existingMeta.checksumSha256 === asset.checksumSha256 &&
410
+ (existingMeta.storageKey === asset.storageKey || existingMeta.storageKey?.startsWith('embedded:')) &&
411
+ assetsDirExists
412
+ ) {
413
+ return;
414
+ }
415
+
416
+ const signed = await withRetry(
417
+ async () => {
418
+ return await bundlesRepository.getSignedAssetsDownloadUrl(appId, bundle.id, {
419
+ redirect: false,
420
+ kind: asset.kind,
421
+ });
422
+ },
423
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4000 }
424
+ );
425
+
426
+ const zipUri = `${bundleDir(key)}assets.zip`;
427
+ await safeReplaceFileFromUrlToPath(signed.url, zipUri, `${appId}:${bundle.id}:${platform}:${asset.kind}`);
428
+
429
+ try {
430
+ await FileSystem.deleteAsync(assetsDir, { idempotent: true }).catch(() => { });
431
+ } catch {
432
+
433
+ }
434
+ await ensureDir(assetsDir);
435
+ await unzipArchive(zipUri, assetsDir);
436
+ await writeJsonFile(metaUri, {
437
+ checksumSha256: asset.checksumSha256 ?? null,
438
+ storageKey: asset.storageKey,
439
+ updatedAt: new Date().toISOString(),
440
+ } satisfies AssetsMeta);
441
+ }
442
+
443
+ async function unzipArchive(sourceUri: string, destDir: string) {
444
+ try {
445
+ await unzip(sourceUri, destDir);
446
+ } catch (e) {
447
+ throw new Error(
448
+ `Failed to extract assets archive. Ensure 'react-native-zip-archive' is installed in the host app. ${String(
449
+ (e as Error)?.message ?? e
450
+ )}`
451
+ );
452
+ }
453
+ }
454
+
247
455
  async function pollBundle(appId: string, bundleId: string, opts: { timeoutMs: number; intervalMs: number }): Promise<Bundle> {
248
456
  const start = Date.now();
249
457
  while (true) {
@@ -291,21 +499,41 @@ async function resolveBundlePath(
291
499
  throw new Error('Bundle build failed.');
292
500
  }
293
501
 
502
+ let bundleWithAssets = finalBundle;
503
+ if (finalBundle.status === 'succeeded' && (!finalBundle.assets || finalBundle.assets.length === 0)) {
504
+ try {
505
+ bundleWithAssets = await bundlesRepository.getById(appId, finalBundle.id);
506
+ } catch {
507
+ bundleWithAssets = finalBundle;
508
+ }
509
+ }
510
+
294
511
  const signed = await withRetry(
295
512
  async () => {
296
513
  return await bundlesRepository.getSignedDownloadUrl(appId, finalBundle.id, { redirect: false });
297
514
  },
298
515
  { attempts: 3, baseDelayMs: 500, maxDelayMs: 4000 }
299
516
  );
517
+ const key =
518
+ mode === 'base'
519
+ ? baseBundleKey(appId, platform)
520
+ : testBundleKey(appId, commitId, platform, finalBundle.id);
521
+ await ensureBundleDir(key);
300
522
  const bundlePath =
301
523
  mode === 'base'
302
524
  ? await safeReplaceFileFromUrl(
303
- signed.url,
304
- toBundleFileUri(baseBundleKey(appId, platform)),
305
- `${appId}:${commitId ?? 'head'}:${platform}:${finalBundle.id}`
306
- )
307
- : await downloadIfMissing(signed.url, toBundleFileUri(testBundleKey(appId, commitId, platform, finalBundle.id)));
308
- return { bundlePath, label: 'Ready', bundle: finalBundle };
525
+ signed.url,
526
+ toBundleFileUri(key, platform),
527
+ `${appId}:${commitId ?? 'head'}:${platform}:${finalBundle.id}`,
528
+ platform
529
+ )
530
+ : await downloadIfMissing(signed.url, toBundleFileUri(key, platform));
531
+ try {
532
+ await ensureAssetsForBundle(appId, bundleWithAssets, key, platform);
533
+ } catch {
534
+
535
+ }
536
+ return { bundlePath, label: 'Ready', bundle: bundleWithAssets };
309
537
  }
310
538
 
311
539
  export function useBundleManager({
@@ -360,8 +588,7 @@ export function useBundleManager({
360
588
  const dir = bundlesCacheDir();
361
589
  await ensureDir(dir);
362
590
  const key = baseBundleKey(appId, platform);
363
- const uri = toBundleFileUri(key);
364
- let existing = await getExistingNonEmptyFileUri(uri);
591
+ let existing = await getExistingBundleFileUri(key, platform);
365
592
  let embeddedMeta: BaseBundleMeta | null = null;
366
593
  if (!existing) {
367
594
  const embedded = embeddedBaseBundlesRef.current?.[platform];
@@ -370,14 +597,29 @@ export function useBundleManager({
370
597
  existing = hydrated.bundlePath;
371
598
  embeddedMeta = hydrated.meta ?? null;
372
599
  if (embeddedMeta) {
600
+ await ensureBundleDir(key);
373
601
  await writeJsonFile(toBundleMetaFileUri(key), embeddedMeta);
602
+ await writeJsonFile(legacyBundleMetaFileUri(key), embeddedMeta);
374
603
  }
375
604
  }
376
605
  }
377
606
  if (existing) {
378
607
  lastBaseBundlePathRef.current = existing;
379
608
  setBundlePath(existing);
380
- const meta = embeddedMeta ?? (await readJsonFile<BaseBundleMeta>(toBundleMetaFileUri(key)));
609
+ const meta =
610
+ embeddedMeta ??
611
+ (await readJsonFile<BaseBundleMeta>(toBundleMetaFileUri(key))) ??
612
+ (await readJsonFile<BaseBundleMeta>(legacyBundleMetaFileUri(key)));
613
+ const embedded = embeddedBaseBundlesRef.current?.[platform];
614
+ const embeddedFingerprint = embedded?.meta?.fingerprint ?? null;
615
+ const actualFingerprint = meta?.fingerprint ?? embeddedMeta?.fingerprint ?? null;
616
+ if (embedded?.assetsModule && embeddedFingerprint && actualFingerprint === embeddedFingerprint) {
617
+ try {
618
+ await hydrateAssetsFromEmbeddedAsset(appId, platform, key, embedded);
619
+ } catch {
620
+
621
+ }
622
+ }
381
623
  if (meta?.fingerprint) {
382
624
  lastBaseFingerprintRef.current = meta.fingerprint;
383
625
  }
@@ -387,7 +629,7 @@ export function useBundleManager({
387
629
  }
388
630
  }
389
631
  } catch {
390
-
632
+
391
633
  }
392
634
  },
393
635
  [platform]
@@ -461,7 +703,16 @@ export function useBundleManager({
461
703
  lastBaseFingerprintRef.current = fingerprint;
462
704
  hasCompletedFirstNetworkBaseLoadRef.current = true;
463
705
  initialHydratedBaseFromDiskRef.current = false;
464
- void writeJsonFile(toBundleMetaFileUri(baseBundleKey(src.appId, platform)), {
706
+ const metaKey = baseBundleKey(src.appId, platform);
707
+ await ensureBundleDir(metaKey);
708
+ void writeJsonFile(toBundleMetaFileUri(metaKey), {
709
+ fingerprint,
710
+ bundleId: bundle.id,
711
+ checksumSha256: bundle.checksumSha256 ?? null,
712
+ size: bundle.size ?? null,
713
+ updatedAt: new Date().toISOString(),
714
+ } satisfies BaseBundleMeta);
715
+ void writeJsonFile(legacyBundleMetaFileUri(metaKey), {
465
716
  fingerprint,
466
717
  bundleId: bundle.id,
467
718
  checksumSha256: bundle.checksumSha256 ?? null,