@hot-updater/aws 0.35.12 → 0.36.0

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/dist/index.cjs CHANGED
@@ -1515,9 +1515,17 @@ function isPlatformDirectoryPrefix(prefix) {
1515
1515
  const lastSegment = getLastDirectorySegment(prefix);
1516
1516
  return lastSegment === "ios" || lastSegment === "android";
1517
1517
  }
1518
+ function isLegacyBundleStorageDirectoryPrefix(prefix) {
1519
+ const lastSegment = getLastDirectorySegment(prefix);
1520
+ return lastSegment !== void 0 && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(lastSegment);
1521
+ }
1518
1522
  function isUpdateJsonKey(key) {
1519
1523
  return key.endsWith(`${S3_DIRECTORY_DELIMITER}update.json`);
1520
1524
  }
1525
+ function isUuidChannelUpdateJsonKey(key, rootPrefix, uuidChannels) {
1526
+ const [channel, platform, target, fileName, ...rest] = getRelativeDirectoryPrefix(key, rootPrefix).split(S3_DIRECTORY_DELIMITER);
1527
+ return channel !== void 0 && uuidChannels.has(channel) && (platform === "ios" || platform === "android") && target !== void 0 && target.length > 0 && fileName === "update.json" && rest.length === 0;
1528
+ }
1521
1529
  async function mapWithConcurrency(items, concurrency, mapper) {
1522
1530
  const results = [];
1523
1531
  let nextIndex = 0;
@@ -1572,7 +1580,7 @@ async function uploadJsonToS3(client, bucket, key, data) {
1572
1580
  }
1573
1581
  async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1574
1582
  const normalizedRootPrefix = normalizeDirectoryPrefix(rootPrefix);
1575
- const listPrefix = async (currentPrefix) => {
1583
+ const listPrefix = async (currentPrefix, delimiter = S3_DIRECTORY_DELIMITER) => {
1576
1584
  let continuationToken;
1577
1585
  const keys = [];
1578
1586
  const commonPrefixes = /* @__PURE__ */ new Set();
@@ -1580,7 +1588,7 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1580
1588
  const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
1581
1589
  Bucket: bucketName,
1582
1590
  Prefix: currentPrefix,
1583
- Delimiter: S3_DIRECTORY_DELIMITER,
1591
+ Delimiter: delimiter ?? void 0,
1584
1592
  ContinuationToken: continuationToken
1585
1593
  }));
1586
1594
  const found = (response.Contents ?? []).map((item) => item.Key).filter((key) => !!key);
@@ -1597,7 +1605,12 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1597
1605
  const { commonPrefixes, keys } = await listPrefix(currentPrefix);
1598
1606
  const depth = getDirectoryDepth(currentPrefix, normalizedRootPrefix);
1599
1607
  if (depth >= 2) return [...keys.filter(isUpdateJsonKey), ...commonPrefixes.map((commonPrefix) => `${commonPrefix}update.json`)];
1600
- return (await mapWithConcurrency(depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix))).flat();
1608
+ const uuidPrefixes = depth === 0 ? commonPrefixes.filter(isLegacyBundleStorageDirectoryPrefix) : [];
1609
+ const uuidPrefixSet = new Set(uuidPrefixes);
1610
+ const nestedKeys = await mapWithConcurrency(depth === 0 ? commonPrefixes.filter((commonPrefix) => !uuidPrefixSet.has(commonPrefix)) : depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix));
1611
+ const uuidChannels = new Set(uuidPrefixes.map(getLastDirectorySegment).filter((segment) => !!segment));
1612
+ const uuidChannelKeys = (await mapWithConcurrency(Array.from(new Set(Array.from(uuidChannels, (channel) => `${normalizedRootPrefix}${channel[0]}`))), S3_LIST_OBJECTS_CONCURRENCY, async (scanPrefix) => (await listPrefix(scanPrefix, null)).keys)).flat().filter((key) => isUuidChannelUpdateJsonKey(key, normalizedRootPrefix, uuidChannels));
1613
+ return [...nestedKeys.flat(), ...uuidChannelKeys];
1601
1614
  };
1602
1615
  const normalizedPrefix = normalizeDirectoryPrefix(prefix);
1603
1616
  return Array.from(new Set(await collectUpdateJsonKeys(normalizedPrefix)));
