expo-updates 0.27.0 → 0.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/updates/UpdatesUtils.kt +1 -1
  4. package/android/src/main/java/expo/modules/updates/launcher/DatabaseLauncher.kt +10 -4
  5. package/android/src/main/java/expo/modules/updates/loader/EmbeddedLoader.kt +3 -0
  6. package/android/src/main/java/expo/modules/updates/loader/FileDownloader.kt +24 -1
  7. package/android/src/main/java/expo/modules/updates/loader/Loader.kt +9 -2
  8. package/android/src/main/java/expo/modules/updates/loader/LoaderFiles.kt +1 -1
  9. package/android/src/main/java/expo/modules/updates/loader/RemoteLoader.kt +4 -1
  10. package/build-cli/cli.d.ts +2 -0
  11. package/build-cli/cli.js +46 -0
  12. package/build-cli/configureCodeSigning.d.ts +3 -0
  13. package/build-cli/configureCodeSigning.js +43 -0
  14. package/build-cli/configureCodeSigningAsync.d.ts +7 -0
  15. package/build-cli/configureCodeSigningAsync.js +42 -0
  16. package/build-cli/generateCodeSigning.d.ts +3 -0
  17. package/build-cli/generateCodeSigning.js +48 -0
  18. package/build-cli/generateCodeSigningAsync.d.ts +8 -0
  19. package/build-cli/generateCodeSigningAsync.js +43 -0
  20. package/build-cli/utils/args.d.ts +16 -0
  21. package/build-cli/utils/args.js +52 -0
  22. package/build-cli/utils/dir.d.ts +1 -0
  23. package/build-cli/utils/dir.js +8 -0
  24. package/build-cli/utils/log.d.ts +7 -0
  25. package/build-cli/utils/log.js +34 -0
  26. package/build-cli/utils/modifyConfigAsync.d.ts +3 -0
  27. package/build-cli/utils/modifyConfigAsync.js +41 -0
  28. package/e2e/setup/project.ts +21 -5
  29. package/ios/EXUpdates/AppLoader/AppLoader.swift +10 -3
  30. package/ios/EXUpdates/AppLoader/EmbeddedAppLoader.swift +1 -1
  31. package/ios/EXUpdates/AppLoader/FileDownloader.swift +24 -0
  32. package/ios/EXUpdates/AppLoader/RemoteAppLoader.swift +2 -3
  33. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -10,7 +10,13 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
