@hot-updater/cli-tools 0.36.7 → 1.0.0-rc.1

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.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Readable } from "stream";
2
2
  import { Key as Key$1 } from "node:readline";
3
3
  import { Readable as Readable$1, Writable } from "node:stream";
4
+ import { Bundle, BundleSigningPlugin, ConfigInput, LocalSigningConfig, Platform, RequiredDeep, SigningConfig, StoragePluginWith, StoragePutResult } from "@hot-updater/plugin-core";
4
5
  import * as _$_hot_updater_core0 from "@hot-updater/core";
5
- import { Bundle, ConfigInput, DatabasePlugin, NodeStoragePlugin, Platform, RequiredDeep } from "@hot-updater/plugin-core";
6
6
 
7
7
  //#region src/BuildLogger.d.ts
8
8
  type LinePattern = string | RegExp;
@@ -33,11 +33,31 @@ declare class BuildLogger {
33
33
  private showLogFileLocation;
34
34
  }
35
35
  //#endregion
36
+ //#region src/apiKeyNote.d.ts
37
+ declare const formatApiKeyNote: (apiKey: string) => string;
38
+ //#endregion
36
39
  //#region src/banner.d.ts
37
40
  declare const link: (url: string) => string;
38
41
  declare const banner: (version?: string) => string;
39
42
  declare const printBanner: (version?: string) => void;
40
43
  //#endregion
44
+ //#region src/bundleSigning.d.ts
45
+ interface BundleSigningSession {
46
+ readonly name: string;
47
+ readonly publicKey: string;
48
+ readonly signFileHash: (fileHash: string) => Promise<string>;
49
+ }
50
+ declare const readBundleSigningPublicKeyFile: (publicKeyPath: string, options?: {
51
+ readonly cwd?: string;
52
+ }) => Promise<string>;
53
+ declare const prepareBundleSigning: (signing: SigningConfig | undefined, options?: {
54
+ readonly cwd?: string;
55
+ }) => Promise<BundleSigningSession | null>;
56
+ /** Resolves the public identity of the configured bundle signer. */
57
+ declare const getBundleSigningPublicKey: (signing: SigningConfig | undefined, options?: {
58
+ readonly cwd?: string;
59
+ }) => Promise<string | null>;
60
+ //#endregion
41
61
  //#region src/ConfigBuilder.d.ts
42
62
  type BuildType = "bare" | "rock" | "expo";
43
63
  type ImportInfo = {
@@ -246,6 +266,7 @@ type ManagedHelperStatement = {
246
266
  name: string;
247
267
  code: string;
248
268
  strategy: ManagedHelperStrategy;
269
+ replaceIncompatibleProperties?: string[];
249
270
  };
250
271
  type CreateHotUpdaterConfigScaffoldOptions = {
251
272
  build: BuildType;
@@ -334,6 +355,31 @@ declare const HotUpdateDirUtil: {
334
355
  }) => string;
335
356
  };
336
357
  //#endregion
358
+ //#region src/infrastructureGeneration.d.ts
359
+ type Fetch = typeof fetch;
360
+ declare const assertInfrastructureGenerationPayload: ({
361
+ payload,
362
+ provider,
363
+ resource
364
+ }: {
365
+ readonly payload: unknown;
366
+ readonly provider: string;
367
+ readonly resource: string;
368
+ }) => void;
369
+ declare const assertInfrastructureGenerationAtUrl: ({
370
+ fetchImpl,
371
+ legacyStatuses,
372
+ provider,
373
+ resource,
374
+ versionUrl
375
+ }: {
376
+ readonly fetchImpl?: Fetch;
377
+ readonly legacyStatuses?: readonly number[];
378
+ readonly provider: string;
379
+ readonly resource: string;
380
+ readonly versionUrl: string;
381
+ }) => Promise<void>;
382
+ //#endregion
337
383
  //#region src/initProvider.d.ts
338
384
  type InitProviderInputPersistence = "always" | "with-consent";
