hot-updater 0.22.2 → 0.23.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/config.cjs +8 -6
- package/dist/config.d.cts +16 -1
- package/dist/config.d.ts +16 -1
- package/dist/config.js +2 -2
- package/dist/index.cjs +858 -345
- package/dist/index.js +513 -1
- package/dist/{fingerprint-B-Krd3Gt.cjs → keyGeneration-BsF6FbpU.cjs} +216 -73
- package/dist/{fingerprint-CrCon-HQ.js → keyGeneration-D_2zTEmt.js} +207 -91
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as
|
|
2
|
+
import { _ as require_base64_js, a as createAndInjectFingerprintFiles, b as __require, c as generateFingerprints, d as getFingerprintDiff, f as showFingerprintDiff, g as require_plist, h as IosConfigParser, i as saveKeyPair, l as nativeFingerprint, m as require_out, n as getPublicKeyFromPrivate, p as isFingerprintEquals, r as loadPrivateKey, t as generateKeyPair, u as readLocalFingerprint, v as AndroidConfigParser, x as __toESM, y as __commonJS } from "./keyGeneration-D_2zTEmt.js";
|
|
3
3
|
import { EventEmitter, addAbortListener, on, once, setMaxListeners } from "node:events";
|
|
4
4
|
import childProcess, { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import path from "node:path";
|
|
@@ -29,6 +29,7 @@ import { createServer } from "http";
|
|
|
29
29
|
import { Http2ServerRequest } from "http2";
|
|
30
30
|
import app from "@hot-updater/console";
|
|
31
31
|
import net from "node:net";
|
|
32
|
+
import crypto$1 from "node:crypto";
|
|
32
33
|
import { HotUpdaterDB } from "@hot-updater/server";
|
|
33
34
|
import { kyselyAdapter } from "@hot-updater/server/adapters/kysely";
|
|
34
35
|
import { Kysely, MysqlDialect, PostgresDialect, SqliteDialect } from "kysely";
|
|
@@ -35436,6 +35437,92 @@ const appendOutputDirectoryIntoGitignore = ({ cwd } = {}) => {
|
|
|
35436
35437
|
});
|
|
35437
35438
|
};
|
|
35438
35439
|
|
|
35440
|
+
//#endregion
|
|
35441
|
+
//#region src/utils/signing/bundleSigning.ts
|
|
35442
|
+
/**
|
|
35443
|
+
* Sign bundle fileHash with private key.
|
|
35444
|
+
* @param fileHash SHA-256 hash of bundle (hex string)
|
|
35445
|
+
* @param privateKeyPath Path to private key file
|
|
35446
|
+
* @returns Base64-encoded RSA-SHA256 signature
|
|
35447
|
+
*/
|
|
35448
|
+
async function signBundle(fileHash, privateKeyPath) {
|
|
35449
|
+
const privateKeyPEM = await loadPrivateKey(privateKeyPath);
|
|
35450
|
+
const fileHashBuffer = Buffer.from(fileHash, "hex");
|
|
35451
|
+
const sign = crypto$1.createSign("RSA-SHA256");
|
|
35452
|
+
sign.update(fileHashBuffer);
|
|
35453
|
+
sign.end();
|
|
35454
|
+
return sign.sign(privateKeyPEM).toString("base64");
|
|
35455
|
+
}
|
|
35456
|
+
|
|
35457
|
+
//#endregion
|
|
35458
|
+
//#region src/utils/signing/validateSigningConfig.ts
|
|
35459
|
+
const ANDROID_KEY$1 = "hot_updater_public_key";
|
|
35460
|
+
const IOS_KEY$1 = "HOT_UPDATER_PUBLIC_KEY";
|
|
35461
|
+
/**
|
|
35462
|
+
* Validates signing configuration consistency between config file and native files.
|
|
35463
|
+
* Detects mismatches that would cause OTA updates to fail.
|
|
35464
|
+
*/
|
|
35465
|
+
async function validateSigningConfig(config) {
|
|
35466
|
+
const signingEnabled = config.signing?.enabled ?? false;
|
|
35467
|
+
const iosParser = new IosConfigParser(config.platform.ios.infoPlistPaths);
|
|
35468
|
+
const androidParser = new AndroidConfigParser(config.platform.android.stringResourcePaths);
|
|
35469
|
+
const [iosExists, androidExists] = await Promise.all([iosParser.exists(), androidParser.exists()]);
|
|
35470
|
+
const [iosResult, androidResult] = await Promise.all([iosExists ? iosParser.get(IOS_KEY$1) : Promise.resolve({
|
|
35471
|
+
value: null,
|
|
35472
|
+
paths: []
|
|
35473
|
+
}), androidExists ? androidParser.get(ANDROID_KEY$1) : Promise.resolve({
|
|
35474
|
+
value: null,
|
|
35475
|
+
paths: []
|
|
35476
|
+
})]);
|
|
35477
|
+
const issues = [];
|
|
35478
|
+
if (signingEnabled) {
|
|
35479
|
+
if (!iosResult.value && iosExists) issues.push({
|
|
35480
|
+
type: "error",
|
|
35481
|
+
platform: "ios",
|
|
35482
|
+
code: "MISSING_PUBLIC_KEY",
|
|
35483
|
+
message: "Signing is enabled but HOT_UPDATER_PUBLIC_KEY is missing from Info.plist",
|
|
35484
|
+
resolution: "Run `npx hot-updater keys export-public` to add the public key, then rebuild your iOS app."
|
|
35485
|
+
});
|
|
35486
|
+
if (!androidResult.value && androidExists) issues.push({
|
|
35487
|
+
type: "error",
|
|
35488
|
+
platform: "android",
|
|
35489
|
+
code: "MISSING_PUBLIC_KEY",
|
|
35490
|
+
message: "Signing is enabled but hot_updater_public_key is missing from strings.xml",
|
|
35491
|
+
resolution: "Run `npx hot-updater keys export-public` to add the public key, then rebuild your Android app."
|
|
35492
|
+
});
|
|
35493
|
+
} else {
|
|
35494
|
+
if (iosResult.value) issues.push({
|
|
35495
|
+
type: "warning",
|
|
35496
|
+
platform: "ios",
|
|
35497
|
+
code: "ORPHAN_PUBLIC_KEY",
|
|
35498
|
+
message: "Signing is disabled but HOT_UPDATER_PUBLIC_KEY exists in Info.plist. This will cause OTA updates to be rejected.",
|
|
35499
|
+
resolution: "Run `npx hot-updater keys remove` to remove public keys, or enable signing in hot-updater.config.ts"
|
|
35500
|
+
});
|
|
35501
|
+
if (androidResult.value) issues.push({
|
|
35502
|
+
type: "warning",
|
|
35503
|
+
platform: "android",
|
|
35504
|
+
code: "ORPHAN_PUBLIC_KEY",
|
|
35505
|
+
message: "Signing is disabled but hot_updater_public_key exists in strings.xml. This will cause OTA updates to be rejected.",
|
|
35506
|
+
resolution: "Run `npx hot-updater keys remove` to remove public keys, or enable signing in hot-updater.config.ts"
|
|
35507
|
+
});
|
|
35508
|
+
}
|
|
35509
|
+
return {
|
|
35510
|
+
isValid: issues.filter((i$1) => i$1.type === "error").length === 0,
|
|
35511
|
+
signingEnabled,
|
|
35512
|
+
nativePublicKeys: {
|
|
35513
|
+
ios: {
|
|
35514
|
+
exists: !!iosResult.value,
|
|
35515
|
+
paths: iosResult.paths
|
|
35516
|
+
},
|
|
35517
|
+
android: {
|
|
35518
|
+
exists: !!androidResult.value,
|
|
35519
|
+
paths: androidResult.paths
|
|
35520
|
+
}
|
|
35521
|
+
},
|
|
35522
|
+
issues
|
|
35523
|
+
};
|
|
35524
|
+
}
|
|
35525
|
+
|
|
35439
35526
|
//#endregion
|
|
35440
35527
|
//#region src/utils/version/getDefaultTargetAppVersion.ts
|
|
35441
35528
|
var import_valid$2 = /* @__PURE__ */ __toESM(require_valid$2(), 1);
|
|
@@ -35455,6 +35542,64 @@ const getDefaultTargetAppVersion = async (platform$2) => {
|
|
|
35455
35542
|
return version$1;
|
|
35456
35543
|
};
|
|
35457
35544
|
|
|
35545
|
+
//#endregion
|
|
35546
|
+
//#region src/signedHashUtils.ts
|
|
35547
|
+
/**
|
|
35548
|
+
* Utilities for handling signed file hashes in Hot Updater.
|
|
35549
|
+
*
|
|
35550
|
+
* The signed hash format uses a simple prefix to indicate signing:
|
|
35551
|
+
* - Signed: `sig:<base64_signature>`
|
|
35552
|
+
* - Unsigned: `<hex_hash>` (plain SHA256)
|
|
35553
|
+
*
|
|
35554
|
+
* Signature verification implicitly verifies hash integrity,
|
|
35555
|
+
* so we only need to store the signature for signed bundles.
|
|
35556
|
+
*
|
|
35557
|
+
* @module signedHashUtils
|
|
35558
|
+
*/
|
|
35559
|
+
/**
|
|
35560
|
+
* Prefix indicating a signed file hash.
|
|
35561
|
+
* @example "sig:MEUCIQDx..."
|
|
35562
|
+
*/
|
|
35563
|
+
const SIGNED_HASH_PREFIX = "sig:";
|
|
35564
|
+
/**
|
|
35565
|
+
* Custom error class for signed hash format errors.
|
|
35566
|
+
*/
|
|
35567
|
+
var SignedHashFormatError = class extends Error {
|
|
35568
|
+
/**
|
|
35569
|
+
* Creates a new SignedHashFormatError.
|
|
35570
|
+
*
|
|
35571
|
+
* @param message - Description of the format error
|
|
35572
|
+
* @param input - The malformed input string that caused the error
|
|
35573
|
+
*/
|
|
35574
|
+
constructor(message, input) {
|
|
35575
|
+
super(message);
|
|
35576
|
+
this.input = input;
|
|
35577
|
+
this.name = "SignedHashFormatError";
|
|
35578
|
+
}
|
|
35579
|
+
};
|
|
35580
|
+
/**
|
|
35581
|
+
* Creates a signed file hash from a signature.
|
|
35582
|
+
*
|
|
35583
|
+
* The format is: `sig:<base64_signature>`
|
|
35584
|
+
*
|
|
35585
|
+
* Note: The hash is not stored because signature verification
|
|
35586
|
+
* implicitly verifies the hash (the signature is computed over the hash).
|
|
35587
|
+
*
|
|
35588
|
+
* @param signature - The Base64-encoded RSA-SHA256 signature
|
|
35589
|
+
* @returns The signed file hash string
|
|
35590
|
+
* @throws {SignedHashFormatError} If the signature is empty
|
|
35591
|
+
*
|
|
35592
|
+
* @example
|
|
35593
|
+
* ```typescript
|
|
35594
|
+
* const signedHash = createSignedFileHash("MEUCIQDx...");
|
|
35595
|
+
* // Returns: "sig:MEUCIQDx..."
|
|
35596
|
+
* ```
|
|
35597
|
+
*/
|
|
35598
|
+
function createSignedFileHash(signature) {
|
|
35599
|
+
if (!signature || signature.trim().length === 0) throw new SignedHashFormatError("Invalid signature: signature cannot be empty", signature ?? "");
|
|
35600
|
+
return `${SIGNED_HASH_PREFIX}${signature}`;
|
|
35601
|
+
}
|
|
35602
|
+
|
|
35458
35603
|
//#endregion
|
|
35459
35604
|
//#region src/commands/deploy.ts
|
|
35460
35605
|
var import_valid$1 = /* @__PURE__ */ __toESM(require_valid$2(), 1);
|
|
@@ -35486,6 +35631,31 @@ const deploy = async (options) => {
|
|
|
35486
35631
|
console.error("No config found. Please run `hot-updater init` first.");
|
|
35487
35632
|
process.exit(1);
|
|
35488
35633
|
}
|
|
35634
|
+
const signingValidation = await validateSigningConfig(config);
|
|
35635
|
+
if (signingValidation.issues.length > 0) {
|
|
35636
|
+
const errors = signingValidation.issues.filter((i$1) => i$1.type === "error");
|
|
35637
|
+
const warnings = signingValidation.issues.filter((i$1) => i$1.type === "warning");
|
|
35638
|
+
if (errors.length > 0) {
|
|
35639
|
+
console.log("");
|
|
35640
|
+
p.log.error("Signing configuration error:");
|
|
35641
|
+
for (const issue of errors) {
|
|
35642
|
+
p.log.error(` ${issue.message}`);
|
|
35643
|
+
p.log.info(` Resolution: ${issue.resolution}`);
|
|
35644
|
+
}
|
|
35645
|
+
console.log("");
|
|
35646
|
+
p.log.error("Deployment blocked. Fix the signing configuration and try again.");
|
|
35647
|
+
process.exit(1);
|
|
35648
|
+
}
|
|
35649
|
+
if (warnings.length > 0) {
|
|
35650
|
+
console.log("");
|
|
35651
|
+
p.log.warn("Signing configuration warning:");
|
|
35652
|
+
for (const warning of warnings) {
|
|
35653
|
+
p.log.warn(` ${warning.message}`);
|
|
35654
|
+
p.log.info(` Resolution: ${warning.resolution}`);
|
|
35655
|
+
}
|
|
35656
|
+
console.log("");
|
|
35657
|
+
}
|
|
35658
|
+
}
|
|
35489
35659
|
const target = {
|
|
35490
35660
|
appVersion: null,
|
|
35491
35661
|
fingerprintHash: null
|
|
@@ -35589,6 +35759,20 @@ const deploy = async (options) => {
|
|
|
35589
35759
|
}
|
|
35590
35760
|
bundleId = taskRef.buildResult.bundleId;
|
|
35591
35761
|
fileHash = await getFileHashFromFile(bundlePath);
|
|
35762
|
+
if (config.signing?.enabled) {
|
|
35763
|
+
if (!config.signing.privateKeyPath) throw new Error("privateKeyPath is required when signing is enabled. Please provide a valid path to your RSA private key in hot-updater.config.ts");
|
|
35764
|
+
const s$1 = p.spinner();
|
|
35765
|
+
s$1.start("Signing bundle");
|
|
35766
|
+
try {
|
|
35767
|
+
fileHash = createSignedFileHash(await signBundle(fileHash, config.signing.privateKeyPath));
|
|
35768
|
+
s$1.stop("Bundle signed successfully");
|
|
35769
|
+
} catch (error) {
|
|
35770
|
+
s$1.stop("Failed to sign bundle", 1);
|
|
35771
|
+
p.log.error(`Signing error: ${error.message}`);
|
|
35772
|
+
p.log.error("Ensure private key path is correct and file has proper permissions");
|
|
35773
|
+
throw error;
|
|
35774
|
+
}
|
|
35775
|
+
}
|
|
35592
35776
|
p.log.success(`Bundle stored at ${colors.blueBright(path$1.relative(cwd, bundlePath))}`);
|
|
35593
35777
|
return `✅ Build Complete (${buildPlugin.name})`;
|
|
35594
35778
|
}
|
|
@@ -37266,6 +37450,323 @@ async function generateStandaloneSQL(options) {
|
|
|
37266
37450
|
}
|
|
37267
37451
|
}
|
|
37268
37452
|
|
|
37453
|
+
//#endregion
|
|
37454
|
+
//#region src/commands/keys.ts
|
|
37455
|
+
const ANDROID_KEY = "hot_updater_public_key";
|
|
37456
|
+
const IOS_KEY = "HOT_UPDATER_PUBLIC_KEY";
|
|
37457
|
+
/**
|
|
37458
|
+
* Generate RSA key pair for code signing.
|
|
37459
|
+
* Usage: npx hot-updater keys:generate [--output ./keys] [--key-size 4096]
|
|
37460
|
+
*/
|
|
37461
|
+
const keysGenerate = async (options = {}) => {
|
|
37462
|
+
const cwd = getCwd();
|
|
37463
|
+
const outputDir = options.output ? path.isAbsolute(options.output) ? options.output : path.join(cwd, options.output) : path.join(cwd, "keys");
|
|
37464
|
+
const keySize = options.keySize ?? 4096;
|
|
37465
|
+
p.log.info(`Generating ${keySize}-bit RSA key pair...`);
|
|
37466
|
+
const spinner = p.spinner();
|
|
37467
|
+
spinner.start("Generating keys");
|
|
37468
|
+
try {
|
|
37469
|
+
await saveKeyPair(await generateKeyPair(keySize), outputDir);
|
|
37470
|
+
spinner.stop("Keys generated successfully");
|
|
37471
|
+
p.log.success(`Private key: ${path.join(outputDir, "private-key.pem")}`);
|
|
37472
|
+
p.log.success(`Public key: ${path.join(outputDir, "public-key.pem")}`);
|
|
37473
|
+
const keysDir = path.basename(outputDir);
|
|
37474
|
+
if (appendToProjectRootGitignore({
|
|
37475
|
+
cwd,
|
|
37476
|
+
globLines: [`${keysDir}/`]
|
|
37477
|
+
})) p.log.success(`Added ${keysDir}/ to .gitignore`);
|
|
37478
|
+
console.log("");
|
|
37479
|
+
p.log.warn("⚠️ Keep private key secure!");
|
|
37480
|
+
p.log.warn(" - Use secure storage for CI/CD (AWS Secrets Manager, etc.)");
|
|
37481
|
+
console.log("");
|
|
37482
|
+
p.log.info("Next steps:");
|
|
37483
|
+
p.log.info("1. Add to hot-updater.config.ts:");
|
|
37484
|
+
p.log.info(" signing: { enabled: true, privateKeyPath: \"./keys/private-key.pem\" }");
|
|
37485
|
+
p.log.info("2. Run: npx hot-updater keys export-public");
|
|
37486
|
+
p.log.info("3. Embed public key in iOS Info.plist and Android strings.xml");
|
|
37487
|
+
p.log.info("4. Rebuild native app");
|
|
37488
|
+
} catch (error) {
|
|
37489
|
+
spinner.stop("Failed to generate keys", 1);
|
|
37490
|
+
p.log.error(error.message);
|
|
37491
|
+
process.exit(1);
|
|
37492
|
+
}
|
|
37493
|
+
};
|
|
37494
|
+
async function writePublicKeyToAndroid(publicKey, customPaths) {
|
|
37495
|
+
try {
|
|
37496
|
+
const androidParser = new AndroidConfigParser(customPaths);
|
|
37497
|
+
if (!await androidParser.exists()) return {
|
|
37498
|
+
platform: "android",
|
|
37499
|
+
paths: [],
|
|
37500
|
+
success: false,
|
|
37501
|
+
error: "No strings.xml files found"
|
|
37502
|
+
};
|
|
37503
|
+
return {
|
|
37504
|
+
platform: "android",
|
|
37505
|
+
paths: (await androidParser.set(ANDROID_KEY, publicKey)).paths,
|
|
37506
|
+
success: true
|
|
37507
|
+
};
|
|
37508
|
+
} catch (error) {
|
|
37509
|
+
return {
|
|
37510
|
+
platform: "android",
|
|
37511
|
+
paths: [],
|
|
37512
|
+
success: false,
|
|
37513
|
+
error: error.message
|
|
37514
|
+
};
|
|
37515
|
+
}
|
|
37516
|
+
}
|
|
37517
|
+
async function writePublicKeyToIos(publicKey, customPaths) {
|
|
37518
|
+
try {
|
|
37519
|
+
const iosParser = new IosConfigParser(customPaths);
|
|
37520
|
+
if (!await iosParser.exists()) return {
|
|
37521
|
+
platform: "ios",
|
|
37522
|
+
paths: [],
|
|
37523
|
+
success: false,
|
|
37524
|
+
error: "No Info.plist files found"
|
|
37525
|
+
};
|
|
37526
|
+
return {
|
|
37527
|
+
platform: "ios",
|
|
37528
|
+
paths: (await iosParser.set(IOS_KEY, publicKey)).paths,
|
|
37529
|
+
success: true
|
|
37530
|
+
};
|
|
37531
|
+
} catch (error) {
|
|
37532
|
+
return {
|
|
37533
|
+
platform: "ios",
|
|
37534
|
+
paths: [],
|
|
37535
|
+
success: false,
|
|
37536
|
+
error: error.message
|
|
37537
|
+
};
|
|
37538
|
+
}
|
|
37539
|
+
}
|
|
37540
|
+
function printPublicKeyInstructions(publicKeyPEM) {
|
|
37541
|
+
console.log("");
|
|
37542
|
+
console.log(colors.cyan("═══════════════════════════════════════════════════════"));
|
|
37543
|
+
console.log(colors.cyan("Public Key (embed in native configuration)"));
|
|
37544
|
+
console.log(colors.cyan("═══════════════════════════════════════════════════════"));
|
|
37545
|
+
console.log("");
|
|
37546
|
+
console.log(publicKeyPEM);
|
|
37547
|
+
console.log("");
|
|
37548
|
+
console.log(colors.yellow("iOS Configuration (Info.plist):"));
|
|
37549
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
37550
|
+
console.log("<key>HOT_UPDATER_PUBLIC_KEY</key>");
|
|
37551
|
+
console.log(`<string>${publicKeyPEM.trim().replace(/\n/g, "\\n")}</string>`);
|
|
37552
|
+
console.log("");
|
|
37553
|
+
console.log(colors.yellow("Android Configuration (res/values/strings.xml):"));
|
|
37554
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
37555
|
+
console.log("<string name=\"hot_updater_public_key\">");
|
|
37556
|
+
console.log(publicKeyPEM.trim());
|
|
37557
|
+
console.log("</string>");
|
|
37558
|
+
console.log("");
|
|
37559
|
+
console.log(colors.cyan("═══════════════════════════════════════════════════════"));
|
|
37560
|
+
}
|
|
37561
|
+
/**
|
|
37562
|
+
* Export public key for embedding in native configuration.
|
|
37563
|
+
* By default, writes the public key to iOS Info.plist and Android strings.xml.
|
|
37564
|
+
* Use --print-only to only display the key without modifying files.
|
|
37565
|
+
*
|
|
37566
|
+
* The private key path is read from hot-updater.config.ts (signing.privateKeyPath)
|
|
37567
|
+
* unless overridden with --input.
|
|
37568
|
+
*
|
|
37569
|
+
* Usage: npx hot-updater keys export-public [--input ./keys/private-key.pem] [--print-only] [--yes]
|
|
37570
|
+
*/
|
|
37571
|
+
const keysExportPublic = async (options = {}) => {
|
|
37572
|
+
const cwd = getCwd();
|
|
37573
|
+
const config = await loadConfig(null);
|
|
37574
|
+
const configPrivateKeyPath = config.signing?.privateKeyPath;
|
|
37575
|
+
let privateKeyPath;
|
|
37576
|
+
if (options.input) privateKeyPath = path.isAbsolute(options.input) ? options.input : path.join(cwd, options.input);
|
|
37577
|
+
else if (configPrivateKeyPath) privateKeyPath = path.isAbsolute(configPrivateKeyPath) ? configPrivateKeyPath : path.join(cwd, configPrivateKeyPath);
|
|
37578
|
+
else privateKeyPath = path.join(cwd, "keys", "private-key.pem");
|
|
37579
|
+
try {
|
|
37580
|
+
const publicKeyPEM = getPublicKeyFromPrivate(await loadPrivateKey(privateKeyPath));
|
|
37581
|
+
if (options.printOnly) {
|
|
37582
|
+
printPublicKeyInstructions(publicKeyPEM);
|
|
37583
|
+
return;
|
|
37584
|
+
}
|
|
37585
|
+
p.log.info("Preparing to write public key to native configuration files...");
|
|
37586
|
+
const androidParser = new AndroidConfigParser(config.platform.android.stringResourcePaths);
|
|
37587
|
+
const iosParser = new IosConfigParser(config.platform.ios.infoPlistPaths);
|
|
37588
|
+
const androidExists = await androidParser.exists();
|
|
37589
|
+
const iosExists = await iosParser.exists();
|
|
37590
|
+
if (!androidExists && !iosExists) {
|
|
37591
|
+
p.log.error("No native configuration files found.");
|
|
37592
|
+
p.log.info("Tip: Use --print-only to display the key for manual configuration.");
|
|
37593
|
+
process.exit(1);
|
|
37594
|
+
}
|
|
37595
|
+
console.log("");
|
|
37596
|
+
p.log.step("Files to be updated:");
|
|
37597
|
+
if (androidExists) {
|
|
37598
|
+
const androidPaths = config.platform.android.stringResourcePaths;
|
|
37599
|
+
if (androidPaths.length === 1) p.log.info(` Android: ${androidPaths[0]} (${ANDROID_KEY})`);
|
|
37600
|
+
else {
|
|
37601
|
+
p.log.info(` Android (${ANDROID_KEY}):`);
|
|
37602
|
+
for (const androidPath of androidPaths) p.log.info(` - ${androidPath}`);
|
|
37603
|
+
}
|
|
37604
|
+
}
|
|
37605
|
+
if (iosExists) {
|
|
37606
|
+
const iosPaths = config.platform.ios.infoPlistPaths;
|
|
37607
|
+
if (iosPaths.length === 1) p.log.info(` iOS: ${iosPaths[0]} (${IOS_KEY})`);
|
|
37608
|
+
else {
|
|
37609
|
+
p.log.info(` iOS (${IOS_KEY}):`);
|
|
37610
|
+
for (const iosPath of iosPaths) p.log.info(` - ${iosPath}`);
|
|
37611
|
+
}
|
|
37612
|
+
}
|
|
37613
|
+
console.log("");
|
|
37614
|
+
if (!options.yes) {
|
|
37615
|
+
const shouldContinue = await p.confirm({
|
|
37616
|
+
message: "Write public key to native files?",
|
|
37617
|
+
initialValue: true
|
|
37618
|
+
});
|
|
37619
|
+
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
|
37620
|
+
p.cancel("Operation cancelled");
|
|
37621
|
+
process.exit(0);
|
|
37622
|
+
}
|
|
37623
|
+
}
|
|
37624
|
+
const results = [];
|
|
37625
|
+
if (androidExists) results.push(await writePublicKeyToAndroid(publicKeyPEM.trim(), config.platform.android.stringResourcePaths));
|
|
37626
|
+
if (iosExists) results.push(await writePublicKeyToIos(publicKeyPEM.trim(), config.platform.ios.infoPlistPaths));
|
|
37627
|
+
console.log("");
|
|
37628
|
+
for (const result of results) if (result.success) p.log.success(`${result.platform}: Updated ${result.paths.join(", ")}`);
|
|
37629
|
+
else p.log.error(`${result.platform}: ${result.error}`);
|
|
37630
|
+
const successCount = results.filter((r) => r.success).length;
|
|
37631
|
+
console.log("");
|
|
37632
|
+
if (successCount === results.length) {
|
|
37633
|
+
p.log.success("Public key written to all native files!");
|
|
37634
|
+
p.log.info("Next step: Rebuild your native app to apply the changes.");
|
|
37635
|
+
} else if (successCount > 0) p.log.warn("Public key written to some files. Check errors above.");
|
|
37636
|
+
else {
|
|
37637
|
+
p.log.error("Failed to write public key to any native files.");
|
|
37638
|
+
process.exit(1);
|
|
37639
|
+
}
|
|
37640
|
+
} catch (error) {
|
|
37641
|
+
p.log.error(`Failed to export public key: ${error.message}`);
|
|
37642
|
+
process.exit(1);
|
|
37643
|
+
}
|
|
37644
|
+
};
|
|
37645
|
+
async function removePublicKeyFromAndroid(customPaths) {
|
|
37646
|
+
try {
|
|
37647
|
+
const androidParser = new AndroidConfigParser(customPaths);
|
|
37648
|
+
if (!await androidParser.exists()) return {
|
|
37649
|
+
platform: "android",
|
|
37650
|
+
paths: [],
|
|
37651
|
+
success: true,
|
|
37652
|
+
found: false
|
|
37653
|
+
};
|
|
37654
|
+
const existing = await androidParser.get(ANDROID_KEY);
|
|
37655
|
+
if (!existing.value) return {
|
|
37656
|
+
platform: "android",
|
|
37657
|
+
paths: existing.paths,
|
|
37658
|
+
success: true,
|
|
37659
|
+
found: false
|
|
37660
|
+
};
|
|
37661
|
+
return {
|
|
37662
|
+
platform: "android",
|
|
37663
|
+
paths: (await androidParser.remove(ANDROID_KEY)).paths,
|
|
37664
|
+
success: true,
|
|
37665
|
+
found: true
|
|
37666
|
+
};
|
|
37667
|
+
} catch (error) {
|
|
37668
|
+
return {
|
|
37669
|
+
platform: "android",
|
|
37670
|
+
paths: [],
|
|
37671
|
+
success: false,
|
|
37672
|
+
found: true,
|
|
37673
|
+
error: error.message
|
|
37674
|
+
};
|
|
37675
|
+
}
|
|
37676
|
+
}
|
|
37677
|
+
async function removePublicKeyFromIos(customPaths) {
|
|
37678
|
+
try {
|
|
37679
|
+
const iosParser = new IosConfigParser(customPaths);
|
|
37680
|
+
if (!await iosParser.exists()) return {
|
|
37681
|
+
platform: "ios",
|
|
37682
|
+
paths: [],
|
|
37683
|
+
success: true,
|
|
37684
|
+
found: false
|
|
37685
|
+
};
|
|
37686
|
+
const existing = await iosParser.get(IOS_KEY);
|
|
37687
|
+
if (!existing.value) return {
|
|
37688
|
+
platform: "ios",
|
|
37689
|
+
paths: existing.paths,
|
|
37690
|
+
success: true,
|
|
37691
|
+
found: false
|
|
37692
|
+
};
|
|
37693
|
+
return {
|
|
37694
|
+
platform: "ios",
|
|
37695
|
+
paths: (await iosParser.remove(IOS_KEY)).paths,
|
|
37696
|
+
success: true,
|
|
37697
|
+
found: true
|
|
37698
|
+
};
|
|
37699
|
+
} catch (error) {
|
|
37700
|
+
return {
|
|
37701
|
+
platform: "ios",
|
|
37702
|
+
paths: [],
|
|
37703
|
+
success: false,
|
|
37704
|
+
found: true,
|
|
37705
|
+
error: error.message
|
|
37706
|
+
};
|
|
37707
|
+
}
|
|
37708
|
+
}
|
|
37709
|
+
/**
|
|
37710
|
+
* Remove public keys from native configuration files.
|
|
37711
|
+
* Automatically detects and removes keys from both iOS and Android.
|
|
37712
|
+
*
|
|
37713
|
+
* Usage: npx hot-updater keys remove [--yes]
|
|
37714
|
+
*/
|
|
37715
|
+
const keysRemove = async (options = {}) => {
|
|
37716
|
+
const config = await loadConfig(null);
|
|
37717
|
+
const androidParser = new AndroidConfigParser(config.platform.android.stringResourcePaths);
|
|
37718
|
+
const iosParser = new IosConfigParser(config.platform.ios.infoPlistPaths);
|
|
37719
|
+
const [androidExists, iosExists] = await Promise.all([androidParser.exists(), iosParser.exists()]);
|
|
37720
|
+
if (!androidExists && !iosExists) {
|
|
37721
|
+
p.log.info("No native configuration files found.");
|
|
37722
|
+
return;
|
|
37723
|
+
}
|
|
37724
|
+
const [androidKey, iosKey] = await Promise.all([androidExists ? androidParser.get(ANDROID_KEY) : Promise.resolve({
|
|
37725
|
+
value: null,
|
|
37726
|
+
paths: []
|
|
37727
|
+
}), iosExists ? iosParser.get(IOS_KEY) : Promise.resolve({
|
|
37728
|
+
value: null,
|
|
37729
|
+
paths: []
|
|
37730
|
+
})]);
|
|
37731
|
+
const foundKeys = [];
|
|
37732
|
+
if (iosKey.value) foundKeys.push(`iOS: ${iosKey.paths.join(", ")}`);
|
|
37733
|
+
if (androidKey.value) foundKeys.push(`Android: ${androidKey.paths.join(", ")}`);
|
|
37734
|
+
if (foundKeys.length === 0) {
|
|
37735
|
+
p.log.info("No public keys found in native files.");
|
|
37736
|
+
return;
|
|
37737
|
+
}
|
|
37738
|
+
console.log("");
|
|
37739
|
+
p.log.step("Found public keys in:");
|
|
37740
|
+
for (const key of foundKeys) p.log.info(` • ${key}`);
|
|
37741
|
+
console.log("");
|
|
37742
|
+
if (!options.yes) {
|
|
37743
|
+
const shouldContinue = await p.confirm({
|
|
37744
|
+
message: "Remove public keys from these files?",
|
|
37745
|
+
initialValue: false
|
|
37746
|
+
});
|
|
37747
|
+
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
|
37748
|
+
p.cancel("Operation cancelled");
|
|
37749
|
+
return;
|
|
37750
|
+
}
|
|
37751
|
+
}
|
|
37752
|
+
const results = [];
|
|
37753
|
+
if (iosKey.value) results.push(await removePublicKeyFromIos(config.platform.ios.infoPlistPaths));
|
|
37754
|
+
if (androidKey.value) results.push(await removePublicKeyFromAndroid(config.platform.android.stringResourcePaths));
|
|
37755
|
+
console.log("");
|
|
37756
|
+
for (const result of results) if (result.success && result.found) p.log.success(`Removed ${result.platform === "ios" ? IOS_KEY : ANDROID_KEY} from ${result.paths.join(", ")}`);
|
|
37757
|
+
else if (!result.success) p.log.error(`${result.platform}: ${result.error}`);
|
|
37758
|
+
const successCount = results.filter((r) => r.success && r.found).length;
|
|
37759
|
+
console.log("");
|
|
37760
|
+
if (successCount > 0) {
|
|
37761
|
+
p.log.success("Public keys removed from native files!");
|
|
37762
|
+
console.log("");
|
|
37763
|
+
p.log.info("Next steps:");
|
|
37764
|
+
p.log.info(" 1. Rebuild your native apps");
|
|
37765
|
+
p.log.info(" 2. Release to app stores");
|
|
37766
|
+
p.log.info(" 3. Deploy unsigned bundles with `npx hot-updater deploy`");
|
|
37767
|
+
}
|
|
37768
|
+
};
|
|
37769
|
+
|
|
37269
37770
|
//#endregion
|
|
37270
37771
|
//#region src/commands/migrate.ts
|
|
37271
37772
|
/**
|
|
@@ -37447,6 +37948,17 @@ fingerprintCommand.command("create").description("Create fingerprint").action(ha
|
|
|
37447
37948
|
const channelCommand = program.command("channel").description("Manage channels");
|
|
37448
37949
|
channelCommand.action(handleChannel);
|
|
37449
37950
|
channelCommand.command("set").description("Set the channel for Android (BuildConfig) and iOS (Info.plist)").argument("<channel>", "the channel to set").action(handleSetChannel);
|
|
37951
|
+
const keysCommand = program.command("keys").description("Code signing key management");
|
|
37952
|
+
keysCommand.command("generate").description("Generate RSA key pair for code signing").option("-o, --output <dir>", "output directory for keys", "./keys").option("-k, --key-size <size>", "key size (2048 or 4096)", (value) => {
|
|
37953
|
+
const size = Number.parseInt(value, 10);
|
|
37954
|
+
if (size !== 2048 && size !== 4096) {
|
|
37955
|
+
p.log.error("Key size must be 2048 or 4096");
|
|
37956
|
+
process.exit(1);
|
|
37957
|
+
}
|
|
37958
|
+
return size;
|
|
37959
|
+
}, 4096).action(keysGenerate);
|
|
37960
|
+
keysCommand.command("export-public").description("Export public key for native configuration").option("-i, --input <path>", "path to private key file (default: from config signing.privateKeyPath in hot-updater.config.ts)").option("-p, --print-only", "only print the public key without writing to native files").option("-y, --yes", "skip confirmation prompt when writing to native files").action(keysExportPublic);
|
|
37961
|
+
keysCommand.command("remove").description("Remove public keys from native configuration files").option("-y, --yes", "skip confirmation prompt").action(keysRemove);
|
|
37450
37962
|
program.command("deploy").description("deploy a new version").addOption(platformCommandOption).addOption(new Option("-t, --target-app-version <targetAppVersion>", "specify the target app version (semver format e.g. 1.0.0, 1.x.x)").argParser((value) => {
|
|
37451
37963
|
if (!(0, import_valid.default)(value)) {
|
|
37452
37964
|
p.log.error("Invalid semver format (e.g. 1.0.0, 1.x.x)");
|