@granite-js/pulumi-aws 2.5.2 → 2.5.3

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.
@@ -100,6 +100,26 @@ import {
100
100
  fromUtf8,
101
101
  toUtf8
102
102
  } from "../chunk-Z4UYYQ4J.js";
103
+ import {
104
+ validateChannel
105
+ } from "../chunk-GEE3OQDP.js";
106
+ import "../chunk-V7TRGEV6.js";
107
+ import "../chunk-3AVBP57V.js";
108
+ import "../chunk-ZIHRRTTA.js";
109
+ import "../chunk-3JKQPIJ2.js";
110
+ import "../chunk-7NN7DVGK.js";
111
+ import "../chunk-63Q3RLAK.js";
112
+ import "../chunk-OCHPLE5N.js";
113
+ import "../chunk-GJFKL6XM.js";
114
+ import "../chunk-24XYHOWM.js";
115
+ import "../chunk-57WYIP4I.js";
116
+ import "../chunk-TDZTH7TL.js";
117
+ import "../chunk-RYIGWCGK.js";
118
+ import "../chunk-H4QJ5O4E.js";
119
+ import "../chunk-THUA35UY.js";
120
+
121
+ // src/lambda/utils/invalidation.ts
122
+ import { randomUUID } from "crypto";
103
123
 
104
124
  // ../../.yarn/cache/@aws-sdk-core-npm-3.940.0-0d32a8e8d2-090b960007.zip/node_modules/@aws-sdk/core/dist-es/submodules/protocols/xml/parseXmlBody.js