339
385
  type InitProviderInputDefinition = {
@@ -432,6 +478,10 @@ declare class MissingInitInputsError extends InitError {
432
478
  declare class InitEnvFileError extends InitError {
433
479
  readonly name = "InitEnvFileError";
434
480
  }
481
+ declare class LegacyInfrastructureError extends InitError {
482
+ readonly name = "LegacyInfrastructureError";
483
+ constructor(provider: string, resource: string);
484
+ }
435
485
  declare const assertInitInputs: ({
436
486
  inputs,
437
487
  strict
@@ -457,12 +507,19 @@ declare const createLogWriter: ({
457
507
  logFilePath?: string;
458
508
  }) => Promise<HotUpdaterLogWriter>;
459
509
  //#endregion
510
+ //#region src/localBundleSigning.d.ts
511
+ declare const normalizeSigningConfig: (signing: SigningConfig | undefined) => BundleSigningPlugin | Extract<LocalSigningConfig, {
512
+ enabled: true;
513
+ }> | undefined;
514
+ //#endregion
460
515
  //#region src/loadConfig.d.ts
461
516
  type HotUpdaterConfigOptions = {
462
517
  platform: Platform;
463
518
  channel: string;
464
519
  } | null;
465
- type ConfigResponse = RequiredDeep<ConfigInput>;
520
+ type ConfigResponse = RequiredDeep<Omit<ConfigInput, "database" | "signing" | "storage">> & Pick<ConfigInput, "database" | "storage"> & {
521
+ signing?: ReturnType<typeof normalizeSigningConfig>;
522
+ };
466
523
  declare const loadConfig: (options: HotUpdaterConfigOptions) => Promise<ConfigResponse>;
467
524
  //#endregion
468
525
  //#region src/log.d.ts
@@ -486,63 +543,34 @@ declare const makeEnv: (newEnvVars: Record<string, EnvVarValue>, filePath?: stri
486
543
  }) => Promise<string>;
487
544
  //#endregion
488
545
  //#region src/promoteBundle.d.ts
546
+ type PromoteStoragePlugin = StoragePluginWith<"get" | "put" | "exists" | "delete">;
489
547
  declare const LEGACY_BUNDLE_ERROR = "This OTA bundle was created by a version that does not support manifest.json. Copy bundle is not available.";
490
- interface PromoteBundleInput {
491
- action: "copy" | "move";
492
- bundleId: string;
493
- nextBundleId?: string;
494
- targetChannel: string;
495
- }
496
- interface PromoteBundleDependencies {
497
- config: ConfigResponse;
498
- databasePlugin: DatabasePlugin;
499
- storagePlugin: NodeStoragePlugin | null;
500
- }
501
548
  declare function createCopiedBundleArchive({
502
549
  bundle,
503
550
  config,
504
551
  nextBundleId,
505
- storagePlugin,
506
- targetChannel
552
+ storagePlugin
507
553
  }: {
508
554
  bundle: Bundle;
509
555
  config: ConfigResponse;
510
556
  nextBundleId: string;
511
- storagePlugin: NodeStoragePlugin;
512
- targetChannel: string;
557
+ storagePlugin: PromoteStoragePlugin;
513
558
  }): Promise<{
514
559
  bundle: {
515
560
  id: string;
516
- channel: string;
561
+ archiveByteSize: number;
517
562
  storageUri: string;
518
563
  fileHash: string;
519
564
  metadata: _$_hot_updater_core0.BundleMetadata | undefined;
520
565
  assetBaseStorageUri: string;
521
566
  patches: never[];
522
- patchBaseBundleId: null;
523
567
  manifestFileHash: string;
524
568
  manifestStorageUri: string;
525
- patchBaseFileHash: null;
526
- patchFileHash: null;
527
- patchStorageUri: null;
528
569
  platform: _$_hot_updater_core0.Platform;
529
- shouldForceUpdate: boolean;
530
- enabled: boolean;
531
570
  gitCommitHash: string | null;
532
- message: string | null;
533
- targetAppVersion: string | null;
534
- fingerprintHash: string | null;
535
- rolloutCohortCount?: number | null;
536
- targetCohorts?: string[] | null;
537
571
  };
538
572
  uploadedStorageUris: string[];
539
573
  }>;
540
- declare function promoteBundle({
541
- action,
542
- bundleId,
543
- nextBundleId,
544
- targetChannel
545
- }: PromoteBundleInput, deps: PromoteBundleDependencies): Promise<Bundle>;
546
574
  //#endregion
547
575
  //#region ../../node_modules/.pnpm/@clack+core@1.4.3/node_modules/@clack/core/dist/index.d.mts
548
576
  declare const actions: readonly ["up", "down", "left", "right", "space", "enter", "cancel"];
@@ -1792,6 +1820,14 @@ declare const resolveHotUpdaterServerVersion: (currentPackageName: string, optio
1792
1820
  searchFrom?: string;
1793
1821
  }) => string;
1794
1822
  //#endregion
1823
+ //#region src/storageFiles.d.ts
1824
+ declare const getStorageFileByteSize: (filePath: string) => Promise<number>;
1825
+ declare const putStorageFile: (storage: StoragePluginWith<"put">, key: string, filePath: string) => Promise<StoragePutResult & {
1826
+ byteSize: number;
1827
+ }>;
1828
+ declare const writeStorageFile: (storage: StoragePluginWith<"get">, storageUri: string, filePath: string) => Promise<void>;
1829
+ declare const writeStorageResponseFile: (response: Response, filePath: string) => Promise<void>;
1830
+ //#endregion
1795
1831
  //#region src/transformEnv.d.ts
1796
1832
  declare const transformEnv: <T extends Record<string, string>>(filename: string, env: T) => string;
1797
1833
  //#endregion
@@ -1809,4 +1845,4 @@ type TransformTemplateArgs<T extends string> = { [Key in ExtractPlaceholders<T>]
1809
1845
  */
1810
1846
  declare function transformTemplate<T extends string>(templateString: T, values: TransformTemplateArgs<T>): string;
1811
1847
  //#endregion
1812
- export { BuildLogger, BuildLoggerConfig, BuildType, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, IConfigBuilder, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, LEGACY_BUNDLE_ERROR, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, PromoteBundleDependencies, PromoteBundleInput, PromptProgress, PromptSpinner, ProviderConfig, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, WriteHotUpdaterConfigResult, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, getAndroidSdkPath, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, link, loadConfig, log, makeEnv, p, printBanner, promoteBundle, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig };
1848
+ export { BuildLogger, BuildLoggerConfig, BuildType, BundleSigningSession, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, IConfigBuilder, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, LEGACY_BUNDLE_ERROR, LegacyInfrastructureError, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, PromptProgress, PromptSpinner, ProviderConfig, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, WriteHotUpdaterConfigResult, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, formatApiKeyNote, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, link, loadConfig, log, makeEnv, p, prepareBundleSigning, printBanner, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig, writeStorageFile, writeStorageResponseFile };
package/dist/index.mjs CHANGED
@@ -17,6 +17,8 @@ import tty, { ReadStream } from "node:tty";
17
17
  import fs$2, { appendFileSync, createReadStream, createWriteStream as createWriteStream$1, existsSync, lstatSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
18
18
  import path$1, { basename, dirname as dirname$1, join, posix, win32 } from "node:path";
19
19
  import os, { constants } from "node:os";
20
+ import crypto$1, { randomBytes, randomUUID } from "node:crypto";
21
+ import fs$3 from "node:fs/promises";
20
22
  import { pipeline } from "stream/promises";
21
23
  import * as Ms from "zlib";
22
24
  import Jr, { constants as constants$1, createBrotliCompress } from "zlib";
@@ -24,8 +26,6 @@ import { EventEmitter as EventEmitter$1, addAbortListener, on, once, setMaxListe
24
26
  import As, { Duplex, PassThrough as PassThrough$1, Readable as Readable$1, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
25
27
  import { StringDecoder } from "node:string_decoder";
26
28
  import no from "node:assert";
27
- import crypto$1, { randomBytes, randomUUID } from "node:crypto";
28
- import fs$3 from "node:fs/promises";
29
29
  import { fileURLToPath } from "node:url";
30
30
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
31
31
  import { scheduler, setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -33,10 +33,10 @@ import { serialize } from "node:v8";
33
33
  import { finished, pipeline as pipeline$1 } from "node:stream/promises";
34
34
  import { Buffer as Buffer$2 } from "node:buffer";
35
35
  import { parseSync } from "oxc-parser";
36
+ import { createBundleStorageKey, createDatabasePlugin, createStoragePlugin, createStorageRootUriWithPath, detectCompressionFormat, getContentType, getManifestAssetDownloadPath, getManifestAssetStoragePath, isContentAddressedAssetFileHash, parseStorageUri, resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
36
37
  import { loadConfig as loadConfig$1 } from "unconfig";
37
- import { brotliDecompressSync, createBrotliCompress as createBrotliCompress$1 } from "node:zlib";
38
+ import { brotliDecompressSync, constants as constants$2, createBrotliCompress as createBrotliCompress$1 } from "node:zlib";
38
39
  import { getManifestFileHash, stripBundleArtifactMetadata } from "@hot-updater/core";
39
- import { createBundleStorageKey, createStorageRootUriWithPath, createUUIDv7, detectCompressionFormat, getContentAddressedAssetStoragePath, getManifestAssetDownloadPath, resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
40
40
  import { transformSync } from "oxc-transform";
41
41
  //#endregion
42
42
  //#region src/colors.ts
@@ -22587,6 +22587,9 @@ var BuildLogger = class {
22587
22587
  }
22588
22588
  };
22589
22589
  //#endregion
22590
+ //#region src/apiKeyNote.ts
22591
+ const formatApiKeyNote = (apiKey) => apiKey;
22592
+ //#endregion
22590
22593
  //#region ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
22591
22594
  function ansiRegex({ onlyFirst = false } = {}) {
22592
22595
  return new RegExp(`(?:\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\u005C|\\u009C))|[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]`, onlyFirst ? void 0 : "g");
@@ -24507,6 +24510,165 @@ const printBanner = (version) => {
24507
24510
  console.log(banner(version));
24508
24511
  };
24509
24512
  //#endregion
24513
+ //#region src/localBundleSigning.ts
24514
+ const resolvePath$1 = (cwd, filePath) => path$1.isAbsolute(filePath) ? filePath : path$1.resolve(cwd, filePath);
24515
+ const loadPrivateKey = async (privateKeyPath) => {
24516
+ try {
24517
+ const privateKey = crypto$1.createPrivateKey(await fs$3.readFile(privateKeyPath, "utf8"));
24518
+ if (privateKey.asymmetricKeyType !== "rsa" || (privateKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw new Error("not rsa");
24519
+ return privateKey;
24520
+ } catch {
24521
+ throw new Error("Failed to load the local bundle signing private key.");
24522
+ }
24523
+ };
24524
+ const createLocalSigningPlugin = ({ privateKeyPath }) => {
24525
+ const privateKeys = /* @__PURE__ */ new Map();
24526
+ const getPrivateKey = (cwd = process.cwd()) => {
24527
+ const resolvedPath = resolvePath$1(cwd, privateKeyPath);
24528
+ const cached = privateKeys.get(resolvedPath);
24529
+ if (cached) return cached;
24530
+ const pending = loadPrivateKey(resolvedPath).catch((error) => {
24531
+ privateKeys.delete(resolvedPath);
24532
+ throw error;
24533
+ });
24534
+ privateKeys.set(resolvedPath, pending);
24535
+ return pending;
24536
+ };
24537
+ return {
24538
+ name: "localSigning",
24539
+ async getPublicKey({ cwd } = {}) {
24540
+ const privateKey = await getPrivateKey(cwd);
24541
+ return { publicKey: crypto$1.createPublicKey(privateKey).export({
24542
+ format: "pem",
24543
+ type: "spki"
24544
+ }).toString() };
24545
+ },
24546
+ async sign({ message, cwd }) {
24547
+ if (!(message instanceof Uint8Array) || message.byteLength !== 32) throw new Error("Local bundle signing messages must be exactly 32 bytes.");
24548
+ const privateKey = await getPrivateKey(cwd);
24549
+ return { signature: crypto$1.sign("RSA-SHA256", message, privateKey) };
24550
+ }
24551
+ };
24552
+ };
24553
+ const invalidSigningConfig = () => /* @__PURE__ */ new Error("Bundle signing must be a local key config or signing plugin. Omit signing to disable it.");
24554
+ const normalizeSigningConfig = (signing) => {
24555
+ if (signing === void 0) return void 0;
24556
+ if (typeof signing !== "object" || signing === null) throw invalidSigningConfig();
24557
+ const hasPrivateKeyPath = Reflect.has(signing, "privateKeyPath");
24558
+ const hasEnabled = Reflect.has(signing, "enabled");
24559
+ const hasPluginMembers = [
24560
+ "name",
24561
+ "getPublicKey",
24562
+ "sign"
24563
+ ].some((key) => Reflect.has(signing, key));
24564
+ if ((hasPrivateKeyPath || hasEnabled) && hasPluginMembers) throw new Error("Bundle signing config cannot combine local signing fields with signing plugin fields.");
24565
+ if (!hasPluginMembers) {
24566
+ if (Object.keys(signing).some((key) => key !== "enabled" && key !== "privateKeyPath")) throw new Error("Local bundle signing accepts only enabled and privateKeyPath.");
24567
+ const enabled = Reflect.get(signing, "enabled");
24568
+ if (enabled === false) return void 0;
24569
+ if (enabled !== true) throw invalidSigningConfig();
24570
+ const privateKeyPath = Reflect.get(signing, "privateKeyPath");
24571
+ if (typeof privateKeyPath !== "string" || !privateKeyPath.trim()) throw new Error("Enabled local bundle signing requires privateKeyPath.");
24572
+ return {
24573
+ enabled: true,
24574
+ privateKeyPath
24575
+ };
24576
+ }
24577
+ if (typeof Reflect.get(signing, "name") !== "string" || typeof Reflect.get(signing, "getPublicKey") !== "function" || typeof Reflect.get(signing, "sign") !== "function") throw invalidSigningConfig();
24578
+ return signing;
24579
+ };
24580
+ //#endregion
24581
+ //#region src/bundleSigning.ts
24582
+ const FILE_HASH_PATTERN = /^[a-f\d]{64}$/iu;
24583
+ const resolvePath = (cwd, filePath) => path$1.isAbsolute(filePath) ? filePath : path$1.resolve(cwd, filePath);
24584
+ const parseRsaPublicKey = (publicKeyPEM) => {
24585
+ try {
24586
+ const normalizedPublicKey = publicKeyPEM.trim();
24587
+ if (!normalizedPublicKey.startsWith("-----BEGIN PUBLIC KEY-----") || !normalizedPublicKey.endsWith("-----END PUBLIC KEY-----")) throw new Error("not spki");
24588
+ const publicKey = crypto$1.createPublicKey(normalizedPublicKey);
24589
+ if (publicKey.asymmetricKeyType !== "rsa" || (publicKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw new Error("not rsa");
24590
+ return publicKey;
24591
+ } catch {
24592
+ throw new Error("Bundle signing public key must be a valid RSA SPKI PEM key with a modulus of at least 2048 bits.");
24593
+ }
24594
+ };
24595
+ const exportPublicKey = (publicKey) => publicKey.export({
24596
+ type: "spki",
24597
+ format: "pem"
24598
+ }).toString();
24599
+ const readBundleSigningPublicKeyFile = async (publicKeyPath, options = {}) => {
24600
+ if (!publicKeyPath.trim()) throw new Error("Bundle signing public key path is required.");
24601
+ try {
24602
+ return exportPublicKey(parseRsaPublicKey(await fs$3.readFile(resolvePath(options.cwd ?? getCwd(), publicKeyPath), "utf8")));
24603
+ } catch {
24604
+ throw new Error("Failed to read the bundle signing public key file.");
24605
+ }
24606
+ };
24607
+ const getProviderPublicKey = async (provider, cwd) => {
24608
+ try {
24609
+ const result = await provider.getPublicKey({ cwd });
24610
+ if (!result || typeof result.publicKey !== "string") throw new Error("invalid result");
24611
+ return parseRsaPublicKey(result.publicKey);
24612
+ } catch {
24613
+ throw new Error("Failed to resolve the bundle signing provider public key.");
24614
+ }
24615
+ };
24616
+ const createMemoizedSigner = ({ publicKey, sign }) => {
24617
+ const signatures = /* @__PURE__ */ new Map();
24618
+ return (fileHash) => {
24619
+ if (!FILE_HASH_PATTERN.test(fileHash)) return Promise.reject(/* @__PURE__ */ new Error("Bundle signing requires a 64-character hexadecimal file hash."));
24620
+ const normalizedFileHash = fileHash.toLowerCase();
24621
+ const cached = signatures.get(normalizedFileHash);
24622
+ if (cached) return cached;
24623
+ const pending = (async () => {
24624
+ const message = Buffer.from(normalizedFileHash, "hex");
24625
+ let signature;
24626
+ try {
24627
+ signature = await sign(new Uint8Array(message));
24628
+ } catch {
24629
+ throw new Error("Bundle signing provider failed to sign the file hash.");
24630
+ }
24631
+ if (!(signature instanceof Uint8Array) || signature.byteLength === 0) throw new Error("Bundle signing provider returned an invalid signature.");
24632
+ if (!crypto$1.verify("RSA-SHA256", message, publicKey, signature)) throw new Error("Bundle signing provider returned a signature that does not match the configured public key.");
24633
+ return Buffer.from(signature).toString("base64");
24634
+ })().catch((error) => {
24635
+ signatures.delete(normalizedFileHash);
24636
+ throw error;
24637
+ });
24638
+ signatures.set(normalizedFileHash, pending);
24639
+ return pending;
24640
+ };
24641
+ };
24642
+ const preparePluginSigning = async (signing, cwd) => {
24643
+ const providerPublicKey = await getProviderPublicKey(signing, cwd);
24644
+ return {
24645
+ name: signing.name,
24646
+ publicKey: exportPublicKey(providerPublicKey),
24647
+ signFileHash: createMemoizedSigner({
24648
+ publicKey: providerPublicKey,
24649
+ sign: async (message) => {
24650
+ return (await signing.sign({
24651
+ cwd,
24652
+ message
24653
+ })).signature;
24654
+ }
24655
+ })
24656
+ };
24657
+ };
24658
+ const prepareBundleSigning = async (signing, options = {}) => {
24659
+ const normalized = normalizeSigningConfig(signing);
24660
+ if (!normalized) return null;
24661
+ const cwd = options.cwd ?? getCwd();
24662
+ return preparePluginSigning("enabled" in normalized ? createLocalSigningPlugin(normalized) : normalized, cwd);
24663
+ };
24664
+ /** Resolves the public identity of the configured bundle signer. */
24665
+ const getBundleSigningPublicKey = async (signing, options = {}) => {
24666
+ const normalized = normalizeSigningConfig(signing);
24667
+ if (!normalized) return null;
24668
+ const cwd = options.cwd ?? getCwd();
24669
+ return exportPublicKey(await getProviderPublicKey("enabled" in normalized ? createLocalSigningPlugin(normalized) : normalized, cwd));
24670
+ };
24671
+ //#endregion
24510
24672
  //#region src/ConfigBuilder.ts
24511
24673
  const normalizeImportInfos = (imports) => {
24512
24674
  const collectedImports = /* @__PURE__ */ new Map();
@@ -42992,8 +43154,10 @@ const CONFIG_FILE_NAME = "hot-updater.config.ts";
42992
43154
  const MANAGED_IMPORT_PACKAGES = new Set([
42993
43155
  "dotenv",
42994
43156
  "firebase-admin",
43157
+ "firebase-admin/app",
42995
43158
  "hot-updater",
42996
43159
  "@aws-sdk/credential-provider-sso",
43160
+ "@aws-sdk/credential-providers",
42997
43161
  "@hot-updater/aws",
42998
43162
  "@hot-updater/bare",
42999
43163
  "@hot-updater/cloudflare",
@@ -43002,7 +43166,12 @@ const MANAGED_IMPORT_PACKAGES = new Set([
43002
43166
  "@hot-updater/rock",
43003
43167
  "@hot-updater/supabase"
43004
43168
  ]);
43005
- const MANAGED_HELPER_NAMES = new Set(["commonOptions", "credential"]);
43169
+ const MANAGED_HELPER_NAMES = new Set([
43170
+ "awsOptions",
43171
+ "commonOptions",
43172
+ "credential",
43173
+ "storageOptions"
43174
+ ]);
43006
43175
  const KNOWN_BUILD_CALLEES = new Set([
43007
43176
  "bare",
43008
43177
  "expo",
@@ -43096,7 +43265,7 @@ const appendMissingProperties = (objectText, propertyTexts, hasExistingPropertie
43096
43265
  const suffix = `,\n${closingIndent}`;
43097
43266
  return `${objectText.slice(0, closeBraceIndex)}${prefix}${formattedProperties}${suffix}${objectText.slice(closeBraceIndex)}`;
43098
43267
  };
43099
- const mergeObjectLiteralText = (existingObject, newObject) => {
43268
+ const mergeObjectLiteralText = (existingObject, newObject, replaceIncompatibleProperties = []) => {
43100
43269
  const existingText = getNodeText(existingObject.source, existingObject.objectExpression);
43101
43270
  const existingPropertyNames = /* @__PURE__ */ new Set();
43102
43271
  const existingSpreadTexts = /* @__PURE__ */ new Set();
@@ -43111,6 +43280,17 @@ const mergeObjectLiteralText = (existingObject, newObject) => {
43111
43280
  existingPropertyNames.add(propertyName);
43112
43281
  const nextProperty = newObject.objectExpression.properties.find((candidate) => getObjectPropertyName(candidate) === propertyName);
43113
43282
  if (!nextProperty || !isDataProperty(property) || !isDataProperty(nextProperty)) continue;
43283
+ const existingCallee = getCallCallee(property.value);
43284
+ const nextCallee = getCallCallee(nextProperty.value);
43285
+ const hasIncompatibleValue = existingCallee !== null && nextCallee !== null && existingCallee !== nextCallee || property.value.type === "ObjectExpression" !== (nextProperty.value.type === "ObjectExpression");
43286
+ if (replaceIncompatibleProperties.includes(propertyName) && hasIncompatibleValue) {
43287
+ edits.push({
43288
+ start: property.value.start - existingObject.objectExpression.start,
43289
+ end: property.value.end - existingObject.objectExpression.start,
43290
+ text: getNodeText(newObject.source, nextProperty.value)
43291
+ });
43292
+ continue;
43293
+ }
43114
43294
  if (property.value.type === "ObjectExpression" && nextProperty.value.type === "ObjectExpression") {
43115
43295
  const mergedValue = mergeObjectLiteralText({
43116
43296
  objectExpression: property.value,
@@ -43118,7 +43298,7 @@ const mergeObjectLiteralText = (existingObject, newObject) => {
43118
43298
  }, {
43119
43299
  objectExpression: nextProperty.value,
43120
43300
  source: newObject.source
43121
- });
43301
+ }, replaceIncompatibleProperties);
43122
43302
  if (!mergedValue) return null;
43123
43303
  edits.push({
43124
43304
  start: property.value.start - existingObject.objectExpression.start,
@@ -43175,7 +43355,7 @@ const mergeHelperStatement = (existingStatementText, helper) => {
43175
43355
  }, {
43176
43356
  objectExpression: nextInitializer,
43177
43357
  source: nextStatement.source
43178
- });
43358
+ }, helper.replaceIncompatibleProperties);
43179
43359
  if (!mergedInitializer) return null;
43180
43360
  return `${existingStatement.statement.kind === "let" || existingStatement.statement.kind === "var" ? existingStatement.statement.kind : "const"} ${helper.name} = ${mergedInitializer};`;
43181
43361
  };
@@ -43230,7 +43410,7 @@ const getManagedHelperName = (statement) => {
43230
43410
  if (statement.type !== "VariableDeclaration") return null;
43231
43411
  const declaration = statement.declarations[0];
43232
43412
  if (declaration?.id.type !== "Identifier") return null;
43233
- return MANAGED_HELPER_NAMES.has(declaration.id.name) ? declaration.id.name : null;
43413
+ return declaration.id.name;
43234
43414
  };
43235
43415
  const rebuildImportBlock = (source, scaffold) => {
43236
43416
  const importDeclarations = source.program.body.filter((statement) => statement.type === "ImportDeclaration");
@@ -43247,7 +43427,7 @@ const rebuildImportBlock = (source, scaffold) => {
43247
43427
  return {
43248
43428
  start: getTopLevelFullStart(source, firstImport),
43249
43429
  end: lastImport.end,
43250
- text: `${nextImportBlock}\n\n`
43430
+ text: nextImportBlock
43251
43431
  };
43252
43432
  };
43253
43433
  const rebuildManagedBody = (source, exportStart, scaffold) => {
@@ -43255,10 +43435,14 @@ const rebuildManagedBody = (source, exportStart, scaffold) => {
43255
43435
  const managedHelpers = new Map(scaffold.helperStatements.map((statement) => [statement.name, statement]));
43256
43436
  const emittedHelpers = /* @__PURE__ */ new Set();
43257
43437
  const bodyStatements = [];
43438
+ const configStatements = [];
43258
43439
  for (const statement of statementsBeforeExport) {
43259
- if (isConfigCallStatement(statement)) continue;
43440
+ if (isConfigCallStatement(statement)) {
43441
+ configStatements.push(getStatementText(source, statement));
43442
+ continue;
43443
+ }
43260
43444
  const helperName = getManagedHelperName(statement);
43261
- if (!helperName) {
43445
+ if (!helperName || !MANAGED_HELPER_NAMES.has(helperName)) {
43262
43446
  bodyStatements.push(getStatementText(source, statement));
43263
43447
  continue;
43264
43448
  }
@@ -43271,7 +43455,7 @@ const rebuildManagedBody = (source, exportStart, scaffold) => {
43271
43455
  }
43272
43456
  for (const helper of scaffold.helperStatements) if (!emittedHelpers.has(helper.name)) bodyStatements.push(helper.code.trim());
43273
43457
  const bodyText = bodyStatements.filter(Boolean).join("\n\n");
43274
- const configStatement = `config({ path: ".env.hotupdater" });`;
43458
+ const configStatement = configStatements.join("\n\n") || `config({ path: ".env.hotupdater" });`;
43275
43459
  const managedBody = bodyText ? `\n\n${configStatement}\n\n${bodyText}\n\n` : `\n\n${configStatement}\n\n`;
43276
43460
  return {
43277
43461
  start: source.program.body.filter((statement) => statement.type === "ImportDeclaration").at(-1)?.end ?? 0,
@@ -43299,7 +43483,9 @@ const mergeHotUpdaterConfigText = (existingText, scaffold) => {
43299
43483
  source: nextSource
43300
43484
  });
43301
43485
  if (!nextObjectText) return { reason: "Existing config uses dynamic build/storage/database expressions that cannot be merged safely." };
43302
- const bodyEdit = rebuildManagedBody(existingSource, getTopLevelFullStart(existingSource, existingConfig.exportDeclaration), scaffold);
43486
+ const exportFullStart = getTopLevelFullStart(existingSource, existingConfig.exportDeclaration);
43487
+ const firstTriviaContent = existingText.slice(exportFullStart, existingConfig.exportDeclaration.start).search(/\S/);
43488
+ const bodyEdit = rebuildManagedBody(existingSource, firstTriviaContent === -1 ? existingConfig.exportDeclaration.start : exportFullStart + firstTriviaContent, scaffold);
43303
43489
  if (!bodyEdit) return { reason: "Existing helper declarations could not be merged safely." };
43304
43490
  return { text: applyTextEdits(existingText, [
43305
43491
  {
@@ -43386,6 +43572,12 @@ var MissingInitInputsError = class extends InitError {
43386
43572
  var InitEnvFileError = class extends InitError {
43387
43573
  name = "InitEnvFileError";
43388
43574
  };
43575
+ var LegacyInfrastructureError = class extends InitError {
43576
+ name = "LegacyInfrastructureError";
43577
+ constructor(provider, resource) {
43578
+ super(`${provider} v0 infrastructure was detected at ${resource}. Hot Updater v1 cannot upgrade it in place. Run init with new provider resources and ship the new endpoint in a new native build. The existing infrastructure was not changed.`);
43579
+ }
43580
+ };
43389
43581
  const assertInitInputs = ({ inputs, strict }) => {
43390
43582
  if (!strict) return;
43391
43583
  const missingInputs = getMissingInitInputs(inputs);
@@ -43461,6 +43653,26 @@ const getHotUpdaterEnvValue = (env, key) => {
43461
43653
  return env[key]?.trim() || void 0;
43462
43654
  };
43463
43655
  //#endregion
43656
+ //#region src/infrastructureGeneration.ts
43657
+ const assertInfrastructureGenerationPayload = ({ payload, provider, resource }) => {
43658
+ if (typeof payload !== "object" || payload === null || !("infrastructureGeneration" in payload) || payload.infrastructureGeneration !== 1) throw new LegacyInfrastructureError(provider, resource);
43659
+ };
43660
+ const assertInfrastructureGenerationAtUrl = async ({ fetchImpl = fetch, legacyStatuses = [404], provider, resource, versionUrl }) => {
43661
+ let response;
43662
+ try {
43663
+ response = await fetchImpl(versionUrl);
43664
+ } catch (error) {
43665
+ throw new InitError(`Could not verify the ${provider} infrastructure generation at ${resource}: ${error instanceof Error ? error.message : String(error)}`);
43666
+ }
43667
+ if (legacyStatuses.includes(response.status)) throw new LegacyInfrastructureError(provider, resource);
43668
+ if (!response.ok) throw new InitError(`Could not verify the ${provider} infrastructure generation at ${resource}: HTTP ${response.status}`);
43669
+ assertInfrastructureGenerationPayload({
43670
+ payload: await response.json().catch(() => void 0),
43671
+ provider,
43672
+ resource
43673
+ });
43674
+ };
43675
+ //#endregion
43464
43676
  //#region src/initProvider.ts
43465
43677
  const defineInitProvider = (provider) => provider;
43466
43678
  const shouldAutoSelectOnlyInitResource = ({ availableResourceCount, savedIdentifier }) => savedIdentifier === void 0 && availableResourceCount === 1;
@@ -43635,6 +43847,91 @@ function isMergeableValue(value) {
43635
43847
  //#endregion
43636
43848
  //#region src/loadConfig.ts
43637
43849
  var import_out = /* @__PURE__ */ __toESM(require_out(), 1);
43850
+ const missingDatabase = createDatabasePlugin({
43851
+ name: "missingDatabase",
43852
+ models: {
43853
+ bundles: {
43854
+ findById: async () => {
43855
+ throw new Error("database plugin is required");
43856
+ },
43857
+ findMany: async () => {
43858
+ throw new Error("database plugin is required");
43859
+ },
43860
+ count: async () => {
43861
+ throw new Error("database plugin is required");
43862
+ }
43863
+ },
43864
+ bundlePatches: { findByBundleIds: async () => {
43865
+ throw new Error("database plugin is required");
43866
+ } },
43867
+ releases: {
43868
+ findById: async () => {
43869
+ throw new Error("database plugin is required");
43870
+ },
43871
+ findMany: async () => {
43872
+ throw new Error("database plugin is required");
43873
+ },
43874
+ findManyByScope: async () => {
43875
+ throw new Error("database plugin is required");
43876
+ }
43877
+ },
43878
+ releaseCatalogs: {
43879
+ findByScopeKey: async () => {
43880
+ throw new Error("database plugin is required");
43881
+ },
43882
+ findMany: async () => {
43883
+ throw new Error("database plugin is required");
43884
+ }
43885
+ },
43886
+ channels: {
43887
+ insert: async () => {
43888
+ throw new Error("database plugin is required");
43889
+ },
43890
+ list: async () => {
43891
+ throw new Error("database plugin is required");
43892
+ },
43893
+ delete: async () => {
43894
+ throw new Error("database plugin is required");
43895
+ }
43896
+ },
43897
+ insights: {
43898
+ append: async () => {
43899
+ throw new Error("database plugin is required");
43900
+ },
43901
+ scan: async () => {
43902
+ throw new Error("database plugin is required");
43903
+ }
43904
+ },
43905
+ apiKeys: {
43906
+ create: async () => {
43907
+ throw new Error("database plugin is required");
43908
+ },
43909
+ findByHash: async () => {
43910
+ throw new Error("database plugin is required");
43911
+ },
43912
+ list: async () => {
43913
+ throw new Error("database plugin is required");
43914
+ },
43915
+ revoke: async () => {
43916
+ throw new Error("database plugin is required");
43917
+ }
43918
+ }
43919
+ },
43920
+ commit: async () => {
43921
+ throw new Error("database plugin is required");
43922
+ }
43923
+ });
43924
+ const missingStorageError = async () => {
43925
+ throw new Error("storage plugin is required");
43926
+ };
43927
+ const missingStorage = createStoragePlugin({
43928
+ name: "missingStorage",
43929
+ protocol: "missing",
43930
+ put: missingStorageError,
43931
+ get: missingStorageError,
43932
+ exists: missingStorageError,
43933
+ delete: missingStorageError
43934
+ });
43638
43935
  const getDefaultPlatformConfig = () => {
43639
43936
  let infoPlistPaths = [];
43640
43937
  try {
@@ -43662,27 +43959,14 @@ const getDefaultPlatformConfig = () => {
43662
43959
  });
43663
43960
  if (manifestFiles.length > 0) androidManifestPaths = manifestFiles.map((file) => path.join("android", file));
43664
43961
  } catch {}
43665
- let stringResourcePaths = [];
43666
- try {
43667
- const stringsFiles = import_out.default.sync(path.join("**", "strings.xml"), {
43668
- cwd: path.join(getCwd(), "android"),
43669
- absolute: false,
43670
- onlyFiles: true
43671
- });
43672
- if (stringsFiles.length > 0) stringResourcePaths = stringsFiles.map((file) => path.join("android", file));
43673
- } catch {}
43674
43962
  return {
43675
- android: {
43676
- androidManifestPaths,
43677
- stringResourcePaths
43678
- },
43963
+ android: { androidManifestPaths },
43679
43964
  ios: { infoPlistPaths }
43680
43965
  };
43681
43966
  };
43682
43967
  const getDefaultConfig = () => {
43683
43968
  return {
43684
43969
  cacheDir: path.join("node_modules", ".hot-updater"),
43685
- releaseChannel: "production",
43686
43970
  updateStrategy: "appVersion",
43687
43971
  compressStrategy: "zip",
43688
43972
  fingerprint: {},
@@ -43699,16 +43983,21 @@ const getDefaultConfig = () => {
43699
43983
  build: () => {
43700
43984
  throw new Error("build plugin is required");
43701
43985
  },
43702
- storage: () => {
43703
- throw new Error("storage plugin is required");
43704
- },
43705
- database: () => {
43706
- throw new Error("database plugin is required");
43707
- }
43986
+ storage: missingStorage,
43987
+ database: missingDatabase
43708
43988
  };
43709
43989
  };
43710
43990
  const mergeConfigSources = (...sources) => {
43711
- return sources.reduceRight((mergedConfig, source) => merge(mergedConfig, source ?? {}), {});
43991
+ const mergedConfig = sources.reduceRight((mergedConfig, source) => merge(mergedConfig, source ?? {}), {});
43992
+ const database = sources.find((source) => source?.database)?.database;
43993
+ const signing = sources.find((source) => source?.signing)?.signing;
43994
+ const storage = sources.find((source) => source?.storage)?.storage;
43995
+ return {
43996
+ ...mergedConfig,
43997
+ ...database ? { database } : {},
43998
+ ...signing ? { signing } : {},
43999
+ ...storage ? { storage } : {}
44000
+ };
43712
44001
  };
43713
44002
  const getConfigLoaderOptions = (options) => {
43714
44003
  const cwd = getCwd();
@@ -43734,7 +44023,13 @@ const getConfigLoaderOptions = (options) => {
43734
44023
  };
43735
44024
  const loadConfig = async (options) => {
43736
44025
  const { config } = await loadConfig$1(getConfigLoaderOptions(options));
43737
- return mergeConfigSources(config, getDefaultConfig());
44026
+ for (const key of ["authorityId", "catalogId"]) if (config && Object.hasOwn(config, key)) throw new Error(`Remove ${key} from hot-updater.config. Catalog identity is managed internally.`);
44027
+ const mergedConfig = mergeConfigSources(config, getDefaultConfig());
44028
+ const signing = normalizeSigningConfig(mergedConfig.signing);
44029
+ return {
44030
+ ...mergedConfig,
44031
+ signing
44032
+ };
43738
44033
  };
43739
44034
  //#endregion
43740
44035
  //#region src/log.ts
@@ -43849,9 +44144,52 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
43849
44144
  }
43850
44145
  };
43851
44146
  //#endregion
44147
+ //#region src/storageFiles.ts
44148
+ const getStorageFileByteSize = async (filePath) => {
44149
+ const { size } = await fs$3.stat(filePath, { bigint: true });
44150
+ if (size < 0n || size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Storage file size must be a non-negative safe integer.");
44151
+ return Number(size);
44152
+ };
44153
+ const putStorageFile = async (storage, key, filePath) => {
44154
+ const byteSize = await getStorageFileByteSize(filePath);
44155
+ const source = createReadStream(filePath);
44156
+ try {
44157
+ return {
44158
+ ...await storage.put({
44159
+ key: path$1.posix.join(key, path$1.basename(filePath)),
44160
+ body: Readable$1.toWeb(source),
44161
+ contentLength: byteSize,
44162
+ contentType: getContentType(filePath)
44163
+ }),
44164
+ byteSize
44165
+ };
44166
+ } finally {
44167
+ source.destroy();
44168
+ }
44169
+ };
44170
+ const writeStorageFile = async (storage, storageUri, filePath) => {
44171
+ const { response } = await storage.get({ storageUri });
44172
+ if (response === null) throw new Error(`Storage object not found: ${storageUri}`);
44173
+ await writeStorageResponseFile(response, filePath);
44174
+ };
44175
+ const writeStorageResponseFile = async (response, filePath) => {
44176
+ await fs$3.mkdir(path$1.dirname(filePath), { recursive: true });
44177
+ if (response.body === null) {
44178
+ await fs$3.writeFile(filePath, new Uint8Array());
44179
+ return;
44180
+ }
44181
+ try {
44182
+ await pipeline$1(Readable$1.fromWeb(response.body), createWriteStream$1(filePath));
44183
+ } catch (error) {
44184
+ await fs$3.rm(filePath, { force: true });
44185
+ throw error;
44186
+ }
44187
+ };
44188
+ //#endregion
43852
44189
  //#region src/promoteBundle.ts
43853
44190
  const LEGACY_BUNDLE_ERROR = "This OTA bundle was created by a version that does not support manifest.json. Copy bundle is not available.";
43854
44191
  const SIGNED_HASH_PREFIX = "sig:";
44192
+ const PROMOTE_ASSET_CONCURRENCY = 8;
43855
44193
  function isSignedFileHash(fileHash) {
43856
44194
  return fileHash.startsWith(SIGNED_HASH_PREFIX);
43857
44195
  }
@@ -43859,16 +44197,27 @@ async function getFileHash(filepath) {
43859
44197
  const file = await fs$3.readFile(filepath);
43860
44198
  return crypto$1.createHash("sha256").update(file).digest("hex");
43861
44199
  }
43862
- async function signFileHash(fileHash, privateKeyPath) {
43863
- const privateKeyPEM = await fs$3.readFile(privateKeyPath, "utf8");
43864
- const sign = crypto$1.createSign("RSA-SHA256");
43865
- sign.update(Buffer.from(fileHash, "hex"));
43866
- sign.end();
43867
- return `${SIGNED_HASH_PREFIX}${sign.sign(privateKeyPEM).toString("base64")}`;
44200
+ async function runWithConcurrency(items, concurrency, task) {
44201
+ let nextIndex = 0;
44202
+ const workerCount = Math.min(concurrency, items.length);
44203
+ await Promise.all(Array.from({ length: workerCount }, async () => {
44204
+ while (nextIndex < items.length) {
44205
+ const itemIndex = nextIndex;
44206
+ nextIndex += 1;
44207
+ await task(items[itemIndex]);
44208
+ }
44209
+ }));
44210
+ }
44211
+ function verifySignedFileHash({ actualFileHash, publicKey, signedFileHash }) {
44212
+ try {
44213
+ return crypto$1.verify("RSA-SHA256", Buffer.from(actualFileHash, "hex"), publicKey, Buffer.from(signedFileHash.slice(4), "base64"));
44214
+ } catch {
44215
+ return false;
44216
+ }
43868
44217
  }
43869
44218
  function getArchiveFilename(storageUri) {
43870
- const { pathname } = new URL(storageUri);
43871
- return path$1.basename(pathname) || "bundle.zip";
44219
+ const { key } = parseStorageUri(storageUri, new URL(storageUri).protocol.replace(":", ""));
44220
+ return path$1.posix.basename(key) || "bundle.zip";
43872
44221
  }
43873
44222
  const getRelativeStorageDir = (relativePath) => {
43874
44223
  const normalized = relativePath.replace(/\\/g, "/");
@@ -43886,7 +44235,7 @@ async function prepareManifestAssetUploadFile({ assetPath, sourcePath, workDir }
43886
44235
  if (getManifestAssetDownloadPath(assetPath) === assetPath) return sourcePath;
43887
44236
  const uploadPath = resolvePreparedUploadPath(workDir, assetPath);
43888
44237
  await fs$3.mkdir(path$1.dirname(uploadPath), { recursive: true });
43889
- await pipeline$1(createReadStream(sourcePath), createBrotliCompress$1(), createWriteStream$1(uploadPath));
44238
+ await pipeline$1(createReadStream(sourcePath), createBrotliCompress$1({ params: { [constants$2.BROTLI_PARAM_QUALITY]: 11 } }), createWriteStream$1(uploadPath));
43890
44239
  return uploadPath;
43891
44240
  }
43892
44241
  async function prepareContentAddressedUploadFile({ sourcePath, storagePath, workDir }) {
@@ -43897,6 +44246,48 @@ async function prepareContentAddressedUploadFile({ sourcePath, storagePath, work
43897
44246
  await fs$3.copyFile(sourcePath, uploadPath);
43898
44247
  return uploadPath;
43899
44248
  }
44249
+ async function prepareManifestAssetUploadTargets({ extractDir, manifest, workDir }) {
44250
+ const targets = /* @__PURE__ */ new Map();
44251
+ const assetPaths = Object.keys(manifest.assets ?? {}).sort((left, right) => left.localeCompare(right));
44252
+ for (const assetPath of assetPaths) {
44253
+ const asset = manifest.assets?.[assetPath];
44254
+ if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
44255
+ const uploadSourcePath = await prepareManifestAssetUploadFile({
44256
+ assetPath,
44257
+ sourcePath: resolveExtractedPath(extractDir, assetPath),
44258
+ workDir
44259
+ });
44260
+ const downloadByteSize = await getStorageFileByteSize(uploadSourcePath);
44261
+ const downloadPath = getManifestAssetDownloadPath(assetPath);
44262
+ const downloadFileHash = downloadPath !== assetPath ? await getFileHash(uploadSourcePath) : void 0;
44263
+ if (downloadFileHash !== void 0 && !isContentAddressedAssetFileHash(downloadFileHash)) throw new Error(`Prepared asset hash must be a lowercase SHA-256 hash: ${assetPath}`);
44264
+ const nextAsset = {
44265
+ ...asset,
44266
+ downloadByteSize
44267
+ };
44268
+ delete nextAsset.downloadFileHash;
44269
+ if (downloadFileHash !== void 0) nextAsset.downloadFileHash = downloadFileHash;
44270
+ manifest.assets[assetPath] = nextAsset;
44271
+ const storagePath = getManifestAssetStoragePath({
44272
+ assetPath: downloadPath,
44273
+ downloadFileHash,
44274
+ fileHash: asset.fileHash
44275
+ });
44276
+ const contentAddressedUploadPath = await prepareContentAddressedUploadFile({
44277
+ sourcePath: uploadSourcePath,
44278
+ storagePath,
44279
+ workDir
44280
+ });
44281
+ targets.set(storagePath, {
44282
+ assetPath: downloadPath,
44283
+ downloadFileHash,
44284
+ fileHash: asset.fileHash,
44285
+ storagePath,
44286
+ uploadSourcePath: contentAddressedUploadPath
44287
+ });
44288
+ }
44289
+ return [...targets.values()];
44290
+ }
43900
44291
  function resolveExtractedPath(rootDir, entryName) {
43901
44292
  const normalizedEntryName = entryName.replaceAll("\\", "/");
43902
44293
  const entryPath = path$1.resolve(rootDir, normalizedEntryName);
@@ -43906,23 +44297,20 @@ function resolveExtractedPath(rootDir, entryName) {
43906
44297
  }
43907
44298
  async function downloadArchive(storageUri, storagePlugin, archivePath) {
43908
44299
  const protocol = new URL(storageUri).protocol.replace(":", "");
44300
+ if (storagePlugin?.protocol === protocol) {
44301
+ await writeStorageFile(storagePlugin, storageUri, archivePath);
44302
+ return;
44303
+ }
43909
44304
  if (protocol === "http" || protocol === "https") {
43910
- const archiveBuffer = await downloadFromUrl(storageUri);
43911
- await fs$3.writeFile(archivePath, archiveBuffer);
44305
+ await downloadFromUrl(storageUri, archivePath);
43912
44306
  return;
43913
44307
  }
43914
- await downloadFromStorage(storageUri, storagePlugin, archivePath);
44308
+ throw new Error(`No storage plugin for protocol: ${protocol}`);
43915
44309
  }
43916
- async function downloadFromUrl(fileUrl) {
44310
+ async function downloadFromUrl(fileUrl, filePath) {
43917
44311
  const response = await fetch(fileUrl);
43918
44312
  if (!response.ok) throw new Error(`Failed to download bundle archive: ${response.statusText}`);
43919
- return new Uint8Array(await response.arrayBuffer());
43920
- }
43921
- async function downloadFromStorage(storageUri, storagePlugin, filePath) {
43922
- if (!storagePlugin) throw new Error("Storage plugin is not configured");
43923
- const protocol = new URL(storageUri).protocol.replace(":", "");
43924
- if (storagePlugin.supportedProtocol !== protocol) throw new Error(`No storage plugin for protocol: ${protocol}`);
43925
- await storagePlugin.profiles.node.downloadFile(storageUri, filePath);
44313
+ await writeStorageResponseFile(response, filePath);
43926
44314
  }
43927
44315
  async function extractZipArchive(archivePath, extractDir) {
43928
44316
  const zip = await import_lib.default.loadAsync(await fs$3.readFile(archivePath));
@@ -44002,7 +44390,7 @@ async function createArchiveFromDirectory(sourceDir, archivePath, format) {
44002
44390
  return;
44003
44391
  }
44004
44392
  }
44005
- async function rewriteManifestBundleId(extractDir, nextBundleId) {
44393
+ async function readCopiedBundleManifest(extractDir, nextBundleId) {
44006
44394
  const manifestPath = path$1.join(extractDir, "manifest.json");
44007
44395
  try {
44008
44396
  await fs$3.access(manifestPath);
@@ -44011,13 +44399,12 @@ async function rewriteManifestBundleId(extractDir, nextBundleId) {
44011
44399
  }
44012
44400
  const manifest = JSON.parse(await fs$3.readFile(manifestPath, "utf8"));
44013
44401
  manifest.bundleId = nextBundleId;
44014
- await fs$3.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
44015
44402
  return {
44016
44403
  manifest,
44017
44404
  manifestPath
44018
44405
  };
44019
44406
  }
44020
- async function createCopiedBundleArchive({ bundle, config, nextBundleId, storagePlugin, targetChannel }) {
44407
+ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storagePlugin }) {
44021
44408
  const archiveFilename = getArchiveFilename(bundle.storageUri);
44022
44409
  const workDir = await fs$3.mkdtemp(path$1.join(os.tmpdir(), "hot-updater-console-promote-"));
44023
44410
  const sourceArchivePath = path$1.join(workDir, archiveFilename);
@@ -44027,65 +44414,73 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
44027
44414
  await fs$3.mkdir(extractDir, { recursive: true });
44028
44415
  try {
44029
44416
  await downloadArchive(bundle.storageUri, storagePlugin, sourceArchivePath);
44417
+ const actualSourceFileHash = await getFileHash(sourceArchivePath);
44418
+ const signingSession = await prepareBundleSigning(config.signing);
44419
+ if (isSignedFileHash(bundle.fileHash)) {
44420
+ if (!signingSession) throw new Error("Cannot copy a signed bundle without enabled bundle signing configuration.");
44421
+ if (!verifySignedFileHash({
44422
+ actualFileHash: actualSourceFileHash,
44423
+ publicKey: signingSession.publicKey,
44424
+ signedFileHash: bundle.fileHash
44425
+ })) throw new Error("Source bundle signature verification failed.");
44426
+ } else if (actualSourceFileHash !== bundle.fileHash.toLowerCase()) throw new Error("Source bundle file hash verification failed.");
44030
44427
  const format = await extractArchive(sourceArchivePath, extractDir);
44031
- const { manifest, manifestPath } = await rewriteManifestBundleId(extractDir, nextBundleId);
44428
+ const { manifest, manifestPath } = await readCopiedBundleManifest(extractDir, nextBundleId);
44429
+ const assetPaths = Object.keys(manifest.assets ?? {}).sort((left, right) => left.localeCompare(right));
44430
+ const sourceIsSigned = [bundle.fileHash, getManifestFileHash(bundle)].filter((hash) => Boolean(hash)).some((hash) => isSignedFileHash(hash));
44431
+ const manifestHasSignatures = assetPaths.some((assetPath) => Boolean(manifest.assets?.[assetPath]?.signature));
44432
+ if (!signingSession && (sourceIsSigned || manifestHasSignatures)) throw new Error("Cannot copy a signed bundle without enabled bundle signing configuration.");
44433
+ if (signingSession) {
44434
+ await runWithConcurrency(assetPaths, PROMOTE_ASSET_CONCURRENCY, async (assetPath) => {
44435
+ const asset = manifest.assets?.[assetPath];
44436
+ if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
44437
+ if (await getFileHash(resolveExtractedPath(extractDir, assetPath)) !== asset.fileHash.toLowerCase()) throw new Error(`Manifest file hash mismatch for ${assetPath}`);
44438
+ });
44439
+ await runWithConcurrency(assetPaths, PROMOTE_ASSET_CONCURRENCY, async (assetPath) => {
44440
+ const asset = manifest.assets?.[assetPath];
44441
+ if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
44442
+ asset.signature = await signingSession.signFileHash(asset.fileHash);
44443
+ });
44444
+ }
44445
+ const assetUploadTargets = await prepareManifestAssetUploadTargets({
44446
+ extractDir,
44447
+ manifest,
44448
+ workDir
44449
+ });
44450
+ await fs$3.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
44032
44451
  await fs$3.rm(sourceArchivePath, { force: true });
44033
44452
  await createArchiveFromDirectory(extractDir, outputArchivePath, format);
44034
44453
  const fileHash = await getFileHash(outputArchivePath);
44035
44454
  const manifestHash = await getFileHash(manifestPath);
44036
- if ([bundle.fileHash, getManifestFileHash(bundle)].filter((hash) => Boolean(hash)).some((hash) => isSignedFileHash(hash)) && !config.signing?.privateKeyPath) throw new Error("Cannot copy a signed bundle without signing.privateKeyPath in hot-updater.config.ts");
44037
- const signingKeyPath = config.signing?.enabled && config.signing.privateKeyPath ? config.signing.privateKeyPath : null;
44038
- const nextFileHash = signingKeyPath ? await signFileHash(fileHash, signingKeyPath) : fileHash;
44039
- const nextManifestFileHash = signingKeyPath ? await signFileHash(manifestHash, signingKeyPath) : manifestHash;
44040
- const archiveUpload = await storagePlugin.profiles.node.upload(createBundleStorageKey(nextBundleId), outputArchivePath);
44455
+ const nextFileHash = signingSession ? `${SIGNED_HASH_PREFIX}${await signingSession.signFileHash(fileHash)}` : fileHash;
44456
+ const nextManifestFileHash = signingSession ? `${SIGNED_HASH_PREFIX}${await signingSession.signFileHash(manifestHash)}` : manifestHash;
44457
+ const archiveUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), outputArchivePath);
44041
44458
  uploadedStorageUris.push(archiveUpload.storageUri);
44042
- const manifestUpload = await storagePlugin.profiles.node.upload(createBundleStorageKey(nextBundleId), manifestPath);
44043
- uploadedStorageUris.push(manifestUpload.storageUri);
44044
- const assetBaseStorageUri = createStorageRootUriWithPath(manifestUpload.storageUri, nextBundleId, "assets");
44045
- const assetPaths = Object.keys(manifest.assets ?? {}).sort((left, right) => left.localeCompare(right));
44046
- for (const assetPath of assetPaths) {
44047
- const asset = manifest.assets?.[assetPath];
44048
- if (!asset?.fileHash) throw new Error(`Manifest file hash not found for ${assetPath}`);
44049
- const uploadPath = await prepareManifestAssetUploadFile({
44050
- assetPath,
44051
- sourcePath: path$1.join(extractDir, assetPath),
44052
- workDir
44053
- });
44054
- const uploadName = getManifestAssetDownloadPath(assetPath);
44055
- const storagePath = getContentAddressedAssetStoragePath({
44056
- assetPath: uploadName,
44057
- fileHash: asset.fileHash
44058
- });
44459
+ const assetBaseStorageUri = createStorageRootUriWithPath(archiveUpload.storageUri, nextBundleId, "assets");
44460
+ for (const assetUploadTarget of assetUploadTargets) {
44059
44461
  const storageUri = resolveManifestAssetStorageUri({
44060
44462
  assetBaseStorageUri,
44061
- assetPath: uploadName,
44062
- fileHash: asset.fileHash
44463
+ assetPath: assetUploadTarget.assetPath,
44464
+ downloadFileHash: assetUploadTarget.downloadFileHash,
44465
+ fileHash: assetUploadTarget.fileHash
44063
44466
  });
44064
- if (!await storagePlugin.profiles.node.exists(storageUri)) {
44065
- const contentAddressedUploadPath = await prepareContentAddressedUploadFile({
44066
- sourcePath: uploadPath,
44067
- storagePath,
44068
- workDir
44069
- });
44070
- await storagePlugin.profiles.node.upload(getRelativeStorageDir(storagePath) ? `assets/${getRelativeStorageDir(storagePath)}` : "assets", contentAddressedUploadPath);
44071
- }
44467
+ const { exists } = await storagePlugin.exists({ storageUri });
44468
+ if (!exists) await putStorageFile(storagePlugin, getRelativeStorageDir(assetUploadTarget.storagePath) ? `assets/${getRelativeStorageDir(assetUploadTarget.storagePath)}` : "assets", assetUploadTarget.uploadSourcePath);
44072
44469
  }
44470
+ const manifestUpload = await putStorageFile(storagePlugin, createBundleStorageKey(nextBundleId), manifestPath);
44471
+ uploadedStorageUris.push(manifestUpload.storageUri);
44073
44472
  return {
44074
44473
  bundle: {
44075
44474
  ...bundle,
44076
44475
  id: nextBundleId,
44077
- channel: targetChannel,
44476
+ archiveByteSize: archiveUpload.byteSize,
44078
44477
  storageUri: archiveUpload.storageUri,
44079
44478
  fileHash: nextFileHash,
44080
44479
  metadata: stripBundleArtifactMetadata(bundle.metadata),
44081
44480
  assetBaseStorageUri,
44082
44481
  patches: [],
44083
- patchBaseBundleId: null,
44084
44482
  manifestFileHash: nextManifestFileHash,
44085
- manifestStorageUri: manifestUpload.storageUri,
44086
- patchBaseFileHash: null,
44087
- patchFileHash: null,
44088
- patchStorageUri: null
44483
+ manifestStorageUri: manifestUpload.storageUri
44089
44484
  },
44090
44485
  uploadedStorageUris
44091
44486
  };
@@ -44102,44 +44497,13 @@ async function createCopiedBundleArchive({ bundle, config, nextBundleId, storage
44102
44497
  async function deleteUploadedCopy(storagePlugin, storageUris) {
44103
44498
  if (storageUris.length === 0) return;
44104
44499
  for (const storageUri of new Set(storageUris)) try {
44105
- await storagePlugin.profiles.node.delete(storageUri);
44500
+ const protocol = new URL(storageUri).protocol.replace(":", "");
44501
+ if (storagePlugin.protocol === protocol) await storagePlugin.delete({ storageUri });
44502
+ else if (protocol !== "http" && protocol !== "https") throw new Error(`No storage plugin for protocol: ${protocol}`);
44106
44503
  } catch (error) {
44107
44504
  console.error("Failed to delete uploaded bundle copy:", error);
44108
44505
  }
44109
44506
  }
44110
- async function promoteBundle({ action, bundleId, nextBundleId, targetChannel }, deps) {
44111
- const normalizedTargetChannel = targetChannel.trim();
44112
- if (!normalizedTargetChannel) throw new Error("Target channel is required");
44113
- const bundle = await deps.databasePlugin.getBundleById(bundleId);
44114
- if (!bundle) throw new Error("Bundle not found");
44115
- if (bundle.channel === normalizedTargetChannel) throw new Error("Target channel must be different from the current channel");
44116
- if (action === "move") {
44117
- await deps.databasePlugin.updateBundle(bundleId, { channel: normalizedTargetChannel });
44118
- await deps.databasePlugin.commitBundle();
44119
- const updatedBundle = await deps.databasePlugin.getBundleById(bundleId);
44120
- if (!updatedBundle) throw new Error("Promoted bundle not found");
44121
- return updatedBundle;
44122
- }
44123
- if (!deps.storagePlugin) throw new Error("Storage plugin is not configured");
44124
- const resolvedNextBundleId = nextBundleId?.trim() || createUUIDv7();
44125
- const { bundle: copiedBundle, uploadedStorageUris } = await createCopiedBundleArchive({
44126
- bundle,
44127
- config: deps.config,
44128
- nextBundleId: resolvedNextBundleId,
44129
- storagePlugin: deps.storagePlugin,
44130
- targetChannel: normalizedTargetChannel
44131
- });
44132
- let shouldCleanupUploadedCopy = true;
44133
- try {
44134
- await deps.databasePlugin.appendBundle(copiedBundle);
44135
- await deps.databasePlugin.commitBundle();
44136
- shouldCleanupUploadedCopy = false;
44137
- return copiedBundle;
44138
- } catch (error) {
44139
- if (shouldCleanupUploadedCopy) await deleteUploadedCopy(deps.storagePlugin, uploadedStorageUris);
44140
- throw error;
44141
- }
44142
- }
44143
44507
  //#endregion
44144
44508
  //#region src/resolvePackageVersion.ts
44145
44509
  const require$1 = createRequire(import.meta.url);
@@ -44206,4 +44570,4 @@ function transformTemplate(templateString, values) {
44206
44570
  return result;
44207
44571
  }
44208
44572
  //#endregion
44209
- export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, InitEnvFileError, InitError, LEGACY_BUNDLE_ERROR, MissingInitInputsError, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, getAndroidSdkPath, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, link, loadConfig, log, makeEnv, p, printBanner, promoteBundle, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig };
44573
+ export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, InitEnvFileError, InitError, LEGACY_BUNDLE_ERROR, LegacyInfrastructureError, MissingInitInputsError, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, banner, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCopiedBundleArchive, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createLogWriter, createTarBr, createTarBrTargetFiles, createTarGz, createTarGzTargetFiles, createZip, createZipTargetFiles, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, formatApiKeyNote, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, link, loadConfig, log, makeEnv, p, prepareBundleSigning, printBanner, putStorageFile, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, renderImportStatements, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig, writeStorageFile, writeStorageResponseFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/cli-tools",
3
- "version": "0.36.7",
3
+ "version": "1.0.0-rc.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -49,8 +49,8 @@
49
49
  "oxc-parser": "0.141.0",
50
50
  "oxc-transform": "0.141.0",
51
51
  "unconfig": "7.5.0",
52
- "@hot-updater/core": "0.36.7",
53
- "@hot-updater/plugin-core": "0.36.7"
52
+ "@hot-updater/plugin-core": "1.0.0-rc.1",
53
+ "@hot-updater/core": "1.0.0-rc.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@clack/prompts": "1.7.0",
@@ -64,7 +64,7 @@
64
64
  "tar": "^7.5.16",
65
65
  "verkit": "0.3.2",
66
66
  "workspace-tools": "^0.41.7",
67
- "@hot-updater/test-utils": "0.36.7"
67
+ "@hot-updater/test-utils": "1.0.0-rc.1"
68
68
  },
69
69
  "inlinedDependencies": {
70
70
  "@babel/code-frame": "7.29.0",