hot-updater 1.0.5 → 1.0.6

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.
Files changed (33) hide show
  1. package/dist/index.mjs +9 -18
  2. package/dist/infra-templates/aws/lambda/index.cjs +217 -17
  3. package/dist/infra-templates/aws/template.json +10 -10
  4. package/dist/infra-templates/azure/azure/function/index.mjs +134 -4
  5. package/dist/infra-templates/azure/template.json +11 -11
  6. package/dist/infra-templates/cloudflare/template.json +10 -10
  7. package/dist/infra-templates/cloudflare/worker/dist/README.md +1 -1
  8. package/dist/infra-templates/cloudflare/worker/dist/index.js +142 -4
  9. package/dist/infra-templates/cloudflare/worker/dist/index.js.map +2 -2
  10. package/dist/infra-templates/firebase/firebase/functions/index.cjs +189 -10
  11. package/dist/infra-templates/firebase/template.json +11 -11
  12. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-plugin-core/dist/serverPlugin/coreReads.d.cts +12 -2
  13. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-plugin-core/dist/serverPlugin/coreReads.d.mts +12 -2
  14. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-protocol/dist/index.cjs +9 -0
  15. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-protocol/dist/index.d.cts +33 -1
  16. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-protocol/dist/index.d.mts +33 -1
  17. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-protocol/dist/index.mjs +8 -1
  18. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/core/api.d.cts +2 -0
  19. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/core/api.d.mts +2 -0
  20. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/core/reads.cjs +17 -3
  21. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/core/reads.mjs +19 -5
  22. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/db/updateArtifacts.cjs +99 -0
  23. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/db/updateArtifacts.mjs +99 -2
  24. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/handler.cjs +2 -0
  25. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/handler.mjs +2 -0
  26. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/handlerReleaseCatalogRoutes.cjs +37 -0
  27. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/handlerReleaseCatalogRoutes.mjs +38 -1
  28. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/package.cjs +1 -1
  29. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/package.mjs +1 -1
  30. package/dist/infra-templates/supabase/template.json +10 -10
  31. package/dist/infra-templates/vercel/template.json +11 -11
  32. package/dist/infra-templates/vercel/vercel/api/index.mjs +134 -4
  33. package/package.json +24 -24
package/dist/index.mjs CHANGED
@@ -25866,13 +25866,6 @@ const createNativeBuild = async ({ platform, outputPath, buildAdapter, builder }
25866
25866
  p.log.info(`Artifact stored at ${colors.blueBright(relativePath)}`);
25867
25867
  };
25868
25868
  //#endregion
25869
- //#region src/utils/setIosMinBundleIdSlotIntoInfoPlist.ts
25870
- const setIosMinBundleIdSlotIntoInfoPlist = async ({ infoPlistPaths }) => {
25871
- const iosParser = new IosConfigParser(infoPlistPaths);
25872
- if (!await iosParser.exists()) throw new Error("No iOS Info.plist files found");
25873
- await iosParser.set("HOT_UPDATER_MIN_BUNDLE_ID", "$(HOT_UPDATER_MIN_BUNDLE_ID)");
25874
- };
25875
- //#endregion
25876
25869
  //#region src/utils/native/prepareNativeBuild.ts