@@ -1682,9 +1695,56 @@ const s3Storage = (0, _hot_updater_plugin_core.createUniversalStoragePlugin)({
1682
1695
  factory: (config) => {
1683
1696
  const { bucketName, ...s3Config } = config;
1684
1697
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
1685
- const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
1698
+ const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
1699
+ const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
1700
+ const getListPrefix = (prefix = "") => {
1701
+ const value = [normalizedBasePath, prefix.replace(/^\/+|\/+$/g, "")].filter(Boolean).join("/");
1702
+ return value ? `${value}/` : "";
1703
+ };
1704
+ const getRelativeKey = (key) => {
1705
+ if (!normalizedBasePath) return key;
1706
+ const basePrefix = `${normalizedBasePath}/`;
1707
+ return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
1708
+ };
1686
1709
  return {
1687
1710
  node: {
1711
+ async listObjects(prefix) {
1712
+ const objects = [];
1713
+ let continuationToken;
1714
+ do {
1715
+ const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
1716
+ Bucket: bucketName,
1717
+ ContinuationToken: continuationToken,
1718
+ Prefix: getListPrefix(prefix)
1719
+ }));
1720
+ for (const object of response.Contents ?? []) {
1721
+ if (!object.Key) continue;
1722
+ objects.push({
1723
+ key: getRelativeKey(object.Key),
1724
+ lastModifiedAt: object.LastModified,
1725
+ size: object.Size ?? 0,
1726
+ storageUri: `s3://${bucketName}/${object.Key}`
1727
+ });
1728
+ }
1729
+ continuationToken = response.NextContinuationToken;
1730
+ } while (continuationToken);
1731
+ return objects;
1732
+ },
1733
+ async deleteObjects(relativeKeys) {
1734
+ const keys = relativeKeys.map((key) => getStorageKey(key));
1735
+ for (let offset = 0; offset < keys.length; offset += 1e3) {
1736
+ const batch = keys.slice(offset, offset + 1e3);
1737
+ if (batch.length === 0) continue;
1738
+ const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
1739
+ Bucket: bucketName,
1740
+ Delete: {
1741
+ Objects: batch.map((Key) => ({ Key })),
1742
+ Quiet: true
1743
+ }
1744
+ }));
1745
+ if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
1746
+ }
1747
+ },
1688
1748
  async delete(storageUri) {
1689
1749
  const { bucket, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "s3");
1690
1750
  if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
package/dist/index.mjs CHANGED
@@ -1490,9 +1490,17 @@ function isPlatformDirectoryPrefix(prefix) {
1490
1490
  const lastSegment = getLastDirectorySegment(prefix);
1491
1491
  return lastSegment === "ios" || lastSegment === "android";
1492
1492
  }
1493
+ function isLegacyBundleStorageDirectoryPrefix(prefix) {
1494
+ const lastSegment = getLastDirectorySegment(prefix);
1495
+ return lastSegment !== void 0 && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(lastSegment);
1496
+ }
1493
1497
  function isUpdateJsonKey(key) {
1494
1498
  return key.endsWith(`${S3_DIRECTORY_DELIMITER}update.json`);
1495
1499
  }
1500
+ function isUuidChannelUpdateJsonKey(key, rootPrefix, uuidChannels) {
1501
+ const [channel, platform, target, fileName, ...rest] = getRelativeDirectoryPrefix(key, rootPrefix).split(S3_DIRECTORY_DELIMITER);
1502
+ return channel !== void 0 && uuidChannels.has(channel) && (platform === "ios" || platform === "android") && target !== void 0 && target.length > 0 && fileName === "update.json" && rest.length === 0;
1503
+ }
1496
1504
  async function mapWithConcurrency(items, concurrency, mapper) {
1497
1505
  const results = [];
1498
1506
  let nextIndex = 0;
@@ -1547,7 +1555,7 @@ async function uploadJsonToS3(client, bucket, key, data) {
1547
1555
  }
1548
1556
  async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1549
1557
  const normalizedRootPrefix = normalizeDirectoryPrefix(rootPrefix);
1550
- const listPrefix = async (currentPrefix) => {
1558
+ const listPrefix = async (currentPrefix, delimiter = S3_DIRECTORY_DELIMITER) => {
1551
1559
  let continuationToken;
1552
1560
  const keys = [];
1553
1561
  const commonPrefixes = /* @__PURE__ */ new Set();
@@ -1555,7 +1563,7 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1555
1563
  const response = await client.send(new ListObjectsV2Command({
1556
1564
  Bucket: bucketName,
1557
1565
  Prefix: currentPrefix,
1558
- Delimiter: S3_DIRECTORY_DELIMITER,
1566
+ Delimiter: delimiter ?? void 0,
1559
1567
  ContinuationToken: continuationToken
1560
1568
  }));
1561
1569
  const found = (response.Contents ?? []).map((item) => item.Key).filter((key) => !!key);
@@ -1572,7 +1580,12 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
1572
1580
  const { commonPrefixes, keys } = await listPrefix(currentPrefix);
1573
1581
  const depth = getDirectoryDepth(currentPrefix, normalizedRootPrefix);
1574
1582
  if (depth >= 2) return [...keys.filter(isUpdateJsonKey), ...commonPrefixes.map((commonPrefix) => `${commonPrefix}update.json`)];
1575
- return (await mapWithConcurrency(depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix))).flat();
1583
+ const uuidPrefixes = depth === 0 ? commonPrefixes.filter(isLegacyBundleStorageDirectoryPrefix) : [];
1584
+ const uuidPrefixSet = new Set(uuidPrefixes);
1585
+ const nestedKeys = await mapWithConcurrency(depth === 0 ? commonPrefixes.filter((commonPrefix) => !uuidPrefixSet.has(commonPrefix)) : depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix));
1586
+ const uuidChannels = new Set(uuidPrefixes.map(getLastDirectorySegment).filter((segment) => !!segment));
1587
+ const uuidChannelKeys = (await mapWithConcurrency(Array.from(new Set(Array.from(uuidChannels, (channel) => `${normalizedRootPrefix}${channel[0]}`))), S3_LIST_OBJECTS_CONCURRENCY, async (scanPrefix) => (await listPrefix(scanPrefix, null)).keys)).flat().filter((key) => isUuidChannelUpdateJsonKey(key, normalizedRootPrefix, uuidChannels));
1588
+ return [...nestedKeys.flat(), ...uuidChannelKeys];
1576
1589
  };
1577
1590
  const normalizedPrefix = normalizeDirectoryPrefix(prefix);
1578
1591
  return Array.from(new Set(await collectUpdateJsonKeys(normalizedPrefix)));
@@ -1657,9 +1670,56 @@ const s3Storage = createUniversalStoragePlugin({
1657
1670
  factory: (config) => {
1658
1671
  const { bucketName, ...s3Config } = config;
1659
1672
  const client = new S3Client(applyS3RuntimeAwsConfig(s3Config));
1660
- const getStorageKey = createStorageKeyBuilder(config.basePath);
1673
+ const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
1674
+ const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
1675
+ const getListPrefix = (prefix = "") => {
1676
+ const value = [normalizedBasePath, prefix.replace(/^\/+|\/+$/g, "")].filter(Boolean).join("/");
1677
+ return value ? `${value}/` : "";
1678
+ };
1679
+ const getRelativeKey = (key) => {
1680
+ if (!normalizedBasePath) return key;
1681
+ const basePrefix = `${normalizedBasePath}/`;
1682
+ return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
1683
+ };
1661
1684
  return {
1662
1685
  node: {
1686
+ async listObjects(prefix) {
1687
+ const objects = [];
1688
+ let continuationToken;
1689
+ do {
1690
+ const response = await client.send(new ListObjectsV2Command({
1691
+ Bucket: bucketName,
1692
+ ContinuationToken: continuationToken,
1693
+ Prefix: getListPrefix(prefix)
1694
+ }));
1695
+ for (const object of response.Contents ?? []) {
1696
+ if (!object.Key) continue;
1697
+ objects.push({
1698
+ key: getRelativeKey(object.Key),
1699
+ lastModifiedAt: object.LastModified,
1700
+ size: object.Size ?? 0,
1701
+ storageUri: `s3://${bucketName}/${object.Key}`
1702
+ });
1703
+ }
1704
+ continuationToken = response.NextContinuationToken;
1705
+ } while (continuationToken);
1706
+ return objects;
1707
+ },
1708
+ async deleteObjects(relativeKeys) {
1709
+ const keys = relativeKeys.map((key) => getStorageKey(key));
1710
+ for (let offset = 0; offset < keys.length; offset += 1e3) {
1711
+ const batch = keys.slice(offset, offset + 1e3);
1712
+ if (batch.length === 0) continue;
1713
+ const response = await client.send(new DeleteObjectsCommand({
1714
+ Bucket: bucketName,
1715
+ Delete: {
1716
+ Objects: batch.map((Key) => ({ Key })),
1717
+ Quiet: true
1718
+ }
1719
+ }));
1720
+ if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
1721
+ }
1722
+ },
1663
1723
  async delete(storageUri) {
1664
1724
  const { bucket, key } = parseStorageUri(storageUri, "s3");
1665
1725
  if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
@@ -35,7 +35,7 @@ let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
35
35
  let _aws_sdk_cloudfront_signer = require("@aws-sdk/cloudfront-signer");
36
36
  //#endregion
37
37
  //#region ../../packages/server/dist/version.mjs
38
- const HOT_UPDATER_SERVER_VERSION = "0.35.12";
38
+ const HOT_UPDATER_SERVER_VERSION = "0.36.0";
39
39
  //#endregion
40
40
  //#region ../../packages/server/dist/internalRouter.mjs
41
41
  const normalizePath = (path) => {
@@ -2340,6 +2340,8 @@ const getContentAddressedAssetStoragePath = ({ assetPath, fileHash }) => {
2340
2340
  const getLegacyManifestAssetStoragePath = ({ assetPath }) => assetPath;
2341
2341
  //#endregion
2342
2342
  //#region ../plugin-core/dist/assetStorageLayout.mjs
2343
+ const isBrotliManifestAssetPath = (assetPath) => /(^|\/)index\.[^/]+\.bundle$/.test(assetPath.replace(/\\/g, "/"));
2344
+ const getManifestAssetDownloadPath = (assetPath) => isBrotliManifestAssetPath(assetPath) ? `${assetPath}.br` : assetPath;
2343
2345
  const createStorageUriWithRelativePath = ({ baseStorageUri, relativePath }) => {
2344
2346
  const storageUrl = new URL(baseStorageUri);
2345
2347
  storageUrl.pathname = `${storageUrl.pathname.replace(/\/+$/, "")}/${relativePath.replace(/\\/g, "/").split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
@@ -3681,10 +3683,17 @@ function addTargetVersionRemoval(mutations, bundle) {
3681
3683
  getTargetVersionMutation(mutations, bundle).removals.add(targetAppVersion);
3682
3684
  }
3683
3685
  function getManagementListPrefixes(where) {
3686
+ if (where?.channel && where.platform && typeof where.targetAppVersion === "string") {
3687
+ const targetAppVersion = normalizeTargetAppVersion(where.targetAppVersion);
3688
+ if (targetAppVersion) return [`${where.channel}/${where.platform}/${targetAppVersion}/`];
3689
+ }
3684
3690
  if (where?.channel && where.platform) return [`${where.channel}/${where.platform}/`];
3685
3691
  if (where?.channel) return [`${where.channel}/`];
3686
3692
  return [""];
3687
3693
  }
3694
+ function getChannelFromUpdateJsonKey(key) {
3695
+ return key.match(/^([^/]+)\/(?:ios|android)\/[^/]+\/update\.json$/)?.[1] ?? null;
3696
+ }
3688
3697
  const DEFAULT_DESC_ORDER = {
3689
3698
  field: "id",
3690
3699
  direction: "desc"
@@ -3701,7 +3710,7 @@ function sortManagedBundles(bundles, orderBy = DEFAULT_DESC_ORDER) {
3701
3710
  */
3702
3711
  const createBlobDatabasePlugin = ({ name, factory }) => {
3703
3712
  return (config, hooks) => {
3704
- const { listObjects, loadObject, uploadObject, deleteObject, shouldSkipLoadObjectError, invalidatePaths, apiBasePath } = factory(config);
3713
+ const { listObjects, loadObject, uploadObject, deleteObject, shouldSkipLoadObjectError, validateChannel, invalidatePaths, apiBasePath } = factory(config);
3705
3714
  const bundlesMap = /* @__PURE__ */ new Map();
3706
3715
  const pendingBundlesMap = /* @__PURE__ */ new Map();
3707
3716
  const locallyDeletedBundleIds = /* @__PURE__ */ new Set();
@@ -3837,10 +3846,12 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
3837
3846
  });
3838
3847
  },
3839
3848
  async getChannels() {
3840
- return [...new Set((await loadAllBundlesForManagementFallback()).map((bundle) => bundle.channel))].sort();
3849
+ const channels = (await listObjects("")).map(getChannelFromUpdateJsonKey).filter((channel) => channel !== null);
3850
+ return [...new Set(channels)].sort();
3841
3851
  },
3842
3852
  async commitBundle({ changedSets }) {
3843
3853
  if (changedSets.length === 0) return;
3854
+ for (const { operation, data } of changedSets) if (operation === "insert" || operation === "update" && data.channel !== void 0) validateChannel?.(data.channel);
3844
3855
  const changedBundlesByKey = {};
3845
3856
  const removalsByKey = {};
3846
3857
  const targetVersionRemovalsByKey = {};
@@ -4003,21 +4014,39 @@ const createProfiledStoragePlugin = ({ createProfiles, name, profileShape, suppo
4003
4014
  return runtime;
4004
4015
  };
4005
4016
  const profiles = {};
4006
- if (profileShape?.node) profiles.node = {
4007
- async delete(storageUri) {
4008
- return requireNodeProfile().delete(storageUri);
4009
- },
4010
- async downloadFile(storageUri, filePath) {
4011
- return requireNodeProfile().downloadFile(storageUri, filePath);
4012
- },
4013
- async exists(storageUri) {
4014
- return requireNodeProfile().exists(storageUri);
4015
- },
4016
- async upload(key, filePath) {
4017
- return requireNodeProfile().upload(key, filePath);
4018
- }
4019
- };
4020
- else if (profileShape?.node !== false) Object.defineProperty(profiles, "node", {
4017
+ if (profileShape?.node) {
4018
+ const nodeProfile = {
4019
+ async delete(storageUri) {
4020
+ return requireNodeProfile().delete(storageUri);
4021
+ },
4022
+ async downloadFile(storageUri, filePath) {
4023
+ return requireNodeProfile().downloadFile(storageUri, filePath);
4024
+ },
4025
+ async exists(storageUri) {
4026
+ return requireNodeProfile().exists(storageUri);
4027
+ },
4028
+ async upload(key, filePath) {
4029
+ return requireNodeProfile().upload(key, filePath);
4030
+ }
4031
+ };
4032
+ Object.defineProperties(nodeProfile, {
4033
+ deleteObjects: {
4034
+ enumerable: true,
4035
+ get: () => {
4036
+ const deleteObjects = requireNodeProfile().deleteObjects;
4037
+ return deleteObjects ? (keys) => deleteObjects(keys) : void 0;
4038
+ }
4039
+ },
4040
+ listObjects: {
4041
+ enumerable: true,
4042
+ get: () => {
4043
+ const listObjects = requireNodeProfile().listObjects;
4044
+ return listObjects ? (prefix) => listObjects(prefix) : void 0;
4045
+ }
4046
+ }
4047
+ });
4048
+ profiles.node = nodeProfile;
4049
+ } else if (profileShape?.node !== false) Object.defineProperty(profiles, "node", {
4021
4050
  enumerable: true,
4022
4051
  get: getNodeProfile
4023
4052
  });
@@ -4098,7 +4127,6 @@ function assertRuntimeStoragePlugin(plugin) {
4098
4127
  //#endregion
4099
4128
  //#region ../../packages/server/dist/db/updateArtifacts.mjs
4100
4129
  const HBC_ASSET_PATH_RE = /\.bundle$/;
4101
- const BR_COMPRESSED_ASSET_PATH_RE = /(^|\/)index\.[^/]+\.bundle$/;
4102
4130
  const resolveUniqueHbcAssetPath = (manifest) => {
4103
4131
  const candidates = Object.keys(manifest.assets).sort((left, right) => left.localeCompare(right)).filter((candidate) => HBC_ASSET_PATH_RE.test(candidate));
4104
4132
  return candidates.length === 1 ? candidates[0] : null;
@@ -4140,10 +4168,11 @@ async function resolveChangedAssets({ assetBaseStorageUri, currentManifest, curr
4140
4168
  });
4141
4169
  const changedEntries = await Promise.all(Object.entries(targetManifest.assets).map(async ([assetPath, asset]) => {
4142
4170
  if ((currentManifest?.assets[assetPath])?.fileHash === asset.fileHash) return null;
4143
- const usesBrotliAsset = BR_COMPRESSED_ASSET_PATH_RE.test(assetPath);
4171
+ const downloadPath = getManifestAssetDownloadPath(assetPath);
4172
+ const usesBrotliAsset = downloadPath !== assetPath;
4144
4173
  const storageUri = resolveManifestAssetStorageUri({
4145
4174
  assetBaseStorageUri,
4146
- assetPath: usesBrotliAsset ? `${assetPath}.br` : assetPath,
4175
+ assetPath: downloadPath,
4147
4176
  fileHash: asset.fileHash
4148
4177
  });
4149
4178
  const patch = patchDescriptor?.assetPath === assetPath ? patchDescriptor.patch : null;
@@ -6528,9 +6557,17 @@ function isPlatformDirectoryPrefix(prefix) {
6528
6557
  const lastSegment = getLastDirectorySegment(prefix);
6529
6558
  return lastSegment === "ios" || lastSegment === "android";
6530
6559
  }
6560
+ function isLegacyBundleStorageDirectoryPrefix(prefix) {
6561
+ const lastSegment = getLastDirectorySegment(prefix);
6562
+ return lastSegment !== void 0 && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(lastSegment);
6563
+ }
6531
6564
  function isUpdateJsonKey(key) {
6532
6565
  return key.endsWith(`${S3_DIRECTORY_DELIMITER}update.json`);
6533
6566
  }
6567
+ function isUuidChannelUpdateJsonKey(key, rootPrefix, uuidChannels) {
6568
+ const [channel, platform, target, fileName, ...rest] = getRelativeDirectoryPrefix(key, rootPrefix).split(S3_DIRECTORY_DELIMITER);
6569
+ return channel !== void 0 && uuidChannels.has(channel) && (platform === "ios" || platform === "android") && target !== void 0 && target.length > 0 && fileName === "update.json" && rest.length === 0;
6570
+ }
6534
6571
  async function mapWithConcurrency(items, concurrency, mapper) {
6535
6572
  const results = [];
6536
6573
  let nextIndex = 0;
@@ -6585,7 +6622,7 @@ async function uploadJsonToS3(client, bucket, key, data) {
6585
6622
  }
6586
6623
  async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
6587
6624
  const normalizedRootPrefix = normalizeDirectoryPrefix(rootPrefix);
6588
- const listPrefix = async (currentPrefix) => {
6625
+ const listPrefix = async (currentPrefix, delimiter = S3_DIRECTORY_DELIMITER) => {
6589
6626
  let continuationToken;
6590
6627
  const keys = [];
6591
6628
  const commonPrefixes = /* @__PURE__ */ new Set();
@@ -6593,7 +6630,7 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
6593
6630
  const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
6594
6631
  Bucket: bucketName,
6595
6632
  Prefix: currentPrefix,
6596
- Delimiter: S3_DIRECTORY_DELIMITER,
6633
+ Delimiter: delimiter ?? void 0,
6597
6634
  ContinuationToken: continuationToken
6598
6635
  }));
6599
6636
  const found = (response.Contents ?? []).map((item) => item.Key).filter((key) => !!key);
@@ -6610,7 +6647,12 @@ async function listObjectsInS3(client, bucketName, prefix, rootPrefix = "") {
6610
6647
  const { commonPrefixes, keys } = await listPrefix(currentPrefix);
6611
6648
  const depth = getDirectoryDepth(currentPrefix, normalizedRootPrefix);
6612
6649
  if (depth >= 2) return [...keys.filter(isUpdateJsonKey), ...commonPrefixes.map((commonPrefix) => `${commonPrefix}update.json`)];
6613
- return (await mapWithConcurrency(depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix))).flat();
6650
+ const uuidPrefixes = depth === 0 ? commonPrefixes.filter(isLegacyBundleStorageDirectoryPrefix) : [];
6651
+ const uuidPrefixSet = new Set(uuidPrefixes);
6652
+ const nestedKeys = await mapWithConcurrency(depth === 0 ? commonPrefixes.filter((commonPrefix) => !uuidPrefixSet.has(commonPrefix)) : depth === 1 ? commonPrefixes.filter(isPlatformDirectoryPrefix) : commonPrefixes, S3_LIST_OBJECTS_CONCURRENCY, (nextPrefix) => collectUpdateJsonKeys(nextPrefix));
6653
+ const uuidChannels = new Set(uuidPrefixes.map(getLastDirectorySegment).filter((segment) => !!segment));
6654
+ const uuidChannelKeys = (await mapWithConcurrency(Array.from(new Set(Array.from(uuidChannels, (channel) => `${normalizedRootPrefix}${channel[0]}`))), S3_LIST_OBJECTS_CONCURRENCY, async (scanPrefix) => (await listPrefix(scanPrefix, null)).keys)).flat().filter((key) => isUuidChannelUpdateJsonKey(key, normalizedRootPrefix, uuidChannels));
6655
+ return [...nestedKeys.flat(), ...uuidChannelKeys];
6614
6656
  };
6615
6657
  const normalizedPrefix = normalizeDirectoryPrefix(prefix);
6616
6658
  return Array.from(new Set(await collectUpdateJsonKeys(normalizedPrefix)));
@@ -6695,9 +6737,56 @@ const s3Storage = createUniversalStoragePlugin({
6695
6737
  factory: (config) => {
6696
6738
  const { bucketName, ...s3Config } = config;
6697
6739
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
6698
- const getStorageKey = createStorageKeyBuilder(config.basePath);
6740
+ const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
6741
+ const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
6742
+ const getListPrefix = (prefix = "") => {
6743
+ const value = [normalizedBasePath, prefix.replace(/^\/+|\/+$/g, "")].filter(Boolean).join("/");
6744
+ return value ? `${value}/` : "";
6745
+ };
6746
+ const getRelativeKey = (key) => {
6747
+ if (!normalizedBasePath) return key;
6748
+ const basePrefix = `${normalizedBasePath}/`;
6749
+ return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
6750
+ };
6699
6751
  return {
6700
6752
  node: {
6753
+ async listObjects(prefix) {
6754
+ const objects = [];
6755
+ let continuationToken;
6756
+ do {
6757
+ const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
6758
+ Bucket: bucketName,
6759
+ ContinuationToken: continuationToken,
6760
+ Prefix: getListPrefix(prefix)
6761
+ }));
6762
+ for (const object of response.Contents ?? []) {
6763
+ if (!object.Key) continue;
6764
+ objects.push({
6765
+ key: getRelativeKey(object.Key),
6766
+ lastModifiedAt: object.LastModified,
6767
+ size: object.Size ?? 0,
6768
+ storageUri: `s3://${bucketName}/${object.Key}`
6769
+ });
6770
+ }
6771
+ continuationToken = response.NextContinuationToken;
6772
+ } while (continuationToken);
6773
+ return objects;
6774
+ },
6775
+ async deleteObjects(relativeKeys) {
6776
+ const keys = relativeKeys.map((key) => getStorageKey(key));
6777
+ for (let offset = 0; offset < keys.length; offset += 1e3) {
6778
+ const batch = keys.slice(offset, offset + 1e3);
6779
+ if (batch.length === 0) continue;
6780
+ const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
6781
+ Bucket: bucketName,
6782
+ Delete: {
6783
+ Objects: batch.map((Key) => ({ Key })),
6784
+ Quiet: true
6785
+ }
6786
+ }));
6787
+ if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
6788
+ }
6789
+ },
6701
6790
  async delete(storageUri) {
6702
6791
  const { bucket, key } = parseStorageUri(storageUri, "s3");
6703
6792
  if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/aws",
3
3
  "type": "module",
4
- "version": "0.35.12",
4
+ "version": "0.36.0",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -50,10 +50,10 @@
50
50
  "es-toolkit": "^1.32.0",
51
51
  "execa": "9.5.2",
52
52
  "mime": "^4.0.4",
53
- "@hot-updater/core": "0.35.12",
54
- "@hot-updater/js": "0.35.12",
55
- "@hot-updater/mock": "0.35.12",
56
- "@hot-updater/test-utils": "0.35.12"
53
+ "@hot-updater/core": "0.36.0",
54
+ "@hot-updater/js": "0.36.0",
55
+ "@hot-updater/mock": "0.36.0",
56
+ "@hot-updater/test-utils": "0.36.0"
57
57
  },
58
58
  "dependencies": {
59
59
  "@aws-sdk/client-cloudfront": "3.1066.0",
@@ -68,9 +68,9 @@
68
68
  "@aws-sdk/lib-storage": "3.1066.0",
69
69
  "hono": "4.12.9",
70
70
  "aws-lambda": "1.0.7",
71
- "@hot-updater/cli-tools": "0.35.12",
72
- "@hot-updater/plugin-core": "0.35.12",
73
- "@hot-updater/server": "0.35.12"
71
+ "@hot-updater/cli-tools": "0.36.0",
72
+ "@hot-updater/plugin-core": "0.36.0",
73
+ "@hot-updater/server": "0.36.0"
74
74
  },
75
75
  "scripts": {
76
76
  "build": "tsdown",