@awsless/cli 0.0.46-next.3 → 0.0.46-next.6

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/bin.js CHANGED
@@ -187,7 +187,7 @@ import {
187
187
  string,
188
188
  updateItem
189
189
  } from "@awsless/dynamodb";
190
- import { execSync } from "child_process";
190
+ import { isAfter, subHours } from "date-fns";
191
191
  import { userInfo as userInfo2 } from "os";
192
192
 
193
193
  // src/formation/cloudfront-kvs.ts
@@ -290,17 +290,16 @@ var getRouteStoreArn = async (cloudfront, name) => {
290
290
  return;
291
291
  }
292
292
  };
293
- var stageRoutes = async (kvs, state2, prior) => {
293
+ var stageRoutes = async (kvs, state2) => {
294
294
  const routes = sortRoutes(state2.routes);
295
295
  const table2 = getRouteTableId(routes);
296
- const priorTable = prior ? getRouteTableId(sortRoutes(prior.routes)) : void 0;
297
296
  const deployment = {
298
297
  id: state2.deploymentId,
299
298
  table: table2,
300
299
  functionVersion: state2.functionVersion
301
300
  };
302
301
  const mutations = [
303
- ...priorTable === table2 ? [] : getTableMutations(table2, routes),
302
+ ...getTableMutations(table2, routes),
304
303
  {
305
304
  type: "put",
306
305
  key: `${DEPLOY_KEY_PREFIX}${deployment.id}`,
@@ -324,9 +323,7 @@ var createCloudFrontKvsProvider = ({ credentials, region }) => {
324
323
  return stageRoutes(kvs, routeDeploymentInputSchema.parse(props.state));
325
324
  },
326
325
  async updateResource(props) {
327
- const state2 = routeDeploymentInputSchema.parse(props.proposedState);
328
- const prior = routeDeploymentInputSchema.parse(props.priorState);
329
- return stageRoutes(kvs, state2, prior.storeArn === state2.storeArn ? prior : void 0);
326
+ return stageRoutes(kvs, routeDeploymentInputSchema.parse(props.proposedState));
330
327
  }
331
328
  }
332
329
  });
@@ -359,6 +356,42 @@ var pruneStoreDeployments = async (kvs, storeArn, deploymentIds) => {
359
356
  });
360
357
  };
361
358
 
359
+ // src/util/git.ts
360
+ import { execFileSync, execSync } from "child_process";
361
+ var git = (command) => {
362
+ try {
363
+ return execSync(`git ${command}`, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim();
364
+ } catch {
365
+ return;
366
+ }
367
+ };
368
+ var checkedOutBranch = () => {
369
+ return git("rev-parse --abbrev-ref HEAD");
370
+ };
371
+ var currentBranch = () => {
372
+ const branch = checkedOutBranch();
373
+ if (branch && branch !== "HEAD") {
374
+ return branch;
375
+ }
376
+ return process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_REF_NAME || process.env.BRANCH_NAME || branch;
377
+ };
378
+ var currentCommit = () => {
379
+ return git("rev-parse HEAD");
380
+ };
381
+ var currentCommitMessage = () => {
382
+ return git("log -1 --pretty=%s");
383
+ };
384
+ var isCommitMerged = (commit, branch) => {
385
+ try {
386
+ execFileSync("git", ["merge-base", "--is-ancestor", commit, branch], {
387
+ stdio: ["ignore", "pipe", "ignore"]
388
+ });
389
+ return true;
390
+ } catch {
391
+ return false;
392
+ }
393
+ };
394
+
362
395
  // src/util/lambda.ts
363
396
  import {
364
397
  CreateAliasCommand,
@@ -1203,7 +1236,7 @@ var pullRemoteState = async (app, stateBackend) => {
1203
1236
  await rm(file);
1204
1237
  }
1205
1238
  } else {
1206
- await writeFile(file, JSON.stringify(state2, void 0, 2));
1239
+ await writeFile(file, JSON.stringify(state2, void 0, 2), { mode: 384 });
1207
1240
  }
1208
1241
  };
