@hot-updater/aws 0.20.13 → 0.20.14

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.
@@ -70,6 +70,8 @@ let crypto = require("crypto");
70
70
  crypto = __toESM(crypto);
71
71
  let __aws_sdk_client_iam = require("@aws-sdk/client-iam");
72
72
  __aws_sdk_client_iam = __toESM(__aws_sdk_client_iam);
73
+ let __aws_sdk_client_sts = require("@aws-sdk/client-sts");
74
+ __aws_sdk_client_sts = __toESM(__aws_sdk_client_sts);
73
75
  let __aws_sdk_client_lambda = require("@aws-sdk/client-lambda");
74
76
  __aws_sdk_client_lambda = __toESM(__aws_sdk_client_lambda);
75
77
  let fs_promises = require("fs/promises");
@@ -8050,6 +8052,11 @@ var IAMManager = class {
8050
8052
  region: this.region,
8051
8053
  credentials: this.credentials
8052
8054
  });
8055
+ const accountId = (await new __aws_sdk_client_sts.STS({
8056
+ region: this.region,
8057
+ credentials: this.credentials
8058
+ }).getCallerIdentity({})).Account;
8059
+ if (!accountId) throw new Error("Failed to get AWS account ID");
8053
8060
  const assumeRolePolicyDocument = JSON.stringify({
8054
8061
  Version: "2012-10-17",
8055
8062
  Statement: [{
@@ -8059,9 +8066,27 @@ var IAMManager = class {
8059
8066
  }]
8060
8067
  });
8061
8068
  const roleName = "hot-updater-edge-role";
8069
+ const ssmPolicyDocument = JSON.stringify({
8070
+ Version: "2012-10-17",
8071
+ Statement: [{
8072
+ Effect: "Allow",
8073
+ Action: ["ssm:GetParameter"],
8074
+ Resource: `arn:aws:ssm:*:${accountId}:parameter/hot-updater/*`
8075
+ }]
8076
+ });
8062
8077
  try {
8063
8078
  const { Role: existingRole } = await iamClient.getRole({ RoleName: roleName });
8064
8079
  if (existingRole?.Arn) {
8080
+ try {
8081
+ await iamClient.putRolePolicy({
8082
+ RoleName: roleName,
8083
+ PolicyName: "HotUpdaterSSMAccess",
8084
+ PolicyDocument: ssmPolicyDocument
8085
+ });
8086
+ f.info("Updated SSM access policy for existing IAM role");
8087
+ } catch {
8088
+ f.warn("Failed to update SSM policy, continuing anyway");
8089
+ }
8065
8090
  f.info(`Using existing IAM role: ${roleName} (${existingRole.Arn})`);
8066
8091
  return existingRole.Arn;
8067
8092
  }
@@ -8070,7 +8095,7 @@ var IAMManager = class {
8070
8095
  const lambdaRoleArn = (await iamClient.createRole({
8071
8096
  RoleName: roleName,
8072
8097
  AssumeRolePolicyDocument: assumeRolePolicyDocument,
8073
- Description: "Role for Lambda@Edge to access S3"
8098
+ Description: "Role for Lambda@Edge to access S3 and SSM"
8074
8099
  })).Role?.Arn;
8075
8100
  f.info(`Created IAM role: ${roleName} (${lambdaRoleArn})`);
8076
8101
  await iamClient.attachRolePolicy({
@@ -8081,7 +8106,13 @@ var IAMManager = class {
8081
8106
  RoleName: roleName,
8082
8107
  PolicyArn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
8083
8108
  });
8084
- f.info(`Attached required policies to ${roleName}`);
8109
+ f.info(`Attached managed policies to ${roleName}`);
8110
+ await iamClient.putRolePolicy({
8111
+ RoleName: roleName,
8112
+ PolicyName: "HotUpdaterSSMAccess",
8113
+ PolicyDocument: ssmPolicyDocument
8114
+ });
8115
+ f.info(`Added SSM access inline policy to ${roleName}`);
8085
8116
  return lambdaRoleArn;
8086
8117
  } catch (createError) {
8087
8118
  if (createError instanceof Error) f.error(`Error setting up IAM role: ${createError.message}`);
@@ -8099,7 +8130,7 @@ var LambdaEdgeDeployer = class {
8099
8130
  constructor(credentials) {
8100
8131
  this.credentials = credentials;
8101
8132
  }
8102
- async deploy(lambdaRoleArn, keyPair) {
8133
+ async deploy(lambdaRoleArn, config) {
8103
8134
  const cwd = (0, __hot_updater_plugin_core.getCwd)();
8104
8135
  const lambdaName = await he({
8105
8136
  message: "Enter the name of the Lambda@Edge function",
@@ -8111,8 +8142,9 @@ var LambdaEdgeDeployer = class {
8111
8142
  const { tmpDir, removeTmpDir } = await (0, __hot_updater_plugin_core.copyDirToTmp)(path.default.dirname(lambdaPath));
8112
8143
  const indexPath = path.default.join(tmpDir, "index.cjs");
8113
8144
  const code = (0, __hot_updater_plugin_core.transformEnv)(indexPath, {
8114
- CLOUDFRONT_KEY_PAIR_ID: keyPair.publicKey,
8115
- CLOUDFRONT_PRIVATE_KEY_BASE64: Buffer.from(keyPair.privateKey).toString("base64")
8145
+ CLOUDFRONT_KEY_PAIR_ID: config.publicKeyId,
8146
+ SSM_PARAMETER_NAME: config.ssmParameterName,
8147
+ SSM_REGION: config.ssmRegion
8116
8148
  });
8117
8149
  await fs_promises.default.writeFile(indexPath, code);
8118
8150
  const lambdaClient = new __aws_sdk_client_lambda.Lambda({
@@ -8873,9 +8905,12 @@ const runInit = async ({ build }) => {
8873
8905
  const keyPair = await new SSMKeyPairManager(bucketRegion, credentials).getOrCreateKeyPair(`/hot-updater/${bucketName}/keypair`);
8874
8906
  const cloudFrontManager = new CloudFrontManager(bucketRegion, credentials);
8875
8907
  const { publicKeyId, keyGroupId } = await cloudFrontManager.getOrCreateKeyGroup(keyPair.publicKey);
8876
- const { functionArn } = await new LambdaEdgeDeployer(credentials).deploy(lambdaRoleArn, {
8877
- publicKey: publicKeyId,
8878
- privateKey: keyPair.privateKey
8908
+ const lambdaEdgeDeployer = new LambdaEdgeDeployer(credentials);
8909
+ const ssmParameterName = `/hot-updater/${bucketName}/keypair`;
8910
+ const { functionArn } = await lambdaEdgeDeployer.deploy(lambdaRoleArn, {
8911
+ publicKeyId,
8912
+ ssmParameterName,
8913
+ ssmRegion: bucketRegion
8879
8914
  });
8880
8915
  const { distributionDomain, distributionId } = await cloudFrontManager.createOrUpdateDistribution({
8881
8916
  keyGroupId,
package/dist/iac/index.js CHANGED
@@ -23,6 +23,7 @@ import { Buffer as Buffer$1 } from "node:buffer";
23
23
  import { CloudFront } from "@aws-sdk/client-cloudfront";
24
24
  import crypto from "crypto";
25
25
  import { IAM } from "@aws-sdk/client-iam";
26
+ import { STS } from "@aws-sdk/client-sts";
26
27
  import { Lambda } from "@aws-sdk/client-lambda";
27
28
  import fs$1 from "fs/promises";
28
29
  import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, S3 } from "@aws-sdk/client-s3";
@@ -8025,6 +8026,11 @@ var IAMManager = class {
8025
8026
  region: this.region,
8026
8027
  credentials: this.credentials
8027
8028
  });
8029
+ const accountId = (await new STS({
8030
+ region: this.region,
8031
+ credentials: this.credentials
8032
+ }).getCallerIdentity({})).Account;
8033
+ if (!accountId) throw new Error("Failed to get AWS account ID");
8028
8034
  const assumeRolePolicyDocument = JSON.stringify({
8029
8035
  Version: "2012-10-17",
8030
8036
  Statement: [{
@@ -8034,9 +8040,27 @@ var IAMManager = class {
8034
8040
  }]
8035
8041
  });
8036
8042
  const roleName = "hot-updater-edge-role";
8043
+ const ssmPolicyDocument = JSON.stringify({
8044
+ Version: "2012-10-17",
8045
+ Statement: [{
8046
+ Effect: "Allow",
8047
+ Action: ["ssm:GetParameter"],
8048
+ Resource: `arn:aws:ssm:*:${accountId}:parameter/hot-updater/*`
8049
+ }]
8050
+ });
8037
8051
  try {
8038
8052
  const { Role: existingRole } = await iamClient.getRole({ RoleName: roleName });
8039
8053
  if (existingRole?.Arn) {
8054
+ try {
8055
+ await iamClient.putRolePolicy({
8056
+ RoleName: roleName,
8057
+ PolicyName: "HotUpdaterSSMAccess",
8058
+ PolicyDocument: ssmPolicyDocument
8059
+ });
8060
+ f.info("Updated SSM access policy for existing IAM role");
8061
+ } catch {
8062
+ f.warn("Failed to update SSM policy, continuing anyway");
8063
+ }
8040
8064
  f.info(`Using existing IAM role: ${roleName} (${existingRole.Arn})`);
8041
8065
  return existingRole.Arn;
8042
8066
  }
@@ -8045,7 +8069,7 @@ var IAMManager = class {
8045
8069
  const lambdaRoleArn = (await iamClient.createRole({
8046
8070
  RoleName: roleName,
8047
8071
  AssumeRolePolicyDocument: assumeRolePolicyDocument,
8048
- Description: "Role for Lambda@Edge to access S3"
8072
+ Description: "Role for Lambda@Edge to access S3 and SSM"
8049
8073
  })).Role?.Arn;
8050
8074
  f.info(`Created IAM role: ${roleName} (${lambdaRoleArn})`);
8051
8075
  await iamClient.attachRolePolicy({
@@ -8056,7 +8080,13 @@ var IAMManager = class {
8056
8080
  RoleName: roleName,
8057
8081
  PolicyArn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
8058
8082
  });
8059
- f.info(`Attached required policies to ${roleName}`);
8083
+ f.info(`Attached managed policies to ${roleName}`);
8084
+ await iamClient.putRolePolicy({
8085
+ RoleName: roleName,
8086
+ PolicyName: "HotUpdaterSSMAccess",
8087
+ PolicyDocument: ssmPolicyDocument
8088
+ });
8089
+ f.info(`Added SSM access inline policy to ${roleName}`);
8060
8090
  return lambdaRoleArn;
8061
8091
  } catch (createError) {
8062
8092
  if (createError instanceof Error) f.error(`Error setting up IAM role: ${createError.message}`);
@@ -8074,7 +8104,7 @@ var LambdaEdgeDeployer = class {
8074
8104
  constructor(credentials) {
8075
8105
  this.credentials = credentials;
8076
8106
  }
8077
- async deploy(lambdaRoleArn, keyPair) {
8107
+ async deploy(lambdaRoleArn, config) {
8078
8108
  const cwd = getCwd();
8079
8109
  const lambdaName = await he({
8080
8110
  message: "Enter the name of the Lambda@Edge function",
@@ -8086,8 +8116,9 @@ var LambdaEdgeDeployer = class {
8086
8116
  const { tmpDir, removeTmpDir } = await copyDirToTmp(path$1.dirname(lambdaPath));
8087
8117
  const indexPath = path$1.join(tmpDir, "index.cjs");
8088
8118
  const code = transformEnv(indexPath, {
8089
- CLOUDFRONT_KEY_PAIR_ID: keyPair.publicKey,
8090
- CLOUDFRONT_PRIVATE_KEY_BASE64: Buffer.from(keyPair.privateKey).toString("base64")
8119
+ CLOUDFRONT_KEY_PAIR_ID: config.publicKeyId,
8120
+ SSM_PARAMETER_NAME: config.ssmParameterName,
8121
+ SSM_REGION: config.ssmRegion
8091
8122
  });
8092
8123
  await fs$1.writeFile(indexPath, code);
8093
8124
  const lambdaClient = new Lambda({
@@ -8848,9 +8879,12 @@ const runInit = async ({ build }) => {
8848
8879
  const keyPair = await new SSMKeyPairManager(bucketRegion, credentials).getOrCreateKeyPair(`/hot-updater/${bucketName}/keypair`);
8849
8880
  const cloudFrontManager = new CloudFrontManager(bucketRegion, credentials);
8850
8881
  const { publicKeyId, keyGroupId } = await cloudFrontManager.getOrCreateKeyGroup(keyPair.publicKey);
8851
- const { functionArn } = await new LambdaEdgeDeployer(credentials).deploy(lambdaRoleArn, {
8852
- publicKey: publicKeyId,
8853
- privateKey: keyPair.privateKey
8882
+ const lambdaEdgeDeployer = new LambdaEdgeDeployer(credentials);
8883
+ const ssmParameterName = `/hot-updater/${bucketName}/keypair`;
8884
+ const { functionArn } = await lambdaEdgeDeployer.deploy(lambdaRoleArn, {
8885
+ publicKeyId,
8886
+ ssmParameterName,
8887
+ ssmRegion: bucketRegion
8854
8888
  });
8855
8889
  const { distributionDomain, distributionId } = await cloudFrontManager.createOrUpdateDistribution({
8856
8890
  keyGroupId,
@@ -24,6 +24,8 @@ var __toESM$1 = (mod, isNodeMode, target) => (target = mod != null ? __create$1(
24
24
  }) : target, mod));
25
25
 
26
26
  //#endregion
27
+ let __aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
28
+ __aws_sdk_client_ssm = __toESM$1(__aws_sdk_client_ssm);
27
29
  let node_crypto = require("node:crypto");
28
30
  node_crypto = __toESM$1(node_crypto);
29
31
 
@@ -3122,7 +3124,32 @@ const withSignedUrl = async ({ data, reqUrl, keyPairId, privateKey, expiresSecon
3122
3124
  //#endregion
3123
3125
  //#region lambda/index.ts
3124
3126
  const CLOUDFRONT_KEY_PAIR_ID = HotUpdater.CLOUDFRONT_KEY_PAIR_ID;
3125
- const CLOUDFRONT_PRIVATE_KEY = Buffer.from(HotUpdater.CLOUDFRONT_PRIVATE_KEY_BASE64, "base64").toString("utf-8");
3127
+ const SSM_PARAMETER_NAME = HotUpdater.SSM_PARAMETER_NAME;
3128
+ const SSM_REGION = HotUpdater.SSM_REGION;
3129
+ let cachedPrivateKey = null;
3130
+ /**
3131
+ * Retrieves CloudFront private key from SSM Parameter Store
3132
+ * Uses global cache to avoid repeated SSM calls on warm Lambda invocations
3133
+ */
3134
+ async function getPrivateKey() {
3135
+ if (cachedPrivateKey !== null) return cachedPrivateKey;
3136
+ if (!SSM_REGION) throw new Error(`Invalid AWS region format: ${SSM_REGION}. Expected format like 'us-east-1' or 'ap-southeast-1'`);
3137
+ const response = await new __aws_sdk_client_ssm.SSM({ region: SSM_REGION }).getParameter({
3138
+ Name: SSM_PARAMETER_NAME,
3139
+ WithDecryption: true
3140
+ });
3141
+ if (!response.Parameter?.Value) throw new Error(`Failed to retrieve private key from SSM parameter: ${SSM_PARAMETER_NAME}`);
3142
+ let keyPair;
3143
+ try {
3144
+ keyPair = JSON.parse(response.Parameter.Value);
3145
+ } catch (error) {
3146
+ throw new Error(`Invalid JSON format in SSM parameter: ${SSM_PARAMETER_NAME}. ${error instanceof Error ? error.message : String(error)}`);
3147
+ }
3148
+ const privateKey = keyPair.privateKey;
3149
+ if (!privateKey || typeof privateKey !== "string") throw new Error(`Invalid private key format in SSM parameter: ${SSM_PARAMETER_NAME}`);
3150
+ cachedPrivateKey = privateKey;
3151
+ return privateKey;
3152
+ }
3126
3153
  const app = new Hono();
3127
3154
  const validatePlatform = (platform) => {
3128
3155
  return ["ios", "android"].includes(platform);
@@ -3139,6 +3166,7 @@ const processDefaultValues = (channel, minBundleId) => ({
3139
3166
  const handleUpdateRequest = async (c, params, strategy, expiresSeconds) => {
3140
3167
  try {
3141
3168
  if (!c.req.header("host")) return c.json({ error: "Missing host header." }, 500);
3169
+ const privateKey = await getPrivateKey();
3142
3170
  const updateConfig = {
3143
3171
  platform: params.platform,
3144
3172
  bundleId: params.bundleId,
@@ -3155,14 +3183,14 @@ const handleUpdateRequest = async (c, params, strategy, expiresSeconds) => {
3155
3183
  const updateInfo = await getUpdateInfo({
3156
3184
  baseUrl: c.req.url,
3157
3185
  keyPairId: CLOUDFRONT_KEY_PAIR_ID,
3158
- privateKey: CLOUDFRONT_PRIVATE_KEY
3186
+ privateKey
3159
3187
  }, updateConfig);
3160
3188
  if (!updateInfo) return c.json(null);
3161
3189
  const appUpdateInfo = await withSignedUrl({
3162
3190
  data: updateInfo,
3163
3191
  reqUrl: c.req.url,
3164
3192
  keyPairId: CLOUDFRONT_KEY_PAIR_ID,
3165
- privateKey: CLOUDFRONT_PRIVATE_KEY,
3193
+ privateKey,
3166
3194
  expiresSeconds
3167
3195
  });
3168
3196
  return c.json(appUpdateInfo);
@@ -4,7 +4,8 @@ import { CloudFrontRequestHandler } from "aws-lambda";
4
4
  declare global {
5
5
  var HotUpdater: {
6
6
  CLOUDFRONT_KEY_PAIR_ID: string;
7
- CLOUDFRONT_PRIVATE_KEY_BASE64: string;
7
+ SSM_PARAMETER_NAME: string;
8
+ SSM_REGION: string;
8
9
  };
9
10
  }
10
11
  declare const handler: CloudFrontRequestHandler;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/aws",
3
3
  "type": "module",
4
- "version": "0.20.13",
4
+ "version": "0.20.14",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
@@ -45,8 +45,8 @@
45
45
  "mime": "^4.0.4",
46
46
  "picocolors": "1.1.1",
47
47
  "@clack/prompts": "0.10.0",
48
- "@hot-updater/js": "0.20.13",
49
- "@hot-updater/core": "0.20.13"
48
+ "@hot-updater/core": "0.20.14",
49
+ "@hot-updater/js": "0.20.14"
50
50
  },
51
51
  "dependencies": {
52
52
  "@aws-sdk/client-cloudfront": "3.772.0",
@@ -54,10 +54,11 @@
54
54
  "@aws-sdk/client-lambda": "3.772.0",
55
55
  "@aws-sdk/client-s3": "3.772.0",
56
56
  "@aws-sdk/client-ssm": "3.772.0",
57
+ "@aws-sdk/client-sts": "3.772.0",
57
58
  "@aws-sdk/credential-providers": "3.772.0",
58
59
  "@aws-sdk/lib-storage": "3.772.0",
59
60
  "aws-lambda": "1.0.7",
60
- "@hot-updater/plugin-core": "0.20.13"
61
+ "@hot-updater/plugin-core": "0.20.14"
61
62
  },
62
63
  "scripts": {
63
64
  "build": "tsdown",