@hot-updater/firebase 1.0.5 → 1.0.7

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.
@@ -4525,7 +4525,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4525
4525
  };
4526
4526
  //#endregion
4527
4527
  //#region ../../packages/server/dist/version.mjs
4528
- const HOT_UPDATER_SERVER_VERSION = "1.0.5";
4528
+ const HOT_UPDATER_SERVER_VERSION = "1.0.7";
4529
4529
  //#endregion
4530
4530
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4531
4531
  /**
@@ -4816,6 +4816,11 @@ async function resolveAssets(plan, resolveFileUrl) {
4816
4816
  const resolvedEntries = entries.filter((entry) => entry !== null);
4817
4817
  return { assets: Object.fromEntries(resolvedEntries.map((entry) => entry.asset)) };
4818
4818
  }
4819
+ /**
4820
+ * Legacy: artifact protocol v1. Signs a URL for every file of the target, which grows with the
4821
+ * Bundle's file count; apps whose SDK predates protocol v2 still ask for it.
4822
+ * Remove with the `/artifacts/v1` route once no supported app uses it.
4823
+ */
4819
4824
  async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
4820
4825
  const manifestStorageUri = targetBundle ? getManifestStorageUri(targetBundle) : null;
4821
4826
  const manifestFileHash = targetBundle ? getManifestFileHash(targetBundle) : null;
@@ -4849,6 +4854,98 @@ async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readSto
4849
4854
  ...archiveUrl ? { archiveUrl } : {}
4850
4855
  };
4851
4856
  }