- ## 0.27.0 — 2025-02-06
13
+ ## 0.27.2 — 2025-02-26
14
+
15
+ ### 💡 Others
16
+
17
+ - Add update id headers to asset requests ([#34453](https://github.com/expo/expo/pull/34453) by [@gabrieldonadel](https://github.com/gabrieldonadel))
18
+
19
+ ## 0.27.1 — 2025-02-21
14
20
 
15
21
  ### 🎉 New features
16
22
 
@@ -21,6 +27,18 @@
21
27
  - Fixed build error on iOS Expo Go. ([#34485](https://github.com/expo/expo/pull/34485) by [@kudo](https://github.com/kudo))
22
28
  - Fixed Android unit test errors in BuilDataTest. ([#34510](https://github.com/expo/expo/pull/34510) by [@kudo](https://github.com/kudo))
23
29
 
30
+ ## 0.26.19 — 2025-02-19
31
+
32
+ ### 💡 Others
33
+
34
+ - Fixed incorrect error log on Android. ([#34785](https://github.com/expo/expo/pull/34785) by [@kudo](https://github.com/kudo))
35
+
36
+ ## 0.26.18 — 2025-02-12
37
+
38
+ ### 🐛 Bug fixes
39
+
40
+ - [Android] Fix `bytesToHex` `ArrayIndexOutOfBoundsException` during conversion. ([#34855](https://github.com/expo/expo/pull/34855) by [@gabrieldonadel](https://github.com/gabrieldonadel))
41
+
24
42
  ## 0.26.17 — 2025-02-06
25
43
 
26
44
  _This version does not introduce any user-facing changes._
@@ -16,7 +16,7 @@ apply plugin: 'com.android.library'
16
16
  apply plugin: 'com.google.devtools.ksp'
17
17
 
18
18
  group = 'host.exp.exponent'
19
- version = '0.27.0'
19
+ version = '0.27.2'
20
20
 
21
21
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
22
22
  apply from: expoModulesCorePlugin
@@ -63,7 +63,7 @@ android {
63
63
  namespace "expo.modules.updates"
64
64
  defaultConfig {
65
65
  versionCode 31
66
- versionName '0.27.0'
66
+ versionName '0.27.2'
67
67
  consumerProguardFiles("proguard-rules.pro")
68
68
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
69
69
 
@@ -185,7 +185,7 @@ object UpdatesUtils {
185
185
  fun bytesToHex(bytes: ByteArray): String {
186
186
  val hexChars = CharArray(bytes.size * 2)
187
187
  for (j in bytes.indices) {
188
- val v = (bytes[j] and 0xFF.toByte()).toInt()
188
+ val v = bytes[j].toInt() and 0xFF
189
189
  hexChars[j * 2] = HEX_ARRAY[v ushr 4]
190
190
  hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
191
191
  }
@@ -15,8 +15,10 @@ import expo.modules.updates.loader.LoaderFiles
15
15
  import expo.modules.updates.logging.UpdatesErrorCode
16
16
  import expo.modules.updates.logging.UpdatesLogger
17
17
  import expo.modules.updates.manifest.EmbeddedManifestUtils
18
+ import expo.modules.updates.manifest.EmbeddedUpdate
18
19
  import expo.modules.updates.manifest.ManifestMetadata
19
20
  import expo.modules.updates.selectionpolicy.SelectionPolicy
21
+ import org.json.JSONObject
20
22
  import java.io.File
21
23
 
22
24
  /**
@@ -92,7 +94,10 @@ class DatabaseLauncher(
92
94
  this.callback!!.onFailure(Exception("Launch asset relative path should not be null. Debug info: ${launchedUpdate!!.debugInfo()}"))
93
95
  }
94
96
 
95
- val launchAssetFile = ensureAssetExists(launchAsset, database)
97
+ val embeddedUpdate = EmbeddedManifestUtils.getEmbeddedUpdate(context, configuration)
98
+ val extraHeaders = FileDownloader.getExtraHeadersForRemoteAssetRequest(launchedUpdate, embeddedUpdate?.updateEntity, launchedUpdate)
99
+
100
+ val launchAssetFile = ensureAssetExists(launchAsset, database, embeddedUpdate, extraHeaders)
96
101
  if (launchAssetFile != null) {
97
102
  this.launchAssetFile = launchAssetFile.toString()
98
103
  }
@@ -107,7 +112,7 @@ class DatabaseLauncher(
107
112
  }
108
113
  val filename = asset.relativePath
109
114
  if (filename != null) {
110
- val assetFile = ensureAssetExists(asset, database)
115
+ val assetFile = ensureAssetExists(asset, database, embeddedUpdate, extraHeaders)
111
116
  if (assetFile != null) {
112
117
  this[asset] = Uri.fromFile(assetFile).toString()
113
118
  }
@@ -170,13 +175,12 @@ class DatabaseLauncher(
170
175
  }
171
176
  }
172
177
 
173
- fun ensureAssetExists(asset: AssetEntity, database: UpdatesDatabase): File? {
178
+ fun ensureAssetExists(asset: AssetEntity, database: UpdatesDatabase, embeddedUpdate: EmbeddedUpdate?, extraHeaders: JSONObject): File? {
174
179
  val assetFile = File(updatesDirectory, asset.relativePath ?: "")
175
180
  var assetFileExists = assetFile.exists()
176
181
  if (!assetFileExists) {
177
182
  // something has gone wrong, we're missing this asset
178
183
  // first we check to see if a copy is embedded in the binary
179
- val embeddedUpdate = EmbeddedManifestUtils.getEmbeddedUpdate(context, configuration)
180
184
  if (embeddedUpdate != null) {
181
185
  val embeddedAssets = embeddedUpdate.assetEntityList
182
186
  var matchingEmbeddedAsset: AssetEntity? = null
@@ -204,9 +208,11 @@ class DatabaseLauncher(
204
208
  return if (!assetFileExists) {
205
209
  // we still don't have the asset locally, so try downloading it remotely
206
210
  assetsToDownload++
211
+
207
212
  fileDownloader.downloadAsset(
208
213
  asset,
209
214
  updatesDirectory,
215
+ extraHeaders,
210
216
  object : AssetDownloadCallback {
211
217
  override fun onFailure(e: Exception, assetEntity: AssetEntity) {
212
218
  logger.error("Failed to load asset from disk or network", e, UpdatesErrorCode.AssetsFailedToLoad)
@@ -7,6 +7,7 @@ import expo.modules.updates.db.UpdatesDatabase
7
7
  import expo.modules.updates.loader.FileDownloader.AssetDownloadCallback
8
8
  import expo.modules.updates.loader.FileDownloader.RemoteUpdateDownloadCallback
9
9
  import expo.modules.updates.UpdatesUtils
10
+ import expo.modules.updates.db.entity.UpdateEntity
10
11
  import expo.modules.updates.logging.UpdatesLogger
11
12
  import java.io.File
12
13
  import java.io.FileNotFoundException
@@ -71,6 +72,8 @@ class EmbeddedLoader internal constructor(
71
72
  assetEntity: AssetEntity,
72
73
  updatesDirectory: File?,
73
74
  configuration: UpdatesConfiguration,
75
+ requestedUpdate: UpdateEntity?,
76
+ embeddedUpdate: UpdateEntity?,
74
77
  callback: AssetDownloadCallback
75
78
  ) {
76
79
  val filename = UpdatesUtils.createFilenameForAsset(assetEntity)
@@ -449,6 +449,7 @@ class FileDownloader(
449
449
  fun downloadAsset(
450
450
  asset: AssetEntity,
451
451
  destinationDirectory: File?,
452
+ extraHeaders: JSONObject,
452
453
  callback: AssetDownloadCallback
453
454
  ) {
454
455
  if (asset.url == null) {
@@ -466,7 +467,7 @@ class FileDownloader(
466
467
  } else {
467
468
  try {
468
469
  downloadAssetAndVerifyHashAndWriteToPath(
469
- createRequestForAsset(asset, configuration, context),
470
+ createRequestForAsset(asset, extraHeaders, configuration, context),
470
471
  asset.expectedHash,
471
472
  path,
472
473
  object : FileDownloadCallback {
@@ -594,12 +595,14 @@ class FileDownloader(
594
595
 
595
596
  internal fun createRequestForAsset(
596
597
  assetEntity: AssetEntity,
598
+ extraHeaders: JSONObject,
597
599
  configuration: UpdatesConfiguration,
598
600
  context: Context
599
601
  ): Request {
600
602
  return Request.Builder()
601
603
  .url(assetEntity.url!!.toString())
602
604
  .addHeadersFromJSONObject(assetEntity.extraRequestHeaders)
605
+ .addHeadersFromJSONObject(extraHeaders)
603
606
  .header("Expo-Platform", "android")
604
607
  .header("Expo-Protocol-Version", "1")
605
608
  .header("Expo-API-Version", "1")
@@ -700,5 +703,25 @@ class FileDownloader(
700
703
 
701
704
  return extraHeaders
702
705
  }
706
+
707
+ fun getExtraHeadersForRemoteAssetRequest(
708
+ launchedUpdate: UpdateEntity?,
709
+ embeddedUpdate: UpdateEntity?,
710
+ requestedUpdate: UpdateEntity?
711
+ ): JSONObject {
712
+ val extraHeaders = JSONObject()
713
+
714
+ launchedUpdate?.let {
715
+ extraHeaders.put("Expo-Current-Update-ID", it.id.toString().lowercase())
716
+ }
717
+ embeddedUpdate?.let {
718
+ extraHeaders.put("Expo-Embedded-Update-ID", it.id.toString().lowercase())
719
+ }
720
+ requestedUpdate?.let {
721
+ extraHeaders.put("Expo-Requested-Update-ID", it.id.toString().lowercase())
722
+ }
723
+
724
+ return extraHeaders
725
+ }
703
726
  }
704
727
  }
@@ -86,6 +86,8 @@ abstract class Loader protected constructor(
86
86
  assetEntity: AssetEntity,
87
87
  updatesDirectory: File?,
88
88
  configuration: UpdatesConfiguration,
89
+ requestedUpdate: UpdateEntity?,
90
+ embeddedUpdate: UpdateEntity?,
89
91
  callback: AssetDownloadCallback
90
92
  )
91
93
 
@@ -204,7 +206,7 @@ abstract class Loader protected constructor(
204
206
  // however, it's not ready, so we should try to download all the assets again.
205
207
  updateEntity = existingUpdateEntity
206
208
  }
207
- downloadAllAssets(update.assetEntityList)
209
+ downloadAllAssets(update)
208
210
  }
209
211
  }
210
212
 
@@ -214,8 +216,11 @@ abstract class Loader protected constructor(
214
216
  ERRORED
215
217
  }
216
218
 
217
- private fun downloadAllAssets(assetList: List<AssetEntity>) {
219
+ private fun downloadAllAssets(update: Update) {
220
+ val assetList = update.assetEntityList
218
221
  assetTotal = assetList.size
222
+
223
+ val embeddedUpdate = loaderFiles.readEmbeddedUpdate(context, configuration)
219
224
  for (assetEntityCur in assetList) {
220
225
  var assetEntity = assetEntityCur
221
226
 
@@ -243,6 +248,8 @@ abstract class Loader protected constructor(
243
248
  assetEntity,
244
249
  updatesDirectory,
245
250
  configuration,
251
+ requestedUpdate = update.updateEntity,
252
+ embeddedUpdate = embeddedUpdate?.updateEntity,
246
253
  object : AssetDownloadCallback {
247
254
  override fun onFailure(e: Exception, assetEntity: AssetEntity) {
248
255
  val identifier = if (assetEntity.hash != null) {
@@ -69,7 +69,7 @@ open class LoaderFiles {
69
69
  context.resources.openRawResource(id)
70
70
  .use { inputStream -> return UpdatesUtils.verifySHA256AndWriteToFile(inputStream, destination, null) }
71
71
  } catch (e: Exception) {
72
- Log.e(TAG, "Failed to copy asset " + asset.embeddedAssetFilename, e)
72
+ Log.e(TAG, "Failed to copy resource asset ${asset.resourcesFolder}/${asset.embeddedAssetFilename}", e)
73
73
  throw e
74
74
  }
75
75
  }
@@ -55,9 +55,12 @@ class RemoteLoader internal constructor(
55
55
  assetEntity: AssetEntity,
56
56
  updatesDirectory: File?,
57
57
  configuration: UpdatesConfiguration,
58
+ requestedUpdate: UpdateEntity?,
59
+ embeddedUpdate: UpdateEntity?,
58
60
  callback: AssetDownloadCallback
59
61
  ) {
60
- mFileDownloader.downloadAsset(assetEntity, updatesDirectory, callback)
62
+ val extraHeaders = FileDownloader.getExtraHeadersForRemoteAssetRequest(launchedUpdate, embeddedUpdate, requestedUpdate)
63
+ mFileDownloader.downloadAsset(assetEntity, updatesDirectory, extraHeaders, callback)
61
64
  }
62
65
 
63
66
  companion object {
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export type Command = (argv?: string[]) => void;
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const tslib_1 = require("tslib");
5
+ const arg_1 = tslib_1.__importDefault(require("arg"));
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const Log = tslib_1.__importStar(require("./utils/log"));
8
+ const commands = {
9
+ // Add a new command here
10
+ 'codesigning:generate': () => import('./generateCodeSigning.js').then((i) => i.generateCodeSigning),
11
+ 'codesigning:configure': () => import('./configureCodeSigning.js').then((i) => i.configureCodeSigning),
12
+ };
13
+ const args = (0, arg_1.default)({
14
+ // Types
15
+ '--help': Boolean,
16
+ // Aliases
17
+ '-h': '--help',
18
+ }, {
19
+ permissive: true,
20
+ });
21
+ const command = args._[0];
22
+ const commandArgs = args._.slice(1);
23
+ // Handle `--help` flag
24
+ if ((args['--help'] && !command) || !command) {
25
+ Log.exit((0, chalk_1.default) `
26
+ {bold Usage}
27
+ {dim $} npx expo-updates <command>
28
+
29
+ {bold Commands}
30
+ ${Object.keys(commands).sort().join(', ')}
31
+
32
+ {bold Options}
33
+ --help, -h Displays this message
34
+
35
+ For more information run a command with the --help flag
36
+ {dim $} npx expo-updates codesigning:generate --help
37
+ `, 0);
38
+ }
39
+ // Push the help flag to the subcommand args.
40
+ if (args['--help']) {
41
+ commandArgs.push('--help');
42
+ }
43
+ // Install exit hooks
44
+ process.on('SIGINT', () => process.exit(0));
45
+ process.on('SIGTERM', () => process.exit(0));
46
+ commands[command]().then((exec) => exec(commandArgs));
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from './cli';
3
+ export declare const configureCodeSigning: Command;
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.configureCodeSigning = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const args_1 = require("./utils/args");
8
+ const Log = tslib_1.__importStar(require("./utils/log"));
9
+ const configureCodeSigning = async (argv) => {
10
+ const args = (0, args_1.assertArgs)({
11
+ // Types
12
+ '--help': Boolean,
13
+ '--certificate-input-directory': String,
14
+ '--key-input-directory': String,
15
+ '--keyid': String,
16
+ // Aliases
17
+ '-h': '--help',
18
+ }, argv !== null && argv !== void 0 ? argv : []);
19
+ if (args['--help']) {
20
+ Log.exit((0, chalk_1.default) `
21
+ {bold Description}
22
+ Configure expo-updates code signing for this project and verify setup
23
+
24
+ {bold Usage}
25
+ {dim $} npx expo-updates codesigning:configure --certificate-input-directory <dir> --key-input-directory <dir>
26
+
27
+ Options
28
+ --certificate-input-directory <string> Directory containing code signing certificate
29
+ --key-input-directory <string> Directory containing private and public keys
30
+ -h, --help Output usage information
31
+ `, 0);
32
+ }
33
+ const { configureCodeSigningAsync } = await import('./configureCodeSigningAsync.js');
34
+ const certificateInput = (0, args_1.requireArg)(args, '--certificate-input-directory');
35
+ const keyInput = (0, args_1.requireArg)(args, '--key-input-directory');
36
+ const keyid = args['--keyid'];
37
+ return await configureCodeSigningAsync((0, args_1.getProjectRoot)(args), {
38
+ certificateInput,
39
+ keyInput,
40
+ keyid,
41
+ });
42
+ };
43
+ exports.configureCodeSigning = configureCodeSigning;
@@ -0,0 +1,7 @@
1
+ type Options = {
2
+ certificateInput: string;
3
+ keyInput: string;
4
+ keyid: string | undefined;
5
+ };
6
+ export declare function configureCodeSigningAsync(projectRoot: string, { certificateInput, keyInput, keyid }: Options): Promise<void>;
7
+ export {};
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configureCodeSigningAsync = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const code_signing_certificates_1 = require("@expo/code-signing-certificates");
6
+ const config_1 = require("@expo/config");
7
+ const fs_1 = require("fs");
8
+ const path_1 = tslib_1.__importDefault(require("path"));
9
+ const log_1 = require("./utils/log");
10
+ const modifyConfigAsync_1 = require("./utils/modifyConfigAsync");
11
+ async function configureCodeSigningAsync(projectRoot, { certificateInput, keyInput, keyid }) {
12
+ const certificateInputDir = path_1.default.resolve(projectRoot, certificateInput);
13
+ const keyInputDir = path_1.default.resolve(projectRoot, keyInput);
14
+ const [certificatePEM, privateKeyPEM, publicKeyPEM] = await Promise.all([
15
+ fs_1.promises.readFile(path_1.default.join(certificateInputDir, 'certificate.pem'), 'utf8'),
16
+ fs_1.promises.readFile(path_1.default.join(keyInputDir, 'private-key.pem'), 'utf8'),
17
+ fs_1.promises.readFile(path_1.default.join(keyInputDir, 'public-key.pem'), 'utf8'),
18
+ ]);
19
+ const certificate = (0, code_signing_certificates_1.convertCertificatePEMToCertificate)(certificatePEM);
20
+ const keyPair = (0, code_signing_certificates_1.convertKeyPairPEMToKeyPair)({ privateKeyPEM, publicKeyPEM });
21
+ (0, code_signing_certificates_1.validateSelfSignedCertificate)(certificate, keyPair);
22
+ const { exp } = (0, config_1.getConfig)(projectRoot, { skipSDKVersionRequirement: true });
23
+ const fields = {
24
+ codeSigningCertificate: `./${path_1.default.relative(projectRoot, certificateInputDir)}/certificate.pem`,
25
+ codeSigningMetadata: {
26
+ keyid: keyid !== null && keyid !== void 0 ? keyid : 'main',
27
+ alg: 'rsa-v1_5-sha256',
28
+ },
29
+ };
30
+ await (0, modifyConfigAsync_1.attemptModification)(projectRoot, {
31
+ updates: {
32
+ ...exp.updates,
33
+ ...fields,
34
+ },
35
+ }, {
36
+ updates: {
37
+ ...fields,
38
+ },
39
+ });
40
+ (0, log_1.log)(`Code signing configuration written to app configuration.`);
41
+ }
42
+ exports.configureCodeSigningAsync = configureCodeSigningAsync;
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from './cli';
3
+ export declare const generateCodeSigning: Command;
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.generateCodeSigning = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const args_1 = require("./utils/args");
8
+ const Log = tslib_1.__importStar(require("./utils/log"));
9
+ const generateCodeSigning = async (argv) => {
10
+ const args = (0, args_1.assertArgs)({
11
+ // Types
12
+ '--help': Boolean,
13
+ '--key-output-directory': String,
14
+ '--certificate-output-directory': String,
15
+ '--certificate-validity-duration-years': Number,
16
+ '--certificate-common-name': String,
17
+ // Aliases
18
+ '-h': '--help',
19
+ }, argv !== null && argv !== void 0 ? argv : []);
20
+ if (args['--help']) {
21
+ Log.exit((0, chalk_1.default) `
22
+ {bold Description}
23
+ Generate expo-updates private key, public key, and code signing certificate using that public key (self-signed by the private key)
24
+
25
+ {bold Usage}
26
+ {dim $} npx expo-updates codesigning:generate --key-output-directory <dir> --certificate-output-directory <dir> --certificate-validity-duration-years <num years> --certificate-common-name <name>
27
+
28
+ Options
29
+ --key-output-directory <string> Directory in which to put the generated private and public keys
30
+ --certificate-output-directory <string> Directory in which to put the generated certificate
31
+ --certificate-validity-duration-years <number> Certificate validity duration in years (number of years before certificate needs rotation)
32
+ --certificate-common-name <string> Common name attribute for certificate (generally the human readable name of the organization owning this application)
33
+ -h, --help Output usage information
34
+ `, 0);
35
+ }
36
+ const { generateCodeSigningAsync } = await import('./generateCodeSigningAsync.js');
37
+ const keyOutput = (0, args_1.requireArg)(args, '--key-output-directory');
38
+ const certificateOutput = (0, args_1.requireArg)(args, '--certificate-output-directory');
39
+ const certificateValidityDurationYears = (0, args_1.requireArg)(args, '--certificate-validity-duration-years');
40
+ const certificateCommonName = (0, args_1.requireArg)(args, '--certificate-common-name');
41
+ return await generateCodeSigningAsync((0, args_1.getProjectRoot)(args), {
42
+ certificateValidityDurationYears,
43
+ keyOutput,
44
+ certificateOutput,
45
+ certificateCommonName,
46
+ });
47
+ };
48
+ exports.generateCodeSigning = generateCodeSigning;
@@ -0,0 +1,8 @@
1
+ type Options = {
2
+ certificateValidityDurationYears: number;
3
+ keyOutput: string;
4
+ certificateOutput: string;
5
+ certificateCommonName: string;
6
+ };
7
+ export declare function generateCodeSigningAsync(projectRoot: string, { certificateValidityDurationYears, keyOutput, certificateOutput, certificateCommonName }: Options): Promise<void>;
8
+ export {};
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateCodeSigningAsync = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const code_signing_certificates_1 = require("@expo/code-signing-certificates");
6
+ const assert_1 = tslib_1.__importDefault(require("assert"));
7
+ const fs_1 = require("fs");
8
+ const path_1 = tslib_1.__importDefault(require("path"));
9
+ const dir_1 = require("./utils/dir");
10
+ const log_1 = require("./utils/log");
11
+ async function generateCodeSigningAsync(projectRoot, { certificateValidityDurationYears, keyOutput, certificateOutput, certificateCommonName }) {
12
+ const validityDurationYears = Math.floor(certificateValidityDurationYears);
13
+ const certificateOutputDir = path_1.default.resolve(projectRoot, certificateOutput);
14
+ const keyOutputDir = path_1.default.resolve(projectRoot, keyOutput);
15
+ await Promise.all([(0, dir_1.ensureDirAsync)(certificateOutputDir), (0, dir_1.ensureDirAsync)(keyOutputDir)]);
16
+ const [certificateOutputDirContents, keyOutputDirContents] = await Promise.all([
17
+ fs_1.promises.readdir(certificateOutputDir),
18
+ fs_1.promises.readdir(keyOutputDir),
19
+ ]);
20
+ (0, assert_1.default)(certificateOutputDirContents.length === 0, 'Certificate output directory must be empty');
21
+ (0, assert_1.default)(keyOutputDirContents.length === 0, 'Key output directory must be empty');
22
+ const keyPair = (0, code_signing_certificates_1.generateKeyPair)();
23
+ const validityNotBefore = new Date();
24
+ const validityNotAfter = new Date();
25
+ validityNotAfter.setFullYear(validityNotAfter.getFullYear() + validityDurationYears);
26
+ const certificate = (0, code_signing_certificates_1.generateSelfSignedCodeSigningCertificate)({
27
+ keyPair,
28
+ validityNotBefore,
29
+ validityNotAfter,
30
+ commonName: certificateCommonName,
31
+ });
32
+ const keyPairPEM = (0, code_signing_certificates_1.convertKeyPairToPEM)(keyPair);
33
+ const certificatePEM = (0, code_signing_certificates_1.convertCertificateToCertificatePEM)(certificate);
34
+ await Promise.all([
35
+ fs_1.promises.writeFile(path_1.default.join(keyOutputDir, 'public-key.pem'), keyPairPEM.publicKeyPEM),
36
+ fs_1.promises.writeFile(path_1.default.join(keyOutputDir, 'private-key.pem'), keyPairPEM.privateKeyPEM),
37
+ fs_1.promises.writeFile(path_1.default.join(certificateOutputDir, 'certificate.pem'), certificatePEM),
38
+ ]);
39
+ (0, log_1.log)(`Generated public and private keys output in ${keyOutputDir}. Remember to add them to .gitignore or to encrypt them. (e.g. with git-crypt)`);
40
+ (0, log_1.log)(`Generated code signing certificate output in ${certificateOutputDir}.`);
41
+ (0, log_1.log)(`To automatically configure this project for code signing, run \`yarn expo-updates codesigning:configure --certificate-input-directory=${certificateOutput} --key-input-directory=${keyOutput}\`.`);
42
+ }
43
+ exports.generateCodeSigningAsync = generateCodeSigningAsync;
@@ -0,0 +1,16 @@
1
+ import arg from 'arg';
2
+ /**
3
+ * Parse the first argument as a project directory.
4
+ *
5
+ * @returns valid project directory.
6
+ */
7
+ export declare function getProjectRoot(args: arg.Result<arg.Spec>): string;
8
+ /**
9
+ * Parse args and assert unknown options.
10
+ *
11
+ * @param schema the `args` schema for parsing the command line arguments.
12
+ * @param argv extra strings
13
+ * @returns processed args object.
14
+ */
15
+ export declare function assertArgs(schema: arg.Spec, argv: string[]): arg.Result<arg.Spec>;
16
+ export declare function requireArg(args: arg.Result<arg.Spec>, name: any): any;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireArg = exports.assertArgs = exports.getProjectRoot = void 0;
4
+ const tslib_1 = require("tslib");
5
+ // Common utilities for interacting with `args` library.
6
+ // These functions should be used by every command.
7
+ const arg_1 = tslib_1.__importDefault(require("arg"));
8
+ const fs_1 = require("fs");
9
+ const path_1 = require("path");
10
+ const Log = tslib_1.__importStar(require("./log"));
11
+ /**
12
+ * Parse the first argument as a project directory.
13
+ *
14
+ * @returns valid project directory.
15
+ */
16
+ function getProjectRoot(args) {
17
+ const projectRoot = (0, path_1.resolve)(args._[0] || '.');
18
+ if (!(0, fs_1.existsSync)(projectRoot)) {
19
+ Log.exit(`Invalid project root: ${projectRoot}`);
20
+ }
21
+ return projectRoot;
22
+ }
23
+ exports.getProjectRoot = getProjectRoot;
24
+ /**
25
+ * Parse args and assert unknown options.
26
+ *
27
+ * @param schema the `args` schema for parsing the command line arguments.
28
+ * @param argv extra strings
29
+ * @returns processed args object.
30
+ */
31
+ function assertArgs(schema, argv) {
32
+ try {
33
+ return (0, arg_1.default)(schema, { argv });
34
+ }
35
+ catch (error) {
36
+ // Ensure unknown options are handled the same way.
37
+ if (error.code === 'ARG_UNKNOWN_OPTION') {
38
+ Log.exit(error.message, 1);
39
+ }
40
+ // Otherwise rethrow the error.
41
+ throw error;
42
+ }
43
+ }
44
+ exports.assertArgs = assertArgs;
45
+ function requireArg(args, name) {
46
+ const value = args[name];
47
+ if (value === undefined || value === null) {
48
+ Log.exit(`${name} must be provided`, 1);
49
+ }
50
+ return value;
51
+ }
52
+ exports.requireArg = requireArg;
@@ -0,0 +1 @@
1
+ export declare function ensureDirAsync(path: string): Promise<string | undefined>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureDirAsync = void 0;
4
+ const fs_1 = require("fs");
5
+ function ensureDirAsync(path) {
6
+ return fs_1.promises.mkdir(path, { recursive: true });
7
+ }
8
+ exports.ensureDirAsync = ensureDirAsync;
@@ -0,0 +1,7 @@
1
+ export declare function time(label?: string): void;
2
+ export declare function timeEnd(label?: string): void;
3
+ export declare function error(...message: string[]): void;
4
+ export declare function warn(...message: string[]): void;
5
+ export declare function log(...message: string[]): void;
6
+ /** Log a message and exit the current process. If the `code` is non-zero then `console.error` will be used instead of `console.log`. */
7
+ export declare function exit(message: string, code?: number): never;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exit = exports.log = exports.warn = exports.error = exports.timeEnd = exports.time = void 0;
4
+ function time(label) {
5
+ console.time(label);
6
+ }
7
+ exports.time = time;
8
+ function timeEnd(label) {
9
+ console.timeEnd(label);
10
+ }
11
+ exports.timeEnd = timeEnd;
12
+ function error(...message) {
13
+ console.error(...message);
14
+ }
15
+ exports.error = error;
16
+ function warn(...message) {
17
+ console.warn(...message);
18
+ }
19
+ exports.warn = warn;
20
+ function log(...message) {
21
+ console.log(...message);
22
+ }
23
+ exports.log = log;
24
+ /** Log a message and exit the current process. If the `code` is non-zero then `console.error` will be used instead of `console.log`. */
25
+ function exit(message, code = 1) {
26
+ if (code === 0) {
27
+ log(message);
28
+ }
29
+ else {
30
+ error(message);
31
+ }
32
+ process.exit(code);
33
+ }
34
+ exports.exit = exit;
@@ -0,0 +1,3 @@
1
+ import { ExpoConfig } from '@expo/config';
2
+ /** Wraps `[@expo/config] modifyConfigAsync()` and adds additional logging. */
3
+ export declare function attemptModification(projectRoot: string, edits: Partial<ExpoConfig>, exactEdits: Partial<ExpoConfig>): Promise<void>;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attemptModification = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const config_1 = require("@expo/config");
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const Log = tslib_1.__importStar(require("./log"));
8
+ /** Wraps `[@expo/config] modifyConfigAsync()` and adds additional logging. */
9
+ async function attemptModification(projectRoot, edits, exactEdits) {
10
+ const modification = await (0, config_1.modifyConfigAsync)(projectRoot, edits, {
11
+ skipSDKVersionRequirement: true,
12
+ });
13
+ if (modification.type === 'success') {
14
+ Log.log();
15
+ }
16
+ else {
17
+ warnAboutConfigAndThrow(modification.type, modification.message, exactEdits);
18
+ }
19
+ }
20
+ exports.attemptModification = attemptModification;
21
+ function logNoConfig() {
22
+ Log.log(chalk_1.default.yellow(`No Expo config was found. Please create an Expo config (${chalk_1.default.bold `app.json`} or ${chalk_1.default.bold `app.config.js`}) in your project root.`));
23
+ }
24
+ function warnAboutConfigAndThrow(type, message, edits) {
25
+ Log.log();
26
+ if (type === 'warn') {
27
+ // The project is using a dynamic config, give the user a helpful log and bail out.
28
+ Log.log(chalk_1.default.yellow(message));
29
+ }
30
+ else {
31
+ logNoConfig();
32
+ }
33
+ notifyAboutManualConfigEdits(edits);
34
+ throw new Error();
35
+ }
36
+ function notifyAboutManualConfigEdits(edits) {
37
+ Log.log(chalk_1.default.cyan(`Please add the following to your Expo config`));
38
+ Log.log();
39
+ Log.log(JSON.stringify(edits, null, 2));
40
+ Log.log();
41
+ }
@@ -312,10 +312,10 @@ async function preparePackageJson(
312
312
 
313
313
  const extraDevDependencies = configureE2E
314
314
  ? {
315
- '@config-plugins/detox': '^5.0.1',
315
+ '@config-plugins/detox': '^9.0.0',
316
316
  '@types/express': '^4.17.17',
317
317
  '@types/jest': '^29.4.0',
318
- detox: '^20.4.0',
318
+ detox: '^20.33.0',
319
319
  express: '^4.18.2',
320
320
  'form-data': '^4.0.0',
321
321
  jest: '^29.3.1',
@@ -458,7 +458,15 @@ function transformAppJsonForE2E(
458
458
  runtimeVersion: string,
459
459
  isTV: boolean
460
460
  ) {
461
- const plugins: any[] = ['expo-updates', '@config-plugins/detox'];
461
+ const plugins: any[] = [
462
+ 'expo-updates',
463
+ [
464
+ '@config-plugins/detox',
465
+ {
466
+ subdomains: Array.from(new Set(['10.0.2.2', 'localhost', process.env.UPDATES_HOST])),
467
+ },
468
+ ],
469
+ ];
462
470
  if (isTV) {
463
471
  plugins.push([
464
472
  '@react-native-tvos/config-tv',
@@ -574,7 +582,15 @@ export function transformAppJsonForUpdatesDisabledE2E(
574
582
  projectName: string,
575
583
  runtimeVersion: string
576
584
  ) {
577
- const plugins: any[] = ['expo-updates', '@config-plugins/detox'];
585
+ const plugins: any[] = [
586
+ 'expo-updates',
587
+ [
588
+ '@config-plugins/detox',
589
+ {
590
+ subdomains: Array.from(new Set(['10.0.2.2', 'localhost', process.env.UPDATES_HOST])),
591
+ },
592
+ ],
593
+ ];
578
594
  return {
579
595
  ...appJson,
580
596
  expo: {
@@ -768,7 +784,7 @@ export async function initAsync(
768
784
  // enable proguard on Android
769
785
  await fs.appendFile(
770
786
  path.join(projectRoot, 'android', 'gradle.properties'),
771
- '\nandroid.enableProguardInReleaseBuilds=true\nandroid.kotlinVersion=1.8.20\nEXPO_UPDATES_NATIVE_DEBUG=true',
787
+ '\nandroid.enableProguardInReleaseBuilds=true\nEXPO_UPDATES_NATIVE_DEBUG=true',
772
788
  'utf-8'
773
789
  );
774
790
 
@@ -97,7 +97,7 @@ open class AppLoader: NSObject {
97
97
  preconditionFailure("Must override in concrete class")
98
98
  }
99
99
 
100
- open func downloadAsset(_ asset: UpdateAsset) {
100
+ open func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
101
101
  preconditionFailure("Must override in concrete class")
102
102
  }
103
103
 
@@ -202,6 +202,13 @@ open class AppLoader: NSObject {
202
202
  if let assets = updateManifest.assets(),
203
203
  !assets.isEmpty {
204
204
  self.assetsToLoad = assets
205
+ let embeddedUpdate = EmbeddedAppLoader.embeddedManifest(withConfig: self.config, database: self.database)
206
+ let extraHeaders = FileDownloader.extraHeadersForRemoteAssetRequest(
207
+ launchedUpdate: self.launchedUpdate,
208
+ embeddedUpdate: embeddedUpdate,
209
+ requestedUpdate: updateManifest
210
+ )
211
+
205
212
  for asset in assets {
206
213
  // before downloading, check to see if we already have this asset in the database
207
214
  let matchingDbEntry = try? self.database.asset(withKey: asset.key)
@@ -226,11 +233,11 @@ open class AppLoader: NSObject {
226
233
  self.handleAssetDownloadAlreadyExists(asset)
227
234
  }
228
235
  } else {
229
- self.downloadAsset(asset)
236
+ self.downloadAsset(asset, extraHeaders: extraHeaders)
230
237
  }
231
238
  }
232
239
  } else {
233
- self.downloadAsset(asset)
240
+ self.downloadAsset(asset, extraHeaders: extraHeaders)
234
241
  }
235
242
  }
236
243
  } else {
@@ -119,7 +119,7 @@ public final class EmbeddedAppLoader: AppLoader {
119
119
  ))
120
120
  }
121
121
 
122
- override public func downloadAsset(_ asset: UpdateAsset) {
122
+ override public func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
123
123
  FileDownloader.assetFilesQueue.async {
124
124
  self.handleAssetDownloadAlreadyExists(asset)
125
125
  }
@@ -221,6 +221,30 @@ public final class FileDownloader {
221
221
  return extraHeaders
222
222
  }
223
223
 
224
+ /**
225
+ * Get extra headers to pass into `downloadAsset:`
226
+ */
227
+ static func extraHeadersForRemoteAssetRequest(
228
+ launchedUpdate: Update?,
229
+ embeddedUpdate: Update?,
230
+ requestedUpdate: Update?
231
+ ) -> [String: Any] {
232
+ var extraHeaders: [String: Any] = [:]
233
+ if let launchedUpdate {
234
+ extraHeaders["Expo-Current-Update-ID"] = launchedUpdate.updateId.uuidString.lowercased()
235
+ }
236
+
237
+ if let embeddedUpdate {
238
+ extraHeaders["Expo-Embedded-Update-ID"] = embeddedUpdate.updateId.uuidString.lowercased()
239
+ }
240
+
241
+ if let requestedUpdate {
242
+ extraHeaders["Expo-Requested-Update-ID"] = requestedUpdate.updateId.uuidString.lowercased()
243
+ }
244
+
245
+ return extraHeaders
246
+ }
247
+
224
248
  private static func setHTTPHeaderFields(_ headers: [String: Any?]?, onRequest request: inout URLRequest) {
225
249
  guard let headers = headers else {
226
250
  return
@@ -84,7 +84,7 @@ public final class RemoteAppLoader: AppLoader {
84
84
  }
85
85
  }
86
86
 
87
- override public func downloadAsset(_ asset: UpdateAsset) {
87
+ override public func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
88
88
  let urlOnDisk = self.directory.appendingPathComponent(asset.filename)
89
89
 
90
90
  FileDownloader.assetFilesQueue.async {
@@ -101,12 +101,11 @@ public final class RemoteAppLoader: AppLoader {
101
101
  )
102
102
  return
103
103
  }
104
-
105
104
  self.downloader.downloadAsset(
106
105
  fromURL: assetUrl,
107
106
  verifyingHash: asset.expectedHash,
108
107
  toPath: urlOnDisk.path,
109
- extraHeaders: asset.extraRequestHeaders ?? [:]
108
+ extraHeaders: extraHeaders.merging(asset.extraRequestHeaders ?? [:]) { current, _ in current }
110
109
  ) { data, response, _ in
111
110
  DispatchQueue.global().async {
112
111
  self.handleAssetDownload(withData: data, response: response, asset: asset)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-updates",
3
- "version": "0.27.0",
3
+ "version": "0.27.2",
4
4
  "description": "Fetches and manages remotely-hosted assets and updates to your app's JS bundle.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -38,13 +38,13 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@expo/code-signing-certificates": "0.0.5",
41
- "@expo/config": "~10.0.8",
42
- "@expo/config-plugins": "~9.0.15",
41
+ "@expo/config": "~10.0.10",
42
+ "@expo/config-plugins": "~9.0.16",
43
43
  "@expo/spawn-async": "^1.7.2",
44
44
  "arg": "4.1.0",
45
45
  "chalk": "^4.1.2",
46
- "expo-eas-client": "~0.13.2",
47
- "expo-manifests": "~0.15.5",
46
+ "expo-eas-client": "~0.13.3",
47
+ "expo-manifests": "~0.15.7",
48
48
  "expo-structured-headers": "~4.0.0",
49
49
  "expo-updates-interface": "~1.0.0",
50
50
  "fast-glob": "^3.3.2",
@@ -56,7 +56,7 @@
56
56
  "@types/jest": "^29.2.1",
57
57
  "@types/node": "^18.19.34",
58
58
  "@types/node-forge": "^1.0.0",
59
- "expo-module-scripts": "^4.0.3",
59
+ "expo-module-scripts": "^4.0.4",
60
60
  "express": "^4.21.1",
61
61
  "form-data": "^4.0.0",
62
62
  "fs-extra": "~8.1.0",
@@ -67,5 +67,5 @@
67
67
  "expo": "*",
68
68
  "react": "*"
69
69
  },
70
- "gitHead": "67ab2bc7efe275088b9213a1a3b27376d56a7778"
70
+ "gitHead": "4f3b834d475d0de0d26cc6b1d39019c8bf11aca6"
71
71
  }