@awsless/cli 0.0.46-next.1 → 0.0.46-next.10

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
@@ -173,7 +173,7 @@ import { DynamoDBClient as DynamoDBClient2, migrate } from "@awsless/dynamodb";
173
173
  // src/util/deployment.ts
174
174
  import { CloudFrontClient as CloudFrontClient2 } from "@aws-sdk/client-cloudfront";
175
175
  import { CloudFrontKeyValueStoreClient as CloudFrontKeyValueStoreClient2 } from "@aws-sdk/client-cloudfront-keyvaluestore";
176
- import { GetFunctionCommand, LambdaClient as LambdaClient3 } from "@aws-sdk/client-lambda";
176
+ import { DeleteFunctionCommand, GetFunctionCommand, LambdaClient as LambdaClient3 } from "@aws-sdk/client-lambda";
177
177
  import {
178
178
  define,
179
179
  deleteItem,
@@ -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,
@@ -366,6 +399,7 @@ import {
366
399
  DeleteFunctionUrlConfigCommand,
367
400
  GetAliasCommand,
368
401
  LambdaClient,
402
+ ListAliasesCommand,
369
403
  UpdateAliasCommand,
370
404
  UpdateFunctionCodeCommand
371
405
  } from "@aws-sdk/client-lambda";
@@ -386,6 +420,22 @@ var getLambdaAlias = async (lambda, functionName, name) => {
386
420
  return;
387
421
  }
388
422
  };
423
+ var listLambdaAliases = async (lambda, functionName, functionVersion) => {
424
+ const aliases = [];
425
+ let marker;
426
+ do {
427
+ const result = await lambda.send(
428
+ new ListAliasesCommand({
429
+ FunctionName: functionName,
430
+ FunctionVersion: functionVersion,
431
+ Marker: marker
432
+ })
433
+ );
434
+ aliases.push(...result.Aliases ?? []);
435
+ marker = result.NextMarker;
436
+ } while (marker);
437
+ return aliases;
438
+ };
389
439
  var upsertLambdaAlias = async (lambda, props) => {
390
440
  const input = {
391
441
  FunctionName: props.functionName,
@@ -528,6 +578,8 @@ import {
528
578
  CreateAliasCommand as CreateAliasCommand2,
529
579
  CreateFunctionUrlConfigCommand,
530
580
  GetFunctionUrlConfigCommand,
581
+ RemovePermissionCommand,
582
+ UpdateFunctionUrlConfigCommand,
531
583
  LambdaClient as LambdaClient2,
532
584
  PutFunctionEventInvokeConfigCommand
533
585
  } from "@aws-sdk/client-lambda";
@@ -559,7 +611,8 @@ var createLambdaProvider = ({ credentials, region }) => {
559
611
  deploymentId: z2.string(),
560
612
  functionName: z2.string(),
561
613
  functionVersion: z2.string(),
562
- onFailureArn: z2.string()
614
+ onFailureArn: z2.string(),
615
+ sourceAccount: z2.string().optional()
563
616
  });
564
617
  const bundleDeploymentStateSchema = bundleDeploymentInputSchema.extend({
565
618
  deploymentAlias: z2.string(),
@@ -596,14 +649,15 @@ var createLambdaProvider = ({ credentials, region }) => {
596
649
  })
597
650
  );
598
651
  };