25877
25870
  async function prepareNativeBuild(options) {
25878
25871
  const cwd = getCwd();
@@ -25946,17 +25939,15 @@ async function prepareNativeBuild(options) {
25946
25939
  }
25947
25940
  const artifactResultStorePath = options.outputPath ?? path.join(HotUpdateDirUtil.getDefaultOutputPath({ cwd }), "build", platform, scheme);
25948
25941
  const resolvedOutputPath = path.isAbsolute(artifactResultStorePath) ? artifactResultStorePath : path.join(cwd, artifactResultStorePath);
25949
- if (platform === "ios") {
25950
- await setIosMinBundleIdSlotIntoInfoPlist({ infoPlistPaths: config.platform?.ios.infoPlistPaths });
25951
- return {
25952
- outputPath: resolvedOutputPath,
25953
- config,
25954
- iosSchemeConfig: await enrichNativeBuildIosScheme({
25955
- scheme: config.nativeBuild.ios[scheme],
25956
- hotUpdaterSchemeName: scheme
25957
- })
25958
- };
25959
- } else if (platform === "android") return {
25942
+ if (platform === "ios") return {
25943
+ outputPath: resolvedOutputPath,
25944
+ config,
25945
+ iosSchemeConfig: await enrichNativeBuildIosScheme({
25946
+ scheme: config.nativeBuild.ios[scheme],
25947
+ hotUpdaterSchemeName: scheme
25948
+ })
25949
+ };
25950
+ else if (platform === "android") return {
25960
25951
  outputPath: resolvedOutputPath,
25961
25952
  config,
25962
25953
  androidSchemeConfig: await enrichNativeBuildAndroidScheme({
@@ -30,7 +30,6 @@ let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
30
30
  let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
31
31
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
32
32
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
33
- //#region ../plugin-core/dist/contentAddressedAssets.mjs
34
33
  const SHA256_FILE_HASH_RE = /^[0-9a-f]{64}$/;
35
34
  const isContentAddressedAssetFileHash = (value) => {
36
35
  return typeof value === "string" && SHA256_FILE_HASH_RE.test(value);
@@ -4550,7 +4549,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4550
4549
  };
4551
4550
  //#endregion
4552
4551
  //#region ../../packages/server/dist/version.mjs
4553
- const HOT_UPDATER_SERVER_VERSION = "1.0.5";
4552
+ const HOT_UPDATER_SERVER_VERSION = "1.0.6";
4554
4553
  //#endregion
4555
4554
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4556
4555
  /**
@@ -4841,6 +4840,11 @@ async function resolveAssets(plan, resolveFileUrl) {
4841
4840
  const resolvedEntries = entries.filter((entry) => entry !== null);
4842
4841
  return { assets: Object.fromEntries(resolvedEntries.map((entry) => entry.asset)) };
4843
4842
  }
4843
+ /**
4844
+ * Legacy: artifact protocol v1. Signs a URL for every file of the target, which grows with the
4845
+ * Bundle's file count; apps whose SDK predates protocol v2 still ask for it.
4846
+ * Remove with the `/artifacts/v1` route once no supported app uses it.
4847
+ */
4844
4848
  async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
4845
4849
  const manifestStorageUri = targetBundle ? getManifestStorageUri(targetBundle) : null;
4846
4850
  const manifestFileHash = targetBundle ? getManifestFileHash(targetBundle) : null;
@@ -4874,6 +4878,98 @@ async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readSto
4874
4878
  ...archiveUrl ? { archiveUrl } : {}
4875
4879
  };
4876
4880
  }
4881
+ /** The target's manifest and its plan, or null for a Bundle without one. */
4882
+ async function loadTargetPlan({ currentBundle, readStorageText, targetBundle }) {
4883
+ const manifestStorageUri = getManifestStorageUri(targetBundle);
4884
+ const manifestFileHash = getManifestFileHash(targetBundle);
4885
+ const assetBaseStorageUri = getAssetBaseStorageUri(targetBundle);
4886
+ if (!manifestStorageUri || !manifestFileHash || !assetBaseStorageUri) return null;
4887
+ const targetManifest = await fetchBundleManifest(manifestStorageUri, readStorageText);
4888
+ if (!targetManifest) return null;
4889
+ return {
4890
+ manifestFileHash,
4891
+ manifestStorageUri,
4892
+ plan: createManifestArtifactPlan({
4893
+ assetBaseStorageUri,
4894
+ currentBundle,
4895
+ targetBundle,
4896
+ targetManifest
4897
+ }),
4898
+ targetManifest
4899
+ };
4900
+ }
4901
+ /**
4902
+ * Artifact protocol v2: the manifest, the archive, and the one Hermes patch,
4903
+ * but no URL per file. A device asks {@link resolveManifestArtifactFiles}
4904
+ * for the files it downloads one by one, so signing and the response stay
4905
+ * the same size however many files the Bundle has.
4906
+ */
4907
+ async function resolveManifestArtifactsV2({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
4908
+ const target = await loadTargetPlan({
4909
+ currentBundle,
4910
+ readStorageText,
4911
+ targetBundle
4912
+ });
4913
+ if (!target) return null;
4914
+ const manifestUrl = await resolveFileUrl(target.manifestStorageUri);
4915
+ if (!manifestUrl) return null;
4916
+ const patched = target.plan.assets.find((asset) => asset.patch !== null);
4917
+ let patch;
4918
+ if (patched?.patch && !(patched.file.byteSize !== null && patched.patch.byteSize !== null && patched.patch.byteSize >= patched.file.byteSize)) try {
4919
+ const patchUrl = await resolveFileUrl(patched.patch.storageUri);
4920
+ if (patchUrl) patch = {
4921
+ algorithm: "bsdiff",
4922
+ assetPath: patched.assetPath,
4923
+ baseBundleId: patched.patch.baseBundleId,
4924
+ baseFileHash: patched.patch.baseFileHash,
4925
+ patchFileHash: patched.patch.patchFileHash,
4926
+ patchUrl,
4927
+ ...patched.patch.byteSize === null ? {} : { byteSize: patched.patch.byteSize }
4928
+ };
4929
+ } catch {}
4930
+ let archiveUrl = null;
4931
+ const archive = target.targetManifest.archive;
4932
+ if (archive && isContentAddressedAssetFileHash(archive.downloadFileHash) && (asByteSize(archive.downloadByteSize) ?? 0) > 0 && (asByteSize(archive.tarByteSize) ?? 0) > 0) try {
4933
+ archiveUrl = await resolveFileUrl(getBundleArchiveStorageUri({
4934
+ manifestStorageUri: target.manifestStorageUri,
4935
+ bundleId: targetBundle.id
4936
+ }));
4937
+ } catch {}
4938
+ return {
4939
+ artifactProtocolVersion: 2,
4940
+ manifestFileHash: target.manifestFileHash,
4941
+ manifestUrl,
4942
+ ...archiveUrl ? { archiveUrl } : {},
4943
+ ...patch ? { patch } : {}
4944
+ };
4945
+ }
4946
+ /**
4947
+ * Download URLs for the files a device downloads one by one. Only paths the
4948
+ * target's manifest names are signed, at most
4949
+ * {@link ARTIFACT_FILES_REQUEST_LIMIT}; null when any path is unknown or a
4950
+ * URL cannot be resolved.
4951
+ */
4952
+ async function resolveManifestArtifactFiles({ paths, resolveFileUrl, readStorageText, targetBundle }) {
4953
+ if (paths.length === 0 || paths.length > 100 || new Set(paths).size !== paths.length) return null;
4954
+ const target = await loadTargetPlan({
4955
+ currentBundle: null,
4956
+ readStorageText,
4957
+ targetBundle
4958
+ });
4959
+ if (!target) return null;
4960
+ const planned = new Map(target.plan.assets.map((asset) => [asset.assetPath, asset]));
4961
+ const requested = paths.map((path) => planned.get(path));
4962
+ if (requested.some((asset) => asset === void 0)) return null;
4963
+ const files = await Promise.all(requested.map(async (asset) => {
4964
+ const url = await resolveFileUrl(asset.file.storageUri);
4965
+ if (!url) return null;
4966
+ const file = { url };
4967
+ if (asset.file.compression) file.compression = asset.file.compression;
4968
+ return [asset.assetPath, file];
4969
+ }));
4970
+ if (files.some((file) => file === null)) return null;
4971
+ return { files: Object.fromEntries(files.filter((file) => file !== null)) };
4972
+ }
4877
4973
  //#endregion
4878
4974
  //#region ../../node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.js
4879
4975
  const LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
@@ -5347,20 +5443,34 @@ const createCoreReads = (db, storage) => {
5347
5443
  return projectReleaseCatalogRow(row === null ? null : toCatalogRow(row), input, scopeKey);
5348
5444
  },
5349
5445
  /** Artifact resolution: one batch read of both bundles and one unique read of their patch. */
5350
- async getArtifactInfo(targetBundleId, currentBundleId, artifactProtocolVersion) {
5446
+ getArtifactInfo: (async (targetBundleId, currentBundleId, artifactProtocolVersion) => {
5351
5447
  const { readStorageText } = storage;
5352
- if (artifactProtocolVersion !== 1 || readStorageText === void 0) return null;
5448
+ if (artifactProtocolVersion !== 1 && artifactProtocolVersion !== 2 || readStorageText === void 0) return null;
5353
5449
  const withCurrent = currentBundleId !== NIL_UUID;
5354
5450
  const [[target, current], patch] = await Promise.all([db.findByKeys("bundles", withCurrent ? [{ id: targetBundleId }, { id: currentBundleId }] : [{ id: targetBundleId }]), withCurrent ? db.findOne("bundle_patches", {
5355
5451
  bundle_id: targetBundleId,
5356
5452
  base_bundle_id: currentBundleId
5357
5453
  }) : null]);
5358
5454
  if (target === null || target === void 0) return null;
5359
- return resolveManifestArtifacts({
5455
+ const input = {
5360
5456
  currentBundle: current ? rowToBundle(toBundleRow(current)) : null,
5361
5457
  readStorageText,
5362
5458
  resolveFileUrl: storage.resolveFileUrl,
5363
5459
  targetBundle: rowToBundle(toBundleRow(target), patch === null ? [] : [toPatchRow(patch)])
5460
+ };
5461
+ return artifactProtocolVersion === 2 ? resolveManifestArtifactsV2(input) : resolveManifestArtifacts(input);
5462
+ }),
5463
+ /** A Bundle's file URLs: one point read of the bundle and its manifest. */
5464
+ async getArtifactFiles(bundleId, paths) {
5465
+ const { readStorageText } = storage;
5466
+ if (readStorageText === void 0) return null;
5467
+ const row = await db.findOne("bundles", { id: bundleId });
5468
+ if (row === null) return null;
5469
+ return resolveManifestArtifactFiles({
5470
+ paths,
5471
+ readStorageText,
5472
+ resolveFileUrl: storage.resolveFileUrl,
5473
+ targetBundle: rowToBundle(toBundleRow(row))
5364
5474
  });
5365
5475
  },
5366
5476
  /** A bundle, its patches, and how many patches start from it. */
@@ -6709,6 +6819,7 @@ const ADMIN_ROUTES = [
6709
6819
  //#region ../../packages/server/dist/handlerReleaseCatalogRoutes.mjs
6710
6820
  const CATALOG_CONTENT_TYPE = "application/vnd.hot-updater.release-catalog+json; version=1";
6711
6821
  const ARTIFACT_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=1";
6822
+ const ARTIFACT_V2_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=2";
6712
6823
  const ORIGIN_CACHE_TTL_MS = 5e3;
6713
6824
  const ORIGIN_CACHE_MAX_ENTRIES = 128;
6714
6825
  /** Marks the 404 of a scope that has no catalog. */
@@ -6731,6 +6842,18 @@ const CATALOG_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
6731
6842
  * `Cache-Control`.
6732
6843
  */
6733
6844
  const CATALOG_CDN_CACHE_CONTROL = "public, max-age=5, stale-while-revalidate=5, stale-if-error=0";
6845
+ /** The paths of an artifact files request, or null when malformed. */
6846
+ const readArtifactFilePaths = async (request) => {
6847
+ let body;
6848
+ try {
6849
+ body = await request.json();
6850
+ } catch {
6851
+ return null;
6852
+ }
6853
+ const paths = body?.paths;
6854
+ 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;
6855
+ return paths;
6856
+ };
6734
6857
  const privateNotFound = () => Response.json({ error: "Not found" }, {
6735
6858
  status: 404,
6736
6859
  headers: { "cache-control": "private, no-store" }
@@ -6851,6 +6974,30 @@ const createReleaseCatalogRouteHandlers = () => {
6851
6974
  if (invalid) return invalid;
6852
6975
  return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
6853
6976
  },
6977
+ artifactV2: async (params, _request, { core }) => {
6978
+ const info = await core.getArtifactInfo(requireRouteParam(params, "targetBundleId"), requireRouteParam(params, "currentBundleId"), 2);
6979
+ if (info === null) return privateNotFound();
6980
+ return new Response(JSON.stringify(info), {
6981
+ status: 200,
6982
+ headers: {
6983
+ "cache-control": "private, no-store",
6984
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
6985
+ }
6986
+ });
6987
+ },
6988
+ artifactFilesV2: async (params, request, { core }) => {
6989
+ const paths = await readArtifactFilePaths(request);
6990
+ if (paths === null) return badRequest(`paths must be 1 to 100 unique strings`);
6991
+ const files = await core.getArtifactFiles(requireRouteParam(params, "targetBundleId"), paths);
6992
+ if (files === null) return privateNotFound();
6993
+ return new Response(JSON.stringify(files), {
6994
+ status: 200,
6995
+ headers: {
6996
+ "cache-control": "private, no-store",
6997
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
6998
+ }
6999
+ });
7000
+ },
6854
7001
  artifactV1: async (params, _request, { core }) => {
6855
7002
  const info = await core.getArtifactInfo(requireRouteParam(params, "targetBundleId"), requireRouteParam(params, "currentBundleId"), 1);
6856
7003
  if (info === null) return privateNotFound();
@@ -7002,6 +7149,8 @@ function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins =
7002
7149
  addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
7003
7150
  addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
7004
7151
  addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
7152
+ addClientRoute("GET", "/artifacts/v2/:targetBundleId/from/:currentBundleId", "artifactV2");
7153
+ addClientRoute("POST", "/artifacts/v2/:targetBundleId/files", "artifactFilesV2");
7005
7154
  const adminRouter = createRouter();
7006
7155
  const addAdminRoute = mount(adminRouter, true);
7007
7156
  addAdminRoute("GET", "/version", "adminVersion");
@@ -13240,6 +13389,8 @@ const applySsmRuntimeAwsConfig = (config) => {
13240
13389
  //#endregion
13241
13390
  //#region src/cloudFrontDownloadUrl.ts
13242
13391
  const ONE_YEAR_IN_SECONDS = 31536e3;
13392
+ /** Signatures kept per process; one per Bundle plus one for shared assets. */
13393
+ const MAX_CACHED_SIGNATURES = 1e3;
13243
13394
  const privateKeyCache = /* @__PURE__ */ new Map();
13244
13395
  const getPrivateKeyFromSsm = async (region, parameterName) => {
13245
13396
  const value = (await new _aws_sdk_client_ssm.SSM(applySsmRuntimeAwsConfig({ region })).getParameter({
@@ -13263,18 +13414,67 @@ const resolvePrivateKey = (config) => {
13263
13414
  privateKeyCache.set(cacheKey, pending);
13264
13415
  return pending;
13265
13416
  };
13266
- const cloudFrontDownloadUrl = (config) => async ({ storageUri }) => {
13267
- const storageUrl = new URL(storageUri);
13268
- if (storageUrl.protocol !== "s3:") throw new Error("CloudFront download URLs require an s3 storage URI.");
13269
- const url = new URL(config.publicBaseUrl);
13270
- url.pathname = storageUrl.pathname;
13271
- url.search = "";
13272
- return { url: (0, _aws_sdk_cloudfront_signer.getSignedUrl)({
13273
- url: url.toString(),
13274
- keyPairId: config.keyPairId,
13275
- privateKey: await resolvePrivateKey(config),
13276
- dateLessThan: new Date(Date.now() + (config.expiresSeconds ?? ONE_YEAR_IN_SECONDS) * 1e3).toISOString()
13277
- }) };
13417
+ /**
13418
+ * The URL path one signature covers: every file of one Bundle
13419
+ * (`bundles/<bundleId>/*`), or every content-addressed asset (`assets/*`),
13420
+ * whose SHA-256 names cannot be listed without a manifest that names them.
13421
+ * Any other object is signed alone.
13422
+ */
13423
+ const getSignedPathScope = (pathname) => {
13424
+ const segments = pathname.split("/");
13425
+ const assets = segments.length - 4;
13426
+ if (segments[assets] === "assets" && segments[assets + 1] === "sha256") return `${segments.slice(0, assets + 1).join("/")}/*`;
13427
+ const bundles = segments.lastIndexOf(BUNDLE_STORAGE_PREFIX);
13428
+ if (bundles >= 0 && segments.length - bundles > 2) return `${segments.slice(0, bundles + 2).join("/")}/*`;
13429
+ return pathname;
13430
+ };
13431
+ /**
13432
+ * Download URLs signed with a CloudFront key pair. An update check returns a
13433
+ * URL for every file of the Bundle, hundreds for an app with many assets, so
13434
+ * one custom-policy signature covers a Bundle's files and another the shared
13435
+ * assets: each is reused for every URL under it until half its lifetime has
13436
+ * passed, instead of one RSA signature per file.
13437
+ */
13438
+ const cloudFrontDownloadUrl = (config) => {
13439
+ /**
13440
+ * Signed query strings by policy resource, for this configuration only.
13441
+ * A signing in flight is shared, so a batch of URLs asks the private key
13442
+ * once per scope, and a cached signature needs no key at all.
13443
+ */
13444
+ const signatureCache = /* @__PURE__ */ new Map();
13445
+ return async ({ storageUri }) => {
13446
+ const storageUrl = new URL(storageUri);
13447
+ if (storageUrl.protocol !== "s3:") throw new Error("CloudFront download URLs require an s3 storage URI.");
13448
+ const url = new URL(config.publicBaseUrl);
13449
+ url.pathname = storageUrl.pathname;
13450
+ url.search = "";
13451
+ url.hash = "";
13452
+ const resource = `${url.origin}${getSignedPathScope(url.pathname)}`;
13453
+ const cached = signatureCache.get(resource);
13454
+ if (cached && Date.now() < cached.renewAt) return { url: `${url}${await cached.query}` };
13455
+ const expiresSeconds = config.expiresSeconds ?? ONE_YEAR_IN_SECONDS;
13456
+ const signedAt = Date.now();
13457
+ const query = resolvePrivateKey(config).then((privateKey) => new URL((0, _aws_sdk_cloudfront_signer.getSignedUrl)({
13458
+ url: url.toString(),
13459
+ keyPairId: config.keyPairId,
13460
+ privateKey,
13461
+ policy: JSON.stringify({ Statement: [{
13462
+ Resource: resource,
13463
+ Condition: { DateLessThan: { "AWS:EpochTime": Math.floor((signedAt + expiresSeconds * 1e3) / 1e3) } }
13464
+ }] })
13465
+ })).search);
13466
+ const entry = {
13467
+ query,
13468
+ renewAt: signedAt + expiresSeconds * 1e3 / 2
13469
+ };
13470
+ signatureCache.delete(resource);
13471
+ signatureCache.set(resource, entry);
13472
+ if (signatureCache.size > MAX_CACHED_SIGNATURES) signatureCache.delete(signatureCache.keys().next().value);
13473
+ query.catch(() => {
13474
+ if (signatureCache.get(resource) === entry) signatureCache.delete(resource);
13475
+ });
13476
+ return { url: `${url}${await query}` };
13477
+ };
13278
13478
  };
13279
13479
  //#endregion
13280
13480
  //#region src/cloudFrontInvalidation.ts
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "provider": "aws",
4
- "cliVersion": "1.0.5",
5
- "providerVersion": "1.0.5",
6
- "serverVersion": "1.0.5",
4
+ "cliVersion": "1.0.6",
5
+ "providerVersion": "1.0.6",
6
+ "serverVersion": "1.0.6",
7
7
  "infrastructureGeneration": 1,
8
8
  "clientAuth": {
9
9
  "plugin": "apiKeys",
@@ -65,13 +65,13 @@
65
65
  }
66
66
  ],
67
67
  "packages": {
68
- "hot-updater": "1.0.5",
69
- "@hot-updater/server": "1.0.5",
70
- "@hot-updater/react-native": "1.0.5",
71
- "@hot-updater/aws": "1.0.5",
72
- "@hot-updater/bare": "1.0.5",
73
- "@hot-updater/rock": "1.0.5",
74
- "@hot-updater/expo": "1.0.5",
68
+ "hot-updater": "1.0.6",
69
+ "@hot-updater/server": "1.0.6",
70
+ "@hot-updater/react-native": "1.0.6",
71
+ "@hot-updater/aws": "1.0.6",
72
+ "@hot-updater/bare": "1.0.6",
73
+ "@hot-updater/rock": "1.0.6",
74
+ "@hot-updater/expo": "1.0.6",
75
75
  "@aws-sdk/credential-providers": "3.1066.0"
76
76
  },
77
77
  "requiredInputs": {
@@ -7998,7 +7998,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
7998
7998
  };
7999
7999
  //#endregion
8000
8000
  //#region ../../packages/server/dist/version.mjs
8001
- const HOT_UPDATER_SERVER_VERSION = "1.0.5";
8001
+ const HOT_UPDATER_SERVER_VERSION = "1.0.6";
8002
8002
  //#endregion
8003
8003
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
8004
8004
  const createVersionRouteHandlers = (plugins = []) => {
@@ -8287,6 +8287,85 @@ async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readSto
8287
8287
  ...archiveUrl ? { archiveUrl } : {}
8288
8288
  };
8289
8289
  }
8290
+ async function loadTargetPlan({ currentBundle, readStorageText, targetBundle }) {
8291
+ const manifestStorageUri = getManifestStorageUri(targetBundle);
8292
+ const manifestFileHash = getManifestFileHash(targetBundle);
8293
+ const assetBaseStorageUri = getAssetBaseStorageUri(targetBundle);
8294
+ if (!manifestStorageUri || !manifestFileHash || !assetBaseStorageUri) return null;
8295
+ const targetManifest = await fetchBundleManifest(manifestStorageUri, readStorageText);
8296
+ if (!targetManifest) return null;
8297
+ return {
8298
+ manifestFileHash,
8299
+ manifestStorageUri,
8300
+ plan: createManifestArtifactPlan({
8301
+ assetBaseStorageUri,
8302
+ currentBundle,
8303
+ targetBundle,
8304
+ targetManifest
8305
+ }),
8306
+ targetManifest
8307
+ };
8308
+ }
8309
+ async function resolveManifestArtifactsV2({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
8310
+ const target = await loadTargetPlan({
8311
+ currentBundle,
8312
+ readStorageText,
8313
+ targetBundle
8314
+ });
8315
+ if (!target) return null;
8316
+ const manifestUrl = await resolveFileUrl(target.manifestStorageUri);
8317
+ if (!manifestUrl) return null;
8318
+ const patched = target.plan.assets.find((asset) => asset.patch !== null);
8319
+ let patch;
8320
+ if (patched?.patch && !(patched.file.byteSize !== null && patched.patch.byteSize !== null && patched.patch.byteSize >= patched.file.byteSize)) try {
8321
+ const patchUrl = await resolveFileUrl(patched.patch.storageUri);
8322
+ if (patchUrl) patch = {
8323
+ algorithm: "bsdiff",
8324
+ assetPath: patched.assetPath,
8325
+ baseBundleId: patched.patch.baseBundleId,
8326
+ baseFileHash: patched.patch.baseFileHash,
8327
+ patchFileHash: patched.patch.patchFileHash,
8328
+ patchUrl,
8329
+ ...patched.patch.byteSize === null ? {} : { byteSize: patched.patch.byteSize }
8330
+ };
8331
+ } catch {}
8332
+ let archiveUrl = null;
8333
+ const archive = target.targetManifest.archive;
8334
+ if (archive && isContentAddressedAssetFileHash(archive.downloadFileHash) && (asByteSize(archive.downloadByteSize) ?? 0) > 0 && (asByteSize(archive.tarByteSize) ?? 0) > 0) try {
8335
+ archiveUrl = await resolveFileUrl(getBundleArchiveStorageUri({
8336
+ manifestStorageUri: target.manifestStorageUri,
8337
+ bundleId: targetBundle.id
8338
+ }));
8339
+ } catch {}
8340
+ return {
8341
+ artifactProtocolVersion: 2,
8342
+ manifestFileHash: target.manifestFileHash,
8343
+ manifestUrl,
8344
+ ...archiveUrl ? { archiveUrl } : {},
8345
+ ...patch ? { patch } : {}
8346
+ };
8347
+ }
8348
+ async function resolveManifestArtifactFiles({ paths, resolveFileUrl, readStorageText, targetBundle }) {
8349
+ if (paths.length === 0 || paths.length > 100 || new Set(paths).size !== paths.length) return null;
8350
+ const target = await loadTargetPlan({
8351
+ currentBundle: null,
8352
+ readStorageText,
8353
+ targetBundle
8354
+ });
8355
+ if (!target) return null;
8356
+ const planned = new Map(target.plan.assets.map((asset) => [asset.assetPath, asset]));
8357
+ const requested = paths.map((path) => planned.get(path));
8358
+ if (requested.some((asset) => asset === void 0)) return null;
8359
+ const files = await Promise.all(requested.map(async (asset) => {
8360
+ const url = await resolveFileUrl(asset.file.storageUri);
8361
+ if (!url) return null;
8362
+ const file = { url };
8363
+ if (asset.file.compression) file.compression = asset.file.compression;
8364
+ return [asset.assetPath, file];
8365
+ }));
8366
+ if (files.some((file) => file === null)) return null;
8367
+ return { files: Object.fromEntries(files.filter((file) => file !== null)) };
8368
+ }
8290
8369
  //#endregion
8291
8370
  //#region ../../node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.js
8292
8371
  const LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
@@ -8748,20 +8827,33 @@ const createCoreReads = (db, storage) => {
8748
8827
  const row = await db.findOne("release_catalogs", { scope_key: scopeKey });
8749
8828
  return projectReleaseCatalogRow(row === null ? null : toCatalogRow(row), input, scopeKey);
8750
8829
  },
8751
- async getArtifactInfo(targetBundleId, currentBundleId, artifactProtocolVersion) {
8830
+ getArtifactInfo: (async (targetBundleId, currentBundleId, artifactProtocolVersion) => {
8752
8831
  const { readStorageText } = storage;
8753
- if (artifactProtocolVersion !== 1 || readStorageText === void 0) return null;
8832
+ if (artifactProtocolVersion !== 1 && artifactProtocolVersion !== 2 || readStorageText === void 0) return null;
8754
8833
  const withCurrent = currentBundleId !== NIL_UUID;
8755
8834
  const [[target, current], patch] = await Promise.all([db.findByKeys("bundles", withCurrent ? [{ id: targetBundleId }, { id: currentBundleId }] : [{ id: targetBundleId }]), withCurrent ? db.findOne("bundle_patches", {
8756
8835
  bundle_id: targetBundleId,
8757
8836
  base_bundle_id: currentBundleId
8758
8837
  }) : null]);
8759
8838
  if (target === null || target === void 0) return null;
8760
- return resolveManifestArtifacts({
8839
+ const input = {
8761
8840
  currentBundle: current ? rowToBundle(toBundleRow(current)) : null,
8762
8841
  readStorageText,
8763
8842
  resolveFileUrl: storage.resolveFileUrl,
8764
8843
  targetBundle: rowToBundle(toBundleRow(target), patch === null ? [] : [toPatchRow(patch)])
8844
+ };
8845
+ return artifactProtocolVersion === 2 ? resolveManifestArtifactsV2(input) : resolveManifestArtifacts(input);
8846
+ }),
8847
+ async getArtifactFiles(bundleId, paths) {
8848
+ const { readStorageText } = storage;
8849
+ if (readStorageText === void 0) return null;
8850
+ const row = await db.findOne("bundles", { id: bundleId });
8851
+ if (row === null) return null;
8852
+ return resolveManifestArtifactFiles({
8853
+ paths,
8854
+ readStorageText,
8855
+ resolveFileUrl: storage.resolveFileUrl,
8856
+ targetBundle: rowToBundle(toBundleRow(row))
8765
8857
  });
8766
8858
  },
8767
8859
  async getBundle(id) {
@@ -10038,11 +10130,23 @@ const ADMIN_ROUTES = [
10038
10130
  //#region ../../packages/server/dist/handlerReleaseCatalogRoutes.mjs
10039
10131
  const CATALOG_CONTENT_TYPE = "application/vnd.hot-updater.release-catalog+json; version=1";
10040
10132
  const ARTIFACT_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=1";
10133
+ const ARTIFACT_V2_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=2";
10041
10134
  const ORIGIN_CACHE_TTL_MS = 5e3;
10042
10135
  const ORIGIN_CACHE_MAX_ENTRIES = 128;
10043
10136
  const NO_CATALOG_HEADER = "x-hot-updater-catalog";
10044
10137
  const CATALOG_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
10045
10138
  const CATALOG_CDN_CACHE_CONTROL = "public, max-age=5, stale-while-revalidate=5, stale-if-error=0";
10139
+ const readArtifactFilePaths = async (request) => {
10140
+ let body;
10141
+ try {
10142
+ body = await request.json();
10143
+ } catch {
10144
+ return null;
10145
+ }
10146
+ const paths = body?.paths;
10147
+ 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;
10148
+ return paths;
10149
+ };
10046
10150
  const privateNotFound = () => Response.json({ error: "Not found" }, {
10047
10151
  status: 404,
10048
10152
  headers: { "cache-control": "private, no-store" }
@@ -10152,6 +10256,30 @@ const createReleaseCatalogRouteHandlers = () => {
10152
10256
  if (invalid) return invalid;
10153
10257
  return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
10154
10258
  },
10259
+ artifactV2: async (params, _request, { core }) => {
10260
+ const info = await core.getArtifactInfo(requireRouteParam(params, "targetBundleId"), requireRouteParam(params, "currentBundleId"), 2);
10261
+ if (info === null) return privateNotFound();
10262
+ return new Response(JSON.stringify(info), {
10263
+ status: 200,
10264
+ headers: {
10265
+ "cache-control": "private, no-store",
10266
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
10267
+ }
10268
+ });
10269
+ },
10270
+ artifactFilesV2: async (params, request, { core }) => {
10271
+ const paths = await readArtifactFilePaths(request);
10272
+ if (paths === null) return badRequest(`paths must be 1 to 100 unique strings`);
10273
+ const files = await core.getArtifactFiles(requireRouteParam(params, "targetBundleId"), paths);
10274
+ if (files === null) return privateNotFound();
10275
+ return new Response(JSON.stringify(files), {
10276
+ status: 200,
10277
+ headers: {
10278
+ "cache-control": "private, no-store",
10279
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
10280
+ }
10281
+ });
10282
+ },
10155
10283
  artifactV1: async (params, _request, { core }) => {
10156
10284
  const info = await core.getArtifactInfo(requireRouteParam(params, "targetBundleId"), requireRouteParam(params, "currentBundleId"), 1);
10157
10285
  if (info === null) return privateNotFound();
@@ -10302,6 +10430,8 @@ function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins =
10302
10430
  addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
10303
10431
  addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
10304
10432
  addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
10433
+ addClientRoute("GET", "/artifacts/v2/:targetBundleId/from/:currentBundleId", "artifactV2");
10434
+ addClientRoute("POST", "/artifacts/v2/:targetBundleId/files", "artifactFilesV2");
10305
10435
  const adminRouter = createRouter();
10306
10436
  const addAdminRoute = mount(adminRouter, true);
10307
10437
  addAdminRoute("GET", "/version", "adminVersion");
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "provider": "azure",
4
- "cliVersion": "1.0.5",
5
- "providerVersion": "1.0.5",
6
- "serverVersion": "1.0.5",
4
+ "cliVersion": "1.0.6",
5
+ "providerVersion": "1.0.6",
6
+ "serverVersion": "1.0.6",
7
7
  "infrastructureGeneration": 1,
8
8
  "clientAuth": {
9
9
  "plugin": "apiKeys",
@@ -60,14 +60,14 @@
60
60
  }
61
61
  ],
62
62
  "packages": {
63
- "hot-updater": "1.0.5",
64
- "@hot-updater/server": "1.0.5",
65
- "@hot-updater/react-native": "1.0.5",
66
- "@hot-updater/azure": "1.0.5",
67
- "@hot-updater/bare": "1.0.5",
68
- "@hot-updater/rock": "1.0.5",
69
- "@hot-updater/expo": "1.0.5",
70
- "@hot-updater/plugin-core": "1.0.5"
63
+ "hot-updater": "1.0.6",
64
+ "@hot-updater/server": "1.0.6",
65
+ "@hot-updater/react-native": "1.0.6",
66
+ "@hot-updater/azure": "1.0.6",
67
+ "@hot-updater/bare": "1.0.6",
68
+ "@hot-updater/rock": "1.0.6",
69
+ "@hot-updater/expo": "1.0.6",
70
+ "@hot-updater/plugin-core": "1.0.6"
71
71
  },
72
72
  "requiredInputs": {
73
73
  "subscriptionId": null,
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "provider": "cloudflare",
4
- "cliVersion": "1.0.5",
5
- "providerVersion": "1.0.5",
6
- "serverVersion": "1.0.5",
4
+ "cliVersion": "1.0.6",
5
+ "providerVersion": "1.0.6",
6
+ "serverVersion": "1.0.6",
7
7
  "infrastructureGeneration": 1,
8
8
  "clientAuth": {
9
9
  "plugin": "apiKeys",
@@ -61,13 +61,13 @@
61
61
  }
62
62
  ],
63
63
  "packages": {
64
- "hot-updater": "1.0.5",
65
- "@hot-updater/server": "1.0.5",
66
- "@hot-updater/react-native": "1.0.5",
67
- "@hot-updater/cloudflare": "1.0.5",
68
- "@hot-updater/bare": "1.0.5",
69
- "@hot-updater/rock": "1.0.5",
70
- "@hot-updater/expo": "1.0.5"
64
+ "hot-updater": "1.0.6",
65
+ "@hot-updater/server": "1.0.6",
66
+ "@hot-updater/react-native": "1.0.6",
67
+ "@hot-updater/cloudflare": "1.0.6",
68
+ "@hot-updater/bare": "1.0.6",
69
+ "@hot-updater/rock": "1.0.6",
70
+ "@hot-updater/expo": "1.0.6"
71
71
  },
72
72
  "requiredInputs": {
73
73
  "workerName": null,
@@ -1 +1 @@
1
- This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-14T06:33:01.516Z.
1
+ This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-14T16:15:36.859Z.