1209
1242
  var pushRemoteState = async (app, stateBackend) => {
@@ -1217,16 +1250,6 @@ var pushRemoteState = async (app, stateBackend) => {
1217
1250
  var slugifyBranch = (branch) => {
1218
1251
  return (branch ?? "").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "local";
1219
1252
  };
1220
- var git = (command) => {
1221
- try {
1222
- return execSync(`git ${command}`, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim();
1223
- } catch {
1224
- return;
1225
- }
1226
- };
1227
- var isCommitMerged = (commit, branch) => {
1228
- return git(`merge-base --is-ancestor ${JSON.stringify(commit)} ${JSON.stringify(branch)}`) !== void 0;
1229
- };
1230
1253
  var table = define("awsless-deployments", {
1231
1254
  hash: "appId",
1232
1255
  sort: "id",
@@ -1246,10 +1269,13 @@ var table = define("awsless-deployments", {
1246
1269
  var deploymentsTable = table;
1247
1270
  var latestBranchDeployment = async (client, appId, branch) => {
1248
1271
  const items = await listDeployments(client, appId, branch);
1249
- return items.reduce((latest, item) => (latest?.seq ?? 0) >= item.seq ? latest : item, void 0);
1272
+ return items.reduce(
1273
+ (latest, item) => (latest?.seq ?? 0) >= item.seq ? latest : item,
1274
+ void 0
1275
+ );
1250
1276
  };
1251
1277
  var claimDeployment = async (props) => {
1252
- const branch = slugifyBranch(git("rev-parse --abbrev-ref HEAD"));
1278
+ const branch = slugifyBranch(currentBranch());
1253
1279
  while (true) {
1254
1280
  const latest = await latestBranchDeployment(props.client, props.appId, branch);
1255
1281
  const seq = (latest?.seq ?? 0) + 1;
@@ -1260,8 +1286,8 @@ var claimDeployment = async (props) => {
1260
1286
  seq,
1261
1287
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1262
1288
  user: userInfo2().username,
1263
- commit: git("rev-parse HEAD"),
1264
- message: git("log -1 --pretty=%s")
1289
+ commit: currentCommit(),
1290
+ message: currentCommitMessage()
1265
1291
  };
1266
1292
  try {
1267
1293
  await putItem(table, deployment, {
@@ -1278,7 +1304,7 @@ var claimDeployment = async (props) => {
1278
1304
  }
1279
1305
  };
1280
1306
  var currentDeployment = async (client, appId) => {
1281
- return latestBranchDeployment(client, appId, slugifyBranch(git("rev-parse --abbrev-ref HEAD")));
1307
+ return latestBranchDeployment(client, appId, slugifyBranch(currentBranch()));
1282
1308
  };
1283
1309
  var getDeployment = async (client, appId, id) => {
1284
1310
  return getItem(table, { appId, id }, { client });
@@ -1328,6 +1354,10 @@ var markPromoted = async (client, appId, id) => {
1328
1354
  var removeDeployment = async (client, appId, id) => {
1329
1355
  await deleteItem(table, { appId, id }, { client });
1330
1356
  };
1357
+ var BUSY_WINDOW_HOURS = 24;
1358
+ var isDeploymentBusy = (item, now = /* @__PURE__ */ new Date()) => {
1359
+ return !item.functionVersion && isAfter(new Date(item.createdAt), subHours(now, BUSY_WINDOW_HOURS));
1360
+ };
1331
1361
  var selectPrunableDeployments = (items, liveId, options) => {
1332
1362
  const rollbackTarget = items.filter((item) => item.promotedAt && item.id !== liveId).sort((a, b) => b.promotedAt.localeCompare(a.promotedAt))[0];
1333
1363
  const keep = Math.max(1, Number(options.keep) || 10);
@@ -1335,16 +1365,15 @@ var selectPrunableDeployments = (items, liveId, options) => {
1335
1365
  const keptMain = new Set(
1336
1366
  items.filter((item) => item.branch === mainSlug && item.functionVersion).map((item) => item.seq).sort((a, b) => b - a).slice(0, keep)
1337
1367
  );
1338
- const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
1339
1368
  return items.filter((item) => {
1340
- if (item.id === liveId || item.id === rollbackTarget?.id) {
1369
+ if (item.id === liveId || item.id === rollbackTarget?.id || isDeploymentBusy(item)) {
1341
1370
  return false;
1342
1371
  }
1343
1372
  if (options.branch) {
1344
1373
  return item.branch === slugifyBranch(options.branch);
1345
1374
  }
1346
1375
  if (!item.functionVersion) {
1347
- return item.createdAt < dayAgo;
1376
+ return true;
1348
1377
  }
1349
1378
  if (item.branch === mainSlug) {
1350
1379
  return !keptMain.has(item.seq);
@@ -1494,7 +1523,10 @@ var promoteDeployment = async (props) => {
1494
1523
  ];
1495
1524
  const failures = (await Promise.allSettled(rollback2)).filter((result) => result.status === "rejected").map((result) => result.reason);
1496
1525
  if (failures.length > 0) {
1497
- throw new AggregateError([error, ...failures], `Deployment promotion failed and couldn't be fully reverted.`);
1526
+ throw new AggregateError(
1527
+ [error, ...failures],
1528
+ `Deployment promotion failed and couldn't be fully reverted.`
1529
+ );
1498
1530
  }
1499
1531
  throw error;
1500
1532
  }
@@ -1956,7 +1988,7 @@ var CodeSchema = z14.union([
1956
1988
  var FnSchema = z14.object({
1957
1989
  code: CodeSchema,
1958
1990
  handler: HandlerSchema.optional()
1959
- });
1991
+ }).strict();
1960
1992
  var FunctionSchema = z14.union([
1961
1993
  LocalFileSchema.transform((code) => ({
1962
1994
  code
@@ -2230,7 +2262,7 @@ var ErrorResponseSchema = z20.union([
2230
2262
  minTTL: MinTTLSchema.optional()
2231
2263
  })
2232
2264
  ]).optional();
2233
- var RouteSchema = z20.string().regex(/^\//, "Route must start with a slash (/)");
2265
+ var RouteSchema = z20.string().regex(/^\//, "Route must start with a slash (/)").regex(/^\/([^/*.]+)?$/, 'Router paths mount a single segment without dots, like "/api".');
2234
2266
  var VisibilitySchema = z20.boolean().default(false).describe("Whether to enable CloudWatch metrics for the WAF rule.");
2235
2267
  var WafSettingsSchema = z20.object({
2236
2268
  rateLimiter: z20.object({
@@ -2729,8 +2761,8 @@ var AppSchema = z29.object({
2729
2761
  layers: LayerSchema,
2730
2762
  router: RouterDefaultSchema
2731
2763
  // dataRetention: z.boolean().describe('Configure how your resources are handled on delete.').default(false),
2732
- }).default({}).describe("Default properties")
2733
- });
2764
+ }).strict().default({}).describe("Default properties")
2765
+ }).strict();
2734
2766
 
2735
2767
  // src/config/stack.ts
2736
2768
  import { z as z45 } from "zod";
@@ -2786,7 +2818,7 @@ var CommandsSchema = z31.record(ResourceIdSchema, CommandSchema).optional().desc
2786
2818
 
2787
2819
  // src/feature/config/schema.ts
2788
2820
  import { z as z32 } from "zod";
2789
- var ConfigNameSchema = z32.string().regex(/[a-z0-9\-]/g, "Invalid config name");
2821
+ var ConfigNameSchema = z32.string().regex(/^[a-z0-9-]+$/, "Invalid config name");
2790
2822
  var ConfigsSchema = z32.array(ConfigNameSchema).optional().describe("Define the config values for your stack.");
2791
2823
 
2792
2824
  // src/feature/cron/schema/index.ts
@@ -3372,7 +3404,7 @@ var StackSchema = z45.object({
3372
3404
  images: ImagesSchema,
3373
3405
  icons: IconsSchema,
3374
3406
  metrics: MetricsSchema
3375
- });
3407
+ }).strict();
3376
3408
 
3377
3409
  // src/config/load/read.ts
3378
3410
  import { readFile as readFile3 } from "fs/promises";
@@ -3831,7 +3863,7 @@ import { readdir as readdir2, readFile as readFile7, writeFile as writeFile4 } f
3831
3863
  import { join as join10 } from "path";
3832
3864
 
3833
3865
  // src/build/index.ts
3834
- import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
3866
+ import { mkdir as mkdir2, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3835
3867
  import { dirname as dirname4, join as join7 } from "path";
3836
3868
 
3837
3869
  // src/util/timer.ts
@@ -3873,6 +3905,7 @@ var build = (type, name, builder, props) => {
3873
3905
  cached: true
3874
3906
  };
3875
3907
  }
3908
+ await rm2(cacheFile, { force: true });
3876
3909
  const time = createTimer();
3877
3910
  const meta = await callback(async (file, data2) => {
3878
3911
  const path = getBuildPath(type, name, file);
@@ -3925,7 +3958,7 @@ var zipFiles = (files) => {
3925
3958
  import { generateFileHash } from "@awsless/ts-file-cache";
3926
3959
  import { kebabCase as kebabCase5 } from "change-case";
3927
3960
  import { createHash as createHash5 } from "crypto";
3928
- import { readFile as readFile6, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3961
+ import { readFile as readFile6, rm as rm4, writeFile as writeFile3 } from "fs/promises";
3929
3962
  import { dirname as dirname5, join as join9 } from "path";
3930
3963
  import { fileURLToPath } from "url";
3931
3964
 
@@ -3937,13 +3970,13 @@ var formatByteSize = (size) => {
3937
3970
  };
3938
3971
 
3939
3972
  // src/util/temp.ts
3940
- import { mkdir as mkdir3, readdir, rm as rm2 } from "fs/promises";
3973
+ import { mkdir as mkdir3, readdir, rm as rm3 } from "fs/promises";
3941
3974
  import { join as join8 } from "path";
3942
3975
  var createTempFolder = async (name) => {
3943
3976
  const path = join8(directories.temp, name);
3944
3977
  await mkdir3(join8(directories.temp, name), { recursive: true });
3945
3978
  process.on("SIGTERM", async () => {
3946
- await rm2(path, { recursive: true });
3979
+ await rm3(path, { recursive: true });
3947
3980
  });
3948
3981
  return {
3949
3982
  path,
@@ -3951,7 +3984,7 @@ var createTempFolder = async (name) => {
3951
3984
  return readdir(path, { recursive: true });
3952
3985
  },
3953
3986
  async delete() {
3954
- await rm2(path, { recursive: true });
3987
+ await rm3(path, { recursive: true });
3955
3988
  }
3956
3989
  };
3957
3990
  };
@@ -3982,6 +4015,9 @@ var bundleTypeScriptWithRolldown = async ({
3982
4015
  moduleSideEffects: (id, isExternal) => isExternal ? external?.includes(id) === true : id.startsWith(`${directories.root}/`) && !id.includes("/node_modules/")
3983
4016
  },
3984
4017
  onwarn: (error) => {
4018
+ if (error.code === "UNRESOLVED_IMPORT") {
4019
+ throw new ExpectedError(error.message);
4020
+ }
3985
4021
  debugError(error.message);
3986
4022
  },
3987
4023
  plugins: [
@@ -4048,14 +4084,17 @@ var bundleTypeScriptWithRolldown = async ({
4048
4084
  files
4049
4085
  };
4050
4086
  };
4051
- var TEST_ONLY_MODULES = ["dynamo-db-local", "@awsless/dynamodb-server", "redis-memory-server"];
4087
+ var TEST_ONLY_MODULES = ["dynamo-db-local", "@awsless/dynamodb-server", "redis-memory-server", "aws-sdk-vitest-mock"];
4088
+ var findPackage = (id, names) => {
4089
+ return names.find((name) => id.includes(`/${name.replace(/^@[^/]+\//, "")}/`));
4090
+ };
4052
4091
  var assertNoTestOnlyModules = (output) => {
4053
4092
  for (const item of output) {
4054
4093
  if (item.type !== "chunk") {
4055
4094
  continue;
4056
4095
  }
4057
4096
  for (const id of item.moduleIds ?? []) {
4058
- const found = TEST_ONLY_MODULES.find((name) => id.includes(`/node_modules/${name}/`));
4097
+ const found = findPackage(id, TEST_ONLY_MODULES);
4059
4098
  if (found) {
4060
4099
  throw new Error(
4061
4100
  `The test-only package "${found}" was bundled into a production build through "${id}". Remove the import from the handler, or keep the package tree-shakeable.`
@@ -4086,7 +4125,7 @@ var registerBundleFunction = (ctx, routeKey, props) => {
4086
4125
  };
4087
4126
  var buildBundle = (props) => {
4088
4127
  return async (build3, { workspace }) => {
4089
- const runtime = props.runtime ?? join9(dirname5(fileURLToPath(import.meta.url)), "/handlers/bundle.mjs");
4128
+ const runtime = props.runtime ?? join9(dirname5(fileURLToPath(import.meta.url)), "/handlers/bundle.js");
4090
4129
  const handlers = [...props.handlers].sort((a, b) => a.routeKey.localeCompare(b.routeKey));
4091
4130
  const entries = handlers.map(({ routeKey, file, exportName }) => {
4092
4131
  const virtualFile = JSON.stringify(`${file}?awsless-route=${encodeURIComponent(routeKey)}`);
@@ -4135,7 +4174,7 @@ ${entries.join("\n")}
4135
4174
  importAsString: importAsString2.length > 0 ? importAsString2 : void 0
4136
4175
  });
4137
4176
  await temp.delete();
4138
- await rm3(getBuildPath("bundle", props.name, "files"), { recursive: true, force: true });
4177
+ await rm4(getBuildPath("bundle", props.name, "files"), { recursive: true, force: true });
4139
4178
  await Promise.all([
4140
4179
  write("HASH", bundle.hash),
4141
4180
  ...bundle.files.map((file) => write(`files/${file.name}`, file.code)),
@@ -4657,7 +4696,7 @@ var SsmStore = class {
4657
4696
  debug("Value:", color.info(value));
4658
4697
  await this.client.send(
4659
4698
  new PutParameterCommand({
4660
- Type: ParameterType.STRING,
4699
+ Type: ParameterType.SECURE_STRING,
4661
4700
  Name: this.getName(name),
4662
4701
  Value: value,
4663
4702
  Overwrite: true
@@ -5093,10 +5132,12 @@ var domainFeature = defineFeature({
5093
5132
  }
5094
5133
  }
5095
5134
  ctx.addGlobalPermission({
5096
- actions: ["ses:*"],
5135
+ actions: ["ses:SendEmail", "ses:SendRawEmail"],
5097
5136
  resources: [
5098
- // `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/*`,
5099
- "*"
5137
+ `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/*`,
5138
+ // Sending through the app configuration set is authorized against
5139
+ // its own ARN, not just the identity.
5140
+ `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:configuration-set/${ctx.app.name}`
5100
5141
  ]
5101
5142
  });
5102
5143
  }
@@ -5186,7 +5227,9 @@ var functionFeature = defineFeature({
5186
5227
  // 'lambda:ListFunctions',
5187
5228
  // 'lambda:GetFunction',
5188
5229
  ],
5189
- resources: [`arn:aws:lambda:*:*:function:${ctx.appConfig.name}--*`]
5230
+ resources: [
5231
+ `arn:aws:lambda:${ctx.appConfig.region}:${ctx.accountId}:function:${ctx.appConfig.name}--*`
5232
+ ]
5190
5233
  });
5191
5234
  },
5192
5235
  onStack(ctx) {
@@ -5228,7 +5271,7 @@ var onErrorLogFeature = defineFeature({
5228
5271
  const consumerRoute = formatRouteKey(ctx.app.name, "on-error-log", "consumer");
5229
5272
  bundle.addHandler({
5230
5273
  routeKey: handlerRoute,
5231
- file: join11(dirname6(fileURLToPath2(import.meta.url)), "/handlers/on-error-log.mjs"),
5274
+ file: join11(dirname6(fileURLToPath2(import.meta.url)), "/handlers/on-error-log.js"),
5232
5275
  exportName: "default"
5233
5276
  });
5234
5277
  bundle.addEnv(formatRouteEnvName(handlerRoute, "CONSUMER"), consumerRoute);
@@ -5453,7 +5496,7 @@ var onFailureFeature = defineFeature({
5453
5496
  const consumer = props.consumer;
5454
5497
  bundle.addHandler({
5455
5498
  routeKey: normalizerRoute,
5456
- file: join12(dirname7(fileURLToPath3(import.meta.url)), "/handlers/on-failure.mjs"),
5499
+ file: join12(dirname7(fileURLToPath3(import.meta.url)), "/handlers/on-failure.js"),
5457
5500
  exportName: "default"
5458
5501
  });
5459
5502
  registerBundleFunction(ctx, consumerRoute, consumer);
@@ -5608,7 +5651,7 @@ var createPubSubService = (parentGroup, ctx, id, props, inputs) => {
5608
5651
  });
5609
5652
  const shortName = shortId(`${ctx.app.name}:pubsub:${id}:${ctx.appId}`);
5610
5653
  const image2 = "public.ecr.aws/aws-cli/aws-cli:arm64";
5611
- const bundleFile = join14(__dirname, "handlers/pubsub-server.mjs");
5654
+ const bundleFile = join14(__dirname, "handlers/pubsub-server.js");
5612
5655
  ctx.registerBuild("pubsub", name, async (build3) => {
5613
5656
  const hash = createHash8("sha1").update(await readFile9(bundleFile)).digest("hex");
5614
5657
  const fingerprint = `${hash}-${ARCHITECTURE}`;
@@ -5709,7 +5752,8 @@ var createPubSubService = (parentGroup, ctx, id, props, inputs) => {
5709
5752
  Statement: list3.map((statement) => ({
5710
5753
  Effect: pascalCase2(statement.effect ?? "allow"),
5711
5754
  Action: statement.actions,
5712
- Resource: statement.resources
5755
+ Resource: statement.resources,
5756
+ Condition: statement.conditions
5713
5757
  }))
5714
5758
  })
5715
5759
  );
@@ -6200,7 +6244,7 @@ var pubsubFeature = defineFeature({
6200
6244
  const publisherRouteKey = formatRouteKey(ctx.app.name, "pubsub", `${id}-publisher`);
6201
6245
  bundle.addHandler({
6202
6246
  routeKey: publisherRouteKey,
6203
- file: join15(__dirname2, "/handlers/pubsub-publisher.mjs"),
6247
+ file: join15(__dirname2, "/handlers/pubsub-publisher.js"),
6204
6248
  exportName: "default"
6205
6249
  });
6206
6250
  bundle.addEnv(formatRouteEnvName3(publisherRouteKey, "REDIS_HOST"), redisHost);
@@ -6549,7 +6593,7 @@ var rpcFeature = defineFeature({
6549
6593
  const serverRouteKey = formatRouteKey(ctx.app.name, "rpc", id);
6550
6594
  bundle.addHandler({
6551
6595
  routeKey: serverRouteKey,
6552
- file: join16(dirname10(fileURLToPath6(import.meta.url)), "/handlers/rpc.mjs"),
6596
+ file: join16(dirname10(fileURLToPath6(import.meta.url)), "/handlers/rpc.js"),
6553
6597
  exportName: "default"
6554
6598
  });
6555
6599
  bundle.addEnv(
@@ -7076,9 +7120,12 @@ var siteFeature = defineFeature({
7076
7120
  new Response(instance.stderr).text(),
7077
7121
  instance.exited
7078
7122
  ]);
7079
- if (instance.exitCode !== null && instance.exitCode > 0) {
7080
- throw new ExpectedError(`Site build failed:
7081
- ${(errors.trim() || output.trim()).slice(-2e3)}`);
7123
+ if (instance.exitCode !== 0) {
7124
+ const reason = instance.signalCode ? ` (${instance.signalCode})` : "";
7125
+ throw new ExpectedError(
7126
+ `Site build failed${reason}:
7127
+ ${(errors.trim() || output.trim()).slice(-2e3)}`
7128
+ );
7082
7129
  }
7083
7130
  await write("HASH", fingerprint);
7084
7131
  return {
@@ -7919,7 +7966,7 @@ var imageFeature = defineFeature({
7919
7966
  const serverRouteKey = formatRouteKey(ctx.stack.name, "image", id);
7920
7967
  bundle.addHandler({
7921
7968
  routeKey: serverRouteKey,
7922
- file: join20(dirname12(fileURLToPath7(import.meta.url)), "/handlers/image.mjs"),
7969
+ file: join20(dirname12(fileURLToPath7(import.meta.url)), "/handlers/image.js"),
7923
7970
  exportName: "default",
7924
7971
  external: ["sharp"]
7925
7972
  });
@@ -8018,7 +8065,7 @@ var iconFeature = defineFeature({
8018
8065
  const serverRouteKey = formatRouteKey(ctx.stack.name, "icon", id);
8019
8066
  bundle.addHandler({
8020
8067
  routeKey: serverRouteKey,
8021
- file: join21(dirname13(fileURLToPath8(import.meta.url)), "/handlers/icon.mjs"),
8068
+ file: join21(dirname13(fileURLToPath8(import.meta.url)), "/handlers/icon.js"),
8022
8069
  exportName: "default"
8023
8070
  });
8024
8071
  addRoutes({
@@ -8277,7 +8324,8 @@ var createFargateJob = (parentGroup, ctx, ns, id, local) => {
8277
8324
  Statement: list3.map((statement) => ({
8278
8325
  Effect: pascalCase3(statement.effect ?? "allow"),
8279
8326
  Action: statement.actions,
8280
- Resource: statement.resources
8327
+ Resource: statement.resources,
8328
+ Condition: statement.conditions
8281
8329
  }))
8282
8330
  })
8283
8331
  );
@@ -8775,7 +8823,8 @@ var createFargateTask = (parentGroup, ctx, ns, id, local) => {
8775
8823
  Statement: list3.map((statement) => ({
8776
8824
  Effect: pascalCase4(statement.effect ?? "allow"),
8777
8825
  Action: statement.actions,
8778
- Resource: statement.resources
8826
+ Resource: statement.resources,
8827
+ Condition: statement.conditions
8779
8828
  }))
8780
8829
  })
8781
8830
  );
@@ -9247,12 +9296,15 @@ var metricFeature = defineFeature({
9247
9296
  });
9248
9297
 
9249
9298
  // src/feature/router/index.ts
9250
- import { days as days10, seconds as seconds6, toSeconds as toSeconds12, years } from "@awsless/duration";
9299
+ import { days as days10, seconds as seconds7, toSeconds as toSeconds13, years } from "@awsless/duration";
9251
9300
  import { Group as Group29 } from "@terraforge/core";
9252
9301
  import { aws as aws30 } from "@terraforge/aws";
9253
9302
  import { camelCase as camelCase9, constantCase as constantCase15 } from "change-case";
9254
9303
 
9255
9304
  // src/feature/router/router-code.ts
9305
+ import { minutes as minutes8, seconds as seconds6, toSeconds as toSeconds12 } from "@awsless/duration";
9306
+ var ORIGIN_READ_TIMEOUT = toSeconds12(minutes8(2));
9307
+ var ORIGIN_CONNECTION_TIMEOUT = toSeconds12(seconds6(10));
9256
9308
  var getViewerRequestFunctionCode = (props) => {
9257
9309
  return CODE(
9258
9310
  [
@@ -9447,6 +9499,11 @@ function isValidRoute(route, method) {
9447
9499
  }
9448
9500
 
9449
9501
  async function findRoute(path, method, prefix) {
9502
+ // only route selection is normalized, the forwarded uri stays untouched
9503
+ if (path.length > 1 && path.slice(-1) === '/') {
9504
+ path = path.slice(0, -1);
9505
+ }
9506
+
9450
9507
  const store = cf.kvs();
9451
9508
  const keys = getPossibleRouteKeys(path);
9452
9509
 
@@ -9528,13 +9585,12 @@ function setS3Origin(route) {
9528
9585
  function setLambdaOrigin(route) {
9529
9586
  const config = getRequestOriginConfig(route);
9530
9587
 
9531
- // CloudFront caps the origin response timeout at 60s without a quota increase.
9532
9588
  if(typeof config.timeouts.readTimeout !== 'number') {
9533
- config.timeouts.readTimeout = 120;
9589
+ config.timeouts.readTimeout = ${ORIGIN_READ_TIMEOUT};
9534
9590
  }
9535
9591
 
9536
9592
  if(typeof config.timeouts.connectionTimeout !== 'number') {
9537
- config.timeouts.connectionTimeout = 10;
9593
+ config.timeouts.connectionTimeout = ${ORIGIN_CONNECTION_TIMEOUT};
9538
9594
  }
9539
9595
 
9540
9596
  cf.updateRequestOrigin(Object.assign(config, {
@@ -9610,6 +9666,8 @@ async function handler(event) {
9610
9666
 
9611
9667
  if(route.forwardHost && headers.host && headers.host.value) {
9612
9668
  headers['x-forwarded-host'] = { value: headers.host.value };
9669
+ } else {
9670
+ delete headers['x-forwarded-host'];
9613
9671
  }
9614
9672
 
9615
9673
  headers['x-origin'] = { value: route.domainName };
@@ -9698,9 +9756,9 @@ var routerFeature = defineFeature({
9698
9756
  });
9699
9757
  const cache = new aws30.cloudfront.CachePolicy(group, "cache", {
9700
9758
  name,
9701
- minTtl: toSeconds12(seconds6(0)),
9702
- maxTtl: toSeconds12(days10(365)),
9703
- defaultTtl: toSeconds12(days10(0)),
9759
+ minTtl: toSeconds13(seconds7(0)),
9760
+ maxTtl: toSeconds13(days10(365)),
9761
+ defaultTtl: toSeconds13(days10(0)),
9704
9762
  parametersInCacheKeyAndForwardedToOrigin: {
9705
9763
  enableAcceptEncodingBrotli: true,
9706
9764
  enableAcceptEncodingGzip: true,
@@ -9753,7 +9811,7 @@ var routerFeature = defineFeature({
9753
9811
  name,
9754
9812
  corsConfig: {
9755
9813
  originOverride: props.cors?.override ?? true,
9756
- accessControlMaxAgeSec: toSeconds12(props.cors?.maxAge ?? years(1)),
9814
+ accessControlMaxAgeSec: toSeconds13(props.cors?.maxAge ?? years(1)),
9757
9815
  accessControlAllowHeaders: { items: props.cors?.headers ?? ["*"] },
9758
9816
  accessControlAllowMethods: { items: props.cors?.methods ?? ["ALL"] },
9759
9817
  accessControlAllowOrigins: { items: props.cors?.origins ?? ["*"] },
@@ -9778,7 +9836,7 @@ var routerFeature = defineFeature({
9778
9836
  strictTransportSecurity: {
9779
9837
  override: true,
9780
9838
  preload: true,
9781
- accessControlMaxAgeSec: toSeconds12(years(1)),
9839
+ accessControlMaxAgeSec: toSeconds13(years(1)),
9782
9840
  includeSubdomains: true
9783
9841
  },
9784
9842
  xssProtection: {
@@ -9798,7 +9856,7 @@ var routerFeature = defineFeature({
9798
9856
  rateBasedStatement: {
9799
9857
  limit: wafSettingsConfig.rateLimiter.limit,
9800
9858
  aggregateKeyType: "IP",
9801
- evaluationWindowSec: toSeconds12(wafSettingsConfig.rateLimiter.window)
9859
+ evaluationWindowSec: toSeconds13(wafSettingsConfig.rateLimiter.window)
9802
9860
  }
9803
9861
  },
9804
9862
  action: {
@@ -9888,12 +9946,12 @@ var routerFeature = defineFeature({
9888
9946
  rule: wafRules,
9889
9947
  captchaConfig: {
9890
9948
  immunityTimeProperty: {
9891
- immunityTime: toSeconds12(wafSettingsConfig.captchaImmunityTime)
9949
+ immunityTime: toSeconds13(wafSettingsConfig.captchaImmunityTime)
9892
9950
  }
9893
9951
  },
9894
9952
  challengeConfig: {
9895
9953
  immunityTimeProperty: {
9896
- immunityTime: toSeconds12(wafSettingsConfig.challengeImmunityTime)
9954
+ immunityTime: toSeconds13(wafSettingsConfig.challengeImmunityTime)
9897
9955
  }
9898
9956
  },
9899
9957
  visibilityConfig: {
@@ -9949,7 +10007,7 @@ var routerFeature = defineFeature({
9949
10007
  }
9950
10008
  return {
9951
10009
  errorCode: Number(errorCode),
9952
- errorCachingMinTtl: item.minTTL ? toSeconds12(item.minTTL) : void 0,
10010
+ errorCachingMinTtl: item.minTTL ? toSeconds13(item.minTTL) : void 0,
9953
10011
  responseCode: item.statusCode?.toString() ?? errorCode,
9954
10012
  responsePagePath: item.path
9955
10013
  };
@@ -10032,7 +10090,7 @@ var routerFeature = defineFeature({
10032
10090
  }
10033
10091
  return {
10034
10092
  errorCode: Number(errorCode),
10035
- errorCachingMinTtl: item.minTTL ? toSeconds12(item.minTTL) : void 0,
10093
+ errorCachingMinTtl: item.minTTL ? toSeconds13(item.minTTL) : void 0,
10036
10094
  responseCode: item.statusCode ?? Number(errorCode),
10037
10095
  responsePagePath: item.path
10038
10096
  };
@@ -10452,6 +10510,12 @@ var logo = () => {
10452
10510
  var layout = async (command, cb) => {
10453
10511
  console.log();
10454
10512
  log9.intro(`${logo()} ${color.line(command)}`);
10513
+ let completed = false;
10514
+ process.on("exit", (code) => {
10515
+ if (code === 0 && !completed) {
10516
+ process.exitCode = 130;
10517
+ }
10518
+ });
10455
10519
  try {
10456
10520
  const options = program.optsWithGlobals();
10457
10521
  const appConfig = await loadAppConfig(options);
@@ -10466,9 +10530,11 @@ var layout = async (command, cb) => {
10466
10530
  appConfig,
10467
10531
  stackConfigs
10468
10532
  });
10533
+ completed = true;
10469
10534
  log9.outro(result ?? void 0);
10470
10535
  process.exit(0);
10471
10536
  } catch (error) {
10537
+ completed = true;
10472
10538
  playErrorSound();
10473
10539
  logError(error);
10474
10540
  log9.outro();
@@ -10852,6 +10918,7 @@ var buildAssets = async (builders, stackFilters, showResult = false) => {
10852
10918
  if (filteredBuilders.length === 0) {
10853
10919
  return;
10854
10920
  }
10921
+ filteredBuilders.sort((a, b) => Number(a.type === "bundle") - Number(b.type === "bundle"));
10855
10922
  const results = [];
10856
10923
  await log10.task({
10857
10924
  initialMessage: `Building assets...`,
@@ -12554,9 +12621,12 @@ var test = (program2) => {
12554
12621
  if (tests.length === 0) {
12555
12622
  return "No tests found.";
12556
12623
  }
12557
- await runTests(tests, stacks, options?.filters, {
12624
+ const passed = await runTests(tests, stacks, options?.filters, {
12558
12625
  showLogs: true
12559
12626
  });
12627
+ if (!passed) {
12628
+ throw new Cancelled();
12629
+ }
12560
12630
  return "All tests finished.";
12561
12631
  });
12562
12632
  });
@@ -12781,14 +12851,14 @@ var parseJsonLog = (message) => {
12781
12851
  json = JSON.parse(message);
12782
12852
  } catch (error) {
12783
12853
  }
12784
- if ("level" in json && typeof json.level === "string" && "timestamp" in json && typeof json.timestamp === "string" && "message" in json) {
12854
+ if (typeof json === "object" && json !== null && "level" in json && typeof json.level === "string" && "timestamp" in json && typeof json.timestamp === "string" && "message" in json) {
12785
12855
  return {
12786
12856
  level: json.level,
12787
12857
  message: typeof json.message === "string" ? json.message : JSON.stringify(json.message, void 0, 2),
12788
12858
  date: new Date(json.timestamp)
12789
12859
  };
12790
12860
  }
12791
- if ("type" in json && typeof json.type === "string" && json.type.startsWith("platform") && "time" in json && typeof json.time === "string" && "record" in json) {
12861
+ if (typeof json === "object" && json !== null && "type" in json && typeof json.type === "string" && json.type.startsWith("platform") && "time" in json && typeof json.time === "string" && "record" in json) {
12792
12862
  return {
12793
12863
  level: "SYSTEM",
12794
12864
  message: JSON.stringify(json.record, void 0, 2),
@@ -13253,11 +13323,11 @@ var createClients = async (appConfig) => {
13253
13323
  };
13254
13324
  };
13255
13325
  var formatAge = (iso) => {
13256
- const minutes8 = Math.floor((Date.now() - Date.parse(iso)) / 6e4);
13257
- if (minutes8 < 1) return "just now";
13258
- if (minutes8 < 60) return `${minutes8}m ago`;
13259
- if (minutes8 < 60 * 24) return `${Math.floor(minutes8 / 60)}h ago`;
13260
- return `${Math.floor(minutes8 / (60 * 24))}d ago`;
13326
+ const minutes9 = Math.floor((Date.now() - Date.parse(iso)) / 6e4);
13327
+ if (minutes9 < 1) return "just now";
13328
+ if (minutes9 < 60) return `${minutes9}m ago`;
13329
+ if (minutes9 < 60 * 24) return `${Math.floor(minutes9 / 60)}h ago`;
13330
+ return `${Math.floor(minutes9 / (60 * 24))}d ago`;
13261
13331
  };
13262
13332
  var formatStatus = (item, liveId) => {
13263
13333
  if (item.id === liveId) return color.success("live ");
@@ -13318,10 +13388,23 @@ var prune = (program2) => {
13318
13388
  initialMessage: "Pruning the deployments",
13319
13389
  successMessage: "Done pruning the deployments.",
13320
13390
  task: () => withAppReleaseLock(appConfig, async () => {
13321
- for (const item of prunable) {
13391
+ const [freshItems, freshLiveId] = await Promise.all([
13392
+ listDeployments(dynamo, appId),
13393
+ readLiveDeploymentId(lambda, functionName)
13394
+ ]);
13395
+ const confirmed = new Set(prunable.map((item) => item.id));
13396
+ const prune2 = selectPrunableDeployments(freshItems, freshLiveId, options).filter(
13397
+ (item) => confirmed.has(item.id)
13398
+ );
13399
+ for (const item of prune2) {
13322
13400
  await deleteLambdaAlias(lambda, functionName, getDeploymentLambdaAliasName(item.id));
13323
13401
  }
13324
- const versions = await selectPrunableVersions({ lambda, functionName, items, prunable });
13402
+ const versions = await selectPrunableVersions({
13403
+ lambda,
13404
+ functionName,
13405
+ items: freshItems,
13406
+ prunable: prune2
13407
+ });
13325
13408
  for (const version of versions) {
13326
13409
  await pruneFunctionVersion(lambda, functionName, version);
13327
13410
  }
@@ -13337,10 +13420,10 @@ var prune = (program2) => {
13337
13420
  await pruneStoreDeployments(
13338
13421
  kvs,
13339
13422
  storeArn,
13340
- prunable.map((item) => item.id)
13423
+ prune2.map((item) => item.id)
13341
13424
  );
13342
13425
  }
13343
- for (const item of prunable) {
13426
+ for (const item of prune2) {
13344
13427
  await removeDeployment(dynamo, appId, item.id);
13345
13428
  }
13346
13429
  })
@@ -13424,4 +13507,10 @@ program.on("option:no-cache", () => {
13424
13507
  commands10.forEach((fn) => fn(program));
13425
13508
 
13426
13509
  // src/bin.ts
13510
+ var interrupt = (code) => () => {
13511
+ process.stdout.write("\x1B[?25h");
13512
+ process.exit(code);
13513
+ };
13514
+ process.on("SIGINT", interrupt(130));
13515
+ process.on("SIGTERM", interrupt(143));
13427
13516
  program.parse(process.argv);