599
- const createPublicUrl = async (functionName, alias) => {
652
+ const createDeploymentUrl = async (functionName, alias, sourceAccount) => {
653
+ const authType = sourceAccount ? "AWS_IAM" : "NONE";
600
654
  let url;
601
655
  try {
602
656
  const result = await lambda.send(
603
657
  new CreateFunctionUrlConfigCommand({
604
658
  FunctionName: functionName,
605
659
  Qualifier: alias,
606
- AuthType: "NONE"
660
+ AuthType: authType
607
661
  })
608
662
  );
609
663
  url = result.FunctionUrl;
@@ -612,21 +666,39 @@ var createLambdaProvider = ({ credentials, region }) => {
612
666
  throw error;
613
667
  }
614
668
  const result = await lambda.send(
615
- new GetFunctionUrlConfigCommand({
669
+ new UpdateFunctionUrlConfigCommand({
616
670
  FunctionName: functionName,
617
- Qualifier: alias
671
+ Qualifier: alias,
672
+ AuthType: authType
618
673
  })
619
674
  );
620
675
  url = result.FunctionUrl;
621
676
  }
622
- const permissions = [
677
+ const permissions = sourceAccount ? [
678
+ {
679
+ StatementId: "cloudfront-url",
680
+ Principal: "cloudfront.amazonaws.com",
681
+ SourceAccount: sourceAccount,
682
+ Action: "lambda:InvokeFunctionUrl",
683
+ FunctionUrlAuthType: "AWS_IAM"
684
+ },
685
+ {
686
+ StatementId: "cloudfront-invoke",
687
+ Principal: "cloudfront.amazonaws.com",
688
+ SourceAccount: sourceAccount,
689
+ Action: "lambda:InvokeFunction",
690
+ InvokedViaFunctionUrl: true
691
+ }
692
+ ] : [
623
693
  {
624
694
  StatementId: "public-url",
695
+ Principal: "*",
625
696
  Action: "lambda:InvokeFunctionUrl",
626
697
  FunctionUrlAuthType: "NONE"
627
698
  },
628
699
  {
629
700
  StatementId: "public-invoke",
701
+ Principal: "*",
630
702
  Action: "lambda:InvokeFunction",
631
703
  InvokedViaFunctionUrl: true
632
704
  }
@@ -637,7 +709,6 @@ var createLambdaProvider = ({ credentials, region }) => {
637
709
  new AddPermissionCommand({
638
710
  FunctionName: functionName,
639
711
  Qualifier: alias,
640
- Principal: "*",
641
712
  ...permission
642
713
  })
643
714
  );
@@ -647,6 +718,23 @@ var createLambdaProvider = ({ credentials, region }) => {
647
718
  }
648
719
  }
649
720
  }
721
+ if (sourceAccount) {
722
+ for (const statementId of ["public-url", "public-invoke"]) {
723
+ try {
724
+ await lambda.send(
725
+ new RemovePermissionCommand({
726
+ FunctionName: functionName,
727
+ Qualifier: alias,
728
+ StatementId: statementId
729
+ })
730
+ );
731
+ } catch (error) {
732
+ if (!isError(error, "ResourceNotFoundException")) {
733
+ throw error;
734
+ }
735
+ }
736
+ }
737
+ }
650
738
  return url;
651
739
  };
652
740
  const createBundleDeployment = async (state2) => {
@@ -658,7 +746,7 @@ var createLambdaProvider = ({ credentials, region }) => {
658
746
  name: deploymentAlias
659
747
  });
660
748
  await configureVersion(state2);
661
- const url = await createPublicUrl(state2.functionName, deploymentAlias);
749
+ const url = await createDeploymentUrl(state2.functionName, deploymentAlias, state2.sourceAccount);
662
750
  return {
663
751
  ...state2,
664
752
  deploymentAlias,
@@ -769,7 +857,21 @@ var createLambdaProvider = ({ credentials, region }) => {
769
857
  deploymentAliases.push(deploymentAlias);
770
858
  }
771
859
  await configureVersion(proposed);
772
- const url = await createPublicUrl(proposed.functionName, deploymentAlias);
860
+ const url = await createDeploymentUrl(proposed.functionName, deploymentAlias, proposed.sourceAccount);
861
+ if (proposed.sourceAccount && prior.sourceAccount !== proposed.sourceAccount) {
862
+ for (const name of deploymentAliases) {
863
+ if (name === deploymentAlias) {
864
+ continue;
865
+ }
866
+ try {
867
+ await createDeploymentUrl(proposed.functionName, name, proposed.sourceAccount);
868
+ } catch (error) {
869
+ if (!isError(error, "ResourceNotFoundException")) {
870
+ throw error;
871
+ }
872
+ }
873
+ }
874
+ }
773
875
  return {
774
876
  ...proposed,
775
877
  deploymentAlias,
@@ -1134,7 +1236,7 @@ var pullRemoteState = async (app, stateBackend) => {
1134
1236
  await rm(file);
1135
1237
  }
1136
1238
  } else {
1137
- await writeFile(file, JSON.stringify(state2, void 0, 2));
1239
+ await writeFile(file, JSON.stringify(state2, void 0, 2), { mode: 384 });
1138
1240
  }
1139
1241
  };
1140
1242
  var pushRemoteState = async (app, stateBackend) => {
@@ -1148,16 +1250,6 @@ var pushRemoteState = async (app, stateBackend) => {
1148
1250
  var slugifyBranch = (branch) => {
1149
1251
  return (branch ?? "").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "local";
1150
1252
  };
1151
- var git = (command) => {
1152
- try {
1153
- return execSync(`git ${command}`, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim();
1154
- } catch {
1155
- return;
1156
- }
1157
- };
1158
- var isCommitMerged = (commit, branch) => {
1159
- return git(`merge-base --is-ancestor ${JSON.stringify(commit)} ${JSON.stringify(branch)}`) !== void 0;
1160
- };
1161
1253
  var table = define("awsless-deployments", {
1162
1254
  hash: "appId",
1163
1255
  sort: "id",
@@ -1177,10 +1269,13 @@ var table = define("awsless-deployments", {
1177
1269
  var deploymentsTable = table;
1178
1270
  var latestBranchDeployment = async (client, appId, branch) => {
1179
1271
  const items = await listDeployments(client, appId, branch);
1180
- 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
+ );
1181
1276
  };
1182
1277
  var claimDeployment = async (props) => {
1183
- const branch = slugifyBranch(git("rev-parse --abbrev-ref HEAD"));
1278
+ const branch = slugifyBranch(currentBranch());
1184
1279
  while (true) {
1185
1280
  const latest = await latestBranchDeployment(props.client, props.appId, branch);
1186
1281
  const seq = (latest?.seq ?? 0) + 1;
@@ -1191,8 +1286,8 @@ var claimDeployment = async (props) => {
1191
1286
  seq,
1192
1287
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1193
1288
  user: userInfo2().username,
1194
- commit: git("rev-parse HEAD"),
1195
- message: git("log -1 --pretty=%s")
1289
+ commit: currentCommit(),
1290
+ message: currentCommitMessage()
1196
1291
  };
1197
1292
  try {
1198
1293
  await putItem(table, deployment, {
@@ -1209,7 +1304,7 @@ var claimDeployment = async (props) => {
1209
1304
  }
1210
1305
  };
1211
1306
  var currentDeployment = async (client, appId) => {
1212
- return latestBranchDeployment(client, appId, slugifyBranch(git("rev-parse --abbrev-ref HEAD")));
1307
+ return latestBranchDeployment(client, appId, slugifyBranch(currentBranch()));
1213
1308
  };
1214
1309
  var getDeployment = async (client, appId, id) => {
1215
1310
  return getItem(table, { appId, id }, { client });
@@ -1259,6 +1354,63 @@ var markPromoted = async (client, appId, id) => {
1259
1354
  var removeDeployment = async (client, appId, id) => {
1260
1355
  await deleteItem(table, { appId, id }, { client });
1261
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
+ };
1361
+ var selectPrunableDeployments = (items, liveId, options) => {
1362
+ const rollbackTarget = items.filter((item) => item.promotedAt && item.id !== liveId).sort((a, b) => b.promotedAt.localeCompare(a.promotedAt))[0];
1363
+ const keep = Math.max(1, Number(options.keep) || 10);
1364
+ const mainSlug = slugifyBranch(options.main);
1365
+ const keptMain = new Set(
1366
+ items.filter((item) => item.branch === mainSlug && item.functionVersion).map((item) => item.seq).sort((a, b) => b - a).slice(0, keep)
1367
+ );
1368
+ return items.filter((item) => {
1369
+ if (item.id === liveId || item.id === rollbackTarget?.id || isDeploymentBusy(item)) {
1370
+ return false;
1371
+ }
1372
+ if (options.branch) {
1373
+ return item.branch === slugifyBranch(options.branch);
1374
+ }
1375
+ if (!item.functionVersion) {
1376
+ return true;
1377
+ }
1378
+ if (item.branch === mainSlug) {
1379
+ return !keptMain.has(item.seq);
1380
+ }
1381
+ return item.commit ? isCommitMerged(item.commit, options.main) : false;
1382
+ });
1383
+ };
1384
+ var pruneFunctionVersion = async (lambda, functionName, version) => {
1385
+ for (const alias of await listLambdaAliases(lambda, functionName, version)) {
1386
+ if (alias.Name && alias.Name !== LIVE_LAMBDA_ALIAS) {
1387
+ await deleteLambdaAlias(lambda, functionName, alias.Name);
1388
+ }
1389
+ }
1390
+ try {
1391
+ await lambda.send(
1392
+ new DeleteFunctionCommand({
1393
+ FunctionName: functionName,
1394
+ Qualifier: version
1395
+ })
1396
+ );
1397
+ } catch (error) {
1398
+ if (!isError(error, "ResourceNotFoundException")) {
1399
+ throw error;
1400
+ }
1401
+ }
1402
+ };
1403
+ var selectPrunableVersions = async (props) => {
1404
+ const surviving = props.items.filter((item) => !props.prunable.includes(item));
1405
+ const kept = new Set(surviving.map((item) => item.functionVersion));
1406
+ const live = await getLambdaAlias(props.lambda, props.functionName, LIVE_LAMBDA_ALIAS);
1407
+ if (live?.FunctionVersion) {
1408
+ kept.add(live.FunctionVersion);
1409
+ }
1410
+ return new Set(
1411
+ props.prunable.map((item) => item.functionVersion).filter((version) => Boolean(version && !kept.has(version)))
1412
+ );
1413
+ };
1262
1414
  var readLiveDeploymentId = async (lambda, functionName) => {
1263
1415
  return (await getLambdaAlias(lambda, functionName, LIVE_LAMBDA_ALIAS))?.Description || void 0;
1264
1416
  };
@@ -1275,21 +1427,21 @@ var readDeployedFunctionVersion = (state2) => {
1275
1427
  return;
1276
1428
  };
1277
1429
  var formatDeploymentSummary = (props) => {
1278
- let previewUrl;
1279
- let lambdaUrl;
1430
+ const previewUrls = /* @__PURE__ */ new Map();
1280
1431
  for (const [urn, node] of readStateNodes(props.state)) {
1281
1432
  if (node.type === "aws_cloudfront_distribution" && urn.endsWith(":{preview}")) {
1282
- previewUrl = `https://${node.output.domainName}`;
1283
- }
1284
- if (node.type === "bundle-deployment") {
1285
- lambdaUrl = node.output.url;
1433
+ const router = urn.match(/router:\{([^}]+)\}/)?.[1];
1434
+ if (router) {
1435
+ previewUrls.set(router, `https://${node.output.domainName}`);
1436
+ }
1286
1437
  }
1287
1438
  }
1288
- return Object.keys(props.appConfig.defaults.router ?? {}).map((routerId, index) => {
1439
+ return Object.keys(props.appConfig.defaults.router ?? {}).map((routerId) => {
1440
+ const previewUrl = previewUrls.get(routerId);
1289
1441
  return [
1290
1442
  `${routerId}: deployment #${props.id}`,
1291
- index === 0 ? previewUrl : void 0,
1292
- index === 0 ? lambdaUrl : void 0
1443
+ previewUrl,
1444
+ previewUrl ? `${previewUrl}/?awsless-deployment=${props.id}` : void 0
1293
1445
  ].filter(Boolean).join("\n");
1294
1446
  });
1295
1447
  };
@@ -1371,7 +1523,10 @@ var promoteDeployment = async (props) => {
1371
1523
  ];
1372
1524
  const failures = (await Promise.allSettled(rollback2)).filter((result) => result.status === "rejected").map((result) => result.reason);
1373
1525
  if (failures.length > 0) {
1374
- 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
+ );
1375
1530
  }
1376
1531
  throw error;
1377
1532
  }
@@ -1833,7 +1988,7 @@ var CodeSchema = z14.union([
1833
1988
  var FnSchema = z14.object({
1834
1989
  code: CodeSchema,
1835
1990
  handler: HandlerSchema.optional()
1836
- });
1991
+ }).strict();
1837
1992
  var FunctionSchema = z14.union([
1838
1993
  LocalFileSchema.transform((code) => ({
1839
1994
  code
@@ -2079,6 +2234,66 @@ var InstanceDefaultSchema = z19.object({
2079
2234
  // src/feature/router/schema.ts
2080
2235
  import { days as days3, minutes as minutes2, parse as parse3 } from "@awsless/duration";
2081
2236
  import { z as z20 } from "zod";
2237
+
2238
+ // src/feature/router/pattern.ts
2239
+ var PARAM_TOKEN = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}|\*/g;
2240
+ var escapeRegex = (value) => {
2241
+ return value.replace(/[|\\{}()[\]^$+*?.\-]/g, "\\$&");
2242
+ };
2243
+ var compileRoutePattern = (pattern) => {
2244
+ if (!pattern.startsWith("/")) {
2245
+ throw new ExpectedError(`Route pattern "${pattern}" must start with a slash (/)`);
2246
+ }
2247
+ if (pattern === "/*") {
2248
+ return { key: pattern };
2249
+ }
2250
+ const params = [];
2251
+ let regex = "";
2252
+ let stars = 0;
2253
+ let last = 0;
2254
+ let token;
2255
+ PARAM_TOKEN.lastIndex = 0;
2256
+ while (token = PARAM_TOKEN.exec(pattern)) {
2257
+ regex += escapeRegex(pattern.slice(last, token.index));
2258
+ const param = token[1];
2259
+ if (param) {
2260
+ if (params.includes(param)) {
2261
+ throw new ExpectedError(`Duplicate param "${param}" in route pattern "${pattern}"`);
2262
+ }
2263
+ params.push(param);
2264
+ regex += "([^/]+)";
2265
+ } else {
2266
+ stars++;
2267
+ regex += ".*";
2268
+ }
2269
+ last = PARAM_TOKEN.lastIndex;
2270
+ }
2271
+ if (params.length === 0 && stars === 0) {
2272
+ return { key: pattern };
2273
+ }
2274
+ regex += escapeRegex(pattern.slice(last));
2275
+ const root2 = pattern.split("/")[1] ?? "";
2276
+ if (root2 === "" || root2.includes("*") || root2.includes("{")) {
2277
+ throw new ExpectedError(
2278
+ `The first path segment of route pattern "${pattern}" must be static when the pattern contains params or wildcards.`
2279
+ );
2280
+ }
2281
+ if (root2.includes(".")) {
2282
+ throw new ExpectedError(
2283
+ `The first path segment of route pattern "${pattern}" can't contain a dot when the pattern contains params or wildcards.`
2284
+ );
2285
+ }
2286
+ if (params.length === 0 && pattern === `/${root2}/*`) {
2287
+ return { key: pattern };
2288
+ }
2289
+ return {
2290
+ key: `/${root2}/*`,
2291
+ match: `^${regex}$`,
2292
+ params: params.length > 0 ? params : void 0
2293
+ };
2294
+ };
2295
+
2296
+ // src/feature/router/schema.ts
2082
2297
  var ErrorResponsePathSchema = z20.string().describe(
2083
2298
  [
2084
2299
  "The path to the custom error page that you want to return to the viewer when your origin returns the HTTP status code specified.",
@@ -2107,7 +2322,29 @@ var ErrorResponseSchema = z20.union([
2107
2322
  minTTL: MinTTLSchema.optional()
2108
2323
  })
2109
2324
  ]).optional();
2110
- var RouteSchema = z20.string().regex(/^\//, "Route must start with a slash (/)");
2325
+ var RouteSchema = z20.string().regex(/^\//, "Route must start with a slash (/)").regex(/^\/([^/*.]+)?$/, 'Router paths mount a single segment without dots, like "/api".');
2326
+ var RoutesSchema = z20.record(
2327
+ ResourceIdSchema.describe("The router id to add your routes to."),
2328
+ z20.record(z20.string().regex(/^\//, "Route must start with a slash (/)"), FunctionSchema).superRefine((routes, ctx) => {
2329
+ for (const pattern of Object.keys(routes)) {
2330
+ try {
2331
+ compileRoutePattern(pattern);
2332
+ } catch (error) {
2333
+ ctx.addIssue({
2334
+ code: z20.ZodIssueCode.custom,
2335
+ path: [pattern],
2336
+ message: error instanceof Error ? error.message : `Invalid route pattern: ${pattern}`
2337
+ });
2338
+ }
2339
+ }
2340
+ }).describe(
2341
+ [
2342
+ "Define the routes and the lambda function that should handle them.",
2343
+ 'Routes can be an exact path like "/sitemap.xml", a wildcard like "/sitemap/*", or contain params like "/sitemap/{locale}/{page}.xml".',
2344
+ 'Param values are passed to the function as "x-param-[NAME]" request headers.'
2345
+ ].join("\n")
2346
+ )
2347
+ ).optional().describe("Add routes to your global Router that link a path pattern to a lambda function.");
2111
2348
  var VisibilitySchema = z20.boolean().default(false).describe("Whether to enable CloudWatch metrics for the WAF rule.");
2112
2349
  var WafSettingsSchema = z20.object({
2113
2350
  rateLimiter: z20.object({
@@ -2606,8 +2843,8 @@ var AppSchema = z29.object({
2606
2843
  layers: LayerSchema,
2607
2844
  router: RouterDefaultSchema
2608
2845
  // dataRetention: z.boolean().describe('Configure how your resources are handled on delete.').default(false),
2609
- }).default({}).describe("Default properties")
2610
- });
2846
+ }).strict().default({}).describe("Default properties")
2847
+ }).strict();
2611
2848
 
2612
2849
  // src/config/stack.ts
2613
2850
  import { z as z45 } from "zod";
@@ -2663,7 +2900,7 @@ var CommandsSchema = z31.record(ResourceIdSchema, CommandSchema).optional().desc
2663
2900
 
2664
2901
  // src/feature/config/schema.ts
2665
2902
  import { z as z32 } from "zod";
2666
- var ConfigNameSchema = z32.string().regex(/[a-z0-9\-]/g, "Invalid config name");
2903
+ var ConfigNameSchema = z32.string().regex(/^[a-z0-9-]+$/, "Invalid config name");
2667
2904
  var ConfigsSchema = z32.array(ConfigNameSchema).optional().describe("Define the config values for your stack.");
2668
2905
 
2669
2906
  // src/feature/cron/schema/index.ts
@@ -3216,14 +3453,13 @@ var TestsSchema = z44.union([
3216
3453
  ]).describe("Define the location of your tests for your stack.").optional();
3217
3454
 
3218
3455
  // src/config/stack.ts
3219
- var DependsSchema = ResourceIdSchema.array().optional().describe("Define the stacks that this stack is depended on.");
3220
3456
  var NameSchema = ResourceIdSchema.refine((name) => !["base", "hostedzones"].includes(name), {
3221
3457
  message: `Stack name can't be a reserved name.`
3222
3458
  }).describe("Stack name.");
3223
3459
  var StackSchema = z45.object({
3224
3460
  $schema: z45.string().optional(),
3225
3461
  name: NameSchema,
3226
- depends: DependsSchema,
3462
+ routes: RoutesSchema,
3227
3463
  commands: CommandsSchema,
3228
3464
  // auth: AuthSchema,
3229
3465
  // http: HttpSchema,
@@ -3249,7 +3485,7 @@ var StackSchema = z45.object({
3249
3485
  images: ImagesSchema,
3250
3486
  icons: IconsSchema,
3251
3487
  metrics: MetricsSchema
3252
- });
3488
+ }).strict();
3253
3489
 
3254
3490
  // src/config/load/read.ts
3255
3491
  import { readFile as readFile3 } from "fs/promises";
@@ -3708,7 +3944,7 @@ import { readdir as readdir2, readFile as readFile7, writeFile as writeFile4 } f
3708
3944
  import { join as join10 } from "path";
3709
3945
 
3710
3946
  // src/build/index.ts
3711
- import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
3947
+ import { mkdir as mkdir2, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3712
3948
  import { dirname as dirname4, join as join7 } from "path";
3713
3949
 
3714
3950
  // src/util/timer.ts
@@ -3750,6 +3986,7 @@ var build = (type, name, builder, props) => {
3750
3986
  cached: true
3751
3987
  };
3752
3988
  }
3989
+ await rm2(cacheFile, { force: true });
3753
3990
  const time = createTimer();
3754
3991
  const meta = await callback(async (file, data2) => {
3755
3992
  const path = getBuildPath(type, name, file);
@@ -3802,7 +4039,7 @@ var zipFiles = (files) => {
3802
4039
  import { generateFileHash } from "@awsless/ts-file-cache";
3803
4040
  import { kebabCase as kebabCase5 } from "change-case";
3804
4041
  import { createHash as createHash5 } from "crypto";
3805
- import { readFile as readFile6, rm as rm3, writeFile as writeFile3 } from "fs/promises";
4042
+ import { readFile as readFile6, rm as rm4, writeFile as writeFile3 } from "fs/promises";
3806
4043
  import { dirname as dirname5, join as join9 } from "path";
3807
4044
  import { fileURLToPath } from "url";
3808
4045
 
@@ -3814,13 +4051,13 @@ var formatByteSize = (size) => {
3814
4051
  };
3815
4052
 
3816
4053
  // src/util/temp.ts
3817
- import { mkdir as mkdir3, readdir, rm as rm2 } from "fs/promises";
4054
+ import { mkdir as mkdir3, readdir, rm as rm3 } from "fs/promises";
3818
4055
  import { join as join8 } from "path";
3819
4056
  var createTempFolder = async (name) => {
3820
4057
  const path = join8(directories.temp, name);
3821
4058
  await mkdir3(join8(directories.temp, name), { recursive: true });
3822
4059
  process.on("SIGTERM", async () => {
3823
- await rm2(path, { recursive: true });
4060
+ await rm3(path, { recursive: true });
3824
4061
  });
3825
4062
  return {
3826
4063
  path,
@@ -3828,7 +4065,7 @@ var createTempFolder = async (name) => {
3828
4065
  return readdir(path, { recursive: true });
3829
4066
  },
3830
4067
  async delete() {
3831
- await rm2(path, { recursive: true });
4068
+ await rm3(path, { recursive: true });
3832
4069
  }
3833
4070
  };
3834
4071
  };
@@ -3843,7 +4080,6 @@ var bundleTypeScriptWithRolldown = async ({
3843
4080
  format: format3 = "esm",
3844
4081
  minify = true,
3845
4082
  file,
3846
- nativeDir,
3847
4083
  external,
3848
4084
  importAsString: importAsStringList
3849
4085
  }) => {
@@ -3854,9 +4090,15 @@ var bundleTypeScriptWithRolldown = async ({
3854
4090
  return importee.startsWith("@aws-sdk") || importee.startsWith("aws-sdk") || external?.includes(importee);
3855
4091
  },
3856
4092
  treeshake: {
3857
- moduleSideEffects: (id, isExternal) => isExternal ? external?.includes(id) === true : id.startsWith(`${directories.root}/`)
4093
+ // Dependencies are treated as side-effect free, so unused imports
4094
+ // like the local test servers never reach the production bundle.
4095
+ // The bundle guard below fails the build if one slips through.
4096
+ moduleSideEffects: (id, isExternal) => isExternal ? external?.includes(id) === true : id.startsWith(`${directories.root}/`) && !id.includes("/node_modules/")
3858
4097
  },
3859
4098
  onwarn: (error) => {
4099
+ if (error.code === "UNRESOLVED_IMPORT") {
4100
+ throw new ExpectedError(error.message);
4101
+ }
3860
4102
  debugError(error.message);
3861
4103
  },
3862
4104
  plugins: [
@@ -3901,6 +4143,7 @@ var bundleTypeScriptWithRolldown = async ({
3901
4143
  chunkFileNames: `[name].${ext}`,
3902
4144
  minify
3903
4145
  });
4146
+ assertNoTestOnlyModules(result.output);
3904
4147
  const hash = createHash4("sha1");
3905
4148
  const files = [];
3906
4149
  for (const item of result.output) {
@@ -3922,6 +4165,25 @@ var bundleTypeScriptWithRolldown = async ({
3922
4165
  files
3923
4166
  };
3924
4167
  };
4168
+ var TEST_ONLY_MODULES = ["dynamo-db-local", "@awsless/dynamodb-server", "redis-memory-server", "aws-sdk-vitest-mock"];
4169
+ var findPackage = (id, names) => {
4170
+ return names.find((name) => id.includes(`/${name.replace(/^@[^/]+\//, "")}/`));
4171
+ };
4172
+ var assertNoTestOnlyModules = (output) => {
4173
+ for (const item of output) {
4174
+ if (item.type !== "chunk") {
4175
+ continue;
4176
+ }
4177
+ for (const id of item.moduleIds ?? []) {
4178
+ const found = findPackage(id, TEST_ONLY_MODULES);
4179
+ if (found) {
4180
+ throw new Error(
4181
+ `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.`
4182
+ );
4183
+ }
4184
+ }
4185
+ }
4186
+ };
3925
4187
 
3926
4188
  // src/feature/bundle/util.ts
3927
4189
  var ROUTE_HEADER = "x-awsless-route";
@@ -3944,7 +4206,7 @@ var registerBundleFunction = (ctx, routeKey, props) => {
3944
4206
  };
3945
4207
  var buildBundle = (props) => {
3946
4208
  return async (build3, { workspace }) => {
3947
- const runtime = props.runtime ?? join9(dirname5(fileURLToPath(import.meta.url)), "/handlers/bundle.mjs");
4209
+ const runtime = props.runtime ?? join9(dirname5(fileURLToPath(import.meta.url)), "/handlers/bundle.js");
3948
4210
  const handlers = [...props.handlers].sort((a, b) => a.routeKey.localeCompare(b.routeKey));
3949
4211
  const entries = handlers.map(({ routeKey, file, exportName }) => {
3950
4212
  const virtualFile = JSON.stringify(`${file}?awsless-route=${encodeURIComponent(routeKey)}`);
@@ -3993,7 +4255,7 @@ ${entries.join("\n")}
3993
4255
  importAsString: importAsString2.length > 0 ? importAsString2 : void 0
3994
4256
  });
3995
4257
  await temp.delete();
3996
- await rm3(getBuildPath("bundle", props.name, "files"), { recursive: true, force: true });
4258
+ await rm4(getBuildPath("bundle", props.name, "files"), { recursive: true, force: true });
3997
4259
  await Promise.all([
3998
4260
  write("HASH", bundle.hash),
3999
4261
  ...bundle.files.map((file) => write(`files/${file.name}`, file.code)),
@@ -4038,6 +4300,11 @@ var bundleFeature = defineFeature({
4038
4300
  const addLayer = (layer) => {
4039
4301
  layers.push(layer);
4040
4302
  };
4303
+ const layerIds = Object.keys(ctx.appConfig.defaults.layers ?? {});
4304
+ const layerPackages = layerIds.flatMap((id) => ctx.shared.entry("layer", "packages", id));
4305
+ for (const id of layerIds) {
4306
+ addLayer(ctx.shared.entry("layer", "arn", id));
4307
+ }
4041
4308
  const name = getBundleFunctionName(ctx.app.name);
4042
4309
  const shortName = formatGlobalResourceName({
4043
4310
  appName: ctx.app.name,
@@ -4051,7 +4318,7 @@ var bundleFeature = defineFeature({
4051
4318
  name,
4052
4319
  handlers,
4053
4320
  minify: defaults.minify,
4054
- external: defaults.external
4321
+ external: [...defaults.external ?? [], ...layerPackages]
4055
4322
  })
4056
4323
  );
4057
4324
  const sourceHash = new Output3(envDeps, async (resolve2) => {
@@ -4207,7 +4474,8 @@ var bundleFeature = defineFeature({
4207
4474
  deploymentId: ctx.deploymentId ?? "local-0",
4208
4475
  functionName: lambda.functionName,
4209
4476
  functionVersion: lambda.version,
4210
- onFailureArn: onFailure
4477
+ onFailureArn: onFailure,
4478
+ sourceAccount: ctx.accountId
4211
4479
  },
4212
4480
  {
4213
4481
  // Make sure the permissions are in place before any event source is wired up.
@@ -4293,11 +4561,6 @@ var bundleFeature = defineFeature({
4293
4561
  addLayer,
4294
4562
  addPermission
4295
4563
  });
4296
- },
4297
- onStack(ctx) {
4298
- const bundle = ctx.shared.get("bundle", "main");
4299
- ctx.onEnv(bundle.addEnv);
4300
- ctx.onPermission(bundle.addPermission);
4301
4564
  }
4302
4565
  });
4303
4566
 
@@ -4509,7 +4772,7 @@ var SsmStore = class {
4509
4772
  debug("Value:", color.info(value));
4510
4773
  await this.client.send(
4511
4774
  new PutParameterCommand({
4512
- Type: ParameterType.STRING,
4775
+ Type: ParameterType.SECURE_STRING,
4513
4776
  Name: this.getName(name),
4514
4777
  Value: value,
4515
4778
  Overwrite: true
@@ -4608,7 +4871,7 @@ var configFeature = defineFeature({
4608
4871
  ctx.addEnv(`CONFIG_${constantCase4(name)}`, name);
4609
4872
  }
4610
4873
  if (configs.length) {
4611
- ctx.addStackPermission({
4874
+ ctx.addGlobalPermission({
4612
4875
  actions: [
4613
4876
  "ssm:GetParameter",
4614
4877
  "ssm:GetParameters",
@@ -4945,10 +5208,12 @@ var domainFeature = defineFeature({
4945
5208
  }
4946
5209
  }
4947
5210
  ctx.addGlobalPermission({
4948
- actions: ["ses:*"],
5211
+ actions: ["ses:SendEmail", "ses:SendRawEmail"],
4949
5212
  resources: [
4950
- // `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/*`,
4951
- "*"
5213
+ `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/*`,
5214
+ // Sending through the app configuration set is authorized against
5215
+ // its own ARN, not just the identity.
5216
+ `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:configuration-set/${ctx.app.name}`
4952
5217
  ]
4953
5218
  });
4954
5219
  }
@@ -5038,7 +5303,9 @@ var functionFeature = defineFeature({
5038
5303
  // 'lambda:ListFunctions',
5039
5304
  // 'lambda:GetFunction',
5040
5305
  ],
5041
- resources: [`arn:aws:lambda:*:*:function:${ctx.appConfig.name}--*`]
5306
+ resources: [
5307
+ `arn:aws:lambda:${ctx.appConfig.region}:${ctx.accountId}:function:${ctx.appConfig.name}--*`
5308
+ ]
5042
5309
  });
5043
5310
  },
5044
5311
  onStack(ctx) {
@@ -5080,7 +5347,7 @@ var onErrorLogFeature = defineFeature({
5080
5347
  const consumerRoute = formatRouteKey(ctx.app.name, "on-error-log", "consumer");
5081
5348
  bundle.addHandler({
5082
5349
  routeKey: handlerRoute,
5083
- file: join11(dirname6(fileURLToPath2(import.meta.url)), "/handlers/on-error-log.mjs"),
5350
+ file: join11(dirname6(fileURLToPath2(import.meta.url)), "/handlers/on-error-log.js"),
5084
5351
  exportName: "default"
5085
5352
  });
5086
5353
  bundle.addEnv(formatRouteEnvName(handlerRoute, "CONSUMER"), consumerRoute);
@@ -5305,7 +5572,7 @@ var onFailureFeature = defineFeature({
5305
5572
  const consumer = props.consumer;
5306
5573
  bundle.addHandler({
5307
5574
  routeKey: normalizerRoute,
5308
- file: join12(dirname7(fileURLToPath3(import.meta.url)), "/handlers/on-failure.mjs"),
5575
+ file: join12(dirname7(fileURLToPath3(import.meta.url)), "/handlers/on-failure.js"),
5309
5576
  exportName: "default"
5310
5577
  });
5311
5578
  registerBundleFunction(ctx, consumerRoute, consumer);
@@ -5460,7 +5727,7 @@ var createPubSubService = (parentGroup, ctx, id, props, inputs) => {
5460
5727
  });
5461
5728
  const shortName = shortId(`${ctx.app.name}:pubsub:${id}:${ctx.appId}`);
5462
5729
  const image2 = "public.ecr.aws/aws-cli/aws-cli:arm64";
5463
- const bundleFile = join14(__dirname, "handlers/pubsub-server.mjs");
5730
+ const bundleFile = join14(__dirname, "handlers/pubsub-server.js");
5464
5731
  ctx.registerBuild("pubsub", name, async (build3) => {
5465
5732
  const hash = createHash8("sha1").update(await readFile9(bundleFile)).digest("hex");
5466
5733
  const fingerprint = `${hash}-${ARCHITECTURE}`;
@@ -5561,7 +5828,8 @@ var createPubSubService = (parentGroup, ctx, id, props, inputs) => {
5561
5828
  Statement: list3.map((statement) => ({
5562
5829
  Effect: pascalCase2(statement.effect ?? "allow"),
5563
5830
  Action: statement.actions,
5564
- Resource: statement.resources
5831
+ Resource: statement.resources,
5832
+ Condition: statement.conditions
5565
5833
  }))
5566
5834
  })
5567
5835
  );
@@ -6052,7 +6320,7 @@ var pubsubFeature = defineFeature({
6052
6320
  const publisherRouteKey = formatRouteKey(ctx.app.name, "pubsub", `${id}-publisher`);
6053
6321
  bundle.addHandler({
6054
6322
  routeKey: publisherRouteKey,
6055
- file: join15(__dirname2, "/handlers/pubsub-publisher.mjs"),
6323
+ file: join15(__dirname2, "/handlers/pubsub-publisher.js"),
6056
6324
  exportName: "default"
6057
6325
  });
6058
6326
  bundle.addEnv(formatRouteEnvName3(publisherRouteKey, "REDIS_HOST"), redisHost);
@@ -6205,7 +6473,7 @@ var queueFeature = defineFeature({
6205
6473
  });
6206
6474
  }
6207
6475
  ctx.addEnv(`QUEUE_${constantCase6(ctx.stack.name)}_${constantCase6(id)}_URL`, queue2.url);
6208
- ctx.addStackPermission({
6476
+ ctx.addGlobalPermission({
6209
6477
  actions: [
6210
6478
  "sqs:SendMessage",
6211
6479
  "sqs:ReceiveMessage",
@@ -6401,7 +6669,7 @@ var rpcFeature = defineFeature({
6401
6669
  const serverRouteKey = formatRouteKey(ctx.app.name, "rpc", id);
6402
6670
  bundle.addHandler({
6403
6671
  routeKey: serverRouteKey,
6404
- file: join16(dirname10(fileURLToPath6(import.meta.url)), "/handlers/rpc.mjs"),
6672
+ file: join16(dirname10(fileURLToPath6(import.meta.url)), "/handlers/rpc.js"),
6405
6673
  exportName: "default"
6406
6674
  });
6407
6675
  bundle.addEnv(
@@ -6565,7 +6833,7 @@ var searchFeature = defineFeature({
6565
6833
  }
6566
6834
  );
6567
6835
  ctx.addEnv(`SEARCH_${constantCase8(ctx.stack.name)}_${constantCase8(id)}_DOMAIN`, openSearch.endpointV2);
6568
- ctx.addStackPermission({
6836
+ ctx.addGlobalPermission({
6569
6837
  actions: ["es:ESHttp*"],
6570
6838
  resources: [
6571
6839
  //
@@ -6922,14 +7190,18 @@ var siteFeature = defineFeature({
6922
7190
  env,
6923
7191
  stdout: "pipe",
6924
7192
  stderr: "pipe"
6925
- // stdout: 'ignore',
6926
- // stderr: ''
6927
- // stdout: 'inherit',
6928
- // stderr: 'inherit',
6929
7193
  });
6930
- await instance.exited;
6931
- if (instance.exitCode !== null && instance.exitCode > 0) {
6932
- throw new Error("Site build failed");
7194
+ const [output, errors] = await Promise.all([
7195
+ new Response(instance.stdout).text(),
7196
+ new Response(instance.stderr).text(),
7197
+ instance.exited
7198
+ ]);
7199
+ if (instance.exitCode !== 0) {
7200
+ const reason = instance.signalCode ? ` (${instance.signalCode})` : "";
7201
+ throw new ExpectedError(
7202
+ `Site build failed${reason}:
7203
+ ${(errors.trim() || output.trim()).slice(-2e3)}`
7204
+ );
6933
7205
  }
6934
7206
  await write("HASH", fingerprint);
6935
7207
  return {
@@ -7770,7 +8042,7 @@ var imageFeature = defineFeature({
7770
8042
  const serverRouteKey = formatRouteKey(ctx.stack.name, "image", id);
7771
8043
  bundle.addHandler({
7772
8044
  routeKey: serverRouteKey,
7773
- file: join20(dirname12(fileURLToPath7(import.meta.url)), "/handlers/image.mjs"),
8045
+ file: join20(dirname12(fileURLToPath7(import.meta.url)), "/handlers/image.js"),
7774
8046
  exportName: "default",
7775
8047
  external: ["sharp"]
7776
8048
  });
@@ -7869,7 +8141,7 @@ var iconFeature = defineFeature({
7869
8141
  const serverRouteKey = formatRouteKey(ctx.stack.name, "icon", id);
7870
8142
  bundle.addHandler({
7871
8143
  routeKey: serverRouteKey,
7872
- file: join21(dirname13(fileURLToPath8(import.meta.url)), "/handlers/icon.mjs"),
8144
+ file: join21(dirname13(fileURLToPath8(import.meta.url)), "/handlers/icon.js"),
7873
8145
  exportName: "default"
7874
8146
  });
7875
8147
  addRoutes({
@@ -8128,7 +8400,8 @@ var createFargateJob = (parentGroup, ctx, ns, id, local) => {
8128
8400
  Statement: list3.map((statement) => ({
8129
8401
  Effect: pascalCase3(statement.effect ?? "allow"),
8130
8402
  Action: statement.actions,
8131
- Resource: statement.resources
8403
+ Resource: statement.resources,
8404
+ Condition: statement.conditions
8132
8405
  }))
8133
8406
  })
8134
8407
  );
@@ -8480,13 +8753,13 @@ var jobFeature = defineFeature({
8480
8753
  const group = new Group25(ctx.stack, "job", id);
8481
8754
  createFargateJob(group, ctx, "job", id, props);
8482
8755
  }
8483
- ctx.addStackPermission({
8756
+ ctx.addGlobalPermission({
8484
8757
  actions: ["ecs:RunTask"],
8485
8758
  resources: [
8486
8759
  `arn:aws:ecs:${ctx.appConfig.region}:*:task-definition/${ctx.app.name}--${ctx.stackConfig.name}--*`
8487
8760
  ]
8488
8761
  });
8489
- ctx.addStackPermission({
8762
+ ctx.addGlobalPermission({
8490
8763
  actions: ["iam:PassRole"],
8491
8764
  resources: ["*"],
8492
8765
  conditions: {
@@ -8626,7 +8899,8 @@ var createFargateTask = (parentGroup, ctx, ns, id, local) => {
8626
8899
  Statement: list3.map((statement) => ({
8627
8900
  Effect: pascalCase4(statement.effect ?? "allow"),
8628
8901
  Action: statement.actions,
8629
- Resource: statement.resources
8902
+ Resource: statement.resources,
8903
+ Condition: statement.conditions
8630
8904
  }))
8631
8905
  })
8632
8906
  );
@@ -9022,7 +9296,7 @@ var metricFeature = defineFeature({
9022
9296
  onStack(ctx) {
9023
9297
  const bundle = ctx.shared.get("bundle", "main");
9024
9298
  const namespace = `awsless/${kebabCase11(ctx.app.name)}/${kebabCase11(ctx.stack.name)}`;
9025
- ctx.addStackPermission({
9299
+ ctx.addGlobalPermission({
9026
9300
  actions: ["cloudwatch:PutMetricData"],
9027
9301
  resources: ["*"],
9028
9302
  conditions: {
@@ -9098,12 +9372,15 @@ var metricFeature = defineFeature({
9098
9372
  });
9099
9373
 
9100
9374
  // src/feature/router/index.ts
9101
- import { days as days10, seconds as seconds6, toSeconds as toSeconds12, years } from "@awsless/duration";
9375
+ import { days as days10, seconds as seconds7, toSeconds as toSeconds13, years } from "@awsless/duration";
9102
9376
  import { Group as Group29 } from "@terraforge/core";
9103
9377
  import { aws as aws30 } from "@terraforge/aws";
9104
- import { camelCase as camelCase9, constantCase as constantCase15 } from "change-case";
9378
+ import { camelCase as camelCase9, constantCase as constantCase15, kebabCase as kebabCase12 } from "change-case";
9105
9379
 
9106
9380
  // src/feature/router/router-code.ts
9381
+ import { minutes as minutes8, seconds as seconds6, toSeconds as toSeconds12 } from "@awsless/duration";
9382
+ var ORIGIN_READ_TIMEOUT = toSeconds12(minutes8(2));
9383
+ var ORIGIN_CONNECTION_TIMEOUT = toSeconds12(seconds6(10));
9107
9384
  var getViewerRequestFunctionCode = (props) => {
9108
9385
  return CODE(
9109
9386
  [
@@ -9117,7 +9394,7 @@ var getViewerRequestFunctionCode = (props) => {
9117
9394
  ].join("\n")
9118
9395
  ) : ""
9119
9396
  ],
9120
- ACTIVE_PREFIX(props.router)
9397
+ props.preview ? PREVIEW_PREFIX(props.router) : ACTIVE_PREFIX(props.router)
9121
9398
  );
9122
9399
  };
9123
9400
  var BLOCK_DIRECT_ACCESS_TO_CLOUDFRONT = `
@@ -9172,11 +9449,64 @@ var PASSWORD_AUTH_CHECK = (password) => `
9172
9449
  authMethods.push('Password realm="Protected"');
9173
9450
 
9174
9451
  if(!isAuthorized) {
9175
- if(authHeader && authHeader.startsWith('Password ') && authHeader.slice(9) === '${password}') {
9452
+ if(authHeader && authHeader.startsWith('Password ') && authHeader.slice(9) === ${JSON.stringify(password)}) {
9176
9453
  isAuthorized = true;
9177
9454
  }
9178
9455
  }
9179
9456
  `;
9457
+ var PREVIEW_PREFIX = (router) => `
9458
+ const router = ${JSON.stringify(router)};
9459
+ let deployment;
9460
+
9461
+ if (request.querystring['awsless-deployment'] && request.querystring['awsless-deployment'].value) {
9462
+ deployment = request.querystring['awsless-deployment'].value;
9463
+ } else if (request.cookies && request.cookies['awsless-deployment'] && request.cookies['awsless-deployment'].value) {
9464
+ deployment = request.cookies['awsless-deployment'].value;
9465
+ }
9466
+
9467
+ let prefix;
9468
+
9469
+ try {
9470
+ const pointer = deployment ? '$deploy:' + deployment : '$active';
9471
+ prefix = (await cf.kvs().get(pointer)).split(':')[0] + ':' + router + ':';
9472
+ } catch (e) {
9473
+ return deployment
9474
+ ? { statusCode: 404, statusDescription: 'Unknown Deployment' }
9475
+ : { statusCode: 503, statusDescription: 'Service Unavailable' };
9476
+ }
9477
+
9478
+ if (deployment && request.querystring['awsless-deployment']) {
9479
+ delete request.querystring['awsless-deployment'];
9480
+
9481
+ const query = [];
9482
+
9483
+ for (const key in request.querystring) {
9484
+ const entry = request.querystring[key];
9485
+
9486
+ if (entry.multiValue) {
9487
+ // The CloudFront js runtime doesn't support for...of.
9488
+ for (const i in entry.multiValue) {
9489
+ query.push(key + '=' + entry.multiValue[i].value);
9490
+ }
9491
+ } else {
9492
+ query.push(key + '=' + entry.value);
9493
+ }
9494
+ }
9495
+
9496
+ return {
9497
+ statusCode: 302,
9498
+ statusDescription: 'Found',
9499
+ headers: {
9500
+ location: { value: request.uri + (query.length ? '?' + query.join('&') : '') }
9501
+ },
9502
+ cookies: {
9503
+ 'awsless-deployment': {
9504
+ value: deployment,
9505
+ attributes: 'Path=/; Secure; SameSite=Lax'
9506
+ }
9507
+ }
9508
+ };
9509
+ }`;
9180
9510
  var ACTIVE_PREFIX = (router) => `
9181
9511
  const router = ${JSON.stringify(router)};
9182
9512
  let prefix;
@@ -9244,20 +9574,77 @@ function isValidRoute(route, method) {
9244
9574
  return true;
9245
9575
  }
9246
9576
 
9577
+ function matchRoute(value, path, method) {
9578
+ const list = Array.isArray(value) ? value : [value];
9579
+
9580
+ for(const i in list) {
9581
+ const route = list[i];
9582
+
9583
+ if(!isValidRoute(route, method)) {
9584
+ continue;
9585
+ }
9586
+
9587
+ if(route.match) {
9588
+ const found = path.match(new RegExp(route.match));
9589
+
9590
+ if(!found) {
9591
+ continue;
9592
+ }
9593
+
9594
+ const params = {};
9595
+
9596
+ if(route.params) {
9597
+ for(const p in route.params) {
9598
+ params[route.params[p]] = found[Number(p) + 1];
9599
+ }
9600
+ }
9601
+
9602
+ return { route: route, params: params };
9603
+ }
9604
+
9605
+ return { route: route };
9606
+ }
9607
+ }
9608
+
9247
9609
  async function findRoute(path, method, prefix) {
9610
+ // only route selection is normalized, the forwarded uri stays untouched
9611
+ if (path.length > 1 && path.slice(-1) === '/') {
9612
+ path = path.slice(0, -1);
9613
+ }
9614
+
9248
9615
  const store = cf.kvs();
9249
9616
  const keys = getPossibleRouteKeys(path);
9250
9617
 
9251
9618
  for(const i in keys) {
9252
9619
  const key = keys[i];
9620
+ let value;
9253
9621
 
9254
9622
  try {
9255
- const route = await store.get(prefix + key, { format: 'json' });
9623
+ value = await store.get(prefix + key, { format: 'json' });
9624
+ } catch (e) {
9625
+ continue;
9626
+ }
9256
9627
 
9257
- if(isValidRoute(route, method)) {
9258
- return route;
9628
+ // Route lists that are too big for a single key value pair
9629
+ // are sharded over multiple entries behind a route index.
9630
+ if(value && value.list) {
9631
+ for(let n = 0; n < value.list; n++) {
9632
+ try {
9633
+ const route = await store.get(prefix + key + '#' + n, { format: 'json' });
9634
+ const result = matchRoute(route, path, method);
9635
+
9636
+ if(result) {
9637
+ return result;
9638
+ }
9639
+ } catch (e) {}
9259
9640
  }
9260
- } catch (e) {}
9641
+ } else {
9642
+ const result = matchRoute(value, path, method);
9643
+
9644
+ if(result) {
9645
+ return result;
9646
+ }
9647
+ }
9261
9648
  }
9262
9649
  }
9263
9650
 
@@ -9326,13 +9713,12 @@ function setS3Origin(route) {
9326
9713
  function setLambdaOrigin(route) {
9327
9714
  const config = getRequestOriginConfig(route);
9328
9715
 
9329
- // CloudFront caps the origin response timeout at 60s without a quota increase.
9330
9716
  if(typeof config.timeouts.readTimeout !== 'number') {
9331
- config.timeouts.readTimeout = 120;
9717
+ config.timeouts.readTimeout = ${ORIGIN_READ_TIMEOUT};
9332
9718
  }
9333
9719
 
9334
9720
  if(typeof config.timeouts.connectionTimeout !== 'number') {
9335
- config.timeouts.connectionTimeout = 10;
9721
+ config.timeouts.connectionTimeout = ${ORIGIN_CONNECTION_TIMEOUT};
9336
9722
  }
9337
9723
 
9338
9724
  cf.updateRequestOrigin(Object.assign(config, {
@@ -9381,15 +9767,36 @@ async function handler(event) {
9381
9767
 
9382
9768
  ${prefixCode}
9383
9769
 
9384
- const route = await findRoute(path, request.method, prefix);
9770
+ const result = await findRoute(path, request.method, prefix);
9385
9771
 
9386
- if(!route) {
9772
+ if(!result) {
9387
9773
  return {
9388
9774
  statusCode: 404,
9389
9775
  statusDescription: 'Not Found'
9390
9776
  };
9391
9777
  }
9392
9778
 
9779
+ const route = result.route;
9780
+
9781
+ // A client provided param header can never reach the origin.
9782
+ const spoofed = [];
9783
+
9784
+ for(const name in headers) {
9785
+ if(name.indexOf('x-param-') === 0) {
9786
+ spoofed.push(name);
9787
+ }
9788
+ }
9789
+
9790
+ for(const i in spoofed) {
9791
+ delete headers[spoofed[i]];
9792
+ }
9793
+
9794
+ if(result.params) {
9795
+ for(const name in result.params) {
9796
+ headers['x-param-' + name.toLowerCase()] = { value: encodeURIComponent(result.params[name]) };
9797
+ }
9798
+ }
9799
+
9393
9800
  if(route.requestHeaders) {
9394
9801
  for(const name in route.requestHeaders) {
9395
9802
  headers[name] = { value: route.requestHeaders[name] };
@@ -9408,6 +9815,8 @@ async function handler(event) {
9408
9815
 
9409
9816
  if(route.forwardHost && headers.host && headers.host.value) {
9410
9817
  headers['x-forwarded-host'] = { value: headers.host.value };
9818
+ } else {
9819
+ delete headers['x-forwarded-host'];
9411
9820
  }
9412
9821
 
9413
9822
  headers['x-origin'] = { value: route.domainName };
@@ -9440,6 +9849,31 @@ async function handler(event) {
9440
9849
  `;
9441
9850
 
9442
9851
  // src/feature/router/index.ts
9852
+ var MAX_VALUE_SIZE = 1e3;
9853
+ var ORIGIN_PLACEHOLDER = "x".repeat(64);
9854
+ var assertRouteValueSize = (key, route) => {
9855
+ const withOrigin = (entry) => {
9856
+ return entry.type === "lambda" ? { ...entry, domainName: ORIGIN_PLACEHOLDER } : entry;
9857
+ };
9858
+ for (const entry of Array.isArray(route) ? route : [route]) {
9859
+ if (Buffer.byteLength(JSON.stringify(withOrigin(entry)), "utf8") > MAX_VALUE_SIZE) {
9860
+ throw new ExpectedError(`The route value of the "${key}" route key is too large.`);
9861
+ }
9862
+ }
9863
+ };
9864
+ var createRouteStoreEntries = (key, route) => {
9865
+ const value = JSON.stringify(route);
9866
+ if (!Array.isArray(route) || Buffer.byteLength(value, "utf8") <= MAX_VALUE_SIZE) {
9867
+ return [{ key, value }];
9868
+ }
9869
+ return [
9870
+ { key, value: JSON.stringify({ list: route.length }) },
9871
+ ...route.map((entry, index) => ({
9872
+ key: `${key}#${index}`,
9873
+ value: JSON.stringify(entry)
9874
+ }))
9875
+ ];
9876
+ };
9443
9877
  var routerFeature = defineFeature({
9444
9878
  name: "router",
9445
9879
  onApp(ctx) {
@@ -9450,7 +9884,6 @@ var routerFeature = defineFeature({
9450
9884
  const distributionIds = [];
9451
9885
  let hasLambdaRoutes = false;
9452
9886
  let routeStore;
9453
- let previewDistribution;
9454
9887
  for (const [id, props] of routers) {
9455
9888
  const group = new Group29(ctx.base, "router", id);
9456
9889
  const name = formatGlobalResourceName({
@@ -9486,20 +9919,21 @@ var routerFeature = defineFeature({
9486
9919
  if (Object.hasOwn(routes, `${id}:${key}`)) {
9487
9920
  throw new ExpectedError(`Duplicate route key: ${key} in the "${id}" router`);
9488
9921
  }
9922
+ assertRouteValueSize(`${id}:${key}`, route);
9489
9923
  routes[`${id}:${key}`] = route;
9490
9924
  }
9491
9925
  for (const dependency of options?.dependsOn ?? []) {
9492
9926
  routeDependencies.add(dependency);
9493
9927
  }
9494
- if (Object.values(newRoutes).some((route) => route.type === "lambda")) {
9928
+ if (Object.values(newRoutes).flat().some((route) => route.type === "lambda")) {
9495
9929
  hasLambdaRoutes = true;
9496
9930
  }
9497
9931
  });
9498
9932
  const cache = new aws30.cloudfront.CachePolicy(group, "cache", {
9499
9933
  name,
9500
- minTtl: toSeconds12(seconds6(0)),
9501
- maxTtl: toSeconds12(days10(365)),
9502
- defaultTtl: toSeconds12(days10(0)),
9934
+ minTtl: toSeconds13(seconds7(0)),
9935
+ maxTtl: toSeconds13(days10(365)),
9936
+ defaultTtl: toSeconds13(days10(0)),
9503
9937
  parametersInCacheKeyAndForwardedToOrigin: {
9504
9938
  enableAcceptEncodingBrotli: true,
9505
9939
  enableAcceptEncodingGzip: true,
@@ -9552,7 +9986,7 @@ var routerFeature = defineFeature({
9552
9986
  name,
9553
9987
  corsConfig: {
9554
9988
  originOverride: props.cors?.override ?? true,
9555
- accessControlMaxAgeSec: toSeconds12(props.cors?.maxAge ?? years(1)),
9989
+ accessControlMaxAgeSec: toSeconds13(props.cors?.maxAge ?? years(1)),
9556
9990
  accessControlAllowHeaders: { items: props.cors?.headers ?? ["*"] },
9557
9991
  accessControlAllowMethods: { items: props.cors?.methods ?? ["ALL"] },
9558
9992
  accessControlAllowOrigins: { items: props.cors?.origins ?? ["*"] },
@@ -9577,7 +10011,7 @@ var routerFeature = defineFeature({
9577
10011
  strictTransportSecurity: {
9578
10012
  override: true,
9579
10013
  preload: true,
9580
- accessControlMaxAgeSec: toSeconds12(years(1)),
10014
+ accessControlMaxAgeSec: toSeconds13(years(1)),
9581
10015
  includeSubdomains: true
9582
10016
  },
9583
10017
  xssProtection: {
@@ -9597,7 +10031,7 @@ var routerFeature = defineFeature({
9597
10031
  rateBasedStatement: {
9598
10032
  limit: wafSettingsConfig.rateLimiter.limit,
9599
10033
  aggregateKeyType: "IP",
9600
- evaluationWindowSec: toSeconds12(wafSettingsConfig.rateLimiter.window)
10034
+ evaluationWindowSec: toSeconds13(wafSettingsConfig.rateLimiter.window)
9601
10035
  }
9602
10036
  },
9603
10037
  action: {
@@ -9687,12 +10121,12 @@ var routerFeature = defineFeature({
9687
10121
  rule: wafRules,
9688
10122
  captchaConfig: {
9689
10123
  immunityTimeProperty: {
9690
- immunityTime: toSeconds12(wafSettingsConfig.captchaImmunityTime)
10124
+ immunityTime: toSeconds13(wafSettingsConfig.captchaImmunityTime)
9691
10125
  }
9692
10126
  },
9693
10127
  challengeConfig: {
9694
10128
  immunityTimeProperty: {
9695
- immunityTime: toSeconds12(wafSettingsConfig.challengeImmunityTime)
10129
+ immunityTime: toSeconds13(wafSettingsConfig.challengeImmunityTime)
9696
10130
  }
9697
10131
  },
9698
10132
  visibilityConfig: {
@@ -9748,7 +10182,7 @@ var routerFeature = defineFeature({
9748
10182
  }
9749
10183
  return {
9750
10184
  errorCode: Number(errorCode),
9751
- errorCachingMinTtl: item.minTTL ? toSeconds12(item.minTTL) : void 0,
10185
+ errorCachingMinTtl: item.minTTL ? toSeconds13(item.minTTL) : void 0,
9752
10186
  responseCode: item.statusCode?.toString() ?? errorCode,
9753
10187
  responsePagePath: item.path
9754
10188
  };
@@ -9787,19 +10221,20 @@ var routerFeature = defineFeature({
9787
10221
  ],
9788
10222
  webAclId: waf?.arn
9789
10223
  });
9790
- if (id === defaultRouter) {
10224
+ {
9791
10225
  const previewFunction = new aws30.cloudfront.Function(group, "preview-function", {
9792
10226
  name: `${name.slice(0, 55)}--preview`,
9793
10227
  runtime: "cloudfront-js-2.0",
9794
10228
  code: getViewerRequestFunctionCode({
9795
10229
  router: id,
10230
+ preview: true,
9796
10231
  basicAuth: props.basicAuth,
9797
10232
  passwordAuth: props.passwordAuth
9798
10233
  }),
9799
10234
  publish: true,
9800
10235
  keyValueStoreAssociations: [routeStore.arn]
9801
10236
  });
9802
- previewDistribution = new aws30.cloudfront.Distribution(group, "preview", {
10237
+ const previewDistribution = new aws30.cloudfront.Distribution(group, "preview", {
9803
10238
  tags: {
9804
10239
  name: `${name}-preview`
9805
10240
  },
@@ -9830,7 +10265,7 @@ var routerFeature = defineFeature({
9830
10265
  }
9831
10266
  return {
9832
10267
  errorCode: Number(errorCode),
9833
- errorCachingMinTtl: item.minTTL ? toSeconds12(item.minTTL) : void 0,
10268
+ errorCachingMinTtl: item.minTTL ? toSeconds13(item.minTTL) : void 0,
9834
10269
  responseCode: item.statusCode ?? Number(errorCode),
9835
10270
  responsePagePath: item.path
9836
10271
  };
@@ -9863,6 +10298,9 @@ var routerFeature = defineFeature({
9863
10298
  webAclId: waf?.arn
9864
10299
  });
9865
10300
  distributionIds.push(previewDistribution.id);
10301
+ ctx.shared.add("router", "preview-id", id, previewDistribution.id);
10302
+ }
10303
+ if (id === defaultRouter) {
9866
10304
  ctx.onReadyLast(() => {
9867
10305
  const bundle = ctx.shared.get("bundle", "main");
9868
10306
  let lambdaUrlHost;
@@ -9889,38 +10327,24 @@ var routerFeature = defineFeature({
9889
10327
  storeArn: routeStore.arn,
9890
10328
  functionVersion: bundle.lambda.version,
9891
10329
  routes: $resolve([routes, lambdaUrlHost], (routes2, lambdaUrlHost2) => {
9892
- return Object.entries(routes2).map(([key, route]) => ({
9893
- key,
9894
- value: JSON.stringify(
9895
- route.type === "lambda" ? { ...route, domainName: lambdaUrlHost2 } : route
10330
+ const withOrigin = (route) => {
10331
+ return route.type === "lambda" ? { ...route, domainName: lambdaUrlHost2 } : route;
10332
+ };
10333
+ return Object.entries(routes2).flatMap(
10334
+ ([key, route]) => createRouteStoreEntries(
10335
+ key,
10336
+ Array.isArray(route) ? route.map(withOrigin) : withOrigin(route)
9896
10337
  )
9897
- }));
10338
+ );
9898
10339
  })
9899
10340
  },
9900
10341
  {
9901
10342
  dependsOn: Array.from(routeDependencies)
9902
10343
  }
9903
10344
  );
9904
- if (!(props.basicAuth ?? props.passwordAuth)) {
9905
- bundle.addEnv(
9906
- "AWSLESS_PREVIEW",
9907
- $resolve(
9908
- [routes],
9909
- (routes2) => JSON.stringify({
9910
- router: id,
9911
- routes: Object.fromEntries(
9912
- Object.entries(routes2).filter(
9913
- ([key, route]) => key.startsWith(`${id}:`) && route.type !== "url"
9914
- )
9915
- )
9916
- })
9917
- )
9918
- );
9919
- }
9920
10345
  });
9921
10346
  }
9922
10347
  ctx.shared.add("router", "id", id, distribution.id);
9923
- ctx.shared.add("router", "preview-id", id, previewDistribution.id);
9924
10348
  distributionIds.push(distribution.id);
9925
10349
  if (props.domain) {
9926
10350
  const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
@@ -9971,6 +10395,41 @@ var routerFeature = defineFeature({
9971
10395
  ctx.bind(`ROUTER_${constantCase15(id)}_ENDPOINT`, domainName);
9972
10396
  }
9973
10397
  }
10398
+ },
10399
+ onStack(ctx) {
10400
+ for (const [id, patterns] of Object.entries(ctx.stackConfig.routes ?? {})) {
10401
+ if (!ctx.appConfig.defaults.router?.[id]) {
10402
+ throw new FileError(ctx.stackConfig.file, `Router "${id}" is not defined on the app level.`);
10403
+ }
10404
+ const addRoutes = ctx.shared.entry("router", "addRoutes", id);
10405
+ const grouped = {};
10406
+ for (const [pattern, props] of Object.entries(patterns)) {
10407
+ const compiled = compileRoutePattern(pattern);
10408
+ const slug = kebabCase12(pattern).slice(0, 20);
10409
+ const routeKey = formatRouteKey(ctx.stack.name, "route", `${slug || "root"}-${shortId(pattern)}`);
10410
+ registerBundleFunction(ctx, routeKey, props);
10411
+ grouped[compiled.key] ??= [];
10412
+ grouped[compiled.key].push({
10413
+ type: "lambda",
10414
+ forwardHost: true,
10415
+ urlEncodedQueryString: true,
10416
+ match: compiled.match,
10417
+ params: compiled.params,
10418
+ requestHeaders: {
10419
+ [ROUTE_HEADER]: routeKey
10420
+ }
10421
+ });
10422
+ }
10423
+ const merged = {};
10424
+ for (const [key, list3] of Object.entries(grouped)) {
10425
+ if (list3.length === 1) {
10426
+ merged[key] = list3[0];
10427
+ } else {
10428
+ merged[key] = [...list3.filter((route) => route.match), ...list3.filter((route) => !route.match)];
10429
+ }
10430
+ }
10431
+ addRoutes(merged);
10432
+ }
9974
10433
  }
9975
10434
  });
9976
10435
 
@@ -10264,6 +10723,12 @@ var logo = () => {
10264
10723
  var layout = async (command, cb) => {
10265
10724
  console.log();
10266
10725
  log9.intro(`${logo()} ${color.line(command)}`);
10726
+ let completed = false;
10727
+ process.on("exit", (code) => {
10728
+ if (code === 0 && !completed) {
10729
+ process.exitCode = 130;
10730
+ }
10731
+ });
10267
10732
  try {
10268
10733
  const options = program.optsWithGlobals();
10269
10734
  const appConfig = await loadAppConfig(options);
@@ -10278,9 +10743,11 @@ var layout = async (command, cb) => {
10278
10743
  appConfig,
10279
10744
  stackConfigs
10280
10745
  });
10746
+ completed = true;
10281
10747
  log9.outro(result ?? void 0);
10282
10748
  process.exit(0);
10283
10749
  } catch (error) {
10750
+ completed = true;
10284
10751
  playErrorSound();
10285
10752
  logError(error);
10286
10753
  log9.outro();
@@ -10348,14 +10815,6 @@ var SharedData = class {
10348
10815
  };
10349
10816
 
10350
10817
  // src/app.ts
10351
- var assertDepsExists = (stack, stacks) => {
10352
- for (const dep of stack.depends ?? []) {
10353
- const found = stacks.find((i) => i.name === dep);
10354
- if (!found) {
10355
- throw new FileError(stack.file, `Stack "${stack.name}" depends on a stack "${dep}" that doesn't exist.`);
10356
- }
10357
- }
10358
- };
10359
10818
  var createApp = (props) => {
10360
10819
  const app = new App2(props.appConfig.name);
10361
10820
  const zones = new Stack(app, "zones");
@@ -10379,17 +10838,10 @@ var createApp = (props) => {
10379
10838
  const bindListeners = [];
10380
10839
  const globalEnv = [];
10381
10840
  const globalEnvListeners = [];
10382
- const allLocalEnv = {};
10383
- const allLocalEnvListeners = {};
10384
10841
  const globalPermissions = [];
10385
10842
  const globalPermissionCallbacks = [];
10386
10843
  const appPermissions = [];
10387
10844
  const appPermissionCallbacks = [];
10388
- const allStackPermissions = {};
10389
- const allStackPermissionCallbacks = {};
10390
- for (const stackConfig of props.stackConfigs) {
10391
- assertDepsExists(stackConfig, props.stackConfigs);
10392
- }
10393
10845
  for (const feature of features) {
10394
10846
  feature.onBefore?.({
10395
10847
  ...props,
@@ -10462,14 +10914,6 @@ var createApp = (props) => {
10462
10914
  }
10463
10915
  for (const stackConfig of props.stackConfigs) {
10464
10916
  const stack = new Stack(app, stackConfig.name);
10465
- const localEnvListeners = [];
10466
- const localEnv = [];
10467
- const stackPermissions = [];
10468
- const stackPermissionCallbacks = [];
10469
- allStackPermissions[stack.name] = stackPermissions;
10470
- allStackPermissionCallbacks[stack.name] = stackPermissionCallbacks;
10471
- allLocalEnvListeners[stack.name] = localEnvListeners;
10472
- allLocalEnv[stack.name] = localEnv;
10473
10917
  for (const feature of features) {
10474
10918
  feature.onStack?.({
10475
10919
  ...props,
@@ -10483,7 +10927,6 @@ var createApp = (props) => {
10483
10927
  shared,
10484
10928
  onPermission(callback) {
10485
10929
  globalPermissionCallbacks.push(callback);
10486
- stackPermissionCallbacks.push(callback);
10487
10930
  },
10488
10931
  addGlobalPermission(permission) {
10489
10932
  globalPermissions.push(permission);
@@ -10491,9 +10934,6 @@ var createApp = (props) => {
10491
10934
  addAppPermission(permission) {
10492
10935
  appPermissions.push(permission);
10493
10936
  },
10494
- addStackPermission(permission) {
10495
- stackPermissions.push(permission);
10496
- },
10497
10937
  addWarning(props2) {
10498
10938
  warnings.push(props2);
10499
10939
  },
@@ -10557,10 +10997,10 @@ var createApp = (props) => {
10557
10997
  bindListeners.push(cb);
10558
10998
  },
10559
10999
  addEnv(name, value) {
10560
- localEnv.push({ name, value });
11000
+ globalEnv.push({ name, value });
10561
11001
  },
10562
11002
  onEnv(cb) {
10563
- localEnvListeners.push(cb);
11003
+ globalEnvListeners.push(cb);
10564
11004
  },
10565
11005
  onReady(cb) {
10566
11006
  readyListeners.push(cb);
@@ -10570,16 +11010,6 @@ var createApp = (props) => {
10570
11010
  }
10571
11011
  });
10572
11012
  }
10573
- for (const callback of stackPermissionCallbacks) {
10574
- for (const permission of stackPermissions) {
10575
- callback(permission);
10576
- }
10577
- }
10578
- for (const listener of localEnvListeners) {
10579
- for (const env of localEnv) {
10580
- listener(env.name, env.value);
10581
- }
10582
- }
10583
11013
  }
10584
11014
  for (const callback of appPermissionCallbacks) {
10585
11015
  for (const permission of appPermissions) {
@@ -10601,24 +11031,6 @@ var createApp = (props) => {
10601
11031
  listener(name, value);
10602
11032
  }
10603
11033
  }
10604
- for (const stackConfig of props.stackConfigs) {
10605
- const envListeners = allLocalEnvListeners[stackConfig.name];
10606
- const permissionCallbacks = allStackPermissionCallbacks[stackConfig.name];
10607
- for (const dependency of stackConfig.depends ?? []) {
10608
- const permissions = allStackPermissions[dependency];
10609
- const env = allLocalEnv[dependency];
10610
- for (const permission of permissions) {
10611
- for (const callback of permissionCallbacks) {
10612
- callback(permission);
10613
- }
10614
- }
10615
- for (const entry of env) {
10616
- for (const listener of envListeners) {
10617
- listener(entry.name, entry.value);
10618
- }
10619
- }
10620
- }
10621
- }
10622
11034
  const ready = () => {
10623
11035
  for (const listener of readyListeners) {
10624
11036
  listener();
@@ -10664,6 +11076,7 @@ var buildAssets = async (builders, stackFilters, showResult = false) => {
10664
11076
  if (filteredBuilders.length === 0) {
10665
11077
  return;
10666
11078
  }
11079
+ filteredBuilders.sort((a, b) => Number(a.type === "bundle") - Number(b.type === "bundle"));
10667
11080
  const results = [];
10668
11081
  await log10.task({
10669
11082
  initialMessage: `Building assets...`,
@@ -11483,9 +11896,10 @@ var deploy = (program2) => {
11483
11896
  }
11484
11897
  });
11485
11898
  playSuccessSound();
11486
- const details = deployments2.length > 0 ? `
11487
- ${deployments2.join("\n")}` : "";
11488
- return `Deployment #${deployment.id} is live.${details}`;
11899
+ for (const summary of deployments2) {
11900
+ log20.message(summary);
11901
+ }
11902
+ return `Deployment #${deployment.id} is live.`;
11489
11903
  });
11490
11904
  });
11491
11905
  };
@@ -11976,7 +12390,7 @@ var bind = (program2) => {
11976
12390
  stderr: "inherit"
11977
12391
  });
11978
12392
  await instance.exited;
11979
- process.exit(0);
12393
+ process.exit(instance.exitCode ?? 1);
11980
12394
  });
11981
12395
  });
11982
12396
  };
@@ -12096,7 +12510,7 @@ var resources = (program2) => {
12096
12510
  return `${color.dim("{")}${color.warning(v)}${color.dim("}")}`;
12097
12511
  }).replaceAll(":", color.dim(":"));
12098
12512
  };
12099
- const formatStatus = (status) => {
12513
+ const formatStatus2 = (status) => {
12100
12514
  if (status === "created") {
12101
12515
  return color.success(status);
12102
12516
  }
@@ -12121,7 +12535,7 @@ var resources = (program2) => {
12121
12535
  stack.resources.map((r) => {
12122
12536
  return [
12123
12537
  //
12124
- formatStatus(r.status),
12538
+ formatStatus2(r.status),
12125
12539
  color.dim(icon.arrow.right),
12126
12540
  formatResource(stack.urn, r.urn)
12127
12541
  ].join(" ");
@@ -12365,9 +12779,12 @@ var test = (program2) => {
12365
12779
  if (tests.length === 0) {
12366
12780
  return "No tests found.";
12367
12781
  }
12368
- await runTests(tests, stacks, options?.filters, {
12782
+ const passed = await runTests(tests, stacks, options?.filters, {
12369
12783
  showLogs: true
12370
12784
  });
12785
+ if (!passed) {
12786
+ throw new Cancelled();
12787
+ }
12371
12788
  return "All tests finished.";
12372
12789
  });
12373
12790
  });
@@ -12592,14 +13009,14 @@ var parseJsonLog = (message) => {
12592
13009
  json = JSON.parse(message);
12593
13010
  } catch (error) {
12594
13011
  }
12595
- if ("level" in json && typeof json.level === "string" && "timestamp" in json && typeof json.timestamp === "string" && "message" in json) {
13012
+ if (typeof json === "object" && json !== null && "level" in json && typeof json.level === "string" && "timestamp" in json && typeof json.timestamp === "string" && "message" in json) {
12596
13013
  return {
12597
13014
  level: json.level,
12598
13015
  message: typeof json.message === "string" ? json.message : JSON.stringify(json.message, void 0, 2),
12599
13016
  date: new Date(json.timestamp)
12600
13017
  };
12601
13018
  }
12602
- if ("type" in json && typeof json.type === "string" && json.type.startsWith("platform") && "time" in json && typeof json.time === "string" && "record" in json) {
13019
+ 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) {
12603
13020
  return {
12604
13021
  level: "SYSTEM",
12605
13022
  message: JSON.stringify(json.record, void 0, 2),
@@ -13047,7 +13464,7 @@ var activity = (program2) => {
13047
13464
  // src/cli/command/deployment.ts
13048
13465
  import { CloudFrontClient as CloudFrontClient6 } from "@aws-sdk/client-cloudfront";
13049
13466
  import { CloudFrontKeyValueStoreClient as CloudFrontKeyValueStoreClient3 } from "@aws-sdk/client-cloudfront-keyvaluestore";
13050
- import { DeleteFunctionCommand, LambdaClient as LambdaClient7 } from "@aws-sdk/client-lambda";
13467
+ import { LambdaClient as LambdaClient7 } from "@aws-sdk/client-lambda";
13051
13468
  import { log as log35, prompt as prompt19 } from "@awsless/clui";
13052
13469
  import { DynamoDBClient as DynamoDBClient6 } from "@awsless/dynamodb";
13053
13470
  var createClients = async (appConfig) => {
@@ -13064,11 +13481,17 @@ var createClients = async (appConfig) => {
13064
13481
  };
13065
13482
  };
13066
13483
  var formatAge = (iso) => {
13067
- const minutes8 = Math.floor((Date.now() - Date.parse(iso)) / 6e4);
13068
- if (minutes8 < 1) return "just now";
13069
- if (minutes8 < 60) return `${minutes8}m ago`;
13070
- if (minutes8 < 60 * 24) return `${Math.floor(minutes8 / 60)}h ago`;
13071
- return `${Math.floor(minutes8 / (60 * 24))}d ago`;
13484
+ const minutes9 = Math.floor((Date.now() - Date.parse(iso)) / 6e4);
13485
+ if (minutes9 < 1) return "just now";
13486
+ if (minutes9 < 60) return `${minutes9}m ago`;
13487
+ if (minutes9 < 60 * 24) return `${Math.floor(minutes9 / 60)}h ago`;
13488
+ return `${Math.floor(minutes9 / (60 * 24))}d ago`;
13489
+ };
13490
+ var formatStatus = (item, liveId) => {
13491
+ if (item.id === liveId) return color.success("live ");
13492
+ if (item.promotedAt) return "promoted";
13493
+ if (item.functionVersion) return color.info("staged ");
13494
+ return color.dim("pending ");
13072
13495
  };
13073
13496
  var deployments = (program2) => {
13074
13497
  program2.command("deployments").description("List the deployment history of your app").action(async () => {
@@ -13083,17 +13506,16 @@ var deployments = (program2) => {
13083
13506
  }
13084
13507
  const idWidth = Math.max(...items.map((item) => item.id.length));
13085
13508
  log35.message(
13086
- items.map((item) => {
13087
- const status = item.id === liveId ? color.success("live ") : item.promotedAt ? "promoted" : item.functionVersion ? color.info("staged ") : color.dim("pending ");
13088
- return [
13509
+ items.map(
13510
+ (item) => [
13089
13511
  color.label(item.id.padEnd(idWidth)),
13090
- status,
13512
+ formatStatus(item, liveId),
13091
13513
  formatAge(item.createdAt).padEnd(8),
13092
13514
  color.dim(item.commit?.slice(0, 7) ?? "-------"),
13093
13515
  (item.message ?? "").slice(0, 50).padEnd(50),
13094
13516
  color.dim(item.user ?? "")
13095
- ].join(" ");
13096
- }).join("\n")
13517
+ ].join(" ")
13518
+ ).join("\n")
13097
13519
  );
13098
13520
  return `Found ${items.length} deployments.`;
13099
13521
  });
@@ -13107,28 +13529,7 @@ var prune = (program2) => {
13107
13529
  listDeployments(dynamo, appId),
13108
13530
  readLiveDeploymentId(lambda, functionName)
13109
13531
  ]);
13110
- const rollbackTarget = items.filter((item) => item.promotedAt && item.id !== liveId).sort((a, b) => b.promotedAt.localeCompare(a.promotedAt))[0];
13111
- const keep = Math.max(1, Number(options.keep) || 10);
13112
- const mainSlug = slugifyBranch(options.main);
13113
- const keptMain = new Set(
13114
- items.filter((item) => item.branch === mainSlug && item.functionVersion).map((item) => item.seq).sort((a, b) => b - a).slice(0, keep)
13115
- );
13116
- const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
13117
- const prunable = items.filter((item) => {
13118
- if (item.id === liveId || item.id === rollbackTarget?.id) {
13119
- return false;
13120
- }
13121
- if (options.branch) {
13122
- return item.branch === slugifyBranch(options.branch);
13123
- }
13124
- if (!item.functionVersion) {
13125
- return item.createdAt < dayAgo;
13126
- }
13127
- if (item.branch === mainSlug) {
13128
- return !keptMain.has(item.seq);
13129
- }
13130
- return item.commit ? isCommitMerged(item.commit, options.main) : false;
13131
- });
13532
+ const prunable = selectPrunableDeployments(items, liveId, options);
13132
13533
  if (prunable.length === 0) {
13133
13534
  return `Nothing to prune.`;
13134
13535
  }
@@ -13145,31 +13546,25 @@ var prune = (program2) => {
13145
13546
  initialMessage: "Pruning the deployments",
13146
13547
  successMessage: "Done pruning the deployments.",
13147
13548
  task: () => withAppReleaseLock(appConfig, async () => {
13148
- for (const item of prunable) {
13549
+ const [freshItems, freshLiveId] = await Promise.all([
13550
+ listDeployments(dynamo, appId),
13551
+ readLiveDeploymentId(lambda, functionName)
13552
+ ]);
13553
+ const confirmed = new Set(prunable.map((item) => item.id));
13554
+ const prune2 = selectPrunableDeployments(freshItems, freshLiveId, options).filter(
13555
+ (item) => confirmed.has(item.id)
13556
+ );
13557
+ for (const item of prune2) {
13149
13558
  await deleteLambdaAlias(lambda, functionName, getDeploymentLambdaAliasName(item.id));
13150
13559
  }
13151
- const surviving = items.filter((item) => !prunable.includes(item));
13152
- const keepVersions = new Set(surviving.map((item) => item.functionVersion));
13153
- const live = await getLambdaAlias(lambda, functionName, LIVE_LAMBDA_ALIAS);
13154
- if (live?.FunctionVersion) {
13155
- keepVersions.add(live.FunctionVersion);
13156
- }
13157
- const versions = new Set(
13158
- prunable.map((item) => item.functionVersion).filter((version) => version && !keepVersions.has(version))
13159
- );
13560
+ const versions = await selectPrunableVersions({
13561
+ lambda,
13562
+ functionName,
13563
+ items: freshItems,
13564
+ prunable: prune2
13565
+ });
13160
13566
  for (const version of versions) {
13161
- try {
13162
- await lambda.send(
13163
- new DeleteFunctionCommand({
13164
- FunctionName: functionName,
13165
- Qualifier: version
13166
- })
13167
- );
13168
- } catch (error) {
13169
- if (!isError(error, "ResourceNotFoundException") && !isError(error, "ResourceConflictException")) {
13170
- throw error;
13171
- }
13172
- }
13567
+ await pruneFunctionVersion(lambda, functionName, version);
13173
13568
  }
13174
13569
  const storeArn = await getRouteStoreArn(
13175
13570
  cloudfront,
@@ -13183,10 +13578,10 @@ var prune = (program2) => {
13183
13578
  await pruneStoreDeployments(
13184
13579
  kvs,
13185
13580
  storeArn,
13186
- prunable.map((item) => item.id)
13581
+ prune2.map((item) => item.id)
13187
13582
  );
13188
13583
  }
13189
- for (const item of prunable) {
13584
+ for (const item of prune2) {
13190
13585
  await removeDeployment(dynamo, appId, item.id);
13191
13586
  }
13192
13587
  })
@@ -13255,8 +13650,8 @@ program.option("--stage <string>", "The stage to use");
13255
13650
  program.option("-c --no-cache", "Always build & test without the cache");
13256
13651
  program.option("-s --skip-prompt", "Skip prompts");
13257
13652
  program.option("-v --verbose", "Print verbose logs");
13258
- program.exitOverride(() => {
13259
- process.exit(0);
13653
+ program.exitOverride((error) => {
13654
+ process.exit(error.exitCode);
13260
13655
  });
13261
13656
  program.on("option:verbose", () => {
13262
13657
  process.env.VERBOSE = program.opts().verbose ? "1" : void 0;
@@ -13270,4 +13665,13 @@ program.on("option:no-cache", () => {
13270
13665
  commands10.forEach((fn) => fn(program));
13271
13666
 
13272
13667
  // src/bin.ts
13668
+ var interrupt = (signal, code) => () => {
13669
+ process.stdout.write("\x1B[?25h");
13670
+ if (signal === "SIGINT" && process.listenerCount(signal) > 1) {
13671
+ return;
13672
+ }
13673
+ process.exit(code);
13674
+ };
13675
+ process.on("SIGINT", interrupt("SIGINT", 130));
13676
+ process.on("SIGTERM", interrupt("SIGTERM", 143));
13273
13677
  program.parse(process.argv);