@awsless/cli 0.0.46-next.0 → 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
  }
@@ -1418,6 +1573,9 @@ var activateDeployment = async (props) => {
1418
1573
  });
1419
1574
  return id;
1420
1575
  };
1576
+ var promoteAppDeployment = (props) => {
1577
+ return activateDeployment({ ...props, rejectStale: true });
1578
+ };
1421
1579
  var rollbackAppDeployment = (props) => {
1422
1580
  return withAppReleaseLock(props.appConfig, () => activateDeployment(props));
1423
1581
  };
@@ -1830,7 +1988,7 @@ var CodeSchema = z14.union([
1830
1988
  var FnSchema = z14.object({
1831
1989
  code: CodeSchema,
1832
1990
  handler: HandlerSchema.optional()
1833
- });
1991
+ }).strict();
1834
1992
  var FunctionSchema = z14.union([
1835
1993
  LocalFileSchema.transform((code) => ({
1836
1994
  code
@@ -2076,6 +2234,66 @@ var InstanceDefaultSchema = z19.object({
2076
2234
  // src/feature/router/schema.ts
2077
2235
  import { days as days3, minutes as minutes2, parse as parse3 } from "@awsless/duration";
2078
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
2079
2297
  var ErrorResponsePathSchema = z20.string().describe(
2080
2298
  [
2081
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.",
@@ -2104,7 +2322,29 @@ var ErrorResponseSchema = z20.union([
2104
2322
  minTTL: MinTTLSchema.optional()
2105
2323
  })
2106
2324
  ]).optional();
2107
- 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.");
2108
2348
  var VisibilitySchema = z20.boolean().default(false).describe("Whether to enable CloudWatch metrics for the WAF rule.");
2109
2349
  var WafSettingsSchema = z20.object({
2110
2350
  rateLimiter: z20.object({
@@ -2153,6 +2393,7 @@ var RouterDefaultSchema = z20.record(
2153
2393
  z20.object({
2154
2394
  domain: ResourceIdSchema.describe("The domain id to link your Router.").optional(),
2155
2395
  subDomain: z20.string().optional(),
2396
+ redirectWww: z20.boolean().default(false).describe("Redirect all www subdomain requests to your root domain."),
2156
2397
  waf: WafSettingsSchema.optional(),
2157
2398
  geoRestrictions: z20.array(z20.string().length(2).toUpperCase()).default([]).describe("Specifies a blacklist of countries that should be blocked."),
2158
2399
  errors: z20.object({
@@ -2241,6 +2482,21 @@ var RouterDefaultSchema = z20.record(
2241
2482
  }).optional().describe(
2242
2483
  "Specifies the cookies, headers, and query values that CloudFront includes in the cache key."
2243
2484
  )
2485
+ }).superRefine((props, ctx) => {
2486
+ if (props.redirectWww && !props.domain) {
2487
+ ctx.addIssue({
2488
+ code: z20.ZodIssueCode.custom,
2489
+ path: ["redirectWww"],
2490
+ message: "The redirectWww option requires a domain to be set."
2491
+ });
2492
+ }
2493
+ if (props.redirectWww && props.subDomain) {
2494
+ ctx.addIssue({
2495
+ code: z20.ZodIssueCode.custom,
2496
+ path: ["redirectWww"],
2497
+ message: `The redirectWww option can't be combined with a subDomain, because the domain certificate only covers single level subdomains.`
2498
+ });
2499
+ }
2244
2500
  })
2245
2501
  ).optional().describe(`Define the global Router. Backed by AWS CloudFront.`);
2246
2502
 
@@ -2587,8 +2843,8 @@ var AppSchema = z29.object({
2587
2843
  layers: LayerSchema,
2588
2844
  router: RouterDefaultSchema
2589
2845
  // dataRetention: z.boolean().describe('Configure how your resources are handled on delete.').default(false),
2590
- }).default({}).describe("Default properties")
2591
- });
2846
+ }).strict().default({}).describe("Default properties")
2847
+ }).strict();
2592
2848
 
2593
2849
  // src/config/stack.ts
2594
2850
  import { z as z45 } from "zod";
@@ -2644,7 +2900,7 @@ var CommandsSchema = z31.record(ResourceIdSchema, CommandSchema).optional().desc
2644
2900
 
2645
2901
  // src/feature/config/schema.ts
2646
2902
  import { z as z32 } from "zod";
2647
- var ConfigNameSchema = z32.string().regex(/[a-z0-9\-]/g, "Invalid config name");
2903
+ var ConfigNameSchema = z32.string().regex(/^[a-z0-9-]+$/, "Invalid config name");
2648
2904
  var ConfigsSchema = z32.array(ConfigNameSchema).optional().describe("Define the config values for your stack.");
2649
2905
 
2650
2906
  // src/feature/cron/schema/index.ts
@@ -3197,14 +3453,13 @@ var TestsSchema = z44.union([
3197
3453
  ]).describe("Define the location of your tests for your stack.").optional();
3198
3454
 
3199
3455
  // src/config/stack.ts
3200
- var DependsSchema = ResourceIdSchema.array().optional().describe("Define the stacks that this stack is depended on.");
3201
3456
  var NameSchema = ResourceIdSchema.refine((name) => !["base", "hostedzones"].includes(name), {
3202
3457
  message: `Stack name can't be a reserved name.`
3203
3458
  }).describe("Stack name.");
3204
3459
  var StackSchema = z45.object({
3205
3460
  $schema: z45.string().optional(),
3206
3461
  name: NameSchema,
3207
- depends: DependsSchema,
3462
+ routes: RoutesSchema,
3208
3463
  commands: CommandsSchema,
3209
3464
  // auth: AuthSchema,
3210
3465
  // http: HttpSchema,
@@ -3230,7 +3485,7 @@ var StackSchema = z45.object({
3230
3485
  images: ImagesSchema,
3231
3486
  icons: IconsSchema,
3232
3487
  metrics: MetricsSchema
3233
- });
3488
+ }).strict();
3234
3489
 
3235
3490
  // src/config/load/read.ts
3236
3491
  import { readFile as readFile3 } from "fs/promises";
@@ -3689,7 +3944,7 @@ import { readdir as readdir2, readFile as readFile7, writeFile as writeFile4 } f
3689
3944
  import { join as join10 } from "path";
3690
3945
 
3691
3946
  // src/build/index.ts
3692
- 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";
3693
3948
  import { dirname as dirname4, join as join7 } from "path";
3694
3949
 
3695
3950
  // src/util/timer.ts
@@ -3731,6 +3986,7 @@ var build = (type, name, builder, props) => {
3731
3986
  cached: true
3732
3987
  };
3733
3988
  }
3989
+ await rm2(cacheFile, { force: true });
3734
3990
  const time = createTimer();
3735
3991
  const meta = await callback(async (file, data2) => {
3736
3992
  const path = getBuildPath(type, name, file);
@@ -3783,7 +4039,7 @@ var zipFiles = (files) => {
3783
4039
  import { generateFileHash } from "@awsless/ts-file-cache";
3784
4040
  import { kebabCase as kebabCase5 } from "change-case";
3785
4041
  import { createHash as createHash5 } from "crypto";
3786
- 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";
3787
4043
  import { dirname as dirname5, join as join9 } from "path";
3788
4044
  import { fileURLToPath } from "url";
3789
4045
 
@@ -3795,13 +4051,13 @@ var formatByteSize = (size) => {
3795
4051
  };
3796
4052
 
3797
4053
  // src/util/temp.ts
3798
- import { mkdir as mkdir3, readdir, rm as rm2 } from "fs/promises";
4054
+ import { mkdir as mkdir3, readdir, rm as rm3 } from "fs/promises";
3799
4055
  import { join as join8 } from "path";
3800
4056
  var createTempFolder = async (name) => {
3801
4057
  const path = join8(directories.temp, name);
3802
4058
  await mkdir3(join8(directories.temp, name), { recursive: true });
3803
4059
  process.on("SIGTERM", async () => {
3804
- await rm2(path, { recursive: true });
4060
+ await rm3(path, { recursive: true });
3805
4061
  });
3806
4062
  return {
3807
4063
  path,
@@ -3809,7 +4065,7 @@ var createTempFolder = async (name) => {
3809
4065
  return readdir(path, { recursive: true });
3810
4066
  },
3811
4067
  async delete() {
3812
- await rm2(path, { recursive: true });
4068
+ await rm3(path, { recursive: true });
3813
4069
  }
3814
4070
  };
3815
4071
  };
@@ -3824,7 +4080,6 @@ var bundleTypeScriptWithRolldown = async ({
3824
4080
  format: format3 = "esm",
3825
4081
  minify = true,
3826
4082
  file,
3827
- nativeDir,
3828
4083
  external,
3829
4084
  importAsString: importAsStringList
3830
4085
  }) => {
@@ -3835,9 +4090,15 @@ var bundleTypeScriptWithRolldown = async ({
3835
4090
  return importee.startsWith("@aws-sdk") || importee.startsWith("aws-sdk") || external?.includes(importee);
3836
4091
  },
3837
4092
  treeshake: {
3838
- 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/")
3839
4097
  },
3840
4098
  onwarn: (error) => {
4099
+ if (error.code === "UNRESOLVED_IMPORT") {
4100
+ throw new ExpectedError(error.message);
4101
+ }
3841
4102
  debugError(error.message);
3842
4103
  },
3843
4104
  plugins: [
@@ -3882,6 +4143,7 @@ var bundleTypeScriptWithRolldown = async ({
3882
4143
  chunkFileNames: `[name].${ext}`,
3883
4144
  minify
3884
4145
  });
4146
+ assertNoTestOnlyModules(result.output);
3885
4147
  const hash = createHash4("sha1");
3886
4148
  const files = [];
3887
4149
  for (const item of result.output) {
@@ -3903,6 +4165,25 @@ var bundleTypeScriptWithRolldown = async ({
3903
4165
  files
3904
4166
  };
3905
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
+ };
3906
4187
 
3907
4188
  // src/feature/bundle/util.ts
3908
4189
  var ROUTE_HEADER = "x-awsless-route";
@@ -3925,7 +4206,7 @@ var registerBundleFunction = (ctx, routeKey, props) => {
3925
4206
  };
3926
4207
  var buildBundle = (props) => {
3927
4208
  return async (build3, { workspace }) => {
3928
- 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");
3929
4210
  const handlers = [...props.handlers].sort((a, b) => a.routeKey.localeCompare(b.routeKey));
3930
4211
  const entries = handlers.map(({ routeKey, file, exportName }) => {
3931
4212
  const virtualFile = JSON.stringify(`${file}?awsless-route=${encodeURIComponent(routeKey)}`);
@@ -3974,7 +4255,7 @@ ${entries.join("\n")}
3974
4255
  importAsString: importAsString2.length > 0 ? importAsString2 : void 0
3975
4256
  });
3976
4257
  await temp.delete();
3977
- await rm3(getBuildPath("bundle", props.name, "files"), { recursive: true, force: true });
4258
+ await rm4(getBuildPath("bundle", props.name, "files"), { recursive: true, force: true });
3978
4259
  await Promise.all([
3979
4260
  write("HASH", bundle.hash),
3980
4261
  ...bundle.files.map((file) => write(`files/${file.name}`, file.code)),
@@ -4019,6 +4300,11 @@ var bundleFeature = defineFeature({
4019
4300
  const addLayer = (layer) => {
4020
4301
  layers.push(layer);
4021
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
+ }
4022
4308
  const name = getBundleFunctionName(ctx.app.name);
4023
4309
  const shortName = formatGlobalResourceName({
4024
4310
  appName: ctx.app.name,
@@ -4032,7 +4318,7 @@ var bundleFeature = defineFeature({
4032
4318
  name,
4033
4319
  handlers,
4034
4320
  minify: defaults.minify,
4035
- external: defaults.external
4321
+ external: [...defaults.external ?? [], ...layerPackages]
4036
4322
  })
4037
4323
  );
4038
4324
  const sourceHash = new Output3(envDeps, async (resolve2) => {
@@ -4188,7 +4474,8 @@ var bundleFeature = defineFeature({
4188
4474
  deploymentId: ctx.deploymentId ?? "local-0",
4189
4475
  functionName: lambda.functionName,
4190
4476
  functionVersion: lambda.version,
4191
- onFailureArn: onFailure
4477
+ onFailureArn: onFailure,
4478
+ sourceAccount: ctx.accountId
4192
4479
  },
4193
4480
  {
4194
4481
  // Make sure the permissions are in place before any event source is wired up.
@@ -4274,11 +4561,6 @@ var bundleFeature = defineFeature({
4274
4561
  addLayer,
4275
4562
  addPermission
4276
4563
  });
4277
- },
4278
- onStack(ctx) {
4279
- const bundle = ctx.shared.get("bundle", "main");
4280
- ctx.onEnv(bundle.addEnv);
4281
- ctx.onPermission(bundle.addPermission);
4282
4564
  }
4283
4565
  });
4284
4566
 
@@ -4329,6 +4611,7 @@ var cacheFeature = defineFeature({
4329
4611
  {
4330
4612
  name,
4331
4613
  engine: "valkey",
4614
+ networkType: "dual_stack",
4332
4615
  dailySnapshotTime: "02:00",
4333
4616
  majorEngineVersion: "8",
4334
4617
  snapshotRetentionLimit: props.snapshotRetentionLimit,
@@ -4354,7 +4637,9 @@ var cacheFeature = defineFeature({
4354
4637
  },
4355
4638
  {
4356
4639
  retainOnDelete: ctx.appConfig.removal === "retain",
4357
- import: ctx.import ? name : void 0
4640
+ import: ctx.import ? name : void 0,
4641
+ // The network type can only be set at creation time.
4642
+ replaceOnChanges: ["networkType"]
4358
4643
  }
4359
4644
  );
4360
4645
  const masterHost = cache.endpoint.pipe((v) => v.at(0).address);
@@ -4487,7 +4772,7 @@ var SsmStore = class {
4487
4772
  debug("Value:", color.info(value));
4488
4773
  await this.client.send(
4489
4774
  new PutParameterCommand({
4490
- Type: ParameterType.STRING,
4775
+ Type: ParameterType.SECURE_STRING,
4491
4776
  Name: this.getName(name),
4492
4777
  Value: value,
4493
4778
  Overwrite: true
@@ -4586,7 +4871,7 @@ var configFeature = defineFeature({
4586
4871
  ctx.addEnv(`CONFIG_${constantCase4(name)}`, name);
4587
4872
  }
4588
4873
  if (configs.length) {
4589
- ctx.addStackPermission({
4874
+ ctx.addGlobalPermission({
4590
4875
  actions: [
4591
4876
  "ssm:GetParameter",
4592
4877
  "ssm:GetParameters",
@@ -4923,10 +5208,12 @@ var domainFeature = defineFeature({
4923
5208
  }
4924
5209
  }
4925
5210
  ctx.addGlobalPermission({
4926
- actions: ["ses:*"],
5211
+ actions: ["ses:SendEmail", "ses:SendRawEmail"],
4927
5212
  resources: [
4928
- // `arn:aws:ses:${ctx.appConfig.region}:${ctx.accountId}:identity/*`,
4929
- "*"
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}`
4930
5217
  ]
4931
5218
  });
4932
5219
  }
@@ -5016,7 +5303,9 @@ var functionFeature = defineFeature({
5016
5303
  // 'lambda:ListFunctions',
5017
5304
  // 'lambda:GetFunction',
5018
5305
  ],
5019
- resources: [`arn:aws:lambda:*:*:function:${ctx.appConfig.name}--*`]
5306
+ resources: [
5307
+ `arn:aws:lambda:${ctx.appConfig.region}:${ctx.accountId}:function:${ctx.appConfig.name}--*`
5308
+ ]
5020
5309
  });
5021
5310
  },
5022
5311
  onStack(ctx) {
@@ -5058,7 +5347,7 @@ var onErrorLogFeature = defineFeature({
5058
5347
  const consumerRoute = formatRouteKey(ctx.app.name, "on-error-log", "consumer");
5059
5348
  bundle.addHandler({
5060
5349
  routeKey: handlerRoute,
5061
- 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"),
5062
5351
  exportName: "default"
5063
5352
  });
5064
5353
  bundle.addEnv(formatRouteEnvName(handlerRoute, "CONSUMER"), consumerRoute);
@@ -5283,7 +5572,7 @@ var onFailureFeature = defineFeature({
5283
5572
  const consumer = props.consumer;
5284
5573
  bundle.addHandler({
5285
5574
  routeKey: normalizerRoute,
5286
- file: join12(dirname7(fileURLToPath3(import.meta.url)), "/handlers/on-failure.mjs"),
5575
+ file: join12(dirname7(fileURLToPath3(import.meta.url)), "/handlers/on-failure.js"),
5287
5576
  exportName: "default"
5288
5577
  });
5289
5578
  registerBundleFunction(ctx, consumerRoute, consumer);
@@ -5438,7 +5727,7 @@ var createPubSubService = (parentGroup, ctx, id, props, inputs) => {
5438
5727
  });
5439
5728
  const shortName = shortId(`${ctx.app.name}:pubsub:${id}:${ctx.appId}`);
5440
5729
  const image2 = "public.ecr.aws/aws-cli/aws-cli:arm64";
5441
- const bundleFile = join14(__dirname, "handlers/pubsub-server.mjs");
5730
+ const bundleFile = join14(__dirname, "handlers/pubsub-server.js");
5442
5731
  ctx.registerBuild("pubsub", name, async (build3) => {
5443
5732
  const hash = createHash8("sha1").update(await readFile9(bundleFile)).digest("hex");
5444
5733
  const fingerprint = `${hash}-${ARCHITECTURE}`;
@@ -5539,7 +5828,8 @@ var createPubSubService = (parentGroup, ctx, id, props, inputs) => {
5539
5828
  Statement: list3.map((statement) => ({
5540
5829
  Effect: pascalCase2(statement.effect ?? "allow"),
5541
5830
  Action: statement.actions,
5542
- Resource: statement.resources
5831
+ Resource: statement.resources,
5832
+ Condition: statement.conditions
5543
5833
  }))
5544
5834
  })
5545
5835
  );
@@ -5893,12 +6183,15 @@ var pubsubFeature = defineFeature({
5893
6183
  {
5894
6184
  name: cacheName,
5895
6185
  engine: "valkey",
6186
+ networkType: "dual_stack",
5896
6187
  majorEngineVersion: "8",
5897
6188
  securityGroupIds: [cacheSecurityGroup.id],
5898
6189
  subnetIds: ctx.shared.get("vpc", "private-subnets")
5899
6190
  },
5900
6191
  {
5901
- import: ctx.import ? cacheName : void 0
6192
+ import: ctx.import ? cacheName : void 0,
6193
+ // The network type can only be set at creation time.
6194
+ replaceOnChanges: ["networkType"]
5902
6195
  }
5903
6196
  );
5904
6197
  const redisHost = cache.endpoint.pipe((v) => v.at(0).address);
@@ -6027,7 +6320,7 @@ var pubsubFeature = defineFeature({
6027
6320
  const publisherRouteKey = formatRouteKey(ctx.app.name, "pubsub", `${id}-publisher`);
6028
6321
  bundle.addHandler({
6029
6322
  routeKey: publisherRouteKey,
6030
- file: join15(__dirname2, "/handlers/pubsub-publisher.mjs"),
6323
+ file: join15(__dirname2, "/handlers/pubsub-publisher.js"),
6031
6324
  exportName: "default"
6032
6325
  });
6033
6326
  bundle.addEnv(formatRouteEnvName3(publisherRouteKey, "REDIS_HOST"), redisHost);
@@ -6180,7 +6473,7 @@ var queueFeature = defineFeature({
6180
6473
  });
6181
6474
  }
6182
6475
  ctx.addEnv(`QUEUE_${constantCase6(ctx.stack.name)}_${constantCase6(id)}_URL`, queue2.url);
6183
- ctx.addStackPermission({
6476
+ ctx.addGlobalPermission({
6184
6477
  actions: [
6185
6478
  "sqs:SendMessage",
6186
6479
  "sqs:ReceiveMessage",
@@ -6376,7 +6669,7 @@ var rpcFeature = defineFeature({
6376
6669
  const serverRouteKey = formatRouteKey(ctx.app.name, "rpc", id);
6377
6670
  bundle.addHandler({
6378
6671
  routeKey: serverRouteKey,
6379
- file: join16(dirname10(fileURLToPath6(import.meta.url)), "/handlers/rpc.mjs"),
6672
+ file: join16(dirname10(fileURLToPath6(import.meta.url)), "/handlers/rpc.js"),
6380
6673
  exportName: "default"
6381
6674
  });
6382
6675
  bundle.addEnv(
@@ -6484,7 +6777,7 @@ var searchFeature = defineFeature({
6484
6777
  {
6485
6778
  domainName: name,
6486
6779
  engineVersion: `OpenSearch_${props.version}`,
6487
- ipAddressType: "ipv4",
6780
+ ipAddressType: "dualstack",
6488
6781
  clusterConfig: {
6489
6782
  instanceType: `${props.type}.search`,
6490
6783
  instanceCount: props.count
@@ -6539,8 +6832,8 @@ var searchFeature = defineFeature({
6539
6832
  import: ctx.import ? name : void 0
6540
6833
  }
6541
6834
  );
6542
- ctx.addEnv(`SEARCH_${constantCase8(ctx.stack.name)}_${constantCase8(id)}_DOMAIN`, openSearch.endpoint);
6543
- ctx.addStackPermission({
6835
+ ctx.addEnv(`SEARCH_${constantCase8(ctx.stack.name)}_${constantCase8(id)}_DOMAIN`, openSearch.endpointV2);
6836
+ ctx.addGlobalPermission({
6544
6837
  actions: ["es:ESHttp*"],
6545
6838
  resources: [
6546
6839
  //
@@ -6897,14 +7190,18 @@ var siteFeature = defineFeature({
6897
7190
  env,
6898
7191
  stdout: "pipe",
6899
7192
  stderr: "pipe"
6900
- // stdout: 'ignore',
6901
- // stderr: ''
6902
- // stdout: 'inherit',
6903
- // stderr: 'inherit',
6904
7193
  });
6905
- await instance.exited;
6906
- if (instance.exitCode !== null && instance.exitCode > 0) {
6907
- 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
+ );
6908
7205
  }
6909
7206
  await write("HASH", fingerprint);
6910
7207
  return {
@@ -7745,7 +8042,7 @@ var imageFeature = defineFeature({
7745
8042
  const serverRouteKey = formatRouteKey(ctx.stack.name, "image", id);
7746
8043
  bundle.addHandler({
7747
8044
  routeKey: serverRouteKey,
7748
- file: join20(dirname12(fileURLToPath7(import.meta.url)), "/handlers/image.mjs"),
8045
+ file: join20(dirname12(fileURLToPath7(import.meta.url)), "/handlers/image.js"),
7749
8046
  exportName: "default",
7750
8047
  external: ["sharp"]
7751
8048
  });
@@ -7844,7 +8141,7 @@ var iconFeature = defineFeature({
7844
8141
  const serverRouteKey = formatRouteKey(ctx.stack.name, "icon", id);
7845
8142
  bundle.addHandler({
7846
8143
  routeKey: serverRouteKey,
7847
- file: join21(dirname13(fileURLToPath8(import.meta.url)), "/handlers/icon.mjs"),
8144
+ file: join21(dirname13(fileURLToPath8(import.meta.url)), "/handlers/icon.js"),
7848
8145
  exportName: "default"
7849
8146
  });
7850
8147
  addRoutes({
@@ -8103,7 +8400,8 @@ var createFargateJob = (parentGroup, ctx, ns, id, local) => {
8103
8400
  Statement: list3.map((statement) => ({
8104
8401
  Effect: pascalCase3(statement.effect ?? "allow"),
8105
8402
  Action: statement.actions,
8106
- Resource: statement.resources
8403
+ Resource: statement.resources,
8404
+ Condition: statement.conditions
8107
8405
  }))
8108
8406
  })
8109
8407
  );
@@ -8455,13 +8753,13 @@ var jobFeature = defineFeature({
8455
8753
  const group = new Group25(ctx.stack, "job", id);
8456
8754
  createFargateJob(group, ctx, "job", id, props);
8457
8755
  }
8458
- ctx.addStackPermission({
8756
+ ctx.addGlobalPermission({
8459
8757
  actions: ["ecs:RunTask"],
8460
8758
  resources: [
8461
8759
  `arn:aws:ecs:${ctx.appConfig.region}:*:task-definition/${ctx.app.name}--${ctx.stackConfig.name}--*`
8462
8760
  ]
8463
8761
  });
8464
- ctx.addStackPermission({
8762
+ ctx.addGlobalPermission({
8465
8763
  actions: ["iam:PassRole"],
8466
8764
  resources: ["*"],
8467
8765
  conditions: {
@@ -8601,7 +8899,8 @@ var createFargateTask = (parentGroup, ctx, ns, id, local) => {
8601
8899
  Statement: list3.map((statement) => ({
8602
8900
  Effect: pascalCase4(statement.effect ?? "allow"),
8603
8901
  Action: statement.actions,
8604
- Resource: statement.resources
8902
+ Resource: statement.resources,
8903
+ Condition: statement.conditions
8605
8904
  }))
8606
8905
  })
8607
8906
  );
@@ -8997,7 +9296,7 @@ var metricFeature = defineFeature({
8997
9296
  onStack(ctx) {
8998
9297
  const bundle = ctx.shared.get("bundle", "main");
8999
9298
  const namespace = `awsless/${kebabCase11(ctx.app.name)}/${kebabCase11(ctx.stack.name)}`;
9000
- ctx.addStackPermission({
9299
+ ctx.addGlobalPermission({
9001
9300
  actions: ["cloudwatch:PutMetricData"],
9002
9301
  resources: ["*"],
9003
9302
  conditions: {
@@ -9073,16 +9372,20 @@ var metricFeature = defineFeature({
9073
9372
  });
9074
9373
 
9075
9374
  // src/feature/router/index.ts
9076
- 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";
9077
9376
  import { Group as Group29 } from "@terraforge/core";
9078
9377
  import { aws as aws30 } from "@terraforge/aws";
9079
- import { camelCase as camelCase9, constantCase as constantCase15 } from "change-case";
9378
+ import { camelCase as camelCase9, constantCase as constantCase15, kebabCase as kebabCase12 } from "change-case";
9080
9379
 
9081
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));
9082
9384
  var getViewerRequestFunctionCode = (props) => {
9083
9385
  return CODE(
9084
9386
  [
9085
9387
  props.blockDirectAccess ? BLOCK_DIRECT_ACCESS_TO_CLOUDFRONT : "",
9388
+ props.redirectWww ? REDIRECT_WWW : "",
9086
9389
  props.passwordAuth ?? props.basicAuth ? AUTH_WRAPPER(
9087
9390
  [
9088
9391
  //
@@ -9091,7 +9394,7 @@ var getViewerRequestFunctionCode = (props) => {
9091
9394
  ].join("\n")
9092
9395
  ) : ""
9093
9396
  ],
9094
- ACTIVE_PREFIX(props.router)
9397
+ props.preview ? PREVIEW_PREFIX(props.router) : ACTIVE_PREFIX(props.router)
9095
9398
  );
9096
9399
  };
9097
9400
  var BLOCK_DIRECT_ACCESS_TO_CLOUDFRONT = `
@@ -9101,6 +9404,38 @@ if (headers.host && headers.host.value.includes('cloudfront.net')) {
9101
9404
  statusDescription: 'Forbidden'
9102
9405
  };
9103
9406
  }`;
9407
+ var REDIRECT_WWW = `
9408
+ if (headers.host && headers.host.value.startsWith('www.')) {
9409
+ let location = 'https://' + headers.host.value.slice(4) + request.uri;
9410
+ const query = [];
9411
+
9412
+ for(const key in request.querystring) {
9413
+ const item = request.querystring[key];
9414
+
9415
+ if(item.multiValue) {
9416
+ for(const i in item.multiValue) {
9417
+ query.push(key + '=' + item.multiValue[i].value);
9418
+ }
9419
+ } else if(item.value === '') {
9420
+ query.push(key);
9421
+ } else {
9422
+ query.push(key + '=' + item.value);
9423
+ }
9424
+ }
9425
+
9426
+ if(query.length > 0) {
9427
+ location += '?' + query.join('&');
9428
+ }
9429
+
9430
+ return {
9431
+ statusCode: 301,
9432
+ statusDescription: 'Moved Permanently',
9433
+ headers: {
9434
+ 'location': { value: location },
9435
+ 'strict-transport-security': { value: 'max-age=31536000; includeSubdomains; preload' }
9436
+ }
9437
+ };
9438
+ }`;
9104
9439
  var BASIC_AUTH_CHECK = (username, password) => `
9105
9440
  authMethods.push('Basic realm="Protected"');
9106
9441
 
@@ -9114,11 +9449,64 @@ var PASSWORD_AUTH_CHECK = (password) => `
9114
9449
  authMethods.push('Password realm="Protected"');
9115
9450
 
9116
9451
  if(!isAuthorized) {
9117
- if(authHeader && authHeader.startsWith('Password ') && authHeader.slice(9) === '${password}') {
9452
+ if(authHeader && authHeader.startsWith('Password ') && authHeader.slice(9) === ${JSON.stringify(password)}) {
9118
9453
  isAuthorized = true;
9119
9454
  }
9120
9455
  }
9121
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
+ }`;
9122
9510
  var ACTIVE_PREFIX = (router) => `
9123
9511
  const router = ${JSON.stringify(router)};
9124
9512
  let prefix;
@@ -9186,20 +9574,77 @@ function isValidRoute(route, method) {
9186
9574
  return true;
9187
9575
  }
9188
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
+
9189
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
+
9190
9615
  const store = cf.kvs();
9191
9616
  const keys = getPossibleRouteKeys(path);
9192
9617
 
9193
9618
  for(const i in keys) {
9194
9619
  const key = keys[i];
9620
+ let value;
9195
9621
 
9196
9622
  try {
9197
- const route = await store.get(prefix + key, { format: 'json' });
9623
+ value = await store.get(prefix + key, { format: 'json' });
9624
+ } catch (e) {
9625
+ continue;
9626
+ }
9627
+
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) {}
9640
+ }
9641
+ } else {
9642
+ const result = matchRoute(value, path, method);
9198
9643
 
9199
- if(isValidRoute(route, method)) {
9200
- return route;
9644
+ if(result) {
9645
+ return result;
9201
9646
  }
9202
- } catch (e) {}
9647
+ }
9203
9648
  }
9204
9649
  }
9205
9650
 
@@ -9268,13 +9713,12 @@ function setS3Origin(route) {
9268
9713
  function setLambdaOrigin(route) {
9269
9714
  const config = getRequestOriginConfig(route);
9270
9715
 
9271
- // CloudFront caps the origin response timeout at 60s without a quota increase.
9272
9716
  if(typeof config.timeouts.readTimeout !== 'number') {
9273
- config.timeouts.readTimeout = 120;
9717
+ config.timeouts.readTimeout = ${ORIGIN_READ_TIMEOUT};
9274
9718
  }
9275
9719
 
9276
9720
  if(typeof config.timeouts.connectionTimeout !== 'number') {
9277
- config.timeouts.connectionTimeout = 10;
9721
+ config.timeouts.connectionTimeout = ${ORIGIN_CONNECTION_TIMEOUT};
9278
9722
  }
9279
9723
 
9280
9724
  cf.updateRequestOrigin(Object.assign(config, {
@@ -9323,15 +9767,36 @@ async function handler(event) {
9323
9767
 
9324
9768
  ${prefixCode}
9325
9769
 
9326
- const route = await findRoute(path, request.method, prefix);
9770
+ const result = await findRoute(path, request.method, prefix);
9327
9771
 
9328
- if(!route) {
9772
+ if(!result) {
9329
9773
  return {
9330
9774
  statusCode: 404,
9331
9775
  statusDescription: 'Not Found'
9332
9776
  };
9333
9777
  }
9334
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
+
9335
9800
  if(route.requestHeaders) {
9336
9801
  for(const name in route.requestHeaders) {
9337
9802
  headers[name] = { value: route.requestHeaders[name] };
@@ -9350,6 +9815,8 @@ async function handler(event) {
9350
9815
 
9351
9816
  if(route.forwardHost && headers.host && headers.host.value) {
9352
9817
  headers['x-forwarded-host'] = { value: headers.host.value };
9818
+ } else {
9819
+ delete headers['x-forwarded-host'];
9353
9820
  }
9354
9821
 
9355
9822
  headers['x-origin'] = { value: route.domainName };
@@ -9382,6 +9849,31 @@ async function handler(event) {
9382
9849
  `;
9383
9850
 
9384
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
+ };
9385
9877
  var routerFeature = defineFeature({
9386
9878
  name: "router",
9387
9879
  onApp(ctx) {
@@ -9392,7 +9884,6 @@ var routerFeature = defineFeature({
9392
9884
  const distributionIds = [];
9393
9885
  let hasLambdaRoutes = false;
9394
9886
  let routeStore;
9395
- let previewDistribution;
9396
9887
  for (const [id, props] of routers) {
9397
9888
  const group = new Group29(ctx.base, "router", id);
9398
9889
  const name = formatGlobalResourceName({
@@ -9416,6 +9907,7 @@ var routerFeature = defineFeature({
9416
9907
  code: getViewerRequestFunctionCode({
9417
9908
  router: id,
9418
9909
  blockDirectAccess: !!props.domain,
9910
+ redirectWww: !!props.domain && props.redirectWww,
9419
9911
  basicAuth: props.basicAuth,
9420
9912
  passwordAuth: props.passwordAuth
9421
9913
  }),
@@ -9427,20 +9919,21 @@ var routerFeature = defineFeature({
9427
9919
  if (Object.hasOwn(routes, `${id}:${key}`)) {
9428
9920
  throw new ExpectedError(`Duplicate route key: ${key} in the "${id}" router`);
9429
9921
  }
9922
+ assertRouteValueSize(`${id}:${key}`, route);
9430
9923
  routes[`${id}:${key}`] = route;
9431
9924
  }
9432
9925
  for (const dependency of options?.dependsOn ?? []) {
9433
9926
  routeDependencies.add(dependency);
9434
9927
  }
9435
- if (Object.values(newRoutes).some((route) => route.type === "lambda")) {
9928
+ if (Object.values(newRoutes).flat().some((route) => route.type === "lambda")) {
9436
9929
  hasLambdaRoutes = true;
9437
9930
  }
9438
9931
  });
9439
9932
  const cache = new aws30.cloudfront.CachePolicy(group, "cache", {
9440
9933
  name,
9441
- minTtl: toSeconds12(seconds6(0)),
9442
- maxTtl: toSeconds12(days10(365)),
9443
- defaultTtl: toSeconds12(days10(0)),
9934
+ minTtl: toSeconds13(seconds7(0)),
9935
+ maxTtl: toSeconds13(days10(365)),
9936
+ defaultTtl: toSeconds13(days10(0)),
9444
9937
  parametersInCacheKeyAndForwardedToOrigin: {
9445
9938
  enableAcceptEncodingBrotli: true,
9446
9939
  enableAcceptEncodingGzip: true,
@@ -9493,7 +9986,7 @@ var routerFeature = defineFeature({
9493
9986
  name,
9494
9987
  corsConfig: {
9495
9988
  originOverride: props.cors?.override ?? true,
9496
- accessControlMaxAgeSec: toSeconds12(props.cors?.maxAge ?? years(1)),
9989
+ accessControlMaxAgeSec: toSeconds13(props.cors?.maxAge ?? years(1)),
9497
9990
  accessControlAllowHeaders: { items: props.cors?.headers ?? ["*"] },
9498
9991
  accessControlAllowMethods: { items: props.cors?.methods ?? ["ALL"] },
9499
9992
  accessControlAllowOrigins: { items: props.cors?.origins ?? ["*"] },
@@ -9518,7 +10011,7 @@ var routerFeature = defineFeature({
9518
10011
  strictTransportSecurity: {
9519
10012
  override: true,
9520
10013
  preload: true,
9521
- accessControlMaxAgeSec: toSeconds12(years(1)),
10014
+ accessControlMaxAgeSec: toSeconds13(years(1)),
9522
10015
  includeSubdomains: true
9523
10016
  },
9524
10017
  xssProtection: {
@@ -9538,7 +10031,7 @@ var routerFeature = defineFeature({
9538
10031
  rateBasedStatement: {
9539
10032
  limit: wafSettingsConfig.rateLimiter.limit,
9540
10033
  aggregateKeyType: "IP",
9541
- evaluationWindowSec: toSeconds12(wafSettingsConfig.rateLimiter.window)
10034
+ evaluationWindowSec: toSeconds13(wafSettingsConfig.rateLimiter.window)
9542
10035
  }
9543
10036
  },
9544
10037
  action: {
@@ -9628,12 +10121,12 @@ var routerFeature = defineFeature({
9628
10121
  rule: wafRules,
9629
10122
  captchaConfig: {
9630
10123
  immunityTimeProperty: {
9631
- immunityTime: toSeconds12(wafSettingsConfig.captchaImmunityTime)
10124
+ immunityTime: toSeconds13(wafSettingsConfig.captchaImmunityTime)
9632
10125
  }
9633
10126
  },
9634
10127
  challengeConfig: {
9635
10128
  immunityTimeProperty: {
9636
- immunityTime: toSeconds12(wafSettingsConfig.challengeImmunityTime)
10129
+ immunityTime: toSeconds13(wafSettingsConfig.challengeImmunityTime)
9637
10130
  }
9638
10131
  },
9639
10132
  visibilityConfig: {
@@ -9689,7 +10182,7 @@ var routerFeature = defineFeature({
9689
10182
  }
9690
10183
  return {
9691
10184
  errorCode: Number(errorCode),
9692
- errorCachingMinTtl: item.minTTL ? toSeconds12(item.minTTL) : void 0,
10185
+ errorCachingMinTtl: item.minTTL ? toSeconds13(item.minTTL) : void 0,
9693
10186
  responseCode: item.statusCode?.toString() ?? errorCode,
9694
10187
  responsePagePath: item.path
9695
10188
  };
@@ -9728,24 +10221,26 @@ var routerFeature = defineFeature({
9728
10221
  ],
9729
10222
  webAclId: waf?.arn
9730
10223
  });
9731
- if (id === defaultRouter) {
10224
+ {
9732
10225
  const previewFunction = new aws30.cloudfront.Function(group, "preview-function", {
9733
10226
  name: `${name.slice(0, 55)}--preview`,
9734
10227
  runtime: "cloudfront-js-2.0",
9735
10228
  code: getViewerRequestFunctionCode({
9736
10229
  router: id,
10230
+ preview: true,
9737
10231
  basicAuth: props.basicAuth,
9738
10232
  passwordAuth: props.passwordAuth
9739
10233
  }),
9740
10234
  publish: true,
9741
10235
  keyValueStoreAssociations: [routeStore.arn]
9742
10236
  });
9743
- previewDistribution = new aws30.cloudfront.Distribution(group, "preview", {
10237
+ const previewDistribution = new aws30.cloudfront.Distribution(group, "preview", {
9744
10238
  tags: {
9745
10239
  name: `${name}-preview`
9746
10240
  },
9747
10241
  comment: `${name} preview`,
9748
10242
  enabled: true,
10243
+ isIpv6Enabled: true,
9749
10244
  waitForDeployment: true,
9750
10245
  origin: [
9751
10246
  {
@@ -9770,7 +10265,7 @@ var routerFeature = defineFeature({
9770
10265
  }
9771
10266
  return {
9772
10267
  errorCode: Number(errorCode),
9773
- errorCachingMinTtl: item.minTTL ? toSeconds12(item.minTTL) : void 0,
10268
+ errorCachingMinTtl: item.minTTL ? toSeconds13(item.minTTL) : void 0,
9774
10269
  responseCode: item.statusCode ?? Number(errorCode),
9775
10270
  responsePagePath: item.path
9776
10271
  };
@@ -9803,6 +10298,9 @@ var routerFeature = defineFeature({
9803
10298
  webAclId: waf?.arn
9804
10299
  });
9805
10300
  distributionIds.push(previewDistribution.id);
10301
+ ctx.shared.add("router", "preview-id", id, previewDistribution.id);
10302
+ }
10303
+ if (id === defaultRouter) {
9806
10304
  ctx.onReadyLast(() => {
9807
10305
  const bundle = ctx.shared.get("bundle", "main");
9808
10306
  let lambdaUrlHost;
@@ -9829,66 +10327,108 @@ var routerFeature = defineFeature({
9829
10327
  storeArn: routeStore.arn,
9830
10328
  functionVersion: bundle.lambda.version,
9831
10329
  routes: $resolve([routes, lambdaUrlHost], (routes2, lambdaUrlHost2) => {
9832
- return Object.entries(routes2).map(([key, route]) => ({
9833
- key,
9834
- value: JSON.stringify(
9835
- 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)
9836
10337
  )
9837
- }));
10338
+ );
9838
10339
  })
9839
10340
  },
9840
10341
  {
9841
10342
  dependsOn: Array.from(routeDependencies)
9842
10343
  }
9843
10344
  );
9844
- if (!(props.basicAuth ?? props.passwordAuth)) {
9845
- bundle.addEnv(
9846
- "AWSLESS_PREVIEW",
9847
- $resolve(
9848
- [routes],
9849
- (routes2) => JSON.stringify({
9850
- router: id,
9851
- routes: Object.fromEntries(
9852
- Object.entries(routes2).filter(
9853
- ([key, route]) => key.startsWith(`${id}:`) && route.type !== "url"
9854
- )
9855
- )
9856
- })
9857
- )
9858
- );
9859
- }
9860
10345
  });
9861
10346
  }
9862
10347
  ctx.shared.add("router", "id", id, distribution.id);
9863
- ctx.shared.add("router", "preview-id", id, previewDistribution.id);
9864
10348
  distributionIds.push(distribution.id);
9865
10349
  if (props.domain) {
9866
10350
  const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
10351
+ const wwwDomainName = props.redirectWww ? `www.${domainName}` : void 0;
9867
10352
  const certificateArn = ctx.shared.entry("domain", `global-certificate-arn`, props.domain);
9868
10353
  const zoneId = ctx.shared.entry("domain", "zone-id", props.domain);
9869
10354
  const connectionGroup = new aws30.cloudfront.ConnectionGroup(group, "connection-group", {
9870
- name
10355
+ name,
10356
+ ipv6Enabled: true
9871
10357
  });
9872
10358
  new aws30.cloudfront.DistributionTenant(group, `tenant`, {
9873
10359
  name,
9874
10360
  enabled: true,
9875
10361
  distributionId: distribution.id,
9876
10362
  connectionGroupId: connectionGroup.id,
9877
- domain: [{ domain: domainName }],
10363
+ domain: [
10364
+ //
10365
+ { domain: domainName },
10366
+ ...wwwDomainName ? [{ domain: wwwDomainName }] : []
10367
+ ],
9878
10368
  customizations: [{ certificate: [{ arn: certificateArn }] }]
9879
10369
  });
9880
- new aws30.route53.Record(group, `record`, {
9881
- zoneId,
9882
- type: "A",
9883
- name: domainName,
9884
- alias: {
9885
- name: connectionGroup.routingEndpoint,
9886
- zoneId: "Z2FDTNDATAQYW2",
9887
- evaluateTargetHealth: false
10370
+ for (const [recordId, recordName] of [
10371
+ ["record", domainName],
10372
+ ...wwwDomainName ? [["www-record", wwwDomainName]] : []
10373
+ ]) {
10374
+ new aws30.route53.Record(group, recordId, {
10375
+ zoneId,
10376
+ type: "A",
10377
+ name: recordName,
10378
+ alias: {
10379
+ name: connectionGroup.routingEndpoint,
10380
+ zoneId: "Z2FDTNDATAQYW2",
10381
+ evaluateTargetHealth: false
10382
+ }
10383
+ });
10384
+ new aws30.route53.Record(group, `${recordId}-ipv6`, {
10385
+ zoneId,
10386
+ type: "AAAA",
10387
+ name: recordName,
10388
+ alias: {
10389
+ name: connectionGroup.routingEndpoint,
10390
+ zoneId: "Z2FDTNDATAQYW2",
10391
+ evaluateTargetHealth: false
10392
+ }
10393
+ });
10394
+ }
10395
+ ctx.bind(`ROUTER_${constantCase15(id)}_ENDPOINT`, domainName);
10396
+ }
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
9888
10420
  }
9889
10421
  });
9890
- ctx.bind(`ROUTER_${constantCase15(id)}_ENDPOINT`, domainName);
9891
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);
9892
10432
  }
9893
10433
  }
9894
10434
  });
@@ -10183,6 +10723,12 @@ var logo = () => {
10183
10723
  var layout = async (command, cb) => {
10184
10724
  console.log();
10185
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
+ });
10186
10732
  try {
10187
10733
  const options = program.optsWithGlobals();
10188
10734
  const appConfig = await loadAppConfig(options);
@@ -10197,9 +10743,11 @@ var layout = async (command, cb) => {
10197
10743
  appConfig,
10198
10744
  stackConfigs
10199
10745
  });
10746
+ completed = true;
10200
10747
  log9.outro(result ?? void 0);
10201
10748
  process.exit(0);
10202
10749
  } catch (error) {
10750
+ completed = true;
10203
10751
  playErrorSound();
10204
10752
  logError(error);
10205
10753
  log9.outro();
@@ -10267,14 +10815,6 @@ var SharedData = class {
10267
10815
  };
10268
10816
 
10269
10817
  // src/app.ts
10270
- var assertDepsExists = (stack, stacks) => {
10271
- for (const dep of stack.depends ?? []) {
10272
- const found = stacks.find((i) => i.name === dep);
10273
- if (!found) {
10274
- throw new FileError(stack.file, `Stack "${stack.name}" depends on a stack "${dep}" that doesn't exist.`);
10275
- }
10276
- }
10277
- };
10278
10818
  var createApp = (props) => {
10279
10819
  const app = new App2(props.appConfig.name);
10280
10820
  const zones = new Stack(app, "zones");
@@ -10298,17 +10838,10 @@ var createApp = (props) => {
10298
10838
  const bindListeners = [];
10299
10839
  const globalEnv = [];
10300
10840
  const globalEnvListeners = [];
10301
- const allLocalEnv = {};
10302
- const allLocalEnvListeners = {};
10303
10841
  const globalPermissions = [];
10304
10842
  const globalPermissionCallbacks = [];
10305
10843
  const appPermissions = [];
10306
10844
  const appPermissionCallbacks = [];
10307
- const allStackPermissions = {};
10308
- const allStackPermissionCallbacks = {};
10309
- for (const stackConfig of props.stackConfigs) {
10310
- assertDepsExists(stackConfig, props.stackConfigs);
10311
- }
10312
10845
  for (const feature of features) {
10313
10846
  feature.onBefore?.({
10314
10847
  ...props,
@@ -10381,14 +10914,6 @@ var createApp = (props) => {
10381
10914
  }
10382
10915
  for (const stackConfig of props.stackConfigs) {
10383
10916
  const stack = new Stack(app, stackConfig.name);
10384
- const localEnvListeners = [];
10385
- const localEnv = [];
10386
- const stackPermissions = [];
10387
- const stackPermissionCallbacks = [];
10388
- allStackPermissions[stack.name] = stackPermissions;
10389
- allStackPermissionCallbacks[stack.name] = stackPermissionCallbacks;
10390
- allLocalEnvListeners[stack.name] = localEnvListeners;
10391
- allLocalEnv[stack.name] = localEnv;
10392
10917
  for (const feature of features) {
10393
10918
  feature.onStack?.({
10394
10919
  ...props,
@@ -10402,7 +10927,6 @@ var createApp = (props) => {
10402
10927
  shared,
10403
10928
  onPermission(callback) {
10404
10929
  globalPermissionCallbacks.push(callback);
10405
- stackPermissionCallbacks.push(callback);
10406
10930
  },
10407
10931
  addGlobalPermission(permission) {
10408
10932
  globalPermissions.push(permission);
@@ -10410,9 +10934,6 @@ var createApp = (props) => {
10410
10934
  addAppPermission(permission) {
10411
10935
  appPermissions.push(permission);
10412
10936
  },
10413
- addStackPermission(permission) {
10414
- stackPermissions.push(permission);
10415
- },
10416
10937
  addWarning(props2) {
10417
10938
  warnings.push(props2);
10418
10939
  },
@@ -10476,10 +10997,10 @@ var createApp = (props) => {
10476
10997
  bindListeners.push(cb);
10477
10998
  },
10478
10999
  addEnv(name, value) {
10479
- localEnv.push({ name, value });
11000
+ globalEnv.push({ name, value });
10480
11001
  },
10481
11002
  onEnv(cb) {
10482
- localEnvListeners.push(cb);
11003
+ globalEnvListeners.push(cb);
10483
11004
  },
10484
11005
  onReady(cb) {
10485
11006
  readyListeners.push(cb);
@@ -10489,16 +11010,6 @@ var createApp = (props) => {
10489
11010
  }
10490
11011
  });
10491
11012
  }
10492
- for (const callback of stackPermissionCallbacks) {
10493
- for (const permission of stackPermissions) {
10494
- callback(permission);
10495
- }
10496
- }
10497
- for (const listener of localEnvListeners) {
10498
- for (const env of localEnv) {
10499
- listener(env.name, env.value);
10500
- }
10501
- }
10502
11013
  }
10503
11014
  for (const callback of appPermissionCallbacks) {
10504
11015
  for (const permission of appPermissions) {
@@ -10520,24 +11031,6 @@ var createApp = (props) => {
10520
11031
  listener(name, value);
10521
11032
  }
10522
11033
  }
10523
- for (const stackConfig of props.stackConfigs) {
10524
- const envListeners = allLocalEnvListeners[stackConfig.name];
10525
- const permissionCallbacks = allStackPermissionCallbacks[stackConfig.name];
10526
- for (const dependency of stackConfig.depends ?? []) {
10527
- const permissions = allStackPermissions[dependency];
10528
- const env = allLocalEnv[dependency];
10529
- for (const permission of permissions) {
10530
- for (const callback of permissionCallbacks) {
10531
- callback(permission);
10532
- }
10533
- }
10534
- for (const entry of env) {
10535
- for (const listener of envListeners) {
10536
- listener(entry.name, entry.value);
10537
- }
10538
- }
10539
- }
10540
- }
10541
11034
  const ready = () => {
10542
11035
  for (const listener of readyListeners) {
10543
11036
  listener();
@@ -10583,6 +11076,7 @@ var buildAssets = async (builders, stackFilters, showResult = false) => {
10583
11076
  if (filteredBuilders.length === 0) {
10584
11077
  return;
10585
11078
  }
11079
+ filteredBuilders.sort((a, b) => Number(a.type === "bundle") - Number(b.type === "bundle"));
10586
11080
  const results = [];
10587
11081
  await log10.task({
10588
11082
  initialMessage: `Building assets...`,
@@ -11391,6 +11885,10 @@ var deploy = (program2) => {
11391
11885
  appConfig,
11392
11886
  id: deployment.id
11393
11887
  });
11888
+ await promoteAppDeployment({
11889
+ appConfig,
11890
+ id: deployment.id
11891
+ });
11394
11892
  return deployments3;
11395
11893
  } finally {
11396
11894
  await release();
@@ -11398,9 +11896,10 @@ var deploy = (program2) => {
11398
11896
  }
11399
11897
  });
11400
11898
  playSuccessSound();
11401
- const details = deployments2.length > 0 ? `
11402
- ${deployments2.join("\n")}` : "";
11403
- 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.`;
11404
11903
  });
11405
11904
  });
11406
11905
  };
@@ -11891,7 +12390,7 @@ var bind = (program2) => {
11891
12390
  stderr: "inherit"
11892
12391
  });
11893
12392
  await instance.exited;
11894
- process.exit(0);
12393
+ process.exit(instance.exitCode ?? 1);
11895
12394
  });
11896
12395
  });
11897
12396
  };
@@ -12011,7 +12510,7 @@ var resources = (program2) => {
12011
12510
  return `${color.dim("{")}${color.warning(v)}${color.dim("}")}`;
12012
12511
  }).replaceAll(":", color.dim(":"));
12013
12512
  };
12014
- const formatStatus = (status) => {
12513
+ const formatStatus2 = (status) => {
12015
12514
  if (status === "created") {
12016
12515
  return color.success(status);
12017
12516
  }
@@ -12036,7 +12535,7 @@ var resources = (program2) => {
12036
12535
  stack.resources.map((r) => {
12037
12536
  return [
12038
12537
  //
12039
- formatStatus(r.status),
12538
+ formatStatus2(r.status),
12040
12539
  color.dim(icon.arrow.right),
12041
12540
  formatResource(stack.urn, r.urn)
12042
12541
  ].join(" ");
@@ -12280,9 +12779,12 @@ var test = (program2) => {
12280
12779
  if (tests.length === 0) {
12281
12780
  return "No tests found.";
12282
12781
  }
12283
- await runTests(tests, stacks, options?.filters, {
12782
+ const passed = await runTests(tests, stacks, options?.filters, {
12284
12783
  showLogs: true
12285
12784
  });
12785
+ if (!passed) {
12786
+ throw new Cancelled();
12787
+ }
12286
12788
  return "All tests finished.";
12287
12789
  });
12288
12790
  });
@@ -12507,14 +13009,14 @@ var parseJsonLog = (message) => {
12507
13009
  json = JSON.parse(message);
12508
13010
  } catch (error) {
12509
13011
  }
12510
- 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) {
12511
13013
  return {
12512
13014
  level: json.level,
12513
13015
  message: typeof json.message === "string" ? json.message : JSON.stringify(json.message, void 0, 2),
12514
13016
  date: new Date(json.timestamp)
12515
13017
  };
12516
13018
  }
12517
- 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) {
12518
13020
  return {
12519
13021
  level: "SYSTEM",
12520
13022
  message: JSON.stringify(json.record, void 0, 2),
@@ -12962,7 +13464,7 @@ var activity = (program2) => {
12962
13464
  // src/cli/command/deployment.ts
12963
13465
  import { CloudFrontClient as CloudFrontClient6 } from "@aws-sdk/client-cloudfront";
12964
13466
  import { CloudFrontKeyValueStoreClient as CloudFrontKeyValueStoreClient3 } from "@aws-sdk/client-cloudfront-keyvaluestore";
12965
- import { DeleteFunctionCommand, LambdaClient as LambdaClient7 } from "@aws-sdk/client-lambda";
13467
+ import { LambdaClient as LambdaClient7 } from "@aws-sdk/client-lambda";
12966
13468
  import { log as log35, prompt as prompt19 } from "@awsless/clui";
12967
13469
  import { DynamoDBClient as DynamoDBClient6 } from "@awsless/dynamodb";
12968
13470
  var createClients = async (appConfig) => {
@@ -12979,11 +13481,17 @@ var createClients = async (appConfig) => {
12979
13481
  };
12980
13482
  };
12981
13483
  var formatAge = (iso) => {
12982
- const minutes8 = Math.floor((Date.now() - Date.parse(iso)) / 6e4);
12983
- if (minutes8 < 1) return "just now";
12984
- if (minutes8 < 60) return `${minutes8}m ago`;
12985
- if (minutes8 < 60 * 24) return `${Math.floor(minutes8 / 60)}h ago`;
12986
- 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 ");
12987
13495
  };
12988
13496
  var deployments = (program2) => {
12989
13497
  program2.command("deployments").description("List the deployment history of your app").action(async () => {
@@ -12998,17 +13506,16 @@ var deployments = (program2) => {
12998
13506
  }
12999
13507
  const idWidth = Math.max(...items.map((item) => item.id.length));
13000
13508
  log35.message(
13001
- items.map((item) => {
13002
- const status = item.id === liveId ? color.success("live ") : item.promotedAt ? "promoted" : item.functionVersion ? color.info("staged ") : color.dim("pending ");
13003
- return [
13509
+ items.map(
13510
+ (item) => [
13004
13511
  color.label(item.id.padEnd(idWidth)),
13005
- status,
13512
+ formatStatus(item, liveId),
13006
13513
  formatAge(item.createdAt).padEnd(8),
13007
13514
  color.dim(item.commit?.slice(0, 7) ?? "-------"),
13008
13515
  (item.message ?? "").slice(0, 50).padEnd(50),
13009
13516
  color.dim(item.user ?? "")
13010
- ].join(" ");
13011
- }).join("\n")
13517
+ ].join(" ")
13518
+ ).join("\n")
13012
13519
  );
13013
13520
  return `Found ${items.length} deployments.`;
13014
13521
  });
@@ -13022,28 +13529,7 @@ var prune = (program2) => {
13022
13529
  listDeployments(dynamo, appId),
13023
13530
  readLiveDeploymentId(lambda, functionName)
13024
13531
  ]);
13025
- const rollbackTarget = items.filter((item) => item.promotedAt && item.id !== liveId).sort((a, b) => b.promotedAt.localeCompare(a.promotedAt))[0];
13026
- const keep = Math.max(1, Number(options.keep) || 10);
13027
- const mainSlug = slugifyBranch(options.main);
13028
- const keptMain = new Set(
13029
- items.filter((item) => item.branch === mainSlug && item.functionVersion).map((item) => item.seq).sort((a, b) => b - a).slice(0, keep)
13030
- );
13031
- const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
13032
- const prunable = items.filter((item) => {
13033
- if (item.id === liveId || item.id === rollbackTarget?.id) {
13034
- return false;
13035
- }
13036
- if (options.branch) {
13037
- return item.branch === slugifyBranch(options.branch);
13038
- }
13039
- if (!item.functionVersion) {
13040
- return item.createdAt < dayAgo;
13041
- }
13042
- if (item.branch === mainSlug) {
13043
- return !keptMain.has(item.seq);
13044
- }
13045
- return item.commit ? isCommitMerged(item.commit, options.main) : false;
13046
- });
13532
+ const prunable = selectPrunableDeployments(items, liveId, options);
13047
13533
  if (prunable.length === 0) {
13048
13534
  return `Nothing to prune.`;
13049
13535
  }
@@ -13060,31 +13546,25 @@ var prune = (program2) => {
13060
13546
  initialMessage: "Pruning the deployments",
13061
13547
  successMessage: "Done pruning the deployments.",
13062
13548
  task: () => withAppReleaseLock(appConfig, async () => {
13063
- 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) {
13064
13558
  await deleteLambdaAlias(lambda, functionName, getDeploymentLambdaAliasName(item.id));
13065
13559
  }
13066
- const surviving = items.filter((item) => !prunable.includes(item));
13067
- const keepVersions = new Set(surviving.map((item) => item.functionVersion));
13068
- const live = await getLambdaAlias(lambda, functionName, LIVE_LAMBDA_ALIAS);
13069
- if (live?.FunctionVersion) {
13070
- keepVersions.add(live.FunctionVersion);
13071
- }
13072
- const versions = new Set(
13073
- prunable.map((item) => item.functionVersion).filter((version) => version && !keepVersions.has(version))
13074
- );
13560
+ const versions = await selectPrunableVersions({
13561
+ lambda,
13562
+ functionName,
13563
+ items: freshItems,
13564
+ prunable: prune2
13565
+ });
13075
13566
  for (const version of versions) {
13076
- try {
13077
- await lambda.send(
13078
- new DeleteFunctionCommand({
13079
- FunctionName: functionName,
13080
- Qualifier: version
13081
- })
13082
- );
13083
- } catch (error) {
13084
- if (!isError(error, "ResourceNotFoundException") && !isError(error, "ResourceConflictException")) {
13085
- throw error;
13086
- }
13087
- }
13567
+ await pruneFunctionVersion(lambda, functionName, version);
13088
13568
  }
13089
13569
  const storeArn = await getRouteStoreArn(
13090
13570
  cloudfront,
@@ -13098,10 +13578,10 @@ var prune = (program2) => {
13098
13578
  await pruneStoreDeployments(
13099
13579
  kvs,
13100
13580
  storeArn,
13101
- prunable.map((item) => item.id)
13581
+ prune2.map((item) => item.id)
13102
13582
  );
13103
13583
  }
13104
- for (const item of prunable) {
13584
+ for (const item of prune2) {
13105
13585
  await removeDeployment(dynamo, appId, item.id);
13106
13586
  }
13107
13587
  })
@@ -13170,8 +13650,8 @@ program.option("--stage <string>", "The stage to use");
13170
13650
  program.option("-c --no-cache", "Always build & test without the cache");
13171
13651
  program.option("-s --skip-prompt", "Skip prompts");
13172
13652
  program.option("-v --verbose", "Print verbose logs");
13173
- program.exitOverride(() => {
13174
- process.exit(0);
13653
+ program.exitOverride((error) => {
13654
+ process.exit(error.exitCode);
13175
13655
  });
13176
13656
  program.on("option:verbose", () => {
13177
13657
  process.env.VERBOSE = program.opts().verbose ? "1" : void 0;
@@ -13185,4 +13665,13 @@ program.on("option:no-cache", () => {
13185
13665
  commands10.forEach((fn) => fn(program));
13186
13666
 
13187
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));
13188
13677
  program.parse(process.argv);