105
125
  var loadRestXmlErrorCode = (output, data) => {
@@ -14076,6 +14096,10 @@ function isCurrentFile(key) {
14076
14096
  const pathParts = key.split("/");
14077
14097
  return pathParts.length === 3 && pathParts[0] === "deployments" && ["CURRENT", "deployment_state"].includes(pathParts[2] ?? "");
14078
14098
  }
14099
+ function isSelectorRegistrationFile(key) {
14100
+ const parts = key.split("/");
14101
+ return parts.length === 4 && parts[0] === "deployments" && parts[2] === "selectors" && Boolean(parts[3]?.endsWith(".json"));
14102
+ }
14079
14103
  function isClusterDeploymentInfoFile(key) {
14080
14104
  const pathParts = key.split("/");
14081
14105
  return Boolean(
@@ -14086,11 +14110,20 @@ function isClusterDeploymentInfoFile(key) {
14086
14110
  // src/lambda/utils/invalidation.ts
14087
14111
  var cloudFrontClient = new CloudFrontClient({});
14088
14112
  function getPathsToInvalidate(key) {
14113
+ if (key.startsWith("channels/")) {
14114
+ const [, channel, ...parts] = key.split("/");
14115
+ try {
14116
+ validateChannel(channel ?? "");
14117
+ } catch {
14118
+ return [];
14119
+ }
14120
+ key = parts.join("/");
14121
+ }
14089
14122
  const appName = extractAppName(key);
14090
14123
  if (!appName) {
14091
14124
  return [];
14092
14125
  }
14093
- if (isCurrentFile(key)) {
14126
+ if (isCurrentFile(key) || isSelectorRegistrationFile(key)) {
14094
14127
  return [`/ios/${appName}/*`, `/android/${appName}/*`];
14095
14128
  }
14096
14129
  if (isClusterDeploymentInfoFile(key)) {
@@ -14102,8 +14135,7 @@ function getPathsToInvalidate(key) {
14102
14135
  return [];
14103
14136
  }
14104
14137
  async function createInvalidation(paths, distributionId) {
14105
- const timestamp = (/* @__PURE__ */ new Date()).getTime();
14106
- const callerReference = `s3-triggered-invalidation-${timestamp}`;
14138
+ const callerReference = `s3-triggered-invalidation-${randomUUID()}`;
14107
14139
  const params = {
14108
14140
  DistributionId: distributionId,
14109
14141
  InvalidationBatch: {
@@ -29416,16 +29416,66 @@ var S3Client2 = class {
29416
29416
  const response = await this.s3Client.send(command);
29417
29417
  return response;
29418
29418
  }
29419
+ async *listObjectKeys(prefix) {
29420
+ let continuationToken;
29421
+ for (; ; ) {
29422
+ const response = await this.s3Client.send(
29423
+ new ListObjectsV2Command({
29424
+ Bucket: this.bucket,
29425
+ Prefix: prefix,
29426
+ ContinuationToken: continuationToken
29427
+ })
29428
+ );
29429
+ for (const object2 of response.Contents ?? []) {
29430
+ if (object2.Key !== void 0) {
29431
+ yield object2.Key;
29432
+ }
29433
+ }
29434
+ if (!response.IsTruncated) {
29435
+ return;
29436
+ }
29437
+ continuationToken = response.NextContinuationToken;
29438
+ if (!continuationToken) {
29439
+ throw new Error("Truncated S3 listing has no continuation token");
29440
+ }
29441
+ }
29442
+ }
29419
29443
  destroy() {
29420
29444
  this.s3Client.destroy();
29421
29445
  }
29422
29446
  };
29447
+ var InvalidRequest3 = class extends Error {
29448
+ constructor(message) {
29449
+ super();
29450
+ this.name = "InvalidRequest";
29451
+ this.message = message;
29452
+ }
29453
+ };
29454
+ function validateChannel(channel) {
29455
+ if (channel === "bundle") {
29456
+ throw new InvalidRequest3("bundle is reserved for the legacy default bundle");
29457
+ }
29458
+ if (!isChannelName(channel)) {
29459
+ throw new InvalidRequest3(
29460
+ "Channel must contain 1-64 letters, digits, underscores or hyphens, starting with a letter or digit"
29461
+ );
29462
+ }
29463
+ return channel;
29464
+ }
29465
+ function isChannelName(value) {
29466
+ return typeof value === "string" && value !== "bundle" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value);
29467
+ }
29468
+ function channelPrefix(channel) {
29469
+ return channel === void 0 ? "" : `channels/${validateChannel(channel)}/`;
29470
+ }
29423
29471
  var paths = {
29424
- bundlePathPrefix: (appName, deploymentId) => `bundles/${appName}/${deploymentId}`,
29425
- bundleList: (appName) => `deployments/${appName}/DEPLOYMENTS`,
29426
- deploymentState: (appName) => `deployments/${appName}/deployment_state`,
29427
- clusterDeploymentState: (appName, clusterId) => `deployments/${appName}/clusters/${clusterId}`,
29428
- clusterDeploymentInfoPath: (appName, clusterId) => `deployments/${appName}/clusters/${clusterId}.deploymentInfo`
29472
+ appBundlePrefix: ({ appName, channel }) => `${channelPrefix(channel)}bundles/${appName}/`,
29473
+ selectorRegistration: ({ appName, selector }) => `deployments/${appName}/selectors/${encodeURIComponent(selector)}.json`,
29474
+ bundlePathPrefix: ({ appName, deploymentId, channel }) => `${channelPrefix(channel)}bundles/${appName}/${deploymentId}`,
29475
+ bundleList: ({ appName, channel }) => `${channelPrefix(channel)}deployments/${appName}/DEPLOYMENTS`,
29476
+ deploymentState: ({ appName, channel }) => `${channelPrefix(channel)}deployments/${appName}/deployment_state`,
29477
+ clusterDeploymentState: ({ appName, clusterId, channel }) => `${channelPrefix(channel)}deployments/${appName}/clusters/${clusterId}`,
29478
+ clusterDeploymentInfoPath: ({ appName, clusterId, channel }) => `${channelPrefix(channel)}deployments/${appName}/clusters/${clusterId}.deploymentInfo`
29429
29479
  };
29430
29480
  function shuffle(arr) {
29431
29481
  const result = arr.slice();
@@ -29540,7 +29590,7 @@ function isFullyRollout(config) {
29540
29590
  }
29541
29591
  async function readBundleList(appName, context) {
29542
29592
  const { s3Client } = context;
29543
- const data = await s3Client.getObject(paths.bundleList(appName));
29593
+ const data = await s3Client.getObject(paths.bundleList({ appName, channel: context.channel }));
29544
29594
  return JSON.parse(data);
29545
29595
  }
29546
29596
  var store$4;
@@ -29906,8 +29956,8 @@ var clusterDeploymentInfo = /* @__PURE__ */ object({
29906
29956
  var deploymentState = /* @__PURE__ */ union([pendingDeploymentState, stableDeploymentState, canaryDeploymentState]);
29907
29957
  async function readCluster({ appName, clusterId }, context) {
29908
29958
  const { s3Client } = context;
29959
+ const deploymentInfoPath = paths.clusterDeploymentInfoPath({ appName, clusterId, channel: context.channel });
29909
29960
  try {
29910
- const deploymentInfoPath = paths.clusterDeploymentInfoPath(appName, clusterId);
29911
29961
  const rawData = await s3Client.getObject(deploymentInfoPath);
29912
29962
  const { deploymentId } = parse2(clusterDeploymentInfo, JSON.parse(rawData));
29913
29963
  return deploymentId;
@@ -29924,7 +29974,7 @@ var InternalServerError = class extends Error {
29924
29974
  };
29925
29975
  async function readDeploymentState(appName, context) {
29926
29976
  const { s3Client } = context;
29927
- const deploymentStatePath = paths.deploymentState(appName);
29977
+ const deploymentStatePath = paths.deploymentState({ appName, channel: context.channel });
29928
29978
  try {
29929
29979
  const rawData = await s3Client.getObject(deploymentStatePath);
29930
29980
  const state2 = parse2(deploymentState, JSON.parse(rawData));
@@ -29936,19 +29986,12 @@ async function readDeploymentState(appName, context) {
29936
29986
  throw error;
29937
29987
  }
29938
29988
  }
29939
- function resolveBundle({ appName, platform: platform2, deploymentId, tag }) {
29940
- const bundleUrlPrefix = paths.bundlePathPrefix(appName, deploymentId);
29989
+ function resolveBundle({ appName, platform: platform2, deploymentId, tag, channel }) {
29990
+ const bundleUrlPrefix = paths.bundlePathPrefix({ appName, deploymentId, channel });
29941
29991
  const extension = "hbc.gz";
29942
29992
  const name = [BUNDLE_BASE_NAME, platform2, tag, extension].filter(Boolean).join(".");
29943
29993
  return `${bundleUrlPrefix}/${name}`;
29944
29994
  }
29945
- var InvalidRequest3 = class extends Error {
29946
- constructor(message) {
29947
- super();
29948
- this.name = "InvalidRequest";
29949
- this.message = message;
29950
- }
29951
- };
29952
29995
  var NotFoundError = class extends Error {
29953
29996
  constructor(message) {
29954
29997
  super();
@@ -29986,8 +30029,8 @@ async function resolveDeploymentIdByGroupId(appName, numericGroupId, context) {
29986
30029
  }
29987
30030
  async function resolveDeploymentIdByCluster(appName, clusterId, context) {
29988
30031
  const { s3Client } = context;
30032
+ const clusterDeploymentInfoPath = paths.clusterDeploymentInfoPath({ appName, clusterId, channel: context.channel });
29989
30033
  try {
29990
- const clusterDeploymentInfoPath = paths.clusterDeploymentInfoPath(appName, clusterId);
29991
30034
  const rawData = await s3Client.getObject(clusterDeploymentInfoPath);
29992
30035
  const deploymentInfo = parse2(clusterDeploymentInfo, JSON.parse(rawData));
29993
30036
  return deploymentInfo.deploymentId;
@@ -29998,9 +30041,94 @@ async function resolveDeploymentIdByCluster(appName, clusterId, context) {
29998
30041
  throw new InternalServerError("resolveDeploymentIdByCluster", error);
29999
30042
  }
30000
30043
  }
30044
+ var registrationSchema = /* @__PURE__ */ object({
30045
+ version: /* @__PURE__ */ literal(1),
30046
+ type: /* @__PURE__ */ union([/* @__PURE__ */ literal("CHANNEL"), /* @__PURE__ */ literal("LEGACY_TAG")])
30047
+ });
30048
+ function isAppName(appName) {
30049
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(appName);
30050
+ }
30051
+ async function readRegistration(options, { s3Client }) {
30052
+ let raw;
30053
+ try {
30054
+ raw = await s3Client.getObject(paths.selectorRegistration(options));
30055
+ } catch (error) {
30056
+ if (error instanceof NoSuchKey) {
30057
+ return void 0;
30058
+ }
30059
+ throw error;
30060
+ }
30061
+ try {
30062
+ return parse2(registrationSchema, JSON.parse(raw));
30063
+ } catch (error) {
30064
+ throw new InternalServerError("Invalid selector registration", error);
30065
+ }
30066
+ }
30067
+ async function resolveChannel(options, context) {
30068
+ if (!isAppName(options.appName) || !isChannelName(options.selector)) {
30069
+ return void 0;
30070
+ }
30071
+ const registration = await readRegistration(options, context);
30072
+ return registration?.type === "CHANNEL" ? options.selector : void 0;
30073
+ }
30074
+ async function registerChannel({ appName, channel }, context) {
30075
+ validateChannel(channel);
30076
+ if (!isAppName(appName)) {
30077
+ throw new InvalidRequest3("Channel registration requires a URL-safe application name");
30078
+ }
30079
+ await claimSelector({ appName, selector: channel }, "CHANNEL", context);
30080
+ }
30081
+ async function reserveLegacyTag(options, context) {
30082
+ if (isAppName(options.appName) && isChannelName(options.selector)) {
30083
+ await claimSelector(options, "LEGACY_TAG", context);
30084
+ }
30085
+ }
30086
+ async function claimSelector(options, type, context) {
30087
+ const { s3Client } = context;
30088
+ let lastConflict;
30089
+ for (let attempt = 0; attempt < 3; attempt++) {
30090
+ const existing = await readRegistration(options, context);
30091
+ if (existing !== void 0) {
30092
+ if (existing.type !== type) {
30093
+ throw new InvalidRequest3(
30094
+ `Selector ${options.selector} for ${options.appName} is already reserved as ${existing.type}`
30095
+ );
30096
+ }
30097
+ return;
30098
+ }
30099
+ if (type === "CHANNEL") {
30100
+ const prefix = paths.appBundlePrefix({ appName: options.appName });
30101
+ for await (const key of s3Client.listObjectKeys(prefix)) {
30102
+ if (["ios", "android"].some((platform2) => key.endsWith(`/bundle.${platform2}.${options.selector}.hbc.gz`))) {
30103
+ throw new InvalidRequest3(
30104
+ `Channel ${options.selector} conflicts with an existing legacy bundle tag for ${options.appName}`
30105
+ );
30106
+ }
30107
+ }
30108
+ }
30109
+ try {
30110
+ await s3Client.putObject(paths.selectorRegistration(options), {
30111
+ Body: JSON.stringify({ version: 1, type }),
30112
+ ContentType: "application/json",
30113
+ CacheControl: "no-store",
30114
+ IfNoneMatch: "*"
30115
+ });
30116
+ return;
30117
+ } catch (error) {
30118
+ if (!(error instanceof S3ServiceException) || ![409, 412].includes(error.$metadata.httpStatusCode ?? 0)) {
30119
+ throw error;
30120
+ }
30121
+ lastConflict = error;
30122
+ }
30123
+ }
30124
+ throw new InternalServerError("Concurrent selector registration could not be completed", lastConflict);
30125
+ }
30001
30126
  async function rollout({ state: state2, appName }, context) {
30002
30127
  const { s3Client } = context;
30003
- await s3Client.putObject(paths.deploymentState(appName), {
30128
+ if (context.channel !== void 0) {
30129
+ await registerChannel({ appName, channel: context.channel }, context);
30130
+ }
30131
+ await s3Client.putObject(paths.deploymentState({ appName, channel: context.channel }), {
30004
30132
  Body: JSON.stringify(state2),
30005
30133
  ContentType: "application/json"
30006
30134
  });
@@ -30008,14 +30136,17 @@ async function rollout({ state: state2, appName }, context) {
30008
30136
  }
30009
30137
  async function rolloutCluster({ appName, deploymentId, clusterId }, context) {
30010
30138
  const { s3Client } = context;
30011
- await s3Client.putObject(paths.clusterDeploymentState(appName, clusterId), {
30139
+ if (context.channel !== void 0) {
30140
+ await registerChannel({ appName, channel: context.channel }, context);
30141
+ }
30142
+ await s3Client.putObject(paths.clusterDeploymentInfoPath({ appName, clusterId, channel: context.channel }), {
30012
30143
  Body: JSON.stringify({ deploymentId }),
30013
30144
  ContentType: "application/json"
30014
30145
  });
30015
30146
  }
30016
30147
  async function writeBundleList(appName, deploymentInfo, context) {
30017
30148
  const { s3Client } = context;
30018
- await s3Client.putObject(paths.bundleList(appName), {
30149
+ await s3Client.putObject(paths.bundleList({ appName, channel: context.channel }), {
30019
30150
  Body: JSON.stringify(deploymentInfo.slice(0, MAX_HISTORY_COUNT)),
30020
30151
  ContentType: "application/json"
30021
30152
  });
@@ -30040,7 +30171,12 @@ function toDeployedAtString(date2) {
30040
30171
  }
30041
30172
  async function uploadBundle({ bundlePath, appName, deploymentId, deployedAt, platform: platform2, tag }, context) {
30042
30173
  const { s3Client } = context;
30043
- const bundlePathKey = resolveBundle({ appName, platform: platform2, deploymentId, tag });
30174
+ const bundlePathKey = resolveBundle({ appName, platform: platform2, deploymentId, tag, channel: context.channel });
30175
+ if (context.channel !== void 0) {
30176
+ await registerChannel({ appName, channel: context.channel }, context);
30177
+ } else if (tag) {
30178
+ await reserveLegacyTag({ appName, selector: tag }, context);
30179
+ }
30044
30180
  await s3Client.putObject(bundlePathKey, {
30045
30181
  Body: fs3.createReadStream(bundlePath),
30046
30182
  Metadata: {
@@ -30057,6 +30193,8 @@ async function uploadBundle({ bundlePath, appName, deploymentId, deployedAt, pla
30057
30193
  return { bundlePathKey, deploymentInfo };
30058
30194
  }
30059
30195
  var DeployManager = {
30196
+ registerChannel,
30197
+ resolveChannel,
30060
30198
  planRollout,
30061
30199
  rollout,
30062
30200
  rolloutCluster,
@@ -30120,6 +30258,14 @@ function createOriginRequestHandler(context) {
30120
30258
  if (appName == null || platform2 == null || groupId == null) {
30121
30259
  throw new InvalidRequest3("invalid request");
30122
30260
  }
30261
+ const channel = await DeployManager.resolveChannel({ appName, selector: suffix }, { s3Client });
30262
+ const tag = channel !== void 0 || suffix === "bundle" ? void 0 : suffix;
30263
+ if (channel !== void 0) {
30264
+ const parts = request2.uri.split("/");
30265
+ if (parts.length !== 5 || !["ios", "android"].includes(parts[1] ?? "") || parts.slice(2).some((part) => !part)) {
30266
+ throw new InvalidRequest3("Expected /<platform>/<app>/<group>/<selector>");
30267
+ }
30268
+ }
30123
30269
  const deploymentId = await DeployManager.resolveDeploymentId(
30124
30270
  {
30125
30271
  appName,
@@ -30127,14 +30273,16 @@ function createOriginRequestHandler(context) {
30127
30273
  allowAccessCluster: context.allowAccessCluster
30128
30274
  },
30129
30275
  {
30130
- s3Client
30276
+ s3Client,
30277
+ channel
30131
30278
  }
30132
30279
  );
30133
30280
  const bundlePath = DeployManager.resolveBundle({
30134
30281
  appName,
30135
30282
  platform: platform2,
30136
30283
  deploymentId,
30137
- tag: suffix !== "bundle" ? suffix : void 0
30284
+ channel,
30285
+ tag
30138
30286
  });
30139
30287
  const absolutePath = `/${bundlePath}`;
30140
30288
  request2.uri = absolutePath;
@@ -30146,6 +30294,9 @@ function createOriginRequestHandler(context) {
30146
30294
  ];
30147
30295
  return request2;
30148
30296
  } catch (error) {
30297
+ if (error instanceof NoSuchKey) {
30298
+ return { status: "404", statusDescription: "Deployment not found" };
30299
+ }
30149
30300
  if (error instanceof NotFoundError) {
30150
30301
  return { status: "404", statusDescription: error.message };
30151
30302
  }
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  DeployManager,
3
3
  InvalidRequest3,
4
+ NoSuchKey,
4
5
  NotFoundError,
5
6
  S3Client2
6
- } from "../chunk-ZBT5SDIQ.js";
7
+ } from "../chunk-GEE3OQDP.js";
7
8
  import "../chunk-V7TRGEV6.js";
8
9
  import "../chunk-3AVBP57V.js";
9
10
  import "../chunk-ZIHRRTTA.js";
@@ -70,6 +71,14 @@ function createOriginRequestHandler(context) {
70
71
  if (appName == null || platform == null || groupId == null) {
71
72
  throw new InvalidRequest3("invalid request");
72
73
  }
74
+ const channel = await DeployManager.resolveChannel({ appName, selector: suffix }, { s3Client });
75
+ const tag = channel !== void 0 || suffix === "bundle" ? void 0 : suffix;
76
+ if (channel !== void 0) {
77
+ const parts = request.uri.split("/");
78
+ if (parts.length !== 5 || !["ios", "android"].includes(parts[1] ?? "") || parts.slice(2).some((part) => !part)) {
79
+ throw new InvalidRequest3("Expected /<platform>/<app>/<group>/<selector>");
80
+ }
81
+ }
73
82
  const deploymentId = await DeployManager.resolveDeploymentId(
74
83
  {
75
84
  appName,
@@ -77,14 +86,16 @@ function createOriginRequestHandler(context) {
77
86
  allowAccessCluster: context.allowAccessCluster
78
87
  },
79
88
  {
80
- s3Client
89
+ s3Client,
90
+ channel
81
91
  }
82
92
  );
83
93
  const bundlePath = DeployManager.resolveBundle({
84
94
  appName,
85
95
  platform,
86
96
  deploymentId,
87
- tag: suffix !== "bundle" ? suffix : void 0
97
+ channel,
98
+ tag
88
99
  });
89
100
  const absolutePath = `/${bundlePath}`;
90
101
  request.uri = absolutePath;
@@ -96,6 +107,9 @@ function createOriginRequestHandler(context) {
96
107
  ];
97
108
  return request;
98
109
  } catch (error) {
110
+ if (error instanceof NoSuchKey) {
111
+ return { status: "404", statusDescription: "Deployment not found" };
112
+ }
99
113
  if (error instanceof NotFoundError) {
100
114
  return { status: "404", statusDescription: error.message };
101
115
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  InvalidRequest3
3
- } from "../chunk-ZBT5SDIQ.js";
3
+ } from "../chunk-GEE3OQDP.js";
4
4
  import "../chunk-V7TRGEV6.js";
5
5
  import "../chunk-3AVBP57V.js";
6
6
  import "../chunk-ZIHRRTTA.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@granite-js/pulumi-aws",
3
3
  "type": "module",
4
- "version": "2.5.2",
4
+ "version": "2.5.3",
5
5
  "main": "./dist/index.cjs",
6
6
  "types": "./dist/index.d.ts",
7
7
  "module": "./dist/index.js",
@@ -65,8 +65,8 @@
65
65
  },
66
66
  "devDependencies": {
67
67
  "@aws-sdk/client-cloudfront": "^3.812.0",
68
- "@granite-js/deployment-manager": "2.5.2",
69
- "@granite-js/utils": "2.5.2",
68
+ "@granite-js/deployment-manager": "2.5.3",
69
+ "@granite-js/utils": "2.5.3",
70
70
  "@pulumi/aws": "^6.80.0",
71
71
  "@pulumi/pulumi": "^3.170.0",
72
72
  "@types/aws-lambda": "^8.10.149",