4857
+ /** The target's manifest and its plan, or null for a Bundle without one. */
4858
+ async function loadTargetPlan({ currentBundle, readStorageText, targetBundle }) {
4859
+ const manifestStorageUri = getManifestStorageUri(targetBundle);
4860
+ const manifestFileHash = getManifestFileHash(targetBundle);
4861
+ const assetBaseStorageUri = getAssetBaseStorageUri(targetBundle);
4862
+ if (!manifestStorageUri || !manifestFileHash || !assetBaseStorageUri) return null;
4863
+ const targetManifest = await fetchBundleManifest(manifestStorageUri, readStorageText);
4864
+ if (!targetManifest) return null;
4865
+ return {
4866
+ manifestFileHash,
4867
+ manifestStorageUri,
4868
+ plan: createManifestArtifactPlan({
4869
+ assetBaseStorageUri,
4870
+ currentBundle,
4871
+ targetBundle,
4872
+ targetManifest
4873
+ }),
4874
+ targetManifest
4875
+ };
4876
+ }
4877
+ /**
4878
+ * Artifact protocol v2: the manifest, the archive, and the one Hermes patch,
4879
+ * but no URL per file. A device asks {@link resolveManifestArtifactFiles}
4880
+ * for the files it downloads one by one, so signing and the response stay
4881
+ * the same size however many files the Bundle has.
4882
+ */
4883
+ async function resolveManifestArtifactsV2({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
4884
+ const target = await loadTargetPlan({
4885
+ currentBundle,
4886
+ readStorageText,
4887
+ targetBundle
4888
+ });
4889
+ if (!target) return null;
4890
+ const manifestUrl = await resolveFileUrl(target.manifestStorageUri);
4891
+ if (!manifestUrl) return null;
4892
+ const patched = target.plan.assets.find((asset) => asset.patch !== null);
4893
+ let patch;
4894
+ if (patched?.patch && !(patched.file.byteSize !== null && patched.patch.byteSize !== null && patched.patch.byteSize >= patched.file.byteSize)) try {
4895
+ const patchUrl = await resolveFileUrl(patched.patch.storageUri);
4896
+ if (patchUrl) patch = {
4897
+ algorithm: "bsdiff",
4898
+ assetPath: patched.assetPath,
4899
+ baseBundleId: patched.patch.baseBundleId,
4900
+ baseFileHash: patched.patch.baseFileHash,
4901
+ patchFileHash: patched.patch.patchFileHash,
4902
+ patchUrl,
4903
+ ...patched.patch.byteSize === null ? {} : { byteSize: patched.patch.byteSize }
4904
+ };
4905
+ } catch {}
4906
+ let archiveUrl = null;
4907
+ const archive = target.targetManifest.archive;
4908
+ if (archive && isContentAddressedAssetFileHash(archive.downloadFileHash) && (asByteSize(archive.downloadByteSize) ?? 0) > 0 && (asByteSize(archive.tarByteSize) ?? 0) > 0) try {
4909
+ archiveUrl = await resolveFileUrl(getBundleArchiveStorageUri({
4910
+ manifestStorageUri: target.manifestStorageUri,
4911
+ bundleId: targetBundle.id
4912
+ }));
4913
+ } catch {}
4914
+ return {
4915
+ artifactProtocolVersion: 2,
4916
+ manifestFileHash: target.manifestFileHash,
4917
+ manifestUrl,
4918
+ ...archiveUrl ? { archiveUrl } : {},
4919
+ ...patch ? { patch } : {}
4920
+ };
4921
+ }
4922
+ /**
4923
+ * Download URLs for the files a device downloads one by one. Only paths the
4924
+ * target's manifest names are signed, at most
4925
+ * {@link ARTIFACT_FILES_REQUEST_LIMIT}; null when any path is unknown or a
4926
+ * URL cannot be resolved.
4927
+ */
4928
+ async function resolveManifestArtifactFiles({ paths, resolveFileUrl, readStorageText, targetBundle }) {
4929
+ if (paths.length === 0 || paths.length > 100 || new Set(paths).size !== paths.length) return null;
4930
+ const target = await loadTargetPlan({
4931
+ currentBundle: null,
4932
+ readStorageText,
4933
+ targetBundle
4934
+ });
4935
+ if (!target) return null;
4936
+ const planned = new Map(target.plan.assets.map((asset) => [asset.assetPath, asset]));
4937
+ const requested = paths.map((path) => planned.get(path));
4938
+ if (requested.some((asset) => asset === void 0)) return null;
4939
+ const files = await Promise.all(requested.map(async (asset) => {
4940
+ const url = await resolveFileUrl(asset.file.storageUri);
4941
+ if (!url) return null;
4942
+ const file = { url };
4943
+ if (asset.file.compression) file.compression = asset.file.compression;
4944
+ return [asset.assetPath, file];
4945
+ }));
4946
+ if (files.some((file) => file === null)) return null;
4947
+ return { files: Object.fromEntries(files.filter((file) => file !== null)) };
4948
+ }
4852
4949
  //#endregion
4853
4950
  //#region ../../node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.js
4854
4951
  const LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
@@ -5322,20 +5419,34 @@ const createCoreReads = (db, storage) => {
5322
5419
  return projectReleaseCatalogRow(row === null ? null : toCatalogRow(row), input, scopeKey);
5323
5420
  },
5324
5421
  /** Artifact resolution: one batch read of both bundles and one unique read of their patch. */
5325
- async getArtifactInfo(targetBundleId, currentBundleId, artifactProtocolVersion) {
5422
+ getArtifactInfo: (async (targetBundleId, currentBundleId, artifactProtocolVersion) => {
5326
5423
  const { readStorageText } = storage;
5327
- if (artifactProtocolVersion !== 1 || readStorageText === void 0) return null;
5424
+ if (artifactProtocolVersion !== 1 && artifactProtocolVersion !== 2 || readStorageText === void 0) return null;
5328
5425
  const withCurrent = currentBundleId !== NIL_UUID;
5329
5426
  const [[target, current], patch] = await Promise.all([db.findByKeys("bundles", withCurrent ? [{ id: targetBundleId }, { id: currentBundleId }] : [{ id: targetBundleId }]), withCurrent ? db.findOne("bundle_patches", {
5330
5427
  bundle_id: targetBundleId,
5331
5428
  base_bundle_id: currentBundleId
5332
5429
  }) : null]);
5333
5430
  if (target === null || target === void 0) return null;
5334
- return resolveManifestArtifacts({
5431
+ const input = {
5335
5432
  currentBundle: current ? rowToBundle(toBundleRow(current)) : null,
5336
5433
  readStorageText,
5337
5434
  resolveFileUrl: storage.resolveFileUrl,
5338
5435
  targetBundle: rowToBundle(toBundleRow(target), patch === null ? [] : [toPatchRow(patch)])
5436
+ };
5437
+ return artifactProtocolVersion === 2 ? resolveManifestArtifactsV2(input) : resolveManifestArtifacts(input);
5438
+ }),
5439
+ /** A Bundle's file URLs: one point read of the bundle and its manifest. */
5440
+ async getArtifactFiles(bundleId, paths) {
5441
+ const { readStorageText } = storage;
5442
+ if (readStorageText === void 0) return null;
5443
+ const row = await db.findOne("bundles", { id: bundleId });
5444
+ if (row === null) return null;
5445
+ return resolveManifestArtifactFiles({
5446
+ paths,
5447
+ readStorageText,
5448
+ resolveFileUrl: storage.resolveFileUrl,
5449
+ targetBundle: rowToBundle(toBundleRow(row))
5339
5450
  });
5340
5451
  },
5341
5452
  /** A bundle, its patches, and how many patches start from it. */
@@ -6684,6 +6795,7 @@ const ADMIN_ROUTES = [
6684
6795
  //#region ../../packages/server/dist/handlerReleaseCatalogRoutes.mjs
6685
6796
  const CATALOG_CONTENT_TYPE = "application/vnd.hot-updater.release-catalog+json; version=1";
6686
6797
  const ARTIFACT_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=1";
6798
+ const ARTIFACT_V2_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=2";
6687
6799
  const ORIGIN_CACHE_TTL_MS = 5e3;
6688
6800
  const ORIGIN_CACHE_MAX_ENTRIES = 128;
6689
6801
  /** Marks the 404 of a scope that has no catalog. */
@@ -6706,6 +6818,18 @@ const CATALOG_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
6706
6818
  * `Cache-Control`.
6707
6819
  */
6708
6820
  const CATALOG_CDN_CACHE_CONTROL = "public, max-age=5, stale-while-revalidate=5, stale-if-error=0";
6821
+ /** The paths of an artifact files request, or null when malformed. */
6822
+ const readArtifactFilePaths = async (request) => {
6823
+ let body;
6824
+ try {
6825
+ body = await request.json();
6826
+ } catch {
6827
+ return null;
6828
+ }
6829
+ const paths = body?.paths;
6830
+ if (!Array.isArray(paths) || paths.length === 0 || paths.length > 100 || !paths.every((path) => typeof path === "string" && path.length > 0) || new Set(paths).size !== paths.length) return null;
6831
+ return paths;
6832
+ };
6709
6833
  const privateNotFound = () => Response.json({ error: "Not found" }, {
6710
6834
  status: 404,
6711
6835
  headers: { "cache-control": "private, no-store" }
@@ -6826,6 +6950,30 @@ const createReleaseCatalogRouteHandlers = () => {
6826
6950
  if (invalid) return invalid;
6827
6951
  return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
6828
6952
  },
6953
+ artifactV2: async (params, _request, { core }) => {
6954
+ const info = await core.getArtifactInfo(requireRouteParam(params, "targetBundleId"), requireRouteParam(params, "currentBundleId"), 2);
6955
+ if (info === null) return privateNotFound();
6956
+ return new Response(JSON.stringify(info), {
6957
+ status: 200,
6958
+ headers: {
6959
+ "cache-control": "private, no-store",
6960
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
6961
+ }
6962
+ });
6963
+ },
6964
+ artifactFilesV2: async (params, request, { core }) => {
6965
+ const paths = await readArtifactFilePaths(request);
6966
+ if (paths === null) return badRequest(`paths must be 1 to 100 unique strings`);
6967
+ const files = await core.getArtifactFiles(requireRouteParam(params, "targetBundleId"), paths);
6968
+ if (files === null) return privateNotFound();
6969
+ return new Response(JSON.stringify(files), {
6970
+ status: 200,
6971
+ headers: {
6972
+ "cache-control": "private, no-store",
6973
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
6974
+ }
6975
+ });
6976
+ },
6829
6977
  artifactV1: async (params, _request, { core }) => {
6830
6978
  const info = await core.getArtifactInfo(requireRouteParam(params, "targetBundleId"), requireRouteParam(params, "currentBundleId"), 1);
6831
6979
  if (info === null) return privateNotFound();
@@ -6977,6 +7125,8 @@ function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins =
6977
7125
  addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
6978
7126
  addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
6979
7127
  addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
7128
+ addClientRoute("GET", "/artifacts/v2/:targetBundleId/from/:currentBundleId", "artifactV2");
7129
+ addClientRoute("POST", "/artifacts/v2/:targetBundleId/files", "artifactFilesV2");
6980
7130
  const adminRouter = createRouter();
6981
7131
  const addAdminRoute = mount(adminRouter, true);
6982
7132
  addAdminRoute("GET", "/version", "adminVersion");
@@ -11476,10 +11626,44 @@ const firebaseDatabase = (config) => createEngineDatabase({
11476
11626
  });
11477
11627
  //#endregion
11478
11628
  //#region src/firebaseStorage.ts
11629
+ /** Signed URLs kept per adapter, enough for a few Bundles' assets. */
11630
+ const MAX_CACHED_SIGNED_URLS = 1e4;
11479
11631
  const firebaseStorage = (config) => {
11480
11632
  const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
11481
11633
  const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
11482
11634
  const getStorageKey = createStorageKeyBuilder(config.basePath);
11635
+ /**
11636
+ * Signed URLs by object. Without a private key, as in Cloud Functions, each
11637
+ * signature is an IAM signBlob request, and an update check returns a URL
11638
+ * for every asset of its Bundle: each URL is reused until half its lifetime
11639
+ * has passed.
11640
+ */
11641
+ const signedUrls = /* @__PURE__ */ new Map();
11642
+ const signUrl = async (key) => {
11643
+ const expiresAt = Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3;
11644
+ const [url] = await bucket.file(key).getSignedUrl({
11645
+ action: "read",
11646
+ expires: expiresAt
11647
+ });
11648
+ if (!url) throw new Error("Failed to generate Firebase download URL");
11649
+ return url;
11650
+ };
11651
+ const getSignedUrl = (key) => {
11652
+ const cached = signedUrls.get(key);
11653
+ if (cached && Date.now() < cached.renewAt) return cached.url;
11654
+ const url = signUrl(key);
11655
+ const entry = {
11656
+ url,
11657
+ renewAt: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3 / 2
11658
+ };
11659
+ signedUrls.delete(key);
11660
+ signedUrls.set(key, entry);
11661
+ if (signedUrls.size > MAX_CACHED_SIGNED_URLS) signedUrls.delete(signedUrls.keys().next().value);
11662
+ url.catch(() => {
11663
+ if (signedUrls.get(key) === entry) signedUrls.delete(key);
11664
+ });
11665
+ return url;
11666
+ };
11483
11667
  const parseAndValidate = (storageUri) => {
11484
11668
  const parsed = parseStorageUri(storageUri, "gs");
11485
11669
  if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
@@ -11525,12 +11709,7 @@ const firebaseStorage = (config) => {
11525
11709
  downloadUrl.hash = "";
11526
11710
  return { url: downloadUrl.toString() };
11527
11711
  }
11528
- const [url] = await bucket.file(key).getSignedUrl({
11529
- action: "read",
11530
- expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
11531
- });
11532
- if (!url) throw new Error("Failed to generate Firebase download URL");
11533
- return { url };
11712
+ return { url: await getSignedUrl(key) };
11534
11713
  },
11535
11714
  async exists({ storageUri }) {
11536
11715
  const { key } = parseAndValidate(storageUri);
package/dist/index.cjs CHANGED
@@ -191,10 +191,44 @@ const firebaseDatabase = (config) => (0, _hot_updater_plugin_core.createEngineDa
191
191
  });
192
192
  //#endregion
193
193
  //#region src/firebaseStorage.ts
194
+ /** Signed URLs kept per adapter, enough for a few Bundles' assets. */
195
+ const MAX_CACHED_SIGNED_URLS = 1e4;
194
196
  const firebaseStorage = (config) => {
195
197
  const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
196
198
  const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
197
199
  const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
200
+ /**
201
+ * Signed URLs by object. Without a private key, as in Cloud Functions, each
202
+ * signature is an IAM signBlob request, and an update check returns a URL
203
+ * for every asset of its Bundle: each URL is reused until half its lifetime
204
+ * has passed.
205
+ */
206
+ const signedUrls = /* @__PURE__ */ new Map();
207
+ const signUrl = async (key) => {
208
+ const expiresAt = Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3;
209
+ const [url] = await bucket.file(key).getSignedUrl({
210
+ action: "read",
211
+ expires: expiresAt
212
+ });
213
+ if (!url) throw new Error("Failed to generate Firebase download URL");
214
+ return url;
215
+ };
216
+ const getSignedUrl = (key) => {
217
+ const cached = signedUrls.get(key);
218
+ if (cached && Date.now() < cached.renewAt) return cached.url;
219
+ const url = signUrl(key);
220
+ const entry = {
221
+ url,
222
+ renewAt: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3 / 2
223
+ };
224
+ signedUrls.delete(key);
225
+ signedUrls.set(key, entry);
226
+ if (signedUrls.size > MAX_CACHED_SIGNED_URLS) signedUrls.delete(signedUrls.keys().next().value);
227
+ url.catch(() => {
228
+ if (signedUrls.get(key) === entry) signedUrls.delete(key);
229
+ });
230
+ return url;
231
+ };
198
232
  const parseAndValidate = (storageUri) => {
199
233
  const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
200
234
  if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
@@ -240,12 +274,7 @@ const firebaseStorage = (config) => {
240
274
  downloadUrl.hash = "";
241
275
  return { url: downloadUrl.toString() };
242
276
  }
243
- const [url] = await bucket.file(key).getSignedUrl({
244
- action: "read",
245
- expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
246
- });
247
- if (!url) throw new Error("Failed to generate Firebase download URL");
248
- return { url };
277
+ return { url: await getSignedUrl(key) };
249
278
  },
250
279
  async exists({ storageUri }) {
251
280
  const { key } = parseAndValidate(storageUri);
package/dist/index.mjs CHANGED
@@ -190,10 +190,44 @@ const firebaseDatabase = (config) => createEngineDatabase({
190
190
  });
191
191
  //#endregion
192
192
  //#region src/firebaseStorage.ts
193
+ /** Signed URLs kept per adapter, enough for a few Bundles' assets. */
194
+ const MAX_CACHED_SIGNED_URLS = 1e4;
193
195
  const firebaseStorage = (config) => {
194
196
  const app = getApps().length ? getApp() : initializeApp(config);
195
197
  const bucket = getStorage(app).bucket(config.storageBucket);
196
198
  const getStorageKey = createStorageKeyBuilder(config.basePath);
199
+ /**
200
+ * Signed URLs by object. Without a private key, as in Cloud Functions, each
201
+ * signature is an IAM signBlob request, and an update check returns a URL
202
+ * for every asset of its Bundle: each URL is reused until half its lifetime
203
+ * has passed.
204
+ */
205
+ const signedUrls = /* @__PURE__ */ new Map();
206
+ const signUrl = async (key) => {
207
+ const expiresAt = Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3;
208
+ const [url] = await bucket.file(key).getSignedUrl({
209
+ action: "read",
210
+ expires: expiresAt
211
+ });
212
+ if (!url) throw new Error("Failed to generate Firebase download URL");
213
+ return url;
214
+ };
215
+ const getSignedUrl = (key) => {
216
+ const cached = signedUrls.get(key);
217
+ if (cached && Date.now() < cached.renewAt) return cached.url;
218
+ const url = signUrl(key);
219
+ const entry = {
220
+ url,
221
+ renewAt: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3 / 2
222
+ };
223
+ signedUrls.delete(key);
224
+ signedUrls.set(key, entry);
225
+ if (signedUrls.size > MAX_CACHED_SIGNED_URLS) signedUrls.delete(signedUrls.keys().next().value);
226
+ url.catch(() => {
227
+ if (signedUrls.get(key) === entry) signedUrls.delete(key);
228
+ });
229
+ return url;
230
+ };
197
231
  const parseAndValidate = (storageUri) => {
198
232
  const parsed = parseStorageUri(storageUri, "gs");
199
233
  if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
@@ -239,12 +273,7 @@ const firebaseStorage = (config) => {
239
273
  downloadUrl.hash = "";
240
274
  return { url: downloadUrl.toString() };
241
275
  }
242
- const [url] = await bucket.file(key).getSignedUrl({
243
- action: "read",
244
- expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
245
- });
246
- if (!url) throw new Error("Failed to generate Firebase download URL");
247
- return { url };
276
+ return { url: await getSignedUrl(key) };
248
277
  },
249
278
  async exists({ storageUri }) {
250
279
  const { key } = parseAndValidate(storageUri);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/firebase",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.0.7",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "types": "dist/index.d.cts",
@@ -37,10 +37,10 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "hono": "4.12.34",
40
- "@hot-updater/cli-tools": "1.0.5",
41
- "@hot-updater/protocol": "1.0.5",
42
- "@hot-updater/plugin-core": "1.0.5",
43
- "@hot-updater/server": "1.0.5"
40
+ "@hot-updater/cli-tools": "1.0.7",
41
+ "@hot-updater/protocol": "1.0.7",
42
+ "@hot-updater/server": "1.0.7",
43
+ "@hot-updater/plugin-core": "1.0.7"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
@@ -55,8 +55,8 @@
55
55
  "firebase-tools": "15.29.0",
56
56
  "fkill": "^9.0.0",
57
57
  "mime": "^4.0.4",
58
- "@hot-updater/mock": "1.0.5",
59
- "@hot-updater/test-utils": "1.0.5"
58
+ "@hot-updater/mock": "1.0.7",
59
+ "@hot-updater/test-utils": "1.0.7"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "firebase-admin": ">=11.